@otto-code/server 0.5.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/.env.example +31 -0
- package/README.md +111 -0
- package/dist/scripts/dev-runner.js +37 -0
- package/dist/scripts/mcp-stdio-socket-bridge-cli.mjs +62 -0
- package/dist/scripts/supervisor-entrypoint.js +141 -0
- package/dist/scripts/supervisor-log-config.js +31 -0
- package/dist/scripts/supervisor.js +252 -0
- package/dist/server/executable-resolution/executable-resolution.d.ts +10 -0
- package/dist/server/executable-resolution/executable-resolution.js +118 -0
- package/dist/server/executable-resolution/windows.d.ts +18 -0
- package/dist/server/executable-resolution/windows.js +65 -0
- package/dist/server/server/agent/activity-curator.d.ts +28 -0
- package/dist/server/server/agent/activity-curator.js +227 -0
- package/dist/server/server/agent/agent-archive.d.ts +11 -0
- package/dist/server/server/agent/agent-archive.js +16 -0
- package/dist/server/server/agent/agent-loading.d.ts +14 -0
- package/dist/server/server/agent/agent-loading.js +54 -0
- package/dist/server/server/agent/agent-manager.d.ts +470 -0
- package/dist/server/server/agent/agent-manager.js +3049 -0
- package/dist/server/server/agent/agent-personalities.d.ts +59 -0
- package/dist/server/server/agent/agent-personalities.js +103 -0
- package/dist/server/server/agent/agent-projections.d.ts +35 -0
- package/dist/server/server/agent/agent-projections.js +488 -0
- package/dist/server/server/agent/agent-prompt.d.ts +76 -0
- package/dist/server/server/agent/agent-prompt.js +230 -0
- package/dist/server/server/agent/agent-response-loop.d.ts +72 -0
- package/dist/server/server/agent/agent-response-loop.js +333 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +736 -0
- package/dist/server/server/agent/agent-sdk-types.js +22 -0
- package/dist/server/server/agent/agent-storage.d.ts +147 -0
- package/dist/server/server/agent/agent-storage.js +326 -0
- package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
- package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
- package/dist/server/server/agent/agent-timeline-store-types.d.ts +50 -0
- package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
- package/dist/server/server/agent/agent-timeline-store.d.ts +27 -0
- package/dist/server/server/agent/agent-timeline-store.js +230 -0
- package/dist/server/server/agent/audio-utils.d.ts +3 -0
- package/dist/server/server/agent/audio-utils.js +19 -0
- package/dist/server/server/agent/create-agent/create.d.ts +91 -0
- package/dist/server/server/agent/create-agent/create.js +370 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +54 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +152 -0
- package/dist/server/server/agent/create-agent-mode.d.ts +13 -0
- package/dist/server/server/agent/create-agent-mode.js +62 -0
- package/dist/server/server/agent/create-agent-title.d.ts +10 -0
- package/dist/server/server/agent/create-agent-title.js +34 -0
- package/dist/server/server/agent/dictation-debug.d.ts +13 -0
- package/dist/server/server/agent/dictation-debug.js +50 -0
- package/dist/server/server/agent/effort-levels.d.ts +2 -0
- package/dist/server/server/agent/effort-levels.js +7 -0
- package/dist/server/server/agent/foreground-run-state.d.ts +50 -0
- package/dist/server/server/agent/foreground-run-state.js +160 -0
- package/dist/server/server/agent/import-sessions.d.ts +47 -0
- package/dist/server/server/agent/import-sessions.js +145 -0
- package/dist/server/server/agent/lifecycle-command.d.ts +73 -0
- package/dist/server/server/agent/lifecycle-command.js +92 -0
- package/dist/server/server/agent/mcp-server.d.ts +5 -0
- package/dist/server/server/agent/mcp-server.js +66 -0
- package/dist/server/server/agent/mcp-shared.d.ts +192 -0
- package/dist/server/server/agent/mcp-shared.js +201 -0
- package/dist/server/server/agent/pcm16-resampler.d.ts +14 -0
- package/dist/server/server/agent/pcm16-resampler.js +63 -0
- package/dist/server/server/agent/permission-response.d.ts +15 -0
- package/dist/server/server/agent/permission-response.js +14 -0
- package/dist/server/server/agent/personality-stats-store.d.ts +23 -0
- package/dist/server/server/agent/personality-stats-store.js +65 -0
- package/dist/server/server/agent/prompt-attachments.d.ts +7 -0
- package/dist/server/server/agent/prompt-attachments.js +117 -0
- package/dist/server/server/agent/provider-history-timestamps.d.ts +2 -0
- package/dist/server/server/agent/provider-history-timestamps.js +16 -0
- package/dist/server/server/agent/provider-launch-config.d.ts +43 -0
- package/dist/server/server/agent/provider-launch-config.js +167 -0
- package/dist/server/server/agent/provider-notices.d.ts +3 -0
- package/dist/server/server/agent/provider-notices.js +5 -0
- package/dist/server/server/agent/provider-registry.d.ts +41 -0
- package/dist/server/server/agent/provider-registry.js +532 -0
- package/dist/server/server/agent/provider-session-import.d.ts +10 -0
- package/dist/server/server/agent/provider-session-import.js +49 -0
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +142 -0
- package/dist/server/server/agent/provider-snapshot-manager.js +690 -0
- package/dist/server/server/agent/providers/acp-agent.d.ts +353 -0
- package/dist/server/server/agent/providers/acp-agent.js +2439 -0
- package/dist/server/server/agent/providers/claude/agent.d.ts +66 -0
- package/dist/server/server/agent/providers/claude/agent.js +4535 -0
- package/dist/server/server/agent/providers/claude/feature-definitions.d.ts +8 -0
- package/dist/server/server/agent/providers/claude/feature-definitions.js +24 -0
- package/dist/server/server/agent/providers/claude/model-manifest.d.ts +90 -0
- package/dist/server/server/agent/providers/claude/model-manifest.js +205 -0
- package/dist/server/server/agent/providers/claude/models.d.ts +11 -0
- package/dist/server/server/agent/providers/claude/models.js +97 -0
- package/dist/server/server/agent/providers/claude/partial-json.d.ts +5 -0
- package/dist/server/server/agent/providers/claude/partial-json.js +306 -0
- package/dist/server/server/agent/providers/claude/project-dir.d.ts +6 -0
- package/dist/server/server/agent/providers/claude/project-dir.js +57 -0
- package/dist/server/server/agent/providers/claude/query.d.ts +17 -0
- package/dist/server/server/agent/providers/claude/query.js +86 -0
- package/dist/server/server/agent/providers/claude/rewind.d.ts +30 -0
- package/dist/server/server/agent/providers/claude/rewind.js +30 -0
- package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts +22 -0
- package/dist/server/server/agent/providers/claude/sidechain-tracker.js +232 -0
- package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +35 -0
- package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +272 -0
- package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.d.ts +24 -0
- package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.js +23 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts +3 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +130 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +16 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +129 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +90 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.js +262 -0
- package/dist/server/server/agent/providers/codex/rewind.d.ts +21 -0
- package/dist/server/server/agent/providers/codex/rewind.js +46 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.d.ts +36 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.js +217 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +12 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +104 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +24 -0
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js +830 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +308 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.js +4515 -0
- package/dist/server/server/agent/providers/codex-feature-definitions.d.ts +11 -0
- package/dist/server/server/agent/providers/codex-feature-definitions.js +45 -0
- package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +26 -0
- package/dist/server/server/agent/providers/copilot-acp-agent.js +188 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +15 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.js +18 -0
- package/dist/server/server/agent/providers/diagnostic-utils.d.ts +36 -0
- package/dist/server/server/agent/providers/diagnostic-utils.js +355 -0
- package/dist/server/server/agent/providers/generic-acp-agent.d.ts +42 -0
- package/dist/server/server/agent/providers/generic-acp-agent.js +160 -0
- package/dist/server/server/agent/providers/kiro-acp-agent.d.ts +17 -0
- package/dist/server/server/agent/providers/kiro-acp-agent.js +78 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +85 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +1010 -0
- package/dist/server/server/agent/providers/mock-slow-provider.d.ts +14 -0
- package/dist/server/server/agent/providers/mock-slow-provider.js +39 -0
- package/dist/server/server/agent/providers/openai-compat-agent.d.ts +422 -0
- package/dist/server/server/agent/providers/openai-compat-agent.js +2536 -0
- package/dist/server/server/agent/providers/openai-compat-feature-definitions.d.ts +40 -0
- package/dist/server/server/agent/providers/openai-compat-feature-definitions.js +82 -0
- package/dist/server/server/agent/providers/openai-compat-mcp.d.ts +87 -0
- package/dist/server/server/agent/providers/openai-compat-mcp.js +353 -0
- package/dist/server/server/agent/providers/openai-compat-otto-tool-permissions.d.ts +29 -0
- package/dist/server/server/agent/providers/openai-compat-otto-tool-permissions.js +96 -0
- package/dist/server/server/agent/providers/openai-compat-tools.d.ts +69 -0
- package/dist/server/server/agent/providers/openai-compat-tools.js +1224 -0
- package/dist/server/server/agent/providers/opencode/paths.d.ts +2 -0
- package/dist/server/server/agent/providers/opencode/paths.js +7 -0
- package/dist/server/server/agent/providers/opencode/rewind.d.ts +18 -0
- package/dist/server/server/agent/providers/opencode/rewind.js +14 -0
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts +91 -0
- package/dist/server/server/agent/providers/opencode/server-manager.js +425 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +25 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.js +39 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.d.ts +92 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.js +260 -0
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts +3 -0
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +256 -0
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +13 -0
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +53 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +286 -0
- package/dist/server/server/agent/providers/opencode-agent.js +2830 -0
- package/dist/server/server/agent/providers/pi/agent.d.ts +132 -0
- package/dist/server/server/agent/providers/pi/agent.js +1649 -0
- package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +21 -0
- package/dist/server/server/agent/providers/pi/cli-runtime.js +261 -0
- package/dist/server/server/agent/providers/pi/history-mapper.d.ts +9 -0
- package/dist/server/server/agent/providers/pi/history-mapper.js +140 -0
- package/dist/server/server/agent/providers/pi/rewind.d.ts +8 -0
- package/dist/server/server/agent/providers/pi/rewind.js +8 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +208 -0
- package/dist/server/server/agent/providers/pi/rpc-types.js +2 -0
- package/dist/server/server/agent/providers/pi/runtime.d.ts +57 -0
- package/dist/server/server/agent/providers/pi/runtime.js +56 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +16 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.js +346 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +83 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +184 -0
- package/dist/server/server/agent/providers/pi/tool-call-mapper.d.ts +112 -0
- package/dist/server/server/agent/providers/pi/tool-call-mapper.js +284 -0
- package/dist/server/server/agent/providers/provider-image-output.d.ts +25 -0
- package/dist/server/server/agent/providers/provider-image-output.js +111 -0
- package/dist/server/server/agent/providers/provider-runner.d.ts +27 -0
- package/dist/server/server/agent/providers/provider-runner.js +81 -0
- package/dist/server/server/agent/providers/test-utils/session-stream-adapter.d.ts +3 -0
- package/dist/server/server/agent/providers/test-utils/session-stream-adapter.js +60 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +884 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +778 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +25 -0
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js +208 -0
- package/dist/server/server/agent/providers/trae-acp-agent.d.ts +15 -0
- package/dist/server/server/agent/providers/trae-acp-agent.js +18 -0
- package/dist/server/server/agent/recordings-debug.d.ts +3 -0
- package/dist/server/server/agent/recordings-debug.js +19 -0
- package/dist/server/server/agent/rewind/rewind.d.ts +10 -0
- package/dist/server/server/agent/rewind/rewind.js +29 -0
- package/dist/server/server/agent/rewind/test-rewind-session.d.ts +48 -0
- package/dist/server/server/agent/rewind/test-rewind-session.js +88 -0
- package/dist/server/server/agent/runtime-mcp-config.d.ts +14 -0
- package/dist/server/server/agent/runtime-mcp-config.js +53 -0
- package/dist/server/server/agent/structured-generation-providers.d.ts +29 -0
- package/dist/server/server/agent/structured-generation-providers.js +236 -0
- package/dist/server/server/agent/stt-debug.d.ts +10 -0
- package/dist/server/server/agent/stt-debug.js +33 -0
- package/dist/server/server/agent/stt-manager.d.ts +38 -0
- package/dist/server/server/agent/stt-manager.js +246 -0
- package/dist/server/server/agent/system-prompt.d.ts +2 -0
- package/dist/server/server/agent/system-prompt.js +8 -0
- package/dist/server/server/agent/timeline-append.d.ts +10 -0
- package/dist/server/server/agent/timeline-append.js +27 -0
- package/dist/server/server/agent/timeline-projection.d.ts +71 -0
- package/dist/server/server/agent/timeline-projection.js +373 -0
- package/dist/server/server/agent/tools/otto-tools.d.ts +80 -0
- package/dist/server/server/agent/tools/otto-tools.js +2912 -0
- package/dist/server/server/agent/tools/types.d.ts +36 -0
- package/dist/server/server/agent/tools/types.js +2 -0
- package/dist/server/server/agent/tts-debug.d.ts +8 -0
- package/dist/server/server/agent/tts-debug.js +24 -0
- package/dist/server/server/agent/tts-manager.d.ts +41 -0
- package/dist/server/server/agent/tts-manager.js +375 -0
- package/dist/server/server/agent-attention-policy.d.ts +29 -0
- package/dist/server/server/agent-attention-policy.js +36 -0
- package/dist/server/server/artifact/artifact-prompt.d.ts +2 -0
- package/dist/server/server/artifact/artifact-prompt.js +15 -0
- package/dist/server/server/artifact/artifact-service.d.ts +91 -0
- package/dist/server/server/artifact/artifact-service.js +419 -0
- package/dist/server/server/artifact/artifact-store.d.ts +24 -0
- package/dist/server/server/artifact/artifact-store.js +168 -0
- package/dist/server/server/artifact/artifact-watcher.d.ts +34 -0
- package/dist/server/server/artifact/artifact-watcher.js +146 -0
- package/dist/server/server/artifact/html-validator.d.ts +26 -0
- package/dist/server/server/artifact/html-validator.js +92 -0
- package/dist/server/server/atomic-file.d.ts +5 -0
- package/dist/server/server/atomic-file.js +19 -0
- package/dist/server/server/auth.d.ts +38 -0
- package/dist/server/server/auth.js +143 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +40 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +92 -0
- package/dist/server/server/auto-archive-on-merge/index.d.ts +8 -0
- package/dist/server/server/auto-archive-on-merge/index.js +15 -0
- package/dist/server/server/bootstrap.d.ts +127 -0
- package/dist/server/server/bootstrap.js +1331 -0
- package/dist/server/server/browser-tools/broker.d.ts +45 -0
- package/dist/server/server/browser-tools/broker.js +390 -0
- package/dist/server/server/browser-tools/errors.d.ts +22 -0
- package/dist/server/server/browser-tools/errors.js +27 -0
- package/dist/server/server/browser-tools/policy.d.ts +10 -0
- package/dist/server/server/browser-tools/policy.js +16 -0
- package/dist/server/server/browser-tools/tools.d.ts +24 -0
- package/dist/server/server/browser-tools/tools.js +967 -0
- package/dist/server/server/chat/chat-mentions.d.ts +47 -0
- package/dist/server/server/chat/chat-mentions.js +113 -0
- package/dist/server/server/chat/chat-service.d.ts +78 -0
- package/dist/server/server/chat/chat-service.js +337 -0
- package/dist/server/server/checkout/status-projection.d.ts +19 -0
- package/dist/server/server/checkout/status-projection.js +117 -0
- package/dist/server/server/checkout-diff-manager.d.ts +41 -0
- package/dist/server/server/checkout-diff-manager.js +189 -0
- package/dist/server/server/checkout-git-utils.d.ts +11 -0
- package/dist/server/server/checkout-git-utils.js +34 -0
- package/dist/server/server/client-message-id.d.ts +3 -0
- package/dist/server/server/client-message-id.js +12 -0
- package/dist/server/server/config.d.ts +22 -0
- package/dist/server/server/config.js +330 -0
- package/dist/server/server/connection-offer.d.ts +20 -0
- package/dist/server/server/connection-offer.js +59 -0
- package/dist/server/server/daemon-config-store.d.ts +44 -0
- package/dist/server/server/daemon-config-store.js +530 -0
- package/dist/server/server/daemon-keypair.d.ts +8 -0
- package/dist/server/server/daemon-keypair.js +42 -0
- package/dist/server/server/daemon-version.d.ts +5 -0
- package/dist/server/server/daemon-version.js +22 -0
- package/dist/server/server/daemon-worker.d.ts +2 -0
- package/dist/server/server/daemon-worker.js +251 -0
- package/dist/server/server/dictation/dictation-stream-manager.d.ts +87 -0
- package/dist/server/server/dictation/dictation-stream-manager.js +572 -0
- package/dist/server/server/exports.d.ts +24 -0
- package/dist/server/server/exports.js +24 -0
- package/dist/server/server/file-download/token-store.d.ts +24 -0
- package/dist/server/server/file-download/token-store.js +40 -0
- package/dist/server/server/file-explorer/code-index.d.ts +28 -0
- package/dist/server/server/file-explorer/code-index.js +127 -0
- package/dist/server/server/file-explorer/file-search.d.ts +72 -0
- package/dist/server/server/file-explorer/file-search.js +297 -0
- package/dist/server/server/file-explorer/file-watcher.d.ts +40 -0
- package/dist/server/server/file-explorer/file-watcher.js +0 -0
- package/dist/server/server/file-explorer/gitignore.d.ts +39 -0
- package/dist/server/server/file-explorer/gitignore.js +191 -0
- package/dist/server/server/file-explorer/service.d.ts +103 -0
- package/dist/server/server/file-explorer/service.js +420 -0
- package/dist/server/server/file-upload/index.d.ts +27 -0
- package/dist/server/server/file-upload/index.js +158 -0
- package/dist/server/server/hostnames.d.ts +13 -0
- package/dist/server/server/hostnames.js +94 -0
- package/dist/server/server/json-utils.d.ts +11 -0
- package/dist/server/server/json-utils.js +45 -0
- package/dist/server/server/lifecycle-reasons.d.ts +4 -0
- package/dist/server/server/lifecycle-reasons.js +6 -0
- package/dist/server/server/logger.d.ts +29 -0
- package/dist/server/server/logger.js +125 -0
- package/dist/server/server/loop-service.d.ts +234 -0
- package/dist/server/server/loop-service.js +804 -0
- package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
- package/dist/server/server/managed-processes/managed-processes.js +326 -0
- package/dist/server/server/messages.d.ts +9 -0
- package/dist/server/server/messages.js +29 -0
- package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
- package/dist/server/server/migrations/backfill-workspace-id.migration.js +64 -0
- package/dist/server/server/otto-env.d.ts +13 -0
- package/dist/server/server/otto-env.js +53 -0
- package/dist/server/server/otto-home.d.ts +2 -0
- package/dist/server/server/otto-home.js +19 -0
- package/dist/server/server/otto-worktree-service.d.ts +52 -0
- package/dist/server/server/otto-worktree-service.js +294 -0
- package/dist/server/server/package-version.d.ts +19 -0
- package/dist/server/server/package-version.js +60 -0
- package/dist/server/server/pagination/cursor.d.ts +16 -0
- package/dist/server/server/pagination/cursor.js +62 -0
- package/dist/server/server/pagination/sortable-pager.d.ts +24 -0
- package/dist/server/server/pagination/sortable-pager.js +68 -0
- package/dist/server/server/pairing-offer.d.ts +18 -0
- package/dist/server/server/pairing-offer.js +47 -0
- package/dist/server/server/pairing-qr.d.ts +7 -0
- package/dist/server/server/pairing-qr.js +45 -0
- package/dist/server/server/path-utils.d.ts +5 -0
- package/dist/server/server/path-utils.js +34 -0
- package/dist/server/server/persisted-config.d.ts +417 -0
- package/dist/server/server/persisted-config.js +471 -0
- package/dist/server/server/persistence-hooks.d.ts +31 -0
- package/dist/server/server/persistence-hooks.js +93 -0
- package/dist/server/server/pid-lock.d.ts +32 -0
- package/dist/server/server/pid-lock.js +170 -0
- package/dist/server/server/preview/dev-server-manager.d.ts +108 -0
- package/dist/server/server/preview/dev-server-manager.js +426 -0
- package/dist/server/server/preview/launch-config.d.ts +37 -0
- package/dist/server/server/preview/launch-config.js +63 -0
- package/dist/server/server/preview/preview-tools.d.ts +30 -0
- package/dist/server/server/preview/preview-tools.js +249 -0
- package/dist/server/server/private-files.d.ts +6 -0
- package/dist/server/server/private-files.js +37 -0
- package/dist/server/server/process-diagnostics.d.ts +17 -0
- package/dist/server/server/process-diagnostics.js +22 -0
- package/dist/server/server/push/notifications.d.ts +9 -0
- package/dist/server/server/push/notifications.js +15 -0
- package/dist/server/server/push/push-service.d.ts +20 -0
- package/dist/server/server/push/push-service.js +66 -0
- package/dist/server/server/push/token-store.d.ts +18 -0
- package/dist/server/server/push/token-store.js +68 -0
- package/dist/server/server/relay-transport.d.ts +31 -0
- package/dist/server/server/relay-transport.js +514 -0
- package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
- package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
- package/dist/server/server/resolve-worktree-creation-intent.d.ts +32 -0
- package/dist/server/server/resolve-worktree-creation-intent.js +121 -0
- package/dist/server/server/schedule/cron.d.ts +4 -0
- package/dist/server/server/schedule/cron.js +81 -0
- package/dist/server/server/schedule/service.d.ts +89 -0
- package/dist/server/server/schedule/service.js +882 -0
- package/dist/server/server/schedule/store.d.ts +27 -0
- package/dist/server/server/schedule/store.js +190 -0
- package/dist/server/server/script-health-monitor.d.ts +39 -0
- package/dist/server/server/script-health-monitor.js +159 -0
- package/dist/server/server/script-proxy.d.ts +3 -0
- package/dist/server/server/script-proxy.js +2 -0
- package/dist/server/server/script-route-branch-handler.d.ts +10 -0
- package/dist/server/server/script-route-branch-handler.js +11 -0
- package/dist/server/server/script-status-projection.d.ts +36 -0
- package/dist/server/server/script-status-projection.js +200 -0
- package/dist/server/server/server-id.d.ts +17 -0
- package/dist/server/server/server-id.js +68 -0
- package/dist/server/server/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session/agent-config/agent-config-session.d.ts +59 -0
- package/dist/server/server/session/agent-config/agent-config-session.js +110 -0
- package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +65 -0
- package/dist/server/server/session/agent-updates/agent-updates-service.js +232 -0
- package/dist/server/server/session/artifact/artifact-session.d.ts +45 -0
- package/dist/server/server/session/artifact/artifact-session.js +293 -0
- package/dist/server/server/session/chat/chat-schedule-loop-session.d.ts +120 -0
- package/dist/server/server/session/chat/chat-schedule-loop-session.js +489 -0
- package/dist/server/server/session/checkout/checkout-session.d.ts +146 -0
- package/dist/server/server/session/checkout/checkout-session.js +1000 -0
- package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
- package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
- package/dist/server/server/session/daemon/daemon-self-update-session-controller.d.ts +32 -0
- package/dist/server/server/session/daemon/daemon-self-update-session-controller.js +88 -0
- package/dist/server/server/session/daemon/daemon-self-updater.d.ts +32 -0
- package/dist/server/server/session/daemon/daemon-self-updater.js +56 -0
- package/dist/server/server/session/daemon/daemon-session.d.ts +76 -0
- package/dist/server/server/session/daemon/daemon-session.js +148 -0
- package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
- package/dist/server/server/session/daemon/diagnostics.js +431 -0
- package/dist/server/server/session/daemon/install-origin.d.ts +7 -0
- package/dist/server/server/session/daemon/install-origin.js +64 -0
- package/dist/server/server/session/daemon/npm-global-cli.d.ts +29 -0
- package/dist/server/server/session/daemon/npm-global-cli.js +98 -0
- package/dist/server/server/session/files/workspace-files-session.d.ts +60 -0
- package/dist/server/server/session/files/workspace-files-session.js +518 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
- package/dist/server/server/session/project-config/project-config-session.d.ts +34 -0
- package/dist/server/server/session/project-config/project-config-session.js +125 -0
- package/dist/server/server/session/provider/provider-catalog-session.d.ts +74 -0
- package/dist/server/server/session/provider/provider-catalog-session.js +343 -0
- package/dist/server/server/session/voice/voice-session.d.ts +167 -0
- package/dist/server/server/session/voice/voice-session.js +912 -0
- package/dist/server/server/session/voice/voice-turn-controller.d.ts +38 -0
- package/dist/server/server/session/voice/voice-turn-controller.js +393 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
- package/dist/server/server/session.d.ts +450 -0
- package/dist/server/server/session.js +4763 -0
- package/dist/server/server/speech/audio.d.ts +10 -0
- package/dist/server/server/speech/audio.js +101 -0
- package/dist/server/server/speech/provider-resolver.d.ts +3 -0
- package/dist/server/server/speech/provider-resolver.js +7 -0
- package/dist/server/server/speech/providers/local/config.d.ts +30 -0
- package/dist/server/server/speech/providers/local/config.js +133 -0
- package/dist/server/server/speech/providers/local/models.d.ts +13 -0
- package/dist/server/server/speech/providers/local/models.js +21 -0
- package/dist/server/server/speech/providers/local/runtime.d.ts +31 -0
- package/dist/server/server/speech/providers/local/runtime.js +135 -0
- package/dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx +0 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +109 -0
- package/dist/server/server/speech/providers/local/sherpa/model-catalog.js +129 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts +16 -0
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +195 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts +49 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +73 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts +9 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +107 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.d.ts +28 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +137 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.d.ts +21 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +133 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.d.ts +18 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.d.ts +24 -0
- package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js +145 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.d.ts +19 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.js +49 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.d.ts +38 -0
- package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.js +176 -0
- package/dist/server/server/speech/providers/local/sherpa/tts-voices.d.ts +10 -0
- package/dist/server/server/speech/providers/local/sherpa/tts-voices.js +86 -0
- package/dist/server/server/speech/providers/local/worker-bytes.d.ts +4 -0
- package/dist/server/server/speech/providers/local/worker-bytes.js +9 -0
- package/dist/server/server/speech/providers/local/worker-client.d.ts +93 -0
- package/dist/server/server/speech/providers/local/worker-client.js +591 -0
- package/dist/server/server/speech/providers/local/worker-process.d.ts +2 -0
- package/dist/server/server/speech/providers/local/worker-process.js +270 -0
- package/dist/server/server/speech/providers/local/worker-protocol.d.ts +96 -0
- package/dist/server/server/speech/providers/local/worker-protocol.js +2 -0
- package/dist/server/server/speech/providers/openai/config.d.ts +19 -0
- package/dist/server/server/speech/providers/openai/config.js +160 -0
- package/dist/server/server/speech/providers/openai/runtime.d.ts +29 -0
- package/dist/server/server/speech/providers/openai/runtime.js +102 -0
- package/dist/server/server/speech/providers/openai/stt.d.ts +23 -0
- package/dist/server/server/speech/providers/openai/stt.js +209 -0
- package/dist/server/server/speech/providers/openai/tts.d.ts +20 -0
- package/dist/server/server/speech/providers/openai/tts.js +70 -0
- package/dist/server/server/speech/speech-config-resolver.d.ts +11 -0
- package/dist/server/server/speech/speech-config-resolver.js +132 -0
- package/dist/server/server/speech/speech-provider.d.ts +71 -0
- package/dist/server/server/speech/speech-provider.js +2 -0
- package/dist/server/server/speech/speech-runtime.d.ts +67 -0
- package/dist/server/server/speech/speech-runtime.js +615 -0
- package/dist/server/server/speech/speech-settings-options.d.ts +14 -0
- package/dist/server/server/speech/speech-settings-options.js +73 -0
- package/dist/server/server/speech/speech-types.d.ts +22 -0
- package/dist/server/server/speech/speech-types.js +8 -0
- package/dist/server/server/speech/turn-detection-provider.d.ts +23 -0
- package/dist/server/server/speech/turn-detection-provider.js +2 -0
- package/dist/server/server/utils/diff-highlighter.d.ts +57 -0
- package/dist/server/server/utils/diff-highlighter.js +279 -0
- package/dist/server/server/voice-config.d.ts +15 -0
- package/dist/server/server/voice-config.js +54 -0
- package/dist/server/server/voice-permission-policy.d.ts +4 -0
- package/dist/server/server/voice-permission-policy.js +13 -0
- package/dist/server/server/voice-types.d.ts +12 -0
- package/dist/server/server/voice-types.js +2 -0
- package/dist/server/server/web-ui.d.ts +10 -0
- package/dist/server/server/web-ui.js +205 -0
- package/dist/server/server/websocket/runtime-metrics.d.ts +94 -0
- package/dist/server/server/websocket/runtime-metrics.js +148 -0
- package/dist/server/server/websocket-server.d.ts +187 -0
- package/dist/server/server/websocket-server.js +1766 -0
- package/dist/server/server/workspace-archive-service.d.ts +65 -0
- package/dist/server/server/workspace-archive-service.js +229 -0
- package/dist/server/server/workspace-auto-name.d.ts +55 -0
- package/dist/server/server/workspace-auto-name.js +128 -0
- package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
- package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
- package/dist/server/server/workspace-directory.d.ts +93 -0
- package/dist/server/server/workspace-directory.js +422 -0
- package/dist/server/server/workspace-git-metadata.d.ts +24 -0
- package/dist/server/server/workspace-git-metadata.js +55 -0
- package/dist/server/server/workspace-git-service.d.ts +270 -0
- package/dist/server/server/workspace-git-service.js +1423 -0
- package/dist/server/server/workspace-reconciliation-service.d.ts +57 -0
- package/dist/server/server/workspace-reconciliation-service.js +278 -0
- package/dist/server/server/workspace-registry-bootstrap.d.ts +13 -0
- package/dist/server/server/workspace-registry-bootstrap.js +121 -0
- package/dist/server/server/workspace-registry-model.d.ts +55 -0
- package/dist/server/server/workspace-registry-model.js +190 -0
- package/dist/server/server/workspace-registry.d.ts +117 -0
- package/dist/server/server/workspace-registry.js +194 -0
- package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
- package/dist/server/server/workspace-script-runtime-store.js +78 -0
- package/dist/server/server/workspace-service-env.d.ts +18 -0
- package/dist/server/server/workspace-service-env.js +85 -0
- package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
- package/dist/server/server/workspace-service-port-registry.js +59 -0
- package/dist/server/server/worktree/commands.d.ts +55 -0
- package/dist/server/server/worktree/commands.js +107 -0
- package/dist/server/server/worktree-bootstrap.d.ts +76 -0
- package/dist/server/server/worktree-bootstrap.js +720 -0
- package/dist/server/server/worktree-branch-name-generator.d.ts +35 -0
- package/dist/server/server/worktree-branch-name-generator.js +91 -0
- package/dist/server/server/worktree-core.d.ts +31 -0
- package/dist/server/server/worktree-core.js +105 -0
- package/dist/server/server/worktree-errors.d.ts +12 -0
- package/dist/server/server/worktree-errors.js +28 -0
- package/dist/server/server/worktree-session.d.ts +152 -0
- package/dist/server/server/worktree-session.js +466 -0
- package/dist/server/services/git-hosting/bitbucket-cloud-service.d.ts +24 -0
- package/dist/server/services/git-hosting/bitbucket-cloud-service.js +754 -0
- package/dist/server/services/git-hosting/github-hosting-service.d.ts +4 -0
- package/dist/server/services/git-hosting/github-hosting-service.js +11 -0
- package/dist/server/services/git-hosting/hosting-http-client.d.ts +20 -0
- package/dist/server/services/git-hosting/hosting-http-client.js +131 -0
- package/dist/server/services/git-hosting/request-cache.d.ts +20 -0
- package/dist/server/services/git-hosting/request-cache.js +68 -0
- package/dist/server/services/git-hosting/resolver.d.ts +32 -0
- package/dist/server/services/git-hosting/resolver.js +152 -0
- package/dist/server/services/git-hosting/router.d.ts +4 -0
- package/dist/server/services/git-hosting/router.js +105 -0
- package/dist/server/services/git-hosting/status-poller.d.ts +39 -0
- package/dist/server/services/git-hosting/status-poller.js +149 -0
- package/dist/server/services/git-hosting/types.d.ts +53 -0
- package/dist/server/services/git-hosting/types.js +75 -0
- package/dist/server/services/github-service.d.ts +339 -0
- package/dist/server/services/github-service.js +2109 -0
- package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
- package/dist/server/services/quota-fetcher/manifest.js +52 -0
- package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
- package/dist/server/services/quota-fetcher/provider.js +2 -0
- package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
- package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
- package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
- package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
- package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
- package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
- package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
- package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
- package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
- package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
- package/dist/server/services/quota-fetcher/providers/minimax.d.ts +29 -0
- package/dist/server/services/quota-fetcher/providers/minimax.js +227 -0
- package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
- package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
- package/dist/server/services/quota-fetcher/service.d.ts +28 -0
- package/dist/server/services/quota-fetcher/service.js +58 -0
- package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
- package/dist/server/services/quota-fetcher/usage.js +49 -0
- package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
- package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
- package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
- package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
- package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
- package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
- package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
- package/dist/server/terminal/shell-integration/zsh/otto-integration.zsh +32 -0
- package/dist/server/terminal/terminal-capture.d.ts +12 -0
- package/dist/server/terminal/terminal-capture.js +43 -0
- package/dist/server/terminal/terminal-manager-factory.d.ts +6 -0
- package/dist/server/terminal/terminal-manager-factory.js +5 -0
- package/dist/server/terminal/terminal-manager.d.ts +78 -0
- package/dist/server/terminal/terminal-manager.js +319 -0
- package/dist/server/terminal/terminal-output-coalescer.d.ts +34 -0
- package/dist/server/terminal/terminal-output-coalescer.js +73 -0
- package/dist/server/terminal/terminal-restore.d.ts +18 -0
- package/dist/server/terminal/terminal-restore.js +52 -0
- package/dist/server/terminal/terminal-session-controller.d.ts +85 -0
- package/dist/server/terminal/terminal-session-controller.js +819 -0
- package/dist/server/terminal/terminal-ts-loader.mjs +20 -0
- package/dist/server/terminal/terminal-worker-process.d.ts +2 -0
- package/dist/server/terminal/terminal-worker-process.js +304 -0
- package/dist/server/terminal/terminal-worker-protocol.d.ts +119 -0
- package/dist/server/terminal/terminal-worker-protocol.js +2 -0
- package/dist/server/terminal/terminal.d.ts +151 -0
- package/dist/server/terminal/terminal.js +1155 -0
- package/dist/server/terminal/worker-terminal-manager.d.ts +20 -0
- package/dist/server/terminal/worker-terminal-manager.js +688 -0
- package/dist/server/utils/build-metadata-prompt.d.ts +19 -0
- package/dist/server/utils/build-metadata-prompt.js +29 -0
- package/dist/server/utils/checkout-git.d.ts +253 -0
- package/dist/server/utils/checkout-git.js +2241 -0
- package/dist/server/utils/directory-suggestions.d.ts +26 -0
- package/dist/server/utils/directory-suggestions.js +830 -0
- package/dist/server/utils/git-rev-parse-path.d.ts +3 -0
- package/dist/server/utils/git-rev-parse-path.js +21 -0
- package/dist/server/utils/github-remote.d.ts +9 -0
- package/dist/server/utils/github-remote.js +62 -0
- package/dist/server/utils/otto-config-file.d.ts +30 -0
- package/dist/server/utils/otto-config-file.js +90 -0
- package/dist/server/utils/path.d.ts +18 -0
- package/dist/server/utils/path.js +123 -0
- package/dist/server/utils/project-icon.d.ts +39 -0
- package/dist/server/utils/project-icon.js +364 -0
- package/dist/server/utils/promise-timeout.d.ts +9 -0
- package/dist/server/utils/promise-timeout.js +25 -0
- package/dist/server/utils/run-git-command.d.ts +37 -0
- package/dist/server/utils/run-git-command.js +232 -0
- package/dist/server/utils/spawn.d.ts +25 -0
- package/dist/server/utils/spawn.js +59 -0
- package/dist/server/utils/string-command-shell.d.ts +13 -0
- package/dist/server/utils/string-command-shell.js +37 -0
- package/dist/server/utils/tree-kill.d.ts +18 -0
- package/dist/server/utils/tree-kill.js +77 -0
- package/dist/server/utils/windows-command.d.ts +15 -0
- package/dist/server/utils/windows-command.js +41 -0
- package/dist/server/utils/worktree-metadata.d.ts +39 -0
- package/dist/server/utils/worktree-metadata.js +174 -0
- package/dist/server/utils/worktree.d.ts +205 -0
- package/dist/server/utils/worktree.js +1120 -0
- package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css +1 -0
- package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.br +0 -0
- package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js +1 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js +1 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js +3199 -0
- package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js +1 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js +3 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js.gz +0 -0
- package/dist/server/web-ui/apple-touch-icon.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@expo-google-fonts/inter/400Regular/Inter_400Regular.51b6ad87261f18b6433ec52871ddfabc.ttf +0 -0
- package/dist/server/web-ui/assets/__node_modules/@expo-google-fonts/jetbrains-mono/400Regular/JetBrainsMono_400Regular.a0147b5ab9e4946e81879aef45313def.ttf +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
- package/dist/server/web-ui/assets/__node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/antigravity.6e91a685c33435e0b466a56db86cf141.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/clion.baa1f4eb572e962a6117bdbee95749e4.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/cursor.c31d6bce4fe9aadc3fe59962f4c4fcf3.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/finder.7f68fc2c475621a672e1be09309d5567.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/goland.0796ab2b9be2637fd1cdcde891fad3ca.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/intellij-idea.33bc9ff9c0815d8ad43f618c8c244401.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/phpstorm.c60b33bc7a8309143d232e14b76554c5.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/pycharm.88f25ac484989c45c25323e0adccfa9f.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/rider.1a1091dc3a0ede0461f0aa38a54f080a.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/rubymine.0e5b66fd7d43f8cbac0f05bba97c9a21.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/rustrover.3590ef05482369f41d3484c90272570d.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/vscode.832bdb4c685d930f1c864c793703600b.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/webstorm.aa5dc2cd8c20cc0a155c4c5c5ab3c5f5.png +0 -0
- package/dist/server/web-ui/assets/assets/images/editor-apps/zed.f3a670b7f9aa226da4fe53fb86f1abbd.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-dark-attention.3b2c331d11122d8f743c73a5878593be.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-dark-running.6077c2ab71021b31c82a42e0b42ad837.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-dark.12984efe6d435a13794a74cd2f858dcd.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-light-attention.3b2c331d11122d8f743c73a5878593be.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-light-running.6077c2ab71021b31c82a42e0b42ad837.png +0 -0
- package/dist/server/web-ui/assets/assets/images/favicon-light.12984efe6d435a13794a74cd2f858dcd.png +0 -0
- package/dist/server/web-ui/assets/assets/images/notification-icon.2394c3b427c0f5ee2bb84864d8fb0f45.png +0 -0
- package/dist/server/web-ui/favicon.ico +0 -0
- package/dist/server/web-ui/index.html +147 -0
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/dist/server/web-ui/manifest.json +27 -0
- package/dist/server/web-ui/manifest.json.br +0 -0
- package/dist/server/web-ui/manifest.json.gz +0 -0
- package/dist/server/web-ui/metadata.json +1 -0
- package/dist/server/web-ui/metadata.json.br +1 -0
- package/dist/server/web-ui/metadata.json.gz +0 -0
- package/dist/server/web-ui/pwa-icon-192.png +0 -0
- package/dist/server/web-ui/pwa-icon-512.png +0 -0
- package/dist/server/web-ui/robots.txt +2 -0
- package/dist/src/executable-resolution/executable-resolution.js +118 -0
- package/dist/src/executable-resolution/windows.js +65 -0
- package/dist/src/server/agent/provider-launch-config.js +167 -0
- package/dist/src/server/otto-env.js +53 -0
- package/dist/src/server/otto-home.js +19 -0
- package/dist/src/server/persisted-config.js +471 -0
- package/dist/src/server/pid-lock.js +170 -0
- package/dist/src/server/private-files.js +37 -0
- package/dist/src/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
- package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
- package/dist/src/terminal/shell-integration/zsh/otto-integration.zsh +32 -0
- package/dist/src/utils/spawn.js +59 -0
- package/dist/src/utils/windows-command.js +41 -0
- package/package.json +118 -0
|
@@ -0,0 +1,2536 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { buildCompatToolPreviewDetail, buildOpenAIToolsPayload, COMPAT_TOOL_SPECS, executeCompatTool, findCompatToolSpec, isPathInsideWorkspace, } from "./openai-compat-tools.js";
|
|
4
|
+
import { ottoToolGroupForName } from "@otto-code/protocol/provider-config";
|
|
5
|
+
import { buildOpenAICompatFeatures, normalizeOpenAICompatAutoCompact, normalizeOpenAICompatReasoningEffort, OPENAI_COMPAT_AUTO_COMPACT_FALLBACK, OPENAI_COMPAT_AUTO_COMPACT_VALUES, OPENAI_COMPAT_DEFAULT_THINKING_OPTION_ID, OPENAI_COMPAT_THINKING_OPTIONS, } from "./openai-compat-feature-definitions.js";
|
|
6
|
+
import { OpenAICompatMcpManager } from "./openai-compat-mcp.js";
|
|
7
|
+
import { ottoToolPermissionKind } from "./openai-compat-otto-tool-permissions.js";
|
|
8
|
+
import { stripInternalOttoMcpServer } from "../runtime-mcp-config.js";
|
|
9
|
+
/**
|
|
10
|
+
* Native provider for OpenAI-compatible HTTP endpoints (LM Studio, Ollama,
|
|
11
|
+
* vLLM, llama.cpp server, gateways). The daemon talks to the endpoint
|
|
12
|
+
* directly — model discovery via GET {base}/models, streaming chat via
|
|
13
|
+
* POST {base}/chat/completions. No external agent binary is involved, so
|
|
14
|
+
* availability means "the server is reachable", not "a CLI is installed".
|
|
15
|
+
*
|
|
16
|
+
* Tool support: the daemon is the tool runtime. Function-calling models get a
|
|
17
|
+
* built-in coding toolset (read/list/grep/write/edit/shell) executed in the
|
|
18
|
+
* agent's cwd, with permission gating per mode.
|
|
19
|
+
*/
|
|
20
|
+
export const OPENAI_COMPAT_EXTENDS = "openai-compatible";
|
|
21
|
+
const DEFAULT_CATALOG_TIMEOUT_MS = 10000;
|
|
22
|
+
/** Upper bound on model→tool→model rounds within a single turn. */
|
|
23
|
+
const MAX_TOOL_ROUNDS = 50;
|
|
24
|
+
/**
|
|
25
|
+
* Per-tool-result budget for the compaction payload. Large results keep their
|
|
26
|
+
* head and tail — truncating harder starves the summarizer of the material
|
|
27
|
+
* (file contents, diffs, command output) the summary is supposed to preserve.
|
|
28
|
+
*/
|
|
29
|
+
const TOOL_RESULT_HEAD_CHARS = 3000;
|
|
30
|
+
const TOOL_RESULT_TAIL_CHARS = 1000;
|
|
31
|
+
/**
|
|
32
|
+
* Compaction keeps the most recent slice of the conversation verbatim and only
|
|
33
|
+
* summarizes the older history before it. Summarization is lossy, so we confine
|
|
34
|
+
* it to the distant past where the loss is cheap; recent turns (the files just
|
|
35
|
+
* read, the diff just applied, the error being debugged) stay intact.
|
|
36
|
+
* Default only — tunable per provider via `compaction.keepRecentTokens`.
|
|
37
|
+
*/
|
|
38
|
+
const COMPACTION_KEEP_RECENT_TOKENS = 20000;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the provider-level auto-compact default that applies when an agent
|
|
41
|
+
* has no explicit `auto_compact` feature value. `autoCompact: false` wins over
|
|
42
|
+
* any configured threshold.
|
|
43
|
+
*/
|
|
44
|
+
function resolveAutoCompactDefault(compaction) {
|
|
45
|
+
if (compaction?.autoCompact === false) {
|
|
46
|
+
return "off";
|
|
47
|
+
}
|
|
48
|
+
if (typeof compaction?.thresholdPercent === "number") {
|
|
49
|
+
const candidate = String(compaction.thresholdPercent);
|
|
50
|
+
if (OPENAI_COMPAT_AUTO_COMPACT_VALUES.includes(candidate)) {
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return OPENAI_COMPAT_AUTO_COMPACT_FALLBACK;
|
|
55
|
+
}
|
|
56
|
+
function resolveKeepRecentTokens(compaction) {
|
|
57
|
+
return typeof compaction?.keepRecentTokens === "number" && compaction.keepRecentTokens > 0
|
|
58
|
+
? compaction.keepRecentTokens
|
|
59
|
+
: COMPACTION_KEEP_RECENT_TOKENS;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pre-summarization pruning (zero-LLM) reclaims context before we spend a model
|
|
63
|
+
* call: uneventful tool results are elided and oversized older tool outputs are
|
|
64
|
+
* truncated, while the newest tool outputs are protected so live work is never
|
|
65
|
+
* trimmed out from under the model.
|
|
66
|
+
*/
|
|
67
|
+
const PRUNE_PROTECT_RECENT_TOOL_TOKENS = 12000;
|
|
68
|
+
const PRUNE_TOOL_RESULT_MIN_CHARS = 2000;
|
|
69
|
+
const PRUNE_TOOL_RESULT_HEAD_CHARS = 800;
|
|
70
|
+
const PRUNE_TOOL_RESULT_TAIL_CHARS = 400;
|
|
71
|
+
const UNEVENTFUL_RESULT_PLACEHOLDER = "[Uneventful result elided]";
|
|
72
|
+
/**
|
|
73
|
+
* True when a tool result carries no signal worth keeping in context — empty
|
|
74
|
+
* output, zero-match searches, or a bare timeout/no-op acknowledgement. These
|
|
75
|
+
* are elided wholesale during pruning. Kept conservative on purpose: anything
|
|
76
|
+
* ambiguous is left intact for the summarizer to judge.
|
|
77
|
+
*/
|
|
78
|
+
export function isUneventfulToolResult(content) {
|
|
79
|
+
const trimmed = content.trim();
|
|
80
|
+
if (trimmed.length === 0) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (trimmed === UNEVENTFUL_RESULT_PLACEHOLDER) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
/^no matches found\.?$/iu,
|
|
88
|
+
/^no results? found\.?$/iu,
|
|
89
|
+
/^found 0 (?:matches|results|files)\b/iu,
|
|
90
|
+
/^0 results?\b/iu,
|
|
91
|
+
/^no files? found\.?$/iu,
|
|
92
|
+
/^\(?no output\)?\.?$/iu,
|
|
93
|
+
/^command (?:completed|finished|exited) with no output\.?$/iu,
|
|
94
|
+
/^no changes\b/iu,
|
|
95
|
+
].some((pattern) => pattern.test(trimmed));
|
|
96
|
+
}
|
|
97
|
+
/** Built-in slash command available for every OpenAI-compatible session. */
|
|
98
|
+
const COMPACT_COMMAND = {
|
|
99
|
+
name: "compact",
|
|
100
|
+
description: "Compress the conversation history to free up context space",
|
|
101
|
+
argumentHint: "[instruction]",
|
|
102
|
+
kind: "command",
|
|
103
|
+
};
|
|
104
|
+
const CAPABILITIES = {
|
|
105
|
+
supportsStreaming: true,
|
|
106
|
+
supportsSessionPersistence: true,
|
|
107
|
+
supportsDynamicModes: false,
|
|
108
|
+
// The daemon itself is the MCP client for this provider: configured servers
|
|
109
|
+
// (provider config merged with per-agent config) are connected per session
|
|
110
|
+
// and their tools exposed to the model. See OpenAICompatMcpManager.
|
|
111
|
+
supportsMcpServers: true,
|
|
112
|
+
// The daemon owns this provider's tool loop, so we inject Otto's tool catalog
|
|
113
|
+
// (browser_*, preview_*, agent management, …) directly rather than via an MCP
|
|
114
|
+
// client the local model's runtime doesn't have. See ottoTools handling below.
|
|
115
|
+
supportsNativeOttoTools: true,
|
|
116
|
+
supportsReasoningStream: true,
|
|
117
|
+
supportsToolInvocations: true,
|
|
118
|
+
supportsRewindConversation: true,
|
|
119
|
+
supportsRewindFiles: false,
|
|
120
|
+
supportsRewindBoth: false,
|
|
121
|
+
};
|
|
122
|
+
// Icons/colorTiers live here (not in AGENT_PROVIDER_DEFINITIONS) because
|
|
123
|
+
// openai-compatible providers are registered dynamically, so the registry's
|
|
124
|
+
// definition-based mode decoration has nothing to merge from.
|
|
125
|
+
export const OPENAI_COMPAT_MODES = [
|
|
126
|
+
{
|
|
127
|
+
id: "default",
|
|
128
|
+
label: "Always Ask",
|
|
129
|
+
description: "Prompts for permission before running commands or editing files",
|
|
130
|
+
icon: "ShieldQuestionMark",
|
|
131
|
+
colorTier: "neutral",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "acceptEdits",
|
|
135
|
+
label: "Accept File Edits",
|
|
136
|
+
description: "Automatically approves file edits inside the workspace; still asks before running commands",
|
|
137
|
+
icon: "ShieldPerson",
|
|
138
|
+
colorTier: "safe",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "plan",
|
|
142
|
+
label: "Read Only",
|
|
143
|
+
description: "Only read tools are available — no edits or commands; web fetches still ask",
|
|
144
|
+
icon: "ShieldToggle",
|
|
145
|
+
colorTier: "planning",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "bypassPermissions",
|
|
149
|
+
label: "Bypass",
|
|
150
|
+
description: "Skip all permission prompts (use with caution)",
|
|
151
|
+
icon: "PrivacyTip",
|
|
152
|
+
colorTier: "dangerous",
|
|
153
|
+
isUnattended: true,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const VALID_MODE_IDS = new Set(OPENAI_COMPAT_MODES.map((mode) => mode.id));
|
|
157
|
+
/** Permission-prompt description per builtin tool kind ("read" never prompts). */
|
|
158
|
+
const COMPAT_TOOL_PROMPT_DESCRIPTIONS = {
|
|
159
|
+
read: "Wants to read from the workspace",
|
|
160
|
+
edit: "Wants to modify a file",
|
|
161
|
+
execute: "Wants to run a shell command",
|
|
162
|
+
network: "Wants to fetch content from the web",
|
|
163
|
+
};
|
|
164
|
+
/** Mirror of the opencode provider's parser: `/name rest` → command + args. */
|
|
165
|
+
function parseSlashCommandInput(text) {
|
|
166
|
+
const trimmed = text.trim();
|
|
167
|
+
if (!trimmed.startsWith("/") || trimmed.length <= 1) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const withoutPrefix = trimmed.slice(1);
|
|
171
|
+
const firstWhitespaceIdx = withoutPrefix.search(/\s/u);
|
|
172
|
+
const commandName = firstWhitespaceIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, firstWhitespaceIdx);
|
|
173
|
+
if (!commandName || commandName.includes("/")) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const rawArgs = firstWhitespaceIdx === -1 ? "" : withoutPrefix.slice(firstWhitespaceIdx + 1).trim();
|
|
177
|
+
return { commandName, args: rawArgs.length > 0 ? rawArgs : null };
|
|
178
|
+
}
|
|
179
|
+
function toWireMessage(message) {
|
|
180
|
+
if (message.role === "system" || message.role === "user") {
|
|
181
|
+
return { role: message.role, content: message.content };
|
|
182
|
+
}
|
|
183
|
+
if (message.role === "assistant") {
|
|
184
|
+
return {
|
|
185
|
+
role: message.role,
|
|
186
|
+
content: message.content,
|
|
187
|
+
...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return message;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Fold a reconstructed tool result's raw text into whichever field the
|
|
194
|
+
* preview detail's type uses for it. Only the flat result text survives
|
|
195
|
+
* persistence — structured metadata that a live run attaches (numMatches,
|
|
196
|
+
* webResults, exitCode, ...) isn't recoverable from a bare
|
|
197
|
+
* { name, arguments, result-text } tuple, so callers only get the text back.
|
|
198
|
+
*/
|
|
199
|
+
function attachReconstructedOutput(detail, outputText) {
|
|
200
|
+
switch (detail.type) {
|
|
201
|
+
case "shell":
|
|
202
|
+
return { ...detail, output: outputText };
|
|
203
|
+
case "read":
|
|
204
|
+
return { ...detail, content: outputText };
|
|
205
|
+
case "search":
|
|
206
|
+
return { ...detail, content: outputText };
|
|
207
|
+
case "fetch":
|
|
208
|
+
return { ...detail, result: outputText };
|
|
209
|
+
case "plain_text":
|
|
210
|
+
return { ...detail, text: outputText };
|
|
211
|
+
case "unknown":
|
|
212
|
+
return { ...detail, output: outputText };
|
|
213
|
+
default:
|
|
214
|
+
return detail;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Reconstruct a tool_call timeline item from a persisted assistant tool call
|
|
219
|
+
* plus its matching "tool" result message. Used to replay history on resume,
|
|
220
|
+
* where the only surviving record is the raw chat-format conversation.
|
|
221
|
+
*/
|
|
222
|
+
function buildReconstructedToolCallItem(call, resultMessage, cwd) {
|
|
223
|
+
let args = {};
|
|
224
|
+
try {
|
|
225
|
+
const parsed = call.function.arguments ? JSON.parse(call.function.arguments) : {};
|
|
226
|
+
if (parsed && typeof parsed === "object") {
|
|
227
|
+
args = parsed;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Malformed persisted arguments shouldn't block reload — fall back to {}.
|
|
232
|
+
}
|
|
233
|
+
const baseDetail = buildCompatToolPreviewDetail(call.function.name, args, cwd);
|
|
234
|
+
return {
|
|
235
|
+
type: "tool_call",
|
|
236
|
+
callId: call.id,
|
|
237
|
+
name: call.function.name,
|
|
238
|
+
detail: resultMessage
|
|
239
|
+
? attachReconstructedOutput(baseDetail, resultMessage.content)
|
|
240
|
+
: baseDetail,
|
|
241
|
+
// No matching "tool" result means the turn was interrupted before the
|
|
242
|
+
// result came back (e.g. the app closed mid-call).
|
|
243
|
+
status: resultMessage ? "completed" : "canceled",
|
|
244
|
+
error: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export function normalizeOpenAICompatBaseUrl(value) {
|
|
248
|
+
const withoutTrailingSlashes = value.trim().replace(/\/+$/u, "");
|
|
249
|
+
if (withoutTrailingSlashes.endsWith("/v1")) {
|
|
250
|
+
return withoutTrailingSlashes;
|
|
251
|
+
}
|
|
252
|
+
return `${withoutTrailingSlashes}/v1`;
|
|
253
|
+
}
|
|
254
|
+
function resolveEndpoint(env, label) {
|
|
255
|
+
const rawBaseUrl = env?.["OPENAI_BASE_URL"]?.trim();
|
|
256
|
+
if (!rawBaseUrl) {
|
|
257
|
+
throw new Error(`${label} has no server URL configured. Set OPENAI_BASE_URL in the provider settings.`);
|
|
258
|
+
}
|
|
259
|
+
const apiKey = env?.["OPENAI_API_KEY"]?.trim() || null;
|
|
260
|
+
return { baseUrl: normalizeOpenAICompatBaseUrl(rawBaseUrl), apiKey };
|
|
261
|
+
}
|
|
262
|
+
function buildHeaders(endpoint) {
|
|
263
|
+
const headers = { "Content-Type": "application/json" };
|
|
264
|
+
if (endpoint.apiKey) {
|
|
265
|
+
headers["Authorization"] = `Bearer ${endpoint.apiKey}`;
|
|
266
|
+
}
|
|
267
|
+
return headers;
|
|
268
|
+
}
|
|
269
|
+
function unreachableError(label, endpoint, cause) {
|
|
270
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
271
|
+
return new Error(`Cannot reach ${label} at ${endpoint.baseUrl} (${detail}). Make sure the server is running and the URL is correct.`);
|
|
272
|
+
}
|
|
273
|
+
function promptToText(prompt) {
|
|
274
|
+
if (typeof prompt === "string") {
|
|
275
|
+
return prompt;
|
|
276
|
+
}
|
|
277
|
+
return prompt
|
|
278
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
279
|
+
.join("\n")
|
|
280
|
+
.trim();
|
|
281
|
+
}
|
|
282
|
+
async function* readLines(body) {
|
|
283
|
+
const decoder = new TextDecoder();
|
|
284
|
+
let buffer = "";
|
|
285
|
+
for await (const chunk of body) {
|
|
286
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
287
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
288
|
+
while (newlineIndex !== -1) {
|
|
289
|
+
yield buffer.slice(0, newlineIndex).replace(/\r$/u, "");
|
|
290
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
291
|
+
newlineIndex = buffer.indexOf("\n");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const rest = buffer.trim();
|
|
295
|
+
if (rest) {
|
|
296
|
+
yield rest;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function parseToolCallDeltas(deltaRecord) {
|
|
300
|
+
const raw = deltaRecord.tool_calls;
|
|
301
|
+
if (!Array.isArray(raw)) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
return raw.flatMap((entry, position) => {
|
|
305
|
+
if (!entry || typeof entry !== "object") {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const record = entry;
|
|
309
|
+
const fn = record.function && typeof record.function === "object"
|
|
310
|
+
? record.function
|
|
311
|
+
: {};
|
|
312
|
+
return [
|
|
313
|
+
{
|
|
314
|
+
index: typeof record.index === "number" ? record.index : position,
|
|
315
|
+
...(typeof record.id === "string" && record.id ? { id: record.id } : {}),
|
|
316
|
+
...(typeof fn.name === "string" && fn.name ? { name: fn.name } : {}),
|
|
317
|
+
...(typeof fn.arguments === "string" ? { argumentsChunk: fn.arguments } : {}),
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function parseStreamChunk(json) {
|
|
323
|
+
const result = {
|
|
324
|
+
content: null,
|
|
325
|
+
reasoning: null,
|
|
326
|
+
toolCalls: [],
|
|
327
|
+
finishReason: null,
|
|
328
|
+
usage: null,
|
|
329
|
+
};
|
|
330
|
+
if (!json || typeof json !== "object") {
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
const chunk = json;
|
|
334
|
+
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
|
|
335
|
+
const firstChoice = choices[0];
|
|
336
|
+
if (firstChoice && typeof firstChoice === "object") {
|
|
337
|
+
const choiceRecord = firstChoice;
|
|
338
|
+
if (typeof choiceRecord.finish_reason === "string") {
|
|
339
|
+
result.finishReason = choiceRecord.finish_reason;
|
|
340
|
+
}
|
|
341
|
+
const delta = choiceRecord.delta;
|
|
342
|
+
if (delta && typeof delta === "object") {
|
|
343
|
+
const deltaRecord = delta;
|
|
344
|
+
if (typeof deltaRecord.content === "string" && deltaRecord.content.length > 0) {
|
|
345
|
+
result.content = deltaRecord.content;
|
|
346
|
+
}
|
|
347
|
+
const reasoning = deltaRecord.reasoning_content ?? deltaRecord.reasoning;
|
|
348
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
349
|
+
result.reasoning = reasoning;
|
|
350
|
+
}
|
|
351
|
+
result.toolCalls = parseToolCallDeltas(deltaRecord);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const usage = chunk.usage;
|
|
355
|
+
if (usage && typeof usage === "object") {
|
|
356
|
+
const usageRecord = usage;
|
|
357
|
+
result.usage = {
|
|
358
|
+
...(typeof usageRecord.prompt_tokens === "number"
|
|
359
|
+
? { inputTokens: usageRecord.prompt_tokens }
|
|
360
|
+
: {}),
|
|
361
|
+
...(typeof usageRecord.completion_tokens === "number"
|
|
362
|
+
? { outputTokens: usageRecord.completion_tokens }
|
|
363
|
+
: {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
function parseModelList(json) {
|
|
369
|
+
if (!json || typeof json !== "object") {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const data = json.data;
|
|
373
|
+
if (!Array.isArray(data)) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
return data.flatMap((entry) => {
|
|
377
|
+
if (entry && typeof entry === "object" && typeof entry.id === "string") {
|
|
378
|
+
return [entry.id];
|
|
379
|
+
}
|
|
380
|
+
return [];
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Optional per-model context-length fields seen across OpenAI-compatible
|
|
385
|
+
* servers. LM Studio's native listing reports `loaded_context_length` (the
|
|
386
|
+
* window the loaded instance actually runs with) ahead of the model's
|
|
387
|
+
* theoretical `max_context_length`; vLLM extends /v1/models with
|
|
388
|
+
* `max_model_len`; other gateways use `context_length`/`context_window`.
|
|
389
|
+
*/
|
|
390
|
+
const MODEL_CONTEXT_LENGTH_FIELDS = [
|
|
391
|
+
"loaded_context_length",
|
|
392
|
+
"max_context_length",
|
|
393
|
+
"max_model_len",
|
|
394
|
+
"context_length",
|
|
395
|
+
"context_window",
|
|
396
|
+
];
|
|
397
|
+
function parseModelContextLength(entry) {
|
|
398
|
+
for (const field of MODEL_CONTEXT_LENGTH_FIELDS) {
|
|
399
|
+
const value = entry[field];
|
|
400
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function parseModelContextLengths(json) {
|
|
407
|
+
const lengths = new Map();
|
|
408
|
+
if (!json || typeof json !== "object") {
|
|
409
|
+
return lengths;
|
|
410
|
+
}
|
|
411
|
+
const data = json.data;
|
|
412
|
+
if (!Array.isArray(data)) {
|
|
413
|
+
return lengths;
|
|
414
|
+
}
|
|
415
|
+
for (const entry of data) {
|
|
416
|
+
if (!entry || typeof entry !== "object")
|
|
417
|
+
continue;
|
|
418
|
+
const record = entry;
|
|
419
|
+
if (typeof record.id !== "string")
|
|
420
|
+
continue;
|
|
421
|
+
const contextLength = parseModelContextLength(record);
|
|
422
|
+
if (contextLength !== null) {
|
|
423
|
+
lengths.set(record.id, contextLength);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return lengths;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Rough token estimate for the breakdown split (~4 chars/token). The split is
|
|
430
|
+
* proportional only; the total is corrected to the server-measured
|
|
431
|
+
* prompt_tokens when one has been observed.
|
|
432
|
+
*/
|
|
433
|
+
function estimateTokens(text) {
|
|
434
|
+
return Math.ceil(text.length / 4);
|
|
435
|
+
}
|
|
436
|
+
function parseToolCallPayloads(raw) {
|
|
437
|
+
if (!Array.isArray(raw)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const calls = raw.flatMap((entry) => {
|
|
441
|
+
if (!entry || typeof entry !== "object") {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
const record = entry;
|
|
445
|
+
const fn = record.function && typeof record.function === "object"
|
|
446
|
+
? record.function
|
|
447
|
+
: null;
|
|
448
|
+
if (typeof record.id !== "string" ||
|
|
449
|
+
!fn ||
|
|
450
|
+
typeof fn.name !== "string" ||
|
|
451
|
+
typeof fn.arguments !== "string") {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
return [
|
|
455
|
+
{ id: record.id, type: "function", function: { name: fn.name, arguments: fn.arguments } },
|
|
456
|
+
];
|
|
457
|
+
});
|
|
458
|
+
return calls.length > 0 ? calls : null;
|
|
459
|
+
}
|
|
460
|
+
function restoreMessages(metadata) {
|
|
461
|
+
const raw = metadata?.["messages"];
|
|
462
|
+
if (!Array.isArray(raw)) {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
const parsed = raw.flatMap((entry) => {
|
|
466
|
+
if (!entry || typeof entry !== "object") {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const record = entry;
|
|
470
|
+
const { role, content } = record;
|
|
471
|
+
if (typeof content !== "string") {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
if (role === "system" || role === "user") {
|
|
475
|
+
const messageId = typeof record.messageId === "string" ? record.messageId : undefined;
|
|
476
|
+
const isCompactionSummary = record.isCompactionSummary === true;
|
|
477
|
+
return [
|
|
478
|
+
{
|
|
479
|
+
role,
|
|
480
|
+
content,
|
|
481
|
+
...(messageId ? { messageId } : {}),
|
|
482
|
+
...(isCompactionSummary ? { isCompactionSummary: true } : {}),
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
if (role === "assistant") {
|
|
487
|
+
const toolCalls = parseToolCallPayloads(record.tool_calls);
|
|
488
|
+
const reasoning = typeof record.reasoning === "string" ? record.reasoning : undefined;
|
|
489
|
+
return [
|
|
490
|
+
{
|
|
491
|
+
role,
|
|
492
|
+
content,
|
|
493
|
+
...(reasoning ? { reasoning } : {}),
|
|
494
|
+
...(toolCalls ? { tool_calls: toolCalls } : {}),
|
|
495
|
+
},
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
if (role === "tool" && typeof record.tool_call_id === "string") {
|
|
499
|
+
return [{ role, content, tool_call_id: record.tool_call_id }];
|
|
500
|
+
}
|
|
501
|
+
return [];
|
|
502
|
+
});
|
|
503
|
+
return sanitizeRestoredMessages(parsed);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Persistence trims to the last N messages, which can orphan tool messages
|
|
507
|
+
* from their assistant tool_calls (or vice versa). OpenAI-compatible servers
|
|
508
|
+
* reject such conversations, so repair the boundary: drop orphan tool
|
|
509
|
+
* results and strip tool_calls that have no results following them.
|
|
510
|
+
*/
|
|
511
|
+
function sanitizeRestoredMessages(messages) {
|
|
512
|
+
const result = [];
|
|
513
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
514
|
+
const message = messages[index];
|
|
515
|
+
if (message.role === "tool") {
|
|
516
|
+
continue; // reached only when not consumed by an assistant tool_calls below
|
|
517
|
+
}
|
|
518
|
+
if (message.role !== "assistant" || !message.tool_calls) {
|
|
519
|
+
result.push(message);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const callIds = new Set(message.tool_calls.map((call) => call.id));
|
|
523
|
+
const results = [];
|
|
524
|
+
let cursor = index + 1;
|
|
525
|
+
while (cursor < messages.length) {
|
|
526
|
+
const candidate = messages[cursor];
|
|
527
|
+
if (candidate.role !== "tool" || !callIds.has(candidate.tool_call_id)) {
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
results.push(candidate);
|
|
531
|
+
cursor += 1;
|
|
532
|
+
}
|
|
533
|
+
if (results.length === message.tool_calls.length) {
|
|
534
|
+
result.push(message, ...results);
|
|
535
|
+
index = cursor - 1;
|
|
536
|
+
}
|
|
537
|
+
else if (message.content || message.reasoning) {
|
|
538
|
+
result.push({
|
|
539
|
+
role: "assistant",
|
|
540
|
+
content: message.content,
|
|
541
|
+
...(message.reasoning ? { reasoning: message.reasoning } : {}),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
export class OpenAICompatAgentClient {
|
|
548
|
+
constructor(options) {
|
|
549
|
+
this.capabilities = CAPABILITIES;
|
|
550
|
+
this.provider = options.providerId;
|
|
551
|
+
this.logger = options.logger;
|
|
552
|
+
this.label = options.label;
|
|
553
|
+
this.env = options.env;
|
|
554
|
+
this.ottoToolGroups = options.ottoToolGroups ?? null;
|
|
555
|
+
this.mcpServers = options.mcpServers ?? null;
|
|
556
|
+
this.mcpToolPermissions = options.mcpToolPermissions ?? "always-ask";
|
|
557
|
+
this.compaction = options.compaction ?? null;
|
|
558
|
+
this.managedProcesses = options.managedProcesses ?? null;
|
|
559
|
+
}
|
|
560
|
+
async isAvailable() {
|
|
561
|
+
// Nothing to install — availability is endpoint reachability, surfaced
|
|
562
|
+
// through fetchCatalog so the UI shows a configuration error, not
|
|
563
|
+
// "not installed".
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
async fetchCatalog(options) {
|
|
567
|
+
const endpoint = resolveEndpoint(this.env, this.label);
|
|
568
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CATALOG_TIMEOUT_MS;
|
|
569
|
+
let response;
|
|
570
|
+
try {
|
|
571
|
+
response = await fetch(`${endpoint.baseUrl}/models`, {
|
|
572
|
+
headers: buildHeaders(endpoint),
|
|
573
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
throw unreachableError(this.label, endpoint, error);
|
|
578
|
+
}
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
throw new Error(`${this.label} at ${endpoint.baseUrl} responded ${response.status} to /models. Check the URL and API key.`);
|
|
581
|
+
}
|
|
582
|
+
const listing = await response.json();
|
|
583
|
+
const modelIds = parseModelList(listing);
|
|
584
|
+
const contextLengths = parseModelContextLengths(listing);
|
|
585
|
+
const models = modelIds.map((id, index) => {
|
|
586
|
+
const model = {
|
|
587
|
+
provider: this.provider,
|
|
588
|
+
id,
|
|
589
|
+
label: id,
|
|
590
|
+
isDefault: index === 0,
|
|
591
|
+
// Whether a given endpoint model honors reasoning_effort isn't
|
|
592
|
+
// discoverable from /models, so every model advertises the full set
|
|
593
|
+
// with "off" as the safe default (off omits the parameter entirely).
|
|
594
|
+
thinkingOptions: [...OPENAI_COMPAT_THINKING_OPTIONS],
|
|
595
|
+
defaultThinkingOptionId: OPENAI_COMPAT_DEFAULT_THINKING_OPTION_ID,
|
|
596
|
+
};
|
|
597
|
+
const contextWindowMaxTokens = contextLengths.get(id);
|
|
598
|
+
if (typeof contextWindowMaxTokens === "number") {
|
|
599
|
+
model.contextWindowMaxTokens = contextWindowMaxTokens;
|
|
600
|
+
}
|
|
601
|
+
return model;
|
|
602
|
+
});
|
|
603
|
+
return { models, modes: OPENAI_COMPAT_MODES };
|
|
604
|
+
}
|
|
605
|
+
async getDiagnostic() {
|
|
606
|
+
let endpoint;
|
|
607
|
+
try {
|
|
608
|
+
endpoint = resolveEndpoint(this.env, this.label);
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
return { diagnostic: error instanceof Error ? error.message : String(error) };
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const catalog = await this.fetchCatalog({ scope: "global", force: true });
|
|
615
|
+
return {
|
|
616
|
+
diagnostic: [
|
|
617
|
+
`${this.label} (OpenAI-compatible endpoint)`,
|
|
618
|
+
` URL: ${endpoint.baseUrl}`,
|
|
619
|
+
` Auth: ${endpoint.apiKey ? "API key configured" : "none"}`,
|
|
620
|
+
` Models: ${catalog.models.length}`,
|
|
621
|
+
` Status: reachable`,
|
|
622
|
+
].join("\n"),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
return {
|
|
627
|
+
diagnostic: [
|
|
628
|
+
`${this.label} (OpenAI-compatible endpoint)`,
|
|
629
|
+
` URL: ${endpoint.baseUrl}`,
|
|
630
|
+
` Auth: ${endpoint.apiKey ? "API key configured" : "none"}`,
|
|
631
|
+
` Status: ${error instanceof Error ? error.message : String(error)}`,
|
|
632
|
+
].join("\n"),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async listFeatures(config) {
|
|
637
|
+
const autoCompactDefault = resolveAutoCompactDefault(this.compaction);
|
|
638
|
+
const hideAutoCompact = this.compaction?.hideSelector === true;
|
|
639
|
+
return buildOpenAICompatFeatures({
|
|
640
|
+
autoCompact: hideAutoCompact
|
|
641
|
+
? autoCompactDefault
|
|
642
|
+
: normalizeOpenAICompatAutoCompact(config.featureValues?.["auto_compact"], autoCompactDefault),
|
|
643
|
+
autoCompactDefault,
|
|
644
|
+
hideAutoCompact,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async createSession(config, launchContext) {
|
|
648
|
+
return new OpenAICompatAgentSession({
|
|
649
|
+
providerId: this.provider,
|
|
650
|
+
label: this.label,
|
|
651
|
+
env: this.env,
|
|
652
|
+
config,
|
|
653
|
+
sessionId: randomUUID(),
|
|
654
|
+
messages: [],
|
|
655
|
+
logger: this.logger,
|
|
656
|
+
ottoTools: launchContext?.ottoTools ?? null,
|
|
657
|
+
ottoToolGroups: this.ottoToolGroups,
|
|
658
|
+
mcpServers: this.mcpServers,
|
|
659
|
+
mcpToolPermissions: this.mcpToolPermissions,
|
|
660
|
+
compaction: this.compaction,
|
|
661
|
+
managedProcesses: this.managedProcesses,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async resumeSession(handle, overrides, launchContext) {
|
|
665
|
+
const metadata = (handle.metadata ?? {});
|
|
666
|
+
const model = typeof metadata.model === "string" ? metadata.model : undefined;
|
|
667
|
+
const modeId = typeof metadata.modeId === "string" ? metadata.modeId : undefined;
|
|
668
|
+
return new OpenAICompatAgentSession({
|
|
669
|
+
providerId: this.provider,
|
|
670
|
+
label: this.label,
|
|
671
|
+
env: this.env,
|
|
672
|
+
config: {
|
|
673
|
+
provider: this.provider,
|
|
674
|
+
cwd: overrides?.cwd ?? process.cwd(),
|
|
675
|
+
...(model ? { model } : {}),
|
|
676
|
+
...(modeId ? { modeId } : {}),
|
|
677
|
+
...overrides,
|
|
678
|
+
},
|
|
679
|
+
sessionId: handle.sessionId,
|
|
680
|
+
messages: restoreMessages(metadata),
|
|
681
|
+
logger: this.logger,
|
|
682
|
+
ottoTools: launchContext?.ottoTools ?? null,
|
|
683
|
+
ottoToolGroups: this.ottoToolGroups,
|
|
684
|
+
mcpServers: this.mcpServers,
|
|
685
|
+
mcpToolPermissions: this.mcpToolPermissions,
|
|
686
|
+
compaction: this.compaction,
|
|
687
|
+
managedProcesses: this.managedProcesses,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function isZodType(schema) {
|
|
692
|
+
return typeof schema.safeParseAsync === "function";
|
|
693
|
+
}
|
|
694
|
+
/** Convert an Otto tool's Zod input schema into the JSON Schema the OpenAI `tools` payload expects. */
|
|
695
|
+
function ottoToolParameters(tool) {
|
|
696
|
+
const schema = tool.inputSchema;
|
|
697
|
+
if (!schema) {
|
|
698
|
+
return { type: "object", properties: {} };
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const zodSchema = isZodType(schema) ? schema : z.object(schema);
|
|
702
|
+
const jsonSchema = z.toJSONSchema(zodSchema);
|
|
703
|
+
delete jsonSchema.$schema;
|
|
704
|
+
return jsonSchema;
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// A schema JSON Schema can't represent (e.g. transforms) still leaves the
|
|
708
|
+
// tool callable; executeTool validates the args when it runs.
|
|
709
|
+
return { type: "object", properties: {} };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/** Flatten an Otto tool result into the text fed back to the model. */
|
|
713
|
+
function ottoResultToText(result) {
|
|
714
|
+
const texts = result.content
|
|
715
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
716
|
+
.map((part) => part.text);
|
|
717
|
+
if (texts.length > 0) {
|
|
718
|
+
return texts.join("\n");
|
|
719
|
+
}
|
|
720
|
+
if (result.structuredContent !== undefined) {
|
|
721
|
+
try {
|
|
722
|
+
return JSON.stringify(result.structuredContent, null, 2);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return String(result.structuredContent);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return result.isError ? "Tool failed" : "Done.";
|
|
729
|
+
}
|
|
730
|
+
export class OpenAICompatAgentSession {
|
|
731
|
+
constructor(options) {
|
|
732
|
+
this.capabilities = CAPABILITIES;
|
|
733
|
+
this.listeners = new Set();
|
|
734
|
+
this.eventHistory = [];
|
|
735
|
+
this.pendingPermissions = new Map();
|
|
736
|
+
/** Connection failures already surfaced as timeline warnings. */
|
|
737
|
+
this.mcpFailuresAnnounced = false;
|
|
738
|
+
/**
|
|
739
|
+
* Loop protection: set when an auto-compaction failed or couldn't bring
|
|
740
|
+
* usage back under the threshold, so the next round doesn't immediately
|
|
741
|
+
* retry. Cleared once measured usage drops below the threshold again
|
|
742
|
+
* (rewind, manual /compact, model switch to a larger window).
|
|
743
|
+
*/
|
|
744
|
+
this.autoCompactDisarmed = false;
|
|
745
|
+
this.activeTurn = null;
|
|
746
|
+
/** Resolved context window for the active model; null until (or unless) discovered. */
|
|
747
|
+
this.contextWindowMaxTokens = null;
|
|
748
|
+
/** Model the cached context window was resolved for; re-probe after a model switch. */
|
|
749
|
+
this.contextWindowProbedModel = null;
|
|
750
|
+
/** Exact context size (prompt + completion tokens) measured by the server on the last round. */
|
|
751
|
+
this.lastContextTokens = null;
|
|
752
|
+
this.provider = options.providerId;
|
|
753
|
+
this.label = options.label;
|
|
754
|
+
this.env = options.env;
|
|
755
|
+
this.logger = options.logger;
|
|
756
|
+
this.id = options.sessionId;
|
|
757
|
+
this.cwd = options.config.cwd;
|
|
758
|
+
this.config = options.config;
|
|
759
|
+
this.ottoTools = options.ottoTools ?? null;
|
|
760
|
+
this.ottoToolGroups = options.ottoToolGroups ?? null;
|
|
761
|
+
this.mcpToolPermissions = options.mcpToolPermissions ?? "always-ask";
|
|
762
|
+
// Provider-level servers merged with per-agent config; the per-agent entry
|
|
763
|
+
// wins per server name. The daemon-injected internal "otto" MCP server is
|
|
764
|
+
// stripped — this provider receives Otto tools natively, and connecting to
|
|
765
|
+
// it over MCP as well would double them.
|
|
766
|
+
const perAgentServers = stripInternalOttoMcpServer(options.config).mcpServers;
|
|
767
|
+
const mergedServers = {
|
|
768
|
+
...options.mcpServers,
|
|
769
|
+
...perAgentServers,
|
|
770
|
+
};
|
|
771
|
+
this.mcpManager =
|
|
772
|
+
Object.keys(mergedServers).length > 0
|
|
773
|
+
? new OpenAICompatMcpManager({
|
|
774
|
+
servers: mergedServers,
|
|
775
|
+
providerId: options.providerId,
|
|
776
|
+
cwd: options.config.cwd,
|
|
777
|
+
logger: options.logger,
|
|
778
|
+
managedProcesses: options.managedProcesses ?? null,
|
|
779
|
+
})
|
|
780
|
+
: null;
|
|
781
|
+
this.modelId = options.config.model ?? null;
|
|
782
|
+
this.modeId =
|
|
783
|
+
options.config.modeId && VALID_MODE_IDS.has(options.config.modeId)
|
|
784
|
+
? options.config.modeId
|
|
785
|
+
: "default";
|
|
786
|
+
// Effort comes from the model-level thinking option like every other
|
|
787
|
+
// provider. COMPAT(openaiCompatReasoningFeature): added in v0.4.5 — agents
|
|
788
|
+
// created before the unification persisted the value as the
|
|
789
|
+
// featureValues.reasoning_effort select instead; drop the fallback when
|
|
790
|
+
// floor >= v0.4.5 (target 2027-01).
|
|
791
|
+
this.reasoningEffort = normalizeOpenAICompatReasoningEffort(options.config.thinkingOptionId || options.config.featureValues?.["reasoning_effort"]);
|
|
792
|
+
this.autoCompactDefault = resolveAutoCompactDefault(options.compaction);
|
|
793
|
+
this.autoCompactHidden = options.compaction?.hideSelector === true;
|
|
794
|
+
this.autoCompact = this.autoCompactHidden
|
|
795
|
+
? this.autoCompactDefault
|
|
796
|
+
: normalizeOpenAICompatAutoCompact(options.config.featureValues?.["auto_compact"], this.autoCompactDefault);
|
|
797
|
+
this.keepRecentTokens = resolveKeepRecentTokens(options.compaction);
|
|
798
|
+
// The system message is always rebuilt so cwd/mode/config changes take
|
|
799
|
+
// effect on resume; restored copies of it are dropped first.
|
|
800
|
+
this.messages = options.messages.filter((message) => message.role !== "system");
|
|
801
|
+
this.messages.unshift({ role: "system", content: this.buildSystemPrompt(options.config) });
|
|
802
|
+
this.rebuildEventHistory();
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Rebuild replayable history from the current conversation so a resumed or
|
|
806
|
+
* rewound session still backfills its transcript, tool calls included.
|
|
807
|
+
* There is no separate durable store for tool traffic — `this.messages` is
|
|
808
|
+
* the only record that survives a resume, so tool_call items are
|
|
809
|
+
* reconstructed from each assistant message's `tool_calls` plus the
|
|
810
|
+
* matching "tool" result message. User messages keep their persisted
|
|
811
|
+
* messageId so the durable timeline and the provider conversation agree on
|
|
812
|
+
* rewind targets.
|
|
813
|
+
*/
|
|
814
|
+
rebuildEventHistory() {
|
|
815
|
+
this.eventHistory.length = 0;
|
|
816
|
+
for (const [index, message] of this.messages.entries()) {
|
|
817
|
+
if (message.role === "user") {
|
|
818
|
+
if (!message.content)
|
|
819
|
+
continue;
|
|
820
|
+
this.eventHistory.push({
|
|
821
|
+
type: "timeline",
|
|
822
|
+
provider: this.provider,
|
|
823
|
+
item: {
|
|
824
|
+
type: "user_message",
|
|
825
|
+
text: message.content,
|
|
826
|
+
messageId: message.messageId ?? randomUUID(),
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (message.role !== "assistant")
|
|
832
|
+
continue;
|
|
833
|
+
// Live order within a round is reasoning → assistant text → tool calls;
|
|
834
|
+
// replay mirrors it.
|
|
835
|
+
if (message.reasoning) {
|
|
836
|
+
this.eventHistory.push({
|
|
837
|
+
type: "timeline",
|
|
838
|
+
provider: this.provider,
|
|
839
|
+
item: { type: "reasoning", text: message.reasoning },
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (message.content) {
|
|
843
|
+
this.eventHistory.push({
|
|
844
|
+
type: "timeline",
|
|
845
|
+
provider: this.provider,
|
|
846
|
+
item: { type: "assistant_message", text: message.content, messageId: randomUUID() },
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
for (const call of message.tool_calls ?? []) {
|
|
850
|
+
const resultMessage = this.messages
|
|
851
|
+
.slice(index + 1)
|
|
852
|
+
.find((candidate) => candidate.role === "tool" && candidate.tool_call_id === call.id);
|
|
853
|
+
this.eventHistory.push({
|
|
854
|
+
type: "timeline",
|
|
855
|
+
provider: this.provider,
|
|
856
|
+
item: buildReconstructedToolCallItem(call, resultMessage, this.cwd),
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
get features() {
|
|
862
|
+
return buildOpenAICompatFeatures({
|
|
863
|
+
autoCompact: this.autoCompact,
|
|
864
|
+
autoCompactDefault: this.autoCompactDefault,
|
|
865
|
+
hideAutoCompact: this.autoCompactHidden,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
async setThinkingOption(thinkingOptionId) {
|
|
869
|
+
// null clears back to the model default; "off" omits reasoning_effort
|
|
870
|
+
// from requests entirely.
|
|
871
|
+
if (thinkingOptionId === null) {
|
|
872
|
+
this.reasoningEffort = OPENAI_COMPAT_DEFAULT_THINKING_OPTION_ID;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (normalizeOpenAICompatReasoningEffort(thinkingOptionId) !== thinkingOptionId) {
|
|
876
|
+
throw new Error(`Invalid effort option: ${String(thinkingOptionId)}`);
|
|
877
|
+
}
|
|
878
|
+
this.reasoningEffort = thinkingOptionId;
|
|
879
|
+
}
|
|
880
|
+
async applyPersonality(update) {
|
|
881
|
+
this.config.personalitySnapshot = update.personalitySnapshot;
|
|
882
|
+
this.config.systemPrompt = update.systemPrompt;
|
|
883
|
+
this.config.daemonAppendSystemPrompt = update.daemonAppendSystemPrompt;
|
|
884
|
+
// The daemon owns this conversation: the system prompt is just messages[0],
|
|
885
|
+
// re-sent wholesale on every request, so rebuilding it in place applies the
|
|
886
|
+
// new personality prompt from the very next turn — no session restart.
|
|
887
|
+
const rebuilt = { role: "system", content: this.buildSystemPrompt(this.config) };
|
|
888
|
+
if (this.messages[0]?.role === "system") {
|
|
889
|
+
this.messages[0] = rebuilt;
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
this.messages.unshift(rebuilt);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async setFeature(featureId, value) {
|
|
896
|
+
// COMPAT(openaiCompatReasoningFeature): added in v0.4.5 — effort is no
|
|
897
|
+
// longer advertised as a feature, but old clients may still send the
|
|
898
|
+
// reasoning_effort select; drop when floor >= v0.4.5 (target 2027-01).
|
|
899
|
+
if (featureId === "reasoning_effort") {
|
|
900
|
+
if (normalizeOpenAICompatReasoningEffort(value) !== value) {
|
|
901
|
+
throw new Error(`Invalid reasoning effort value: ${String(value)}`);
|
|
902
|
+
}
|
|
903
|
+
this.reasoningEffort = value;
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (featureId === "auto_compact") {
|
|
907
|
+
if (!OPENAI_COMPAT_AUTO_COMPACT_VALUES.includes(value)) {
|
|
908
|
+
throw new Error(`Invalid auto-compact value: ${String(value)}`);
|
|
909
|
+
}
|
|
910
|
+
this.autoCompact = value;
|
|
911
|
+
// A deliberate setting change is a fresh mandate — retry even if a
|
|
912
|
+
// previous auto-compaction was paused for lack of gain.
|
|
913
|
+
this.autoCompactDisarmed = false;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
throw new Error(`Unknown feature: ${featureId}`);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Live re-apply of provider-level compaction config so settings edits reach
|
|
920
|
+
* running chats without a restart. Sessions still sitting on the old default
|
|
921
|
+
* follow the new default; an explicit per-agent pick is kept unless the
|
|
922
|
+
* selector is hidden, which always forces the default.
|
|
923
|
+
*/
|
|
924
|
+
applyCompactionConfig(compaction) {
|
|
925
|
+
const previousDefault = this.autoCompactDefault;
|
|
926
|
+
const previousHidden = this.autoCompactHidden;
|
|
927
|
+
const previousValue = this.autoCompact;
|
|
928
|
+
this.autoCompactDefault = resolveAutoCompactDefault(compaction);
|
|
929
|
+
this.autoCompactHidden = compaction?.hideSelector === true;
|
|
930
|
+
this.keepRecentTokens = resolveKeepRecentTokens(compaction);
|
|
931
|
+
if (this.autoCompactHidden || previousValue === previousDefault) {
|
|
932
|
+
this.autoCompact = this.autoCompactDefault;
|
|
933
|
+
}
|
|
934
|
+
if (this.autoCompact !== previousValue) {
|
|
935
|
+
// Same fresh-mandate rule as setFeature: a deliberate settings change
|
|
936
|
+
// re-arms a paused auto-compaction.
|
|
937
|
+
this.autoCompactDisarmed = false;
|
|
938
|
+
}
|
|
939
|
+
return (this.autoCompact !== previousValue ||
|
|
940
|
+
this.autoCompactHidden !== previousHidden ||
|
|
941
|
+
this.autoCompactDefault !== previousDefault);
|
|
942
|
+
}
|
|
943
|
+
buildSystemPrompt(config) {
|
|
944
|
+
const parts = [
|
|
945
|
+
[
|
|
946
|
+
`You are a coding agent running inside Otto against ${this.label}.`,
|
|
947
|
+
`Workspace directory: ${this.cwd}`,
|
|
948
|
+
`Platform: ${process.platform}`,
|
|
949
|
+
"Use the available tools to read, search, and modify files and to run shell commands.",
|
|
950
|
+
"Prefer tools over guessing: inspect files before editing them and verify your changes.",
|
|
951
|
+
"When the task is complete, reply with a concise summary of what you did.",
|
|
952
|
+
].join("\n"),
|
|
953
|
+
this.buildPreviewWorkflowPrompt(),
|
|
954
|
+
config.systemPrompt?.trim(),
|
|
955
|
+
config.daemonAppendSystemPrompt?.trim(),
|
|
956
|
+
];
|
|
957
|
+
return parts.filter((part) => Boolean(part)).join("\n\n");
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Workflow doctrine for the preview/browser tool families. Tool descriptions
|
|
961
|
+
* alone don't reliably steer local models, so when both families are exposed
|
|
962
|
+
* the system prompt spells out the split: preview_start owns dev servers and
|
|
963
|
+
* their designated tab; browser_new_tab is for everything else.
|
|
964
|
+
*/
|
|
965
|
+
buildPreviewWorkflowPrompt() {
|
|
966
|
+
const hasTool = (name) => Boolean(this.ottoTools?.getTool(name)) && this.isOttoToolGroupEnabled(name);
|
|
967
|
+
const hasPreview = hasTool("preview_start");
|
|
968
|
+
const hasBrowser = hasTool("browser_snapshot");
|
|
969
|
+
if (!hasPreview && !hasBrowser) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
const lines = ["## Verifying changes in the browser"];
|
|
973
|
+
if (hasPreview) {
|
|
974
|
+
lines.push("- Start dev servers with preview_start — never with run_command or other shell commands. It manages the process, its logs (preview_logs), and its preview tab.");
|
|
975
|
+
}
|
|
976
|
+
if (hasPreview && hasBrowser) {
|
|
977
|
+
lines.push("- preview_start returns browser.browserId: the server's designated preview tab, the same tab the user watches. Verify your changes against that browserId with browser_snapshot, browser_inspect, browser_logs, browser_click, and browser_screenshot.", "- Never open a dev server URL with browser_new_tab or in another tab — the daemon rejects it. browser_new_tab is only for external sites and general browsing.");
|
|
978
|
+
}
|
|
979
|
+
if (hasBrowser) {
|
|
980
|
+
lines.push("- After changing code that a running dev server renders, verify it: reload or snapshot the page, check browser_logs for errors, then share proof (a snapshot or screenshot) instead of asking the user to check manually.");
|
|
981
|
+
}
|
|
982
|
+
return lines.join("\n");
|
|
983
|
+
}
|
|
984
|
+
availableToolSpecs() {
|
|
985
|
+
return COMPAT_TOOL_SPECS.filter((spec) => {
|
|
986
|
+
// Read-only "plan" mode offers no actions against the local machine.
|
|
987
|
+
// "network" tools (web_fetch) stay available for research but prompt.
|
|
988
|
+
if (this.modeId === "plan" && spec.kind !== "read" && spec.kind !== "network")
|
|
989
|
+
return false;
|
|
990
|
+
// The builtin web tools (web_search/web_fetch) are gated by the "web"
|
|
991
|
+
// tool group, just like the injected Otto tool groups.
|
|
992
|
+
if (ottoToolGroupForName(spec.name) === "web" && !this.isOttoToolGroupEnabled(spec.name)) {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
return true;
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
toolNeedsApproval(spec, args) {
|
|
999
|
+
if (spec.kind === "read")
|
|
1000
|
+
return false;
|
|
1001
|
+
if (this.modeId === "bypassPermissions")
|
|
1002
|
+
return false;
|
|
1003
|
+
if (this.modeId === "acceptEdits" && spec.kind === "edit") {
|
|
1004
|
+
// Auto-approval is scoped to the workspace: a write to ~/.bashrc or
|
|
1005
|
+
// Otto's own config is not the "file edits" the mode label promises,
|
|
1006
|
+
// so anything outside the cwd subtree still prompts.
|
|
1007
|
+
const target = args["path"];
|
|
1008
|
+
return typeof target !== "string" || !isPathInsideWorkspace(this.cwd, target);
|
|
1009
|
+
}
|
|
1010
|
+
// Everything else that can act — execute always, and network because an
|
|
1011
|
+
// unprompted web_fetch is an exfiltration channel for unprompted reads.
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Otto's injected catalog (browser_*, preview_*, agent management, …) rendered
|
|
1016
|
+
* as OpenAI function specs so the local model can call them. Excluded in
|
|
1017
|
+
* read-only "plan" mode since they can take actions; builtin coding tools win
|
|
1018
|
+
* on name collisions. Empty when no catalog was injected (e.g. tools disabled).
|
|
1019
|
+
*/
|
|
1020
|
+
/** Whether an Otto tool's group is enabled for this provider (null groups = all). */
|
|
1021
|
+
isOttoToolGroupEnabled(name) {
|
|
1022
|
+
const groups = this.ottoToolGroups;
|
|
1023
|
+
return !groups || groups.includes(ottoToolGroupForName(name));
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Connect the configured MCP servers before the first model round. A server
|
|
1027
|
+
* that fails to connect is skipped — surfaced once as a timeline warning,
|
|
1028
|
+
* never fatal to the session.
|
|
1029
|
+
*/
|
|
1030
|
+
async ensureMcpReady(turn) {
|
|
1031
|
+
if (!this.mcpManager) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
await this.mcpManager.ensureConnected();
|
|
1035
|
+
if (this.mcpFailuresAnnounced) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
this.mcpFailuresAnnounced = true;
|
|
1039
|
+
for (const failure of this.mcpManager.failures) {
|
|
1040
|
+
this.emit({
|
|
1041
|
+
type: "timeline",
|
|
1042
|
+
provider: this.provider,
|
|
1043
|
+
turnId: turn.turnId,
|
|
1044
|
+
item: {
|
|
1045
|
+
type: "error",
|
|
1046
|
+
message: `MCP server '${failure.name}' unavailable: ${failure.error}`,
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/** Built-in slash commands plus MCP prompts from connected servers. */
|
|
1052
|
+
async listCommands() {
|
|
1053
|
+
const commands = [COMPACT_COMMAND];
|
|
1054
|
+
if (!this.mcpManager) {
|
|
1055
|
+
return commands;
|
|
1056
|
+
}
|
|
1057
|
+
const prompts = await this.mcpManager.listPrompts();
|
|
1058
|
+
return commands.concat(prompts.map((prompt) => ({
|
|
1059
|
+
name: prompt.commandName,
|
|
1060
|
+
description: prompt.description ?? `MCP prompt from server '${prompt.serverName}'`,
|
|
1061
|
+
argumentHint: prompt.argumentNames.join(" "),
|
|
1062
|
+
})));
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Handle `/compact [instruction]` by summarizing the older conversation
|
|
1066
|
+
* history while keeping the most recent slice verbatim, then splicing the
|
|
1067
|
+
* summary in ahead of the retained tail. Runs entirely in-process — no
|
|
1068
|
+
* external compaction service. Returns the turn's usage so the caller can
|
|
1069
|
+
* attach it to turn_completed.
|
|
1070
|
+
*
|
|
1071
|
+
* Pipeline (adapted from oh-my-pi's compaction design):
|
|
1072
|
+
* 1. Prune tool outputs (zero-LLM): elide uneventful results, truncate
|
|
1073
|
+
* oversized older ones, protect the newest.
|
|
1074
|
+
* 2. Split at the keep-recent boundary: [system] + [older → summarize] +
|
|
1075
|
+
* [recent → keep verbatim].
|
|
1076
|
+
* 3. Summarize the older region. If a prior compaction summary is present,
|
|
1077
|
+
* merge into it (incremental update) instead of re-summarizing a summary.
|
|
1078
|
+
* 4. Rebuild: [system, summary, ...recent].
|
|
1079
|
+
*/
|
|
1080
|
+
async handleCompact(turn, instruction, trigger) {
|
|
1081
|
+
this.emit({
|
|
1082
|
+
type: "timeline",
|
|
1083
|
+
provider: this.provider,
|
|
1084
|
+
turnId: turn.turnId,
|
|
1085
|
+
item: { type: "compaction", status: "loading", trigger },
|
|
1086
|
+
});
|
|
1087
|
+
// Pre-compaction context size: prefer the server-measured figure from the
|
|
1088
|
+
// last round (what the context ring currently shows); fall back to a local
|
|
1089
|
+
// estimate of the full request payload.
|
|
1090
|
+
const preTokens = this.lastContextTokens ?? this.estimateFullContextTokens();
|
|
1091
|
+
// 1. Zero-LLM pruning first, so both the summary input and the retained
|
|
1092
|
+
// tail are already free of noise and oversized old tool dumps.
|
|
1093
|
+
this.pruneToolOutputs();
|
|
1094
|
+
// 2. Split at the keep-recent boundary. keepFromIndex is the first message
|
|
1095
|
+
// (after the system prompt at index 0) that stays verbatim.
|
|
1096
|
+
const keepFromIndex = this.computeCompactionKeepFromIndex();
|
|
1097
|
+
const summarizeRegion = this.messages.slice(1, keepFromIndex);
|
|
1098
|
+
const keptRegion = this.messages.slice(keepFromIndex);
|
|
1099
|
+
if (summarizeRegion.length === 0) {
|
|
1100
|
+
// Nothing old enough to summarize (conversation already within the
|
|
1101
|
+
// keep-recent budget). Report a no-op rather than summarizing nothing.
|
|
1102
|
+
const postTokens = this.estimateFullContextTokens();
|
|
1103
|
+
this.lastContextTokens = postTokens;
|
|
1104
|
+
this.emit({
|
|
1105
|
+
type: "timeline",
|
|
1106
|
+
provider: this.provider,
|
|
1107
|
+
turnId: turn.turnId,
|
|
1108
|
+
item: { type: "compaction", status: "completed", trigger, preTokens, postTokens },
|
|
1109
|
+
});
|
|
1110
|
+
const maxTokens = await this.resolveContextWindowMaxTokens();
|
|
1111
|
+
const usage = {
|
|
1112
|
+
contextWindowUsedTokens: postTokens,
|
|
1113
|
+
...(maxTokens !== null ? { contextWindowMaxTokens: maxTokens } : {}),
|
|
1114
|
+
};
|
|
1115
|
+
this.emit({ type: "usage_updated", provider: this.provider, usage, turnId: turn.turnId });
|
|
1116
|
+
return usage;
|
|
1117
|
+
}
|
|
1118
|
+
// 3. Incremental update when a prior summary sits in the region: feed it as
|
|
1119
|
+
// <previous-summary> and merge the newer messages into it, rather than
|
|
1120
|
+
// summarizing a summary (which degrades badly across repeated compacts).
|
|
1121
|
+
const priorSummaryIndex = summarizeRegion.findIndex((message) => message.role === "user" && message.isCompactionSummary === true);
|
|
1122
|
+
const priorSummary = priorSummaryIndex >= 0 ? summarizeRegion[priorSummaryIndex].content : null;
|
|
1123
|
+
const messagesToSummarize = priorSummaryIndex >= 0
|
|
1124
|
+
? summarizeRegion.filter((_, index) => index !== priorSummaryIndex)
|
|
1125
|
+
: summarizeRegion;
|
|
1126
|
+
const conversationText = this.serializeConversationForCompaction(messagesToSummarize);
|
|
1127
|
+
const endpoint = resolveEndpoint(this.env, this.label);
|
|
1128
|
+
const model = await this.resolveModel(endpoint);
|
|
1129
|
+
const systemPrompt = priorSummary !== null
|
|
1130
|
+
? this.buildCompactionUpdateSystemPrompt(instruction)
|
|
1131
|
+
: this.buildCompactionSystemPrompt(instruction);
|
|
1132
|
+
const userContent = priorSummary !== null
|
|
1133
|
+
? `<previous-summary>\n${priorSummary}\n</previous-summary>\n\n<new-messages>\n${conversationText}\n</new-messages>`
|
|
1134
|
+
: conversationText;
|
|
1135
|
+
const summaryResult = await this.runCompactionCompletion({
|
|
1136
|
+
endpoint,
|
|
1137
|
+
model,
|
|
1138
|
+
systemPrompt,
|
|
1139
|
+
userContent,
|
|
1140
|
+
signal: turn.abort.signal,
|
|
1141
|
+
});
|
|
1142
|
+
const summary = summaryResult.content;
|
|
1143
|
+
// 4. Rebuild: system + summary (marked for future incremental updates) +
|
|
1144
|
+
// the retained recent tail, untouched.
|
|
1145
|
+
this.messages.length = 0;
|
|
1146
|
+
this.messages.push({ role: "system", content: this.buildSystemPrompt(this.config) });
|
|
1147
|
+
this.messages.push({
|
|
1148
|
+
role: "user",
|
|
1149
|
+
content: summary,
|
|
1150
|
+
messageId: randomUUID(),
|
|
1151
|
+
isCompactionSummary: true,
|
|
1152
|
+
});
|
|
1153
|
+
// Assistant ack keeps the roles alternating so the next user turn doesn't
|
|
1154
|
+
// sit directly after the synthetic summary user message.
|
|
1155
|
+
this.messages.push({ role: "assistant", content: "Conversation history has been compacted." });
|
|
1156
|
+
this.messages.push(...keptRegion);
|
|
1157
|
+
// Post-compaction context size. Must count the rebuilt system prompt and
|
|
1158
|
+
// the tool schemas — both are re-sent with every request — or the ring
|
|
1159
|
+
// reads near-zero here and jumps back up on the next real turn.
|
|
1160
|
+
const postTokens = this.estimateFullContextTokens();
|
|
1161
|
+
this.lastContextTokens = postTokens;
|
|
1162
|
+
this.emit({
|
|
1163
|
+
type: "timeline",
|
|
1164
|
+
provider: this.provider,
|
|
1165
|
+
turnId: turn.turnId,
|
|
1166
|
+
item: {
|
|
1167
|
+
type: "compaction",
|
|
1168
|
+
status: "completed",
|
|
1169
|
+
trigger,
|
|
1170
|
+
preTokens,
|
|
1171
|
+
postTokens,
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
// usage_updated replaces agent.lastUsage wholesale and the client hides
|
|
1175
|
+
// the context ring unless both bounds are present, so the compaction
|
|
1176
|
+
// usage must carry the post-compaction context size alongside the
|
|
1177
|
+
// request's own token cost.
|
|
1178
|
+
const maxTokens = await this.resolveContextWindowMaxTokens();
|
|
1179
|
+
const usageData = summaryResult.usage;
|
|
1180
|
+
const usage = {
|
|
1181
|
+
...(typeof usageData?.prompt_tokens === "number"
|
|
1182
|
+
? { inputTokens: usageData.prompt_tokens }
|
|
1183
|
+
: {}),
|
|
1184
|
+
...(typeof usageData?.completion_tokens === "number"
|
|
1185
|
+
? { outputTokens: usageData.completion_tokens }
|
|
1186
|
+
: {}),
|
|
1187
|
+
contextWindowUsedTokens: postTokens,
|
|
1188
|
+
...(maxTokens !== null ? { contextWindowMaxTokens: maxTokens } : {}),
|
|
1189
|
+
};
|
|
1190
|
+
this.emit({
|
|
1191
|
+
type: "usage_updated",
|
|
1192
|
+
provider: this.provider,
|
|
1193
|
+
usage,
|
|
1194
|
+
turnId: turn.turnId,
|
|
1195
|
+
});
|
|
1196
|
+
return usage;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Auto-compaction check, run at turn start (before the new user message
|
|
1200
|
+
* joins the conversation, so it always survives verbatim) and before every
|
|
1201
|
+
* subsequent model round. Uses the freshest server-measured context size —
|
|
1202
|
+
* the active turn's last round, falling back to the previous turn's final
|
|
1203
|
+
* figure. Endpoints that report no context length never auto-compact:
|
|
1204
|
+
* without a denominator there is no percentage (manual /compact still
|
|
1205
|
+
* works).
|
|
1206
|
+
*
|
|
1207
|
+
* Loop protection: a compaction that fails or can't bring usage back under
|
|
1208
|
+
* the threshold disarms the trigger. Without that, a conversation whose
|
|
1209
|
+
* retained tail alone exceeds the threshold would re-summarize on every
|
|
1210
|
+
* round, burning a model call each time for no reclaimed space. The trigger
|
|
1211
|
+
* re-arms once measured usage drops below the threshold by other means
|
|
1212
|
+
* (rewind, manual /compact, a model switch to a larger window) or when the
|
|
1213
|
+
* user changes the auto-compact setting.
|
|
1214
|
+
*
|
|
1215
|
+
* Auto-compaction failures never fail the user's turn — the turn proceeds
|
|
1216
|
+
* with the uncompacted conversation (interrupts still propagate).
|
|
1217
|
+
*/
|
|
1218
|
+
async maybeAutoCompact(turn) {
|
|
1219
|
+
if (this.autoCompact === "off") {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const maxTokens = await this.resolveContextWindowMaxTokens();
|
|
1223
|
+
if (maxTokens === null) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const threshold = Math.floor((Number(this.autoCompact) / 100) * maxTokens);
|
|
1227
|
+
const used = turn.usage && typeof turn.usage.inputTokens === "number"
|
|
1228
|
+
? turn.usage.inputTokens + (turn.usage.outputTokens ?? 0)
|
|
1229
|
+
: this.lastContextTokens;
|
|
1230
|
+
if (used === null || used < threshold) {
|
|
1231
|
+
if (used !== null) {
|
|
1232
|
+
this.autoCompactDisarmed = false;
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (this.autoCompactDisarmed) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
let usage;
|
|
1240
|
+
try {
|
|
1241
|
+
usage = await this.handleCompact(turn, null, "auto");
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
// Settle the "loading" compaction row before deciding how to proceed.
|
|
1245
|
+
this.emit({
|
|
1246
|
+
type: "timeline",
|
|
1247
|
+
provider: this.provider,
|
|
1248
|
+
turnId: turn.turnId,
|
|
1249
|
+
item: { type: "compaction", status: "failed", trigger: "auto" },
|
|
1250
|
+
});
|
|
1251
|
+
if (turn.abort.signal.aborted) {
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
this.autoCompactDisarmed = true;
|
|
1255
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1256
|
+
this.emit({
|
|
1257
|
+
type: "timeline",
|
|
1258
|
+
provider: this.provider,
|
|
1259
|
+
turnId: turn.turnId,
|
|
1260
|
+
item: {
|
|
1261
|
+
type: "error",
|
|
1262
|
+
message: `Auto-compaction failed: ${message}. Auto-compaction is paused until context usage drops below the threshold — run /compact to retry manually.`,
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
// The pre-compaction round figures no longer describe the conversation;
|
|
1268
|
+
// the next round's stream re-measures it.
|
|
1269
|
+
turn.usage = null;
|
|
1270
|
+
const postTokens = usage.contextWindowUsedTokens ?? this.lastContextTokens;
|
|
1271
|
+
if (typeof postTokens === "number" && postTokens >= threshold) {
|
|
1272
|
+
this.autoCompactDisarmed = true;
|
|
1273
|
+
this.emit({
|
|
1274
|
+
type: "timeline",
|
|
1275
|
+
provider: this.provider,
|
|
1276
|
+
turnId: turn.turnId,
|
|
1277
|
+
item: {
|
|
1278
|
+
type: "error",
|
|
1279
|
+
message: `Auto-compaction reclaimed too little space (still ~${postTokens} of ` +
|
|
1280
|
+
`${maxTokens} tokens). Auto-compaction is paused until usage ` +
|
|
1281
|
+
`drops below the ${this.autoCompact}% threshold — run /compact with an ` +
|
|
1282
|
+
`instruction, rewind, or start a fresh agent.`,
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Pre-summarization pruning (zero-LLM). Mutates the message array in place:
|
|
1289
|
+
* elides uneventful tool results everywhere, then truncates oversized tool
|
|
1290
|
+
* results older than the protected-recent window to head + tail. Structure is
|
|
1291
|
+
* never changed (only tool `content`), so tool_call ordering stays valid.
|
|
1292
|
+
*/
|
|
1293
|
+
pruneToolOutputs() {
|
|
1294
|
+
for (const message of this.messages) {
|
|
1295
|
+
if (message.role === "tool" && isUneventfulToolResult(message.content)) {
|
|
1296
|
+
message.content = UNEVENTFUL_RESULT_PLACEHOLDER;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// Walk tool results newest → oldest; protect the freshest by token budget,
|
|
1300
|
+
// truncate the rest.
|
|
1301
|
+
let protectedTokens = 0;
|
|
1302
|
+
for (let index = this.messages.length - 1; index >= 0; index -= 1) {
|
|
1303
|
+
const message = this.messages[index];
|
|
1304
|
+
if (message.role !== "tool") {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
const tokens = estimateTokens(message.content);
|
|
1308
|
+
if (protectedTokens < PRUNE_PROTECT_RECENT_TOOL_TOKENS) {
|
|
1309
|
+
protectedTokens += tokens;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (message.content.length > PRUNE_TOOL_RESULT_MIN_CHARS) {
|
|
1313
|
+
const { content } = message;
|
|
1314
|
+
message.content = `${content.slice(0, PRUNE_TOOL_RESULT_HEAD_CHARS)}\n[... ${content.length - PRUNE_TOOL_RESULT_HEAD_CHARS - PRUNE_TOOL_RESULT_TAIL_CHARS} chars pruned ...]\n${content.slice(-PRUNE_TOOL_RESULT_TAIL_CHARS)}`;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Index of the first message (after the system prompt at 0) to keep verbatim
|
|
1320
|
+
* through compaction: walk newest → oldest accumulating token estimates until
|
|
1321
|
+
* the keep-recent budget is exceeded, then advance past any leading tool
|
|
1322
|
+
* results so the retained tail never starts with a tool message orphaned from
|
|
1323
|
+
* its assistant tool_calls. Returns messages.length when the whole
|
|
1324
|
+
* conversation is older than the budget (summarize everything, keep none).
|
|
1325
|
+
*/
|
|
1326
|
+
computeCompactionKeepFromIndex() {
|
|
1327
|
+
let keepFrom = this.messages.length;
|
|
1328
|
+
let recentTokens = 0;
|
|
1329
|
+
for (let index = this.messages.length - 1; index >= 1; index -= 1) {
|
|
1330
|
+
recentTokens += estimateTokens(JSON.stringify(this.messages[index]));
|
|
1331
|
+
if (recentTokens > this.keepRecentTokens) {
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
keepFrom = index;
|
|
1335
|
+
}
|
|
1336
|
+
// Never keep the entire conversation: a manual /compact should always
|
|
1337
|
+
// compress something, so a conversation within the budget summarizes fully.
|
|
1338
|
+
if (keepFrom <= 1) {
|
|
1339
|
+
return this.messages.length;
|
|
1340
|
+
}
|
|
1341
|
+
// Don't strand tool results at the head of the retained tail.
|
|
1342
|
+
while (keepFrom < this.messages.length && this.messages[keepFrom].role === "tool") {
|
|
1343
|
+
keepFrom += 1;
|
|
1344
|
+
}
|
|
1345
|
+
return keepFrom;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* POST a non-streaming chat completion for compaction and return the assistant
|
|
1349
|
+
* text plus raw usage. Throws on transport failure or an empty response.
|
|
1350
|
+
*/
|
|
1351
|
+
async runCompactionCompletion(options) {
|
|
1352
|
+
const response = await fetch(`${options.endpoint.baseUrl}/chat/completions`, {
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
headers: buildHeaders(options.endpoint),
|
|
1355
|
+
signal: options.signal,
|
|
1356
|
+
body: JSON.stringify({
|
|
1357
|
+
model: options.model,
|
|
1358
|
+
messages: [
|
|
1359
|
+
{ role: "system", content: options.systemPrompt },
|
|
1360
|
+
{ role: "user", content: options.userContent },
|
|
1361
|
+
],
|
|
1362
|
+
}),
|
|
1363
|
+
});
|
|
1364
|
+
if (!response.ok) {
|
|
1365
|
+
const bodyText = await response.text().catch(() => "");
|
|
1366
|
+
throw new Error(`${this.label} responded ${response.status} to /chat/completions${bodyText ? `: ${bodyText.slice(0, 400)}` : ""}`);
|
|
1367
|
+
}
|
|
1368
|
+
const data = (await response.json());
|
|
1369
|
+
const choices = Array.isArray(data.choices) ? data.choices : [];
|
|
1370
|
+
const firstChoice = choices[0];
|
|
1371
|
+
if (!firstChoice || typeof firstChoice !== "object") {
|
|
1372
|
+
throw new Error(`${this.label} returned an empty compaction response`);
|
|
1373
|
+
}
|
|
1374
|
+
const responseMessage = firstChoice.message;
|
|
1375
|
+
if (!responseMessage || typeof responseMessage !== "object") {
|
|
1376
|
+
throw new Error(`${this.label} returned an empty compaction response`);
|
|
1377
|
+
}
|
|
1378
|
+
const rawContent = responseMessage.content;
|
|
1379
|
+
const content = typeof rawContent === "string" ? rawContent : "";
|
|
1380
|
+
if (!content) {
|
|
1381
|
+
throw new Error(`${this.label} returned an empty compaction summary`);
|
|
1382
|
+
}
|
|
1383
|
+
const usage = data.usage && typeof data.usage === "object"
|
|
1384
|
+
? data.usage
|
|
1385
|
+
: undefined;
|
|
1386
|
+
return { content, ...(usage ? { usage } : {}) };
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Local estimate of the full next-request context: conversation messages
|
|
1390
|
+
* (including the system prompt) plus the tool schemas that are re-sent with
|
|
1391
|
+
* every request. Char-based, so it's approximate — the next real turn's
|
|
1392
|
+
* server-measured usage replaces it.
|
|
1393
|
+
*/
|
|
1394
|
+
estimateFullContextTokens() {
|
|
1395
|
+
const toolsPayload = [
|
|
1396
|
+
...buildOpenAIToolsPayload(this.availableToolSpecs()),
|
|
1397
|
+
...this.buildOttoToolPayload(),
|
|
1398
|
+
...this.buildMcpToolPayload(),
|
|
1399
|
+
];
|
|
1400
|
+
return (estimateTokens(JSON.stringify(toolsPayload)) +
|
|
1401
|
+
this.messages.reduce((sum, message) => sum + estimateTokens(JSON.stringify(message)), 0));
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Serialize the given conversation messages into a text blob suitable for a
|
|
1405
|
+
* compaction prompt. Tool calls are summarized rather than included verbatim
|
|
1406
|
+
* to keep the payload manageable.
|
|
1407
|
+
*/
|
|
1408
|
+
serializeConversationForCompaction(messages) {
|
|
1409
|
+
const lines = [];
|
|
1410
|
+
const conversationMessages = messages.filter((message) => message.role !== "system");
|
|
1411
|
+
for (const message of conversationMessages) {
|
|
1412
|
+
if (message.role === "user") {
|
|
1413
|
+
lines.push(`[user]: ${message.content}`);
|
|
1414
|
+
}
|
|
1415
|
+
else if (message.role === "assistant") {
|
|
1416
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
1417
|
+
const toolSummaries = message.tool_calls
|
|
1418
|
+
.map((call) => {
|
|
1419
|
+
let argsPreview = "";
|
|
1420
|
+
try {
|
|
1421
|
+
const parsed = JSON.parse(call.function.arguments);
|
|
1422
|
+
argsPreview = ` (${JSON.stringify(parsed).slice(0, 500)})`;
|
|
1423
|
+
}
|
|
1424
|
+
catch {
|
|
1425
|
+
argsPreview = ` (${call.function.arguments.slice(0, 500)})`;
|
|
1426
|
+
}
|
|
1427
|
+
return ` - called ${call.function.name}${argsPreview}`;
|
|
1428
|
+
})
|
|
1429
|
+
.join("\n");
|
|
1430
|
+
lines.push(`[assistant]: (tool calls)`);
|
|
1431
|
+
lines.push(toolSummaries);
|
|
1432
|
+
if (message.content) {
|
|
1433
|
+
lines.push(` text: ${message.content}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
lines.push(`[assistant]: ${message.content}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
else if (message.role === "tool") {
|
|
1441
|
+
// Keep head + tail of large results: file reads and command output
|
|
1442
|
+
// carry their signal at both ends, and the summarizer can't preserve
|
|
1443
|
+
// what it never sees.
|
|
1444
|
+
const content = message.content;
|
|
1445
|
+
const contentPreview = content.length > TOOL_RESULT_HEAD_CHARS + TOOL_RESULT_TAIL_CHARS
|
|
1446
|
+
? `${content.slice(0, TOOL_RESULT_HEAD_CHARS)}\n[... ${content.length - TOOL_RESULT_HEAD_CHARS - TOOL_RESULT_TAIL_CHARS} chars truncated ...]\n${content.slice(-TOOL_RESULT_TAIL_CHARS)}`
|
|
1447
|
+
: content;
|
|
1448
|
+
lines.push(`[tool result ${message.tool_call_id}]: ${contentPreview}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return lines.join("\n");
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* System prompt for a fresh compaction summary. The older conversation is
|
|
1455
|
+
* summarized while recent messages are kept verbatim, so this summary becomes
|
|
1456
|
+
* the model's memory of the distant past — it must preserve exact references,
|
|
1457
|
+
* not paraphrase them away. Modeled on oh-my-pi's structured handoff format.
|
|
1458
|
+
*/
|
|
1459
|
+
buildCompactionSystemPrompt(instruction) {
|
|
1460
|
+
const base = `You summarize a coding-agent conversation into a structured handoff summary so another LLM can resume the task. Only the older part of the conversation is being summarized; recent messages are retained verbatim after your summary. Preserve detail — a thorough summary is expected. Do NOT aim for brevity.
|
|
1461
|
+
|
|
1462
|
+
NEVER continue the conversation. NEVER answer questions found in it. Output ONLY the structured summary below.
|
|
1463
|
+
|
|
1464
|
+
If the conversation ends with an unanswered question or a request awaiting a response (e.g. "run this and paste the output"), you MUST preserve that exact question/request under Critical Context.
|
|
1465
|
+
|
|
1466
|
+
Use this format (omit a section only if it truly does not apply):
|
|
1467
|
+
|
|
1468
|
+
## Goal
|
|
1469
|
+
[The user's goals; list multiple if the session covers different tasks.]
|
|
1470
|
+
|
|
1471
|
+
## Constraints & Preferences
|
|
1472
|
+
- [Requirements, constraints, and preferences the user stated]
|
|
1473
|
+
|
|
1474
|
+
## Progress
|
|
1475
|
+
### Done
|
|
1476
|
+
- [x] [Completed changes/tasks]
|
|
1477
|
+
### In Progress
|
|
1478
|
+
- [ ] [Work underway when compaction ran]
|
|
1479
|
+
### Blocked
|
|
1480
|
+
- [Anything preventing progress]
|
|
1481
|
+
|
|
1482
|
+
## Key Decisions
|
|
1483
|
+
- **[Decision]**: [Brief rationale]
|
|
1484
|
+
|
|
1485
|
+
## Files & Changes
|
|
1486
|
+
- [Exact path — what was read/created/modified and why it matters; include the important code snippets verbatim]
|
|
1487
|
+
|
|
1488
|
+
## Next Steps
|
|
1489
|
+
1. [Ordered next actions, aligned with the user's most recent request]
|
|
1490
|
+
|
|
1491
|
+
## Critical Context
|
|
1492
|
+
- [Important data, exact error messages, repository state (branch, uncommitted changes), and any pending question awaiting a user response]
|
|
1493
|
+
|
|
1494
|
+
## Additional Notes
|
|
1495
|
+
[Anything important not captured above]
|
|
1496
|
+
|
|
1497
|
+
You MUST preserve exact file paths, function names, error messages, and relevant tool outputs or command results.`;
|
|
1498
|
+
const instructionPart = instruction
|
|
1499
|
+
? `\n\nAdditional user instruction — follow it for emphasis and focus, on top of the format above:\n<user-instruction>${instruction}</user-instruction>`
|
|
1500
|
+
: "";
|
|
1501
|
+
return base + instructionPart;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* System prompt for an incremental compaction: merge newer messages into an
|
|
1505
|
+
* existing summary rather than re-summarizing a summary (which degrades fast
|
|
1506
|
+
* across repeated compacts). The prior summary arrives in <previous-summary>
|
|
1507
|
+
* tags and the newer messages in <new-messages> tags.
|
|
1508
|
+
*/
|
|
1509
|
+
buildCompactionUpdateSystemPrompt(instruction) {
|
|
1510
|
+
const base = `You update an existing structured handoff summary by incorporating newer conversation messages, so another LLM can resume the task. The prior summary is in <previous-summary> tags; the newer messages are in <new-messages> tags.
|
|
1511
|
+
|
|
1512
|
+
NEVER continue the conversation. NEVER answer questions found in it. Output ONLY the updated structured summary.
|
|
1513
|
+
|
|
1514
|
+
Rules:
|
|
1515
|
+
- Preserve ALL information from the previous summary that is still relevant.
|
|
1516
|
+
- Fold in new progress, decisions, files, and context from the newer messages.
|
|
1517
|
+
- Move items from "In Progress" to "Done" when completed; update "Next Steps".
|
|
1518
|
+
- You MAY drop anything the newer messages made irrelevant.
|
|
1519
|
+
- If the newer messages end with an unanswered question or a request awaiting a response, record it under Critical Context (replacing a prior pending question once answered).
|
|
1520
|
+
- You MUST preserve exact file paths, function names, error messages, and relevant tool outputs or command results.
|
|
1521
|
+
|
|
1522
|
+
Keep the same section format as the previous summary (## Goal, ## Constraints & Preferences, ## Progress with Done/In Progress/Blocked, ## Key Decisions, ## Files & Changes, ## Next Steps, ## Critical Context, ## Additional Notes).`;
|
|
1523
|
+
const instructionPart = instruction
|
|
1524
|
+
? `\n\nAdditional user instruction — follow it for emphasis and focus:\n<user-instruction>${instruction}</user-instruction>`
|
|
1525
|
+
: "";
|
|
1526
|
+
return base + instructionPart;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* When the typed prompt is `/mcp_{server}_{prompt} args`, resolve it via
|
|
1530
|
+
* prompts/get and feed the resolved text to the model. The remainder of the
|
|
1531
|
+
* line maps to the prompt's first declared argument (multi-argument prompts
|
|
1532
|
+
* receive only that one). Any resolution failure falls back to sending the
|
|
1533
|
+
* raw text as a plain prompt, matching the opencode provider.
|
|
1534
|
+
*/
|
|
1535
|
+
async resolveMcpPromptText(promptText) {
|
|
1536
|
+
const manager = this.mcpManager;
|
|
1537
|
+
if (!manager) {
|
|
1538
|
+
return promptText;
|
|
1539
|
+
}
|
|
1540
|
+
const parsed = parseSlashCommandInput(promptText);
|
|
1541
|
+
if (!parsed || !parsed.commandName.startsWith("mcp_")) {
|
|
1542
|
+
return promptText;
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
const prompts = await manager.listPrompts();
|
|
1546
|
+
const binding = prompts.find((prompt) => prompt.commandName === parsed.commandName);
|
|
1547
|
+
if (!binding) {
|
|
1548
|
+
return promptText;
|
|
1549
|
+
}
|
|
1550
|
+
return await manager.getPrompt(binding, parsed.args);
|
|
1551
|
+
}
|
|
1552
|
+
catch (error) {
|
|
1553
|
+
this.logger?.warn({ err: error, commandName: parsed.commandName }, "Failed to resolve MCP prompt command; falling back to plain prompt input");
|
|
1554
|
+
return promptText;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* MCP tools rendered as OpenAI function specs under namespaced
|
|
1559
|
+
* mcp_{server}_{tool} names. Hard-excluded in read-only "plan" mode — MCP
|
|
1560
|
+
* tools are opaque and may take actions regardless of what they claim.
|
|
1561
|
+
*/
|
|
1562
|
+
buildMcpToolPayload() {
|
|
1563
|
+
if (!this.mcpManager || this.modeId === "plan") {
|
|
1564
|
+
return [];
|
|
1565
|
+
}
|
|
1566
|
+
return this.mcpManager.getToolBindings().map((binding) => ({
|
|
1567
|
+
type: "function",
|
|
1568
|
+
function: {
|
|
1569
|
+
name: binding.modelName,
|
|
1570
|
+
description: binding.description,
|
|
1571
|
+
parameters: binding.parameters,
|
|
1572
|
+
},
|
|
1573
|
+
}));
|
|
1574
|
+
}
|
|
1575
|
+
buildOttoToolPayload() {
|
|
1576
|
+
if (!this.ottoTools || this.modeId === "plan") {
|
|
1577
|
+
return [];
|
|
1578
|
+
}
|
|
1579
|
+
const payload = [];
|
|
1580
|
+
for (const tool of this.ottoTools.tools.values()) {
|
|
1581
|
+
if (findCompatToolSpec(tool.name) || !this.isOttoToolGroupEnabled(tool.name)) {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
payload.push({
|
|
1585
|
+
type: "function",
|
|
1586
|
+
function: {
|
|
1587
|
+
name: tool.name,
|
|
1588
|
+
description: tool.description,
|
|
1589
|
+
parameters: ottoToolParameters(tool),
|
|
1590
|
+
},
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return payload;
|
|
1594
|
+
}
|
|
1595
|
+
async run(prompt, options) {
|
|
1596
|
+
const { turnId } = await this.startTurn(prompt, options);
|
|
1597
|
+
const turn = this.activeTurn;
|
|
1598
|
+
if (!turn || turn.turnId !== turnId) {
|
|
1599
|
+
throw new Error(`${this.label} turn did not start`);
|
|
1600
|
+
}
|
|
1601
|
+
return turn.completed;
|
|
1602
|
+
}
|
|
1603
|
+
async startTurn(prompt, options) {
|
|
1604
|
+
if (this.activeTurn) {
|
|
1605
|
+
throw new Error(`${this.label} already has an active turn`);
|
|
1606
|
+
}
|
|
1607
|
+
const turnId = randomUUID();
|
|
1608
|
+
let resolve;
|
|
1609
|
+
let reject;
|
|
1610
|
+
const completed = new Promise((promiseResolve, promiseReject) => {
|
|
1611
|
+
resolve = promiseResolve;
|
|
1612
|
+
reject = promiseReject;
|
|
1613
|
+
});
|
|
1614
|
+
// startTurn callers observe failure via turn_failed events, not this
|
|
1615
|
+
// promise — swallow to avoid unhandled rejections. run() awaits the same
|
|
1616
|
+
// promise and still sees the rejection.
|
|
1617
|
+
completed.catch(() => { });
|
|
1618
|
+
const turn = {
|
|
1619
|
+
turnId,
|
|
1620
|
+
assistantMessageId: randomUUID(),
|
|
1621
|
+
abort: new AbortController(),
|
|
1622
|
+
roundText: "",
|
|
1623
|
+
roundReasoning: "",
|
|
1624
|
+
finalTextParts: [],
|
|
1625
|
+
pendingToolCalls: new Map(),
|
|
1626
|
+
finishReason: null,
|
|
1627
|
+
usage: null,
|
|
1628
|
+
resolve,
|
|
1629
|
+
reject,
|
|
1630
|
+
completed,
|
|
1631
|
+
};
|
|
1632
|
+
this.activeTurn = turn;
|
|
1633
|
+
const promptText = promptToText(prompt);
|
|
1634
|
+
// One id shared by the timeline item and the conversation entry so
|
|
1635
|
+
// revertConversation can map a timeline user_message back to its message.
|
|
1636
|
+
const userMessageId = options?.messageId ?? randomUUID();
|
|
1637
|
+
this.emit({
|
|
1638
|
+
type: "timeline",
|
|
1639
|
+
provider: this.provider,
|
|
1640
|
+
turnId,
|
|
1641
|
+
item: {
|
|
1642
|
+
type: "user_message",
|
|
1643
|
+
text: promptText,
|
|
1644
|
+
messageId: userMessageId,
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1647
|
+
void this.executeTurn(turn, promptText, userMessageId);
|
|
1648
|
+
return { turnId };
|
|
1649
|
+
}
|
|
1650
|
+
async executeTurn(turn, promptText, userMessageId) {
|
|
1651
|
+
this.emit({ type: "turn_started", provider: this.provider, turnId: turn.turnId });
|
|
1652
|
+
// Intercept built-in slash commands before delegating to the model.
|
|
1653
|
+
const slashCommand = parseSlashCommandInput(promptText);
|
|
1654
|
+
if (slashCommand && slashCommand.commandName === "compact") {
|
|
1655
|
+
let usage;
|
|
1656
|
+
try {
|
|
1657
|
+
usage = await this.handleCompact(turn, slashCommand.args, "manual");
|
|
1658
|
+
}
|
|
1659
|
+
catch (error) {
|
|
1660
|
+
// Settle the "loading" compaction row — without this it spins
|
|
1661
|
+
// forever. Clients that predate the "failed" status drop this event
|
|
1662
|
+
// and keep the spinning row, which matches their pre-fix behavior.
|
|
1663
|
+
this.emit({
|
|
1664
|
+
type: "timeline",
|
|
1665
|
+
provider: this.provider,
|
|
1666
|
+
turnId: turn.turnId,
|
|
1667
|
+
item: { type: "compaction", status: "failed", trigger: "manual" },
|
|
1668
|
+
});
|
|
1669
|
+
this.settleTurnFailure(turn, error);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (this.activeTurn !== turn) {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
this.activeTurn = null;
|
|
1676
|
+
// turn_completed is the manager's terminal signal — without it the
|
|
1677
|
+
// foreground turn stream never ends and the agent stays "running".
|
|
1678
|
+
this.emit({
|
|
1679
|
+
type: "turn_completed",
|
|
1680
|
+
provider: this.provider,
|
|
1681
|
+
turnId: turn.turnId,
|
|
1682
|
+
usage,
|
|
1683
|
+
});
|
|
1684
|
+
turn.resolve({
|
|
1685
|
+
sessionId: this.id,
|
|
1686
|
+
finalText: "",
|
|
1687
|
+
usage,
|
|
1688
|
+
timeline: [],
|
|
1689
|
+
canceled: false,
|
|
1690
|
+
});
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
await this.ensureMcpReady(turn);
|
|
1695
|
+
// The timeline keeps the typed `/command args` text; the model receives
|
|
1696
|
+
// the resolved MCP prompt (or the raw text when nothing matches).
|
|
1697
|
+
const modelText = await this.resolveMcpPromptText(promptText);
|
|
1698
|
+
// Turn-start auto-compaction runs before the new user message joins the
|
|
1699
|
+
// conversation: when the whole history is summarized ("never keep the
|
|
1700
|
+
// entire conversation"), the instruction the model is about to act on
|
|
1701
|
+
// must not be folded into the summary with it.
|
|
1702
|
+
await this.maybeAutoCompact(turn);
|
|
1703
|
+
this.messages.push({ role: "user", content: modelText, messageId: userMessageId });
|
|
1704
|
+
await this.runToolLoop(turn);
|
|
1705
|
+
}
|
|
1706
|
+
catch (error) {
|
|
1707
|
+
this.settleTurnFailure(turn, error);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (this.activeTurn !== turn) {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
this.activeTurn = null;
|
|
1714
|
+
const finalText = turn.finalTextParts.join("\n\n");
|
|
1715
|
+
const usage = await this.buildTurnUsage(turn);
|
|
1716
|
+
this.emit({
|
|
1717
|
+
type: "turn_completed",
|
|
1718
|
+
provider: this.provider,
|
|
1719
|
+
turnId: turn.turnId,
|
|
1720
|
+
...(usage ? { usage } : {}),
|
|
1721
|
+
});
|
|
1722
|
+
turn.resolve({
|
|
1723
|
+
sessionId: this.id,
|
|
1724
|
+
finalText,
|
|
1725
|
+
...(usage ? { usage } : {}),
|
|
1726
|
+
timeline: [],
|
|
1727
|
+
canceled: false,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
async runToolLoop(turn) {
|
|
1731
|
+
// Warm the context-window cache before streaming: emitStreamUsageUpdated
|
|
1732
|
+
// runs on the hot chunk path and can only read the cached value, and a
|
|
1733
|
+
// usage_updated without contextWindowMaxTokens blanks the client's
|
|
1734
|
+
// context ring (agent.lastUsage is replaced wholesale, and the ring
|
|
1735
|
+
// needs both bounds). Cached per model, so this is one probe per session.
|
|
1736
|
+
await this.resolveContextWindowMaxTokens();
|
|
1737
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
1738
|
+
// Round 0 was already checked at turn start; re-check between tool
|
|
1739
|
+
// rounds so a long tool loop compacts mid-turn instead of overflowing.
|
|
1740
|
+
if (round > 0) {
|
|
1741
|
+
await this.maybeAutoCompact(turn);
|
|
1742
|
+
}
|
|
1743
|
+
await this.streamCompletion(turn);
|
|
1744
|
+
const toolCalls = [...turn.pendingToolCalls.entries()]
|
|
1745
|
+
.sort(([a], [b]) => a - b)
|
|
1746
|
+
.map(([, call]) => call);
|
|
1747
|
+
if (turn.roundText || turn.roundReasoning || toolCalls.length > 0) {
|
|
1748
|
+
this.messages.push({
|
|
1749
|
+
role: "assistant",
|
|
1750
|
+
content: turn.roundText,
|
|
1751
|
+
...(turn.roundReasoning ? { reasoning: turn.roundReasoning } : {}),
|
|
1752
|
+
...(toolCalls.length > 0
|
|
1753
|
+
? {
|
|
1754
|
+
tool_calls: toolCalls.map((call) => ({
|
|
1755
|
+
id: call.id,
|
|
1756
|
+
type: "function",
|
|
1757
|
+
function: { name: call.name, arguments: call.argumentsJson },
|
|
1758
|
+
})),
|
|
1759
|
+
}
|
|
1760
|
+
: {}),
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
if (turn.roundText) {
|
|
1764
|
+
turn.finalTextParts.push(turn.roundText);
|
|
1765
|
+
}
|
|
1766
|
+
// Consumed — settleTurnFailure must not re-append these if a later
|
|
1767
|
+
// tool round gets interrupted.
|
|
1768
|
+
turn.roundText = "";
|
|
1769
|
+
turn.roundReasoning = "";
|
|
1770
|
+
if (toolCalls.length === 0) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
for (const call of toolCalls) {
|
|
1774
|
+
if (turn.abort.signal.aborted) {
|
|
1775
|
+
throw new Error("Interrupted");
|
|
1776
|
+
}
|
|
1777
|
+
const resultContent = await this.executeToolCall(turn, call);
|
|
1778
|
+
this.messages.push({ role: "tool", content: resultContent, tool_call_id: call.id });
|
|
1779
|
+
}
|
|
1780
|
+
if (turn.abort.signal.aborted) {
|
|
1781
|
+
throw new Error("Interrupted");
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
this.emit({
|
|
1785
|
+
type: "timeline",
|
|
1786
|
+
provider: this.provider,
|
|
1787
|
+
turnId: turn.turnId,
|
|
1788
|
+
item: {
|
|
1789
|
+
type: "error",
|
|
1790
|
+
message: `Stopped after ${MAX_TOOL_ROUNDS} tool rounds without a final answer.`,
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
parseToolCallArgs(turn, call) {
|
|
1795
|
+
try {
|
|
1796
|
+
const parsed = call.argumentsJson ? JSON.parse(call.argumentsJson) : {};
|
|
1797
|
+
return {
|
|
1798
|
+
args: parsed && typeof parsed === "object" ? parsed : {},
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
catch {
|
|
1802
|
+
this.emitToolItem(turn, call, "failed", buildCompatToolPreviewDetail(call.name, {}, this.cwd), "Invalid tool arguments (malformed JSON)");
|
|
1803
|
+
return { error: `Error: arguments for ${call.name} were not valid JSON` };
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async executeToolCall(turn, call) {
|
|
1807
|
+
const spec = findCompatToolSpec(call.name);
|
|
1808
|
+
const parsed = this.parseToolCallArgs(turn, call);
|
|
1809
|
+
if ("error" in parsed) {
|
|
1810
|
+
return parsed.error;
|
|
1811
|
+
}
|
|
1812
|
+
const args = parsed.args;
|
|
1813
|
+
// Otto catalog tools are dispatched to the catalog, not the builtin coding
|
|
1814
|
+
// tools. Builtins take precedence on name; plan mode offers neither actions
|
|
1815
|
+
// nor Otto tools, so a plan-mode call for one falls through to "not available".
|
|
1816
|
+
if (!spec && this.modeId !== "plan") {
|
|
1817
|
+
const ottoTool = this.isOttoToolGroupEnabled(call.name)
|
|
1818
|
+
? this.ottoTools?.getTool(call.name)
|
|
1819
|
+
: undefined;
|
|
1820
|
+
if (ottoTool) {
|
|
1821
|
+
return this.executeOttoToolCall(turn, call, args, ottoTool);
|
|
1822
|
+
}
|
|
1823
|
+
// MCP tools live under namespaced mcp_{server}_{tool} names, so they can
|
|
1824
|
+
// never collide with (or shadow) builtins or Otto tools.
|
|
1825
|
+
const mcpBinding = this.mcpManager?.resolveTool(call.name);
|
|
1826
|
+
if (mcpBinding) {
|
|
1827
|
+
return this.executeMcpToolCall(turn, call, args, mcpBinding);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const previewDetail = buildCompatToolPreviewDetail(call.name, args, this.cwd);
|
|
1831
|
+
if (!spec || !this.availableToolSpecs().some((candidate) => candidate.name === spec.name)) {
|
|
1832
|
+
this.emitToolItem(turn, call, "failed", previewDetail, `Tool ${call.name} is not available`);
|
|
1833
|
+
return `Error: tool ${call.name} is not available in the current mode`;
|
|
1834
|
+
}
|
|
1835
|
+
if (this.toolNeedsApproval(spec, args)) {
|
|
1836
|
+
const response = await this.requestPermission(turn, {
|
|
1837
|
+
name: spec.name,
|
|
1838
|
+
title: spec.name,
|
|
1839
|
+
description: COMPAT_TOOL_PROMPT_DESCRIPTIONS[spec.kind],
|
|
1840
|
+
args,
|
|
1841
|
+
detail: previewDetail,
|
|
1842
|
+
});
|
|
1843
|
+
if (response.behavior === "deny") {
|
|
1844
|
+
this.emitToolItem(turn, call, "failed", previewDetail, "Denied by user");
|
|
1845
|
+
if (response.interrupt) {
|
|
1846
|
+
turn.abort.abort();
|
|
1847
|
+
}
|
|
1848
|
+
const message = response.message?.trim();
|
|
1849
|
+
return message
|
|
1850
|
+
? `The user declined this tool call: ${message}`
|
|
1851
|
+
: "The user declined this tool call.";
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
this.emitToolItem(turn, call, "running", previewDetail, null);
|
|
1855
|
+
const outcome = await executeCompatTool({
|
|
1856
|
+
name: call.name,
|
|
1857
|
+
arguments: args,
|
|
1858
|
+
cwd: this.cwd,
|
|
1859
|
+
signal: turn.abort.signal,
|
|
1860
|
+
});
|
|
1861
|
+
if (turn.abort.signal.aborted) {
|
|
1862
|
+
this.emitToolItem(turn, call, "canceled", outcome.detail, null);
|
|
1863
|
+
return outcome.output;
|
|
1864
|
+
}
|
|
1865
|
+
this.emitToolItem(turn, call, outcome.isError ? "failed" : "completed", outcome.detail, outcome.isError ? outcome.output : null);
|
|
1866
|
+
return outcome.output;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* MCP tools are opaque — the daemon cannot know whether one is destructive.
|
|
1870
|
+
* default mode always asks; acceptEdits asks unless the provider opted into
|
|
1871
|
+
* "trust-read-only" AND the server self-declares readOnlyHint (that
|
|
1872
|
+
* annotation is untrusted, so it never skips prompts in default mode);
|
|
1873
|
+
* bypassPermissions auto-approves like everything else. Plan mode never
|
|
1874
|
+
* reaches here — MCP tools are excluded from the payload entirely.
|
|
1875
|
+
*/
|
|
1876
|
+
mcpToolNeedsApproval(binding) {
|
|
1877
|
+
if (this.modeId === "bypassPermissions")
|
|
1878
|
+
return false;
|
|
1879
|
+
if (this.modeId === "acceptEdits") {
|
|
1880
|
+
return !(this.mcpToolPermissions === "trust-read-only" && binding.readOnlyHint);
|
|
1881
|
+
}
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
async executeMcpToolCall(turn, call, args, binding) {
|
|
1885
|
+
const manager = this.mcpManager;
|
|
1886
|
+
if (!manager) {
|
|
1887
|
+
return `Error: tool ${call.name} is not available`;
|
|
1888
|
+
}
|
|
1889
|
+
const detail = { type: "unknown", input: args, output: null };
|
|
1890
|
+
if (this.mcpToolNeedsApproval(binding)) {
|
|
1891
|
+
const response = await this.requestPermission(turn, {
|
|
1892
|
+
name: binding.modelName,
|
|
1893
|
+
title: `${binding.serverName}: ${binding.toolName}`,
|
|
1894
|
+
description: `Wants to run an MCP tool from server '${binding.serverName}'`,
|
|
1895
|
+
args,
|
|
1896
|
+
detail,
|
|
1897
|
+
});
|
|
1898
|
+
if (response.behavior === "deny") {
|
|
1899
|
+
this.emitToolItem(turn, call, "failed", detail, "Denied by user");
|
|
1900
|
+
if (response.interrupt) {
|
|
1901
|
+
turn.abort.abort();
|
|
1902
|
+
}
|
|
1903
|
+
const message = response.message?.trim();
|
|
1904
|
+
return message
|
|
1905
|
+
? `The user declined this tool call: ${message}`
|
|
1906
|
+
: "The user declined this tool call.";
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
this.emitToolItem(turn, call, "running", detail, null);
|
|
1910
|
+
const outcome = await manager.callTool(binding, args, { signal: turn.abort.signal });
|
|
1911
|
+
if (turn.abort.signal.aborted) {
|
|
1912
|
+
this.emitToolItem(turn, call, "canceled", { ...detail, output: outcome.output }, null);
|
|
1913
|
+
return outcome.output;
|
|
1914
|
+
}
|
|
1915
|
+
this.emitToolItem(turn, call, outcome.isError ? "failed" : "completed", { ...detail, output: outcome.output }, outcome.isError ? outcome.output : null);
|
|
1916
|
+
return outcome.output;
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Otto tools skip prompts only when classified read-only; "interact" tools
|
|
1920
|
+
* are auto-approved in acceptEdits like file edits, and "execute" tools
|
|
1921
|
+
* always prompt outside bypassPermissions. CLI providers get this gating
|
|
1922
|
+
* from their own permission system in front of the MCP client — here the
|
|
1923
|
+
* daemon is the runtime, so it prompts itself.
|
|
1924
|
+
* See openai-compat-otto-tool-permissions.ts.
|
|
1925
|
+
*/
|
|
1926
|
+
ottoToolNeedsApproval(name) {
|
|
1927
|
+
const kind = ottoToolPermissionKind(name);
|
|
1928
|
+
if (kind === "read")
|
|
1929
|
+
return false;
|
|
1930
|
+
if (this.modeId === "bypassPermissions")
|
|
1931
|
+
return false;
|
|
1932
|
+
if (this.modeId === "acceptEdits")
|
|
1933
|
+
return kind === "execute";
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
1936
|
+
async executeOttoToolCall(turn, call, args, tool) {
|
|
1937
|
+
const catalog = this.ottoTools;
|
|
1938
|
+
if (!catalog) {
|
|
1939
|
+
return `Error: tool ${call.name} is not available`;
|
|
1940
|
+
}
|
|
1941
|
+
const detail = { type: "unknown", input: args, output: null };
|
|
1942
|
+
if (this.ottoToolNeedsApproval(tool.name)) {
|
|
1943
|
+
const response = await this.requestPermission(turn, {
|
|
1944
|
+
name: tool.name,
|
|
1945
|
+
title: tool.name,
|
|
1946
|
+
description: ottoToolPermissionKind(tool.name) === "execute"
|
|
1947
|
+
? "Wants to run an Otto tool that can execute code or manage agents"
|
|
1948
|
+
: "Wants to interact with the Otto browser or preview servers",
|
|
1949
|
+
args,
|
|
1950
|
+
detail,
|
|
1951
|
+
});
|
|
1952
|
+
if (response.behavior === "deny") {
|
|
1953
|
+
this.emitToolItem(turn, call, "failed", detail, "Denied by user");
|
|
1954
|
+
if (response.interrupt) {
|
|
1955
|
+
turn.abort.abort();
|
|
1956
|
+
}
|
|
1957
|
+
const message = response.message?.trim();
|
|
1958
|
+
return message
|
|
1959
|
+
? `The user declined this tool call: ${message}`
|
|
1960
|
+
: "The user declined this tool call.";
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
this.emitToolItem(turn, call, "running", { type: "unknown", input: args, output: null }, null);
|
|
1964
|
+
try {
|
|
1965
|
+
const result = await catalog.executeTool(tool.name, args, { signal: turn.abort.signal });
|
|
1966
|
+
const output = ottoResultToText(result);
|
|
1967
|
+
if (turn.abort.signal.aborted) {
|
|
1968
|
+
this.emitToolItem(turn, call, "canceled", { type: "unknown", input: args, output }, null);
|
|
1969
|
+
return output;
|
|
1970
|
+
}
|
|
1971
|
+
this.emitToolItem(turn, call, result.isError ? "failed" : "completed", { type: "unknown", input: args, output }, result.isError ? output : null);
|
|
1972
|
+
return output;
|
|
1973
|
+
}
|
|
1974
|
+
catch (error) {
|
|
1975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1976
|
+
this.emitToolItem(turn, call, "failed", { type: "unknown", input: args, output: null }, message);
|
|
1977
|
+
return `Error: ${message}`;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
emitToolItem(turn, call, status, detail, error) {
|
|
1981
|
+
this.emit({
|
|
1982
|
+
type: "timeline",
|
|
1983
|
+
provider: this.provider,
|
|
1984
|
+
turnId: turn.turnId,
|
|
1985
|
+
item: status === "failed"
|
|
1986
|
+
? {
|
|
1987
|
+
type: "tool_call",
|
|
1988
|
+
callId: call.id,
|
|
1989
|
+
name: call.name,
|
|
1990
|
+
detail,
|
|
1991
|
+
status,
|
|
1992
|
+
error: error ?? "Tool failed",
|
|
1993
|
+
}
|
|
1994
|
+
: { type: "tool_call", callId: call.id, name: call.name, detail, status, error: null },
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
async requestPermission(turn, input) {
|
|
1998
|
+
const request = {
|
|
1999
|
+
id: randomUUID(),
|
|
2000
|
+
provider: this.provider,
|
|
2001
|
+
name: input.name,
|
|
2002
|
+
kind: "tool",
|
|
2003
|
+
title: input.title,
|
|
2004
|
+
description: input.description,
|
|
2005
|
+
input: input.args,
|
|
2006
|
+
detail: input.detail,
|
|
2007
|
+
};
|
|
2008
|
+
const response = await new Promise((resolve) => {
|
|
2009
|
+
this.pendingPermissions.set(request.id, { request, resolve });
|
|
2010
|
+
this.emit({
|
|
2011
|
+
type: "permission_requested",
|
|
2012
|
+
provider: this.provider,
|
|
2013
|
+
request,
|
|
2014
|
+
turnId: turn.turnId,
|
|
2015
|
+
});
|
|
2016
|
+
});
|
|
2017
|
+
return response;
|
|
2018
|
+
}
|
|
2019
|
+
async streamCompletion(turn) {
|
|
2020
|
+
const endpoint = resolveEndpoint(this.env, this.label);
|
|
2021
|
+
const model = await this.resolveModel(endpoint);
|
|
2022
|
+
turn.assistantMessageId = randomUUID();
|
|
2023
|
+
turn.roundText = "";
|
|
2024
|
+
turn.roundReasoning = "";
|
|
2025
|
+
turn.pendingToolCalls = new Map();
|
|
2026
|
+
turn.finishReason = null;
|
|
2027
|
+
const toolSpecs = this.availableToolSpecs();
|
|
2028
|
+
const toolsPayload = [
|
|
2029
|
+
...buildOpenAIToolsPayload(toolSpecs),
|
|
2030
|
+
...this.buildOttoToolPayload(),
|
|
2031
|
+
...this.buildMcpToolPayload(),
|
|
2032
|
+
];
|
|
2033
|
+
const response = await fetch(`${endpoint.baseUrl}/chat/completions`, {
|
|
2034
|
+
method: "POST",
|
|
2035
|
+
headers: buildHeaders(endpoint),
|
|
2036
|
+
signal: turn.abort.signal,
|
|
2037
|
+
body: JSON.stringify({
|
|
2038
|
+
model,
|
|
2039
|
+
messages: this.messages.map(toWireMessage),
|
|
2040
|
+
stream: true,
|
|
2041
|
+
stream_options: { include_usage: true },
|
|
2042
|
+
...(this.reasoningEffort !== "off" ? { reasoning_effort: this.reasoningEffort } : {}),
|
|
2043
|
+
...(toolsPayload.length > 0 ? { tools: toolsPayload, tool_choice: "auto" } : {}),
|
|
2044
|
+
}),
|
|
2045
|
+
});
|
|
2046
|
+
if (!response.ok) {
|
|
2047
|
+
const bodyText = await response.text().catch(() => "");
|
|
2048
|
+
throw new Error(`${this.label} responded ${response.status} to /chat/completions${bodyText ? `: ${bodyText.slice(0, 400)}` : ""}`);
|
|
2049
|
+
}
|
|
2050
|
+
if (!response.body) {
|
|
2051
|
+
throw new Error(`${this.label} returned an empty response body`);
|
|
2052
|
+
}
|
|
2053
|
+
for await (const line of readLines(response.body)) {
|
|
2054
|
+
if (!line.startsWith("data:"))
|
|
2055
|
+
continue;
|
|
2056
|
+
const payload = line.slice(5).trim();
|
|
2057
|
+
if (!payload || payload === "[DONE]")
|
|
2058
|
+
continue;
|
|
2059
|
+
this.applyStreamPayload(turn, payload);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
applyStreamPayload(turn, payload) {
|
|
2063
|
+
let parsed;
|
|
2064
|
+
try {
|
|
2065
|
+
parsed = JSON.parse(payload);
|
|
2066
|
+
}
|
|
2067
|
+
catch {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
const delta = parseStreamChunk(parsed);
|
|
2071
|
+
if (delta.reasoning) {
|
|
2072
|
+
turn.roundReasoning += delta.reasoning;
|
|
2073
|
+
this.emit({
|
|
2074
|
+
type: "timeline",
|
|
2075
|
+
provider: this.provider,
|
|
2076
|
+
turnId: turn.turnId,
|
|
2077
|
+
item: { type: "reasoning", text: delta.reasoning },
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
if (delta.content) {
|
|
2081
|
+
turn.roundText += delta.content;
|
|
2082
|
+
this.emit({
|
|
2083
|
+
type: "timeline",
|
|
2084
|
+
provider: this.provider,
|
|
2085
|
+
turnId: turn.turnId,
|
|
2086
|
+
item: {
|
|
2087
|
+
type: "assistant_message",
|
|
2088
|
+
text: delta.content,
|
|
2089
|
+
messageId: turn.assistantMessageId,
|
|
2090
|
+
},
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
for (const toolCallDelta of delta.toolCalls) {
|
|
2094
|
+
const existing = turn.pendingToolCalls.get(toolCallDelta.index) ?? {
|
|
2095
|
+
id: "",
|
|
2096
|
+
name: "",
|
|
2097
|
+
argumentsJson: "",
|
|
2098
|
+
};
|
|
2099
|
+
turn.pendingToolCalls.set(toolCallDelta.index, {
|
|
2100
|
+
id: toolCallDelta.id ?? (existing.id || `call_${toolCallDelta.index}`),
|
|
2101
|
+
name: existing.name || (toolCallDelta.name ?? ""),
|
|
2102
|
+
argumentsJson: existing.argumentsJson + (toolCallDelta.argumentsChunk ?? ""),
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
if (delta.finishReason) {
|
|
2106
|
+
turn.finishReason = delta.finishReason;
|
|
2107
|
+
}
|
|
2108
|
+
if (delta.usage) {
|
|
2109
|
+
turn.usage = delta.usage;
|
|
2110
|
+
this.emitStreamUsageUpdated(turn);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Emit a usage_updated event so the agent manager updates agent.lastUsage
|
|
2115
|
+
* and pushes the context usage to the client immediately, rather than
|
|
2116
|
+
* waiting for turn_completed at the end of the entire tool loop.
|
|
2117
|
+
*
|
|
2118
|
+
* resolveContextWindowMaxTokens() is async (it may probe the endpoint) and
|
|
2119
|
+
* this runs on the hot streaming path, so only the already-cached value is
|
|
2120
|
+
* included. The event is always emitted regardless — the client and agent
|
|
2121
|
+
* manager handle partial usage snapshots correctly.
|
|
2122
|
+
*/
|
|
2123
|
+
emitStreamUsageUpdated(turn) {
|
|
2124
|
+
if (!turn.usage || typeof turn.usage.inputTokens !== "number") {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
const usage = {
|
|
2128
|
+
...turn.usage,
|
|
2129
|
+
contextWindowUsedTokens: turn.usage.inputTokens + (turn.usage.outputTokens ?? 0),
|
|
2130
|
+
};
|
|
2131
|
+
if (this.contextWindowMaxTokens !== null) {
|
|
2132
|
+
usage.contextWindowMaxTokens = this.contextWindowMaxTokens;
|
|
2133
|
+
}
|
|
2134
|
+
this.emit({
|
|
2135
|
+
type: "usage_updated",
|
|
2136
|
+
provider: this.provider,
|
|
2137
|
+
usage,
|
|
2138
|
+
turnId: turn.turnId,
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* A turn can end (interrupt or failure) between an assistant message's
|
|
2143
|
+
* tool_calls and their tool results — e.g. the user interrupts during call
|
|
2144
|
+
* #1 of 3. Strict OpenAI-compatible servers reject the next request over
|
|
2145
|
+
* such a conversation with a 400, so append synthetic results for the
|
|
2146
|
+
* unanswered calls. Resume gets the equivalent repair from
|
|
2147
|
+
* sanitizeRestoredMessages; this covers the live conversation.
|
|
2148
|
+
*/
|
|
2149
|
+
repairDanglingToolCalls() {
|
|
2150
|
+
// Only the trailing round can dangle: earlier rounds pushed all their
|
|
2151
|
+
// results before the loop moved on.
|
|
2152
|
+
for (let index = this.messages.length - 1; index >= 0; index -= 1) {
|
|
2153
|
+
const message = this.messages[index];
|
|
2154
|
+
if (message.role === "user") {
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
if (message.role !== "assistant" || !message.tool_calls) {
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
const answered = new Set(this.messages
|
|
2161
|
+
.slice(index + 1)
|
|
2162
|
+
.flatMap((candidate) => (candidate.role === "tool" ? [candidate.tool_call_id] : [])));
|
|
2163
|
+
for (const call of message.tool_calls) {
|
|
2164
|
+
if (!answered.has(call.id)) {
|
|
2165
|
+
this.messages.push({
|
|
2166
|
+
role: "tool",
|
|
2167
|
+
content: "[Tool call was interrupted before it completed]",
|
|
2168
|
+
tool_call_id: call.id,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
settleTurnFailure(turn, error) {
|
|
2176
|
+
if (this.activeTurn !== turn) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
this.activeTurn = null;
|
|
2180
|
+
this.failPendingPermissions();
|
|
2181
|
+
this.repairDanglingToolCalls();
|
|
2182
|
+
if (turn.abort.signal.aborted) {
|
|
2183
|
+
this.emit({
|
|
2184
|
+
type: "turn_canceled",
|
|
2185
|
+
provider: this.provider,
|
|
2186
|
+
reason: "Interrupted",
|
|
2187
|
+
turnId: turn.turnId,
|
|
2188
|
+
});
|
|
2189
|
+
if (turn.roundText || turn.roundReasoning) {
|
|
2190
|
+
if (turn.roundText) {
|
|
2191
|
+
turn.finalTextParts.push(turn.roundText);
|
|
2192
|
+
}
|
|
2193
|
+
this.messages.push({
|
|
2194
|
+
role: "assistant",
|
|
2195
|
+
content: turn.roundText,
|
|
2196
|
+
...(turn.roundReasoning ? { reasoning: turn.roundReasoning } : {}),
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
turn.resolve({
|
|
2200
|
+
sessionId: this.id,
|
|
2201
|
+
finalText: turn.finalTextParts.join("\n\n"),
|
|
2202
|
+
timeline: [],
|
|
2203
|
+
canceled: true,
|
|
2204
|
+
});
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2208
|
+
this.logger?.warn({ err: error, provider: this.provider }, "OpenAI-compatible turn failed");
|
|
2209
|
+
this.emit({
|
|
2210
|
+
type: "timeline",
|
|
2211
|
+
provider: this.provider,
|
|
2212
|
+
turnId: turn.turnId,
|
|
2213
|
+
item: { type: "error", message },
|
|
2214
|
+
});
|
|
2215
|
+
this.emit({
|
|
2216
|
+
type: "turn_failed",
|
|
2217
|
+
provider: this.provider,
|
|
2218
|
+
error: message,
|
|
2219
|
+
turnId: turn.turnId,
|
|
2220
|
+
});
|
|
2221
|
+
turn.reject(error instanceof Error ? error : new Error(message));
|
|
2222
|
+
}
|
|
2223
|
+
failPendingPermissions() {
|
|
2224
|
+
// Map iterators tolerate deleting the current entry mid-iteration.
|
|
2225
|
+
for (const [requestId, pending] of this.pendingPermissions) {
|
|
2226
|
+
this.pendingPermissions.delete(requestId);
|
|
2227
|
+
pending.resolve({ behavior: "deny", message: "Turn ended", interrupt: false });
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
async resolveModel(endpoint) {
|
|
2231
|
+
if (this.modelId) {
|
|
2232
|
+
return this.modelId;
|
|
2233
|
+
}
|
|
2234
|
+
let response;
|
|
2235
|
+
try {
|
|
2236
|
+
response = await fetch(`${endpoint.baseUrl}/models`, {
|
|
2237
|
+
headers: buildHeaders(endpoint),
|
|
2238
|
+
signal: AbortSignal.timeout(DEFAULT_CATALOG_TIMEOUT_MS),
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
catch (error) {
|
|
2242
|
+
throw unreachableError(this.label, endpoint, error);
|
|
2243
|
+
}
|
|
2244
|
+
const modelIds = response.ok ? parseModelList(await response.json()) : [];
|
|
2245
|
+
const first = modelIds[0];
|
|
2246
|
+
if (!first) {
|
|
2247
|
+
throw new Error(`${this.label} has no models loaded. Load a model in the server, then try again.`);
|
|
2248
|
+
}
|
|
2249
|
+
this.modelId = first;
|
|
2250
|
+
return first;
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Best-effort context window discovery, cached per model. Checks the
|
|
2254
|
+
* standard /v1/models listing for extended context-length fields first,
|
|
2255
|
+
* then LM Studio's native /api/v0/models listing (same host, /v1 stripped),
|
|
2256
|
+
* which reports the loaded instance's actual window. Servers that expose
|
|
2257
|
+
* neither leave the meter without a max — never an error.
|
|
2258
|
+
*/
|
|
2259
|
+
async resolveContextWindowMaxTokens() {
|
|
2260
|
+
const model = this.modelId;
|
|
2261
|
+
if (!model) {
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
if (this.contextWindowProbedModel === model) {
|
|
2265
|
+
return this.contextWindowMaxTokens;
|
|
2266
|
+
}
|
|
2267
|
+
let endpoint;
|
|
2268
|
+
try {
|
|
2269
|
+
endpoint = resolveEndpoint(this.env, this.label);
|
|
2270
|
+
}
|
|
2271
|
+
catch {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
const candidateUrls = [
|
|
2275
|
+
`${endpoint.baseUrl}/models`,
|
|
2276
|
+
`${endpoint.baseUrl.replace(/\/v1$/u, "")}/api/v0/models`,
|
|
2277
|
+
];
|
|
2278
|
+
let resolved = null;
|
|
2279
|
+
for (const url of candidateUrls) {
|
|
2280
|
+
try {
|
|
2281
|
+
const response = await fetch(url, {
|
|
2282
|
+
headers: buildHeaders(endpoint),
|
|
2283
|
+
signal: AbortSignal.timeout(DEFAULT_CATALOG_TIMEOUT_MS),
|
|
2284
|
+
});
|
|
2285
|
+
if (!response.ok)
|
|
2286
|
+
continue;
|
|
2287
|
+
const match = parseModelContextLengths(await response.json()).get(model);
|
|
2288
|
+
if (typeof match === "number") {
|
|
2289
|
+
resolved = match;
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
catch {
|
|
2294
|
+
// Discovery is optional; unreachable probe endpoints are expected.
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
this.contextWindowMaxTokens = resolved;
|
|
2298
|
+
this.contextWindowProbedModel = model;
|
|
2299
|
+
return resolved;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Promote the raw per-request token counts into AgentUsage. The final
|
|
2303
|
+
* round's prompt already contains the full conversation, so prompt +
|
|
2304
|
+
* completion tokens is the context content size as the server measured it.
|
|
2305
|
+
*/
|
|
2306
|
+
async buildTurnUsage(turn) {
|
|
2307
|
+
if (!turn.usage) {
|
|
2308
|
+
return undefined;
|
|
2309
|
+
}
|
|
2310
|
+
const usage = { ...turn.usage };
|
|
2311
|
+
if (typeof turn.usage.inputTokens === "number") {
|
|
2312
|
+
const usedTokens = turn.usage.inputTokens + (turn.usage.outputTokens ?? 0);
|
|
2313
|
+
usage.contextWindowUsedTokens = usedTokens;
|
|
2314
|
+
this.lastContextTokens = usedTokens;
|
|
2315
|
+
}
|
|
2316
|
+
const maxTokens = await this.resolveContextWindowMaxTokens();
|
|
2317
|
+
if (maxTokens !== null) {
|
|
2318
|
+
usage.contextWindowMaxTokens = maxTokens;
|
|
2319
|
+
}
|
|
2320
|
+
return usage;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Context makeup for the client's popup. The daemon owns this provider's
|
|
2324
|
+
* entire prompt (system message, tool schemas, conversation), so the split
|
|
2325
|
+
* is computed locally: char-based estimates per category, scaled so the
|
|
2326
|
+
* counted total matches the server-measured context size when one exists.
|
|
2327
|
+
*/
|
|
2328
|
+
async getContextUsage() {
|
|
2329
|
+
const maxTokens = await this.resolveContextWindowMaxTokens();
|
|
2330
|
+
if (maxTokens === null) {
|
|
2331
|
+
return null;
|
|
2332
|
+
}
|
|
2333
|
+
const systemMessage = this.messages.find((message) => message.role === "system");
|
|
2334
|
+
const conversation = this.messages.filter((message) => message.role !== "system");
|
|
2335
|
+
const toolsPayload = [
|
|
2336
|
+
...buildOpenAIToolsPayload(this.availableToolSpecs()),
|
|
2337
|
+
...this.buildOttoToolPayload(),
|
|
2338
|
+
...this.buildMcpToolPayload(),
|
|
2339
|
+
];
|
|
2340
|
+
let categories = [
|
|
2341
|
+
{
|
|
2342
|
+
name: "Messages",
|
|
2343
|
+
tokens: conversation.reduce((sum, message) => sum + estimateTokens(JSON.stringify(message)), 0),
|
|
2344
|
+
},
|
|
2345
|
+
{ name: "Tools", tokens: estimateTokens(JSON.stringify(toolsPayload)) },
|
|
2346
|
+
{ name: "System prompt", tokens: estimateTokens(systemMessage?.content ?? "") },
|
|
2347
|
+
];
|
|
2348
|
+
let totalTokens = categories.reduce((sum, category) => sum + category.tokens, 0);
|
|
2349
|
+
if (this.lastContextTokens !== null && totalTokens > 0) {
|
|
2350
|
+
const scale = this.lastContextTokens / totalTokens;
|
|
2351
|
+
categories = categories.map((category) => ({
|
|
2352
|
+
...category,
|
|
2353
|
+
tokens: Math.round(category.tokens * scale),
|
|
2354
|
+
}));
|
|
2355
|
+
totalTokens = this.lastContextTokens;
|
|
2356
|
+
}
|
|
2357
|
+
return {
|
|
2358
|
+
categories: categories.filter((category) => category.tokens > 0),
|
|
2359
|
+
totalTokens,
|
|
2360
|
+
maxTokens,
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
subscribe(callback) {
|
|
2364
|
+
this.listeners.add(callback);
|
|
2365
|
+
return () => {
|
|
2366
|
+
this.listeners.delete(callback);
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
async *streamHistory() {
|
|
2370
|
+
for (const event of this.eventHistory) {
|
|
2371
|
+
yield event;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
async getRuntimeInfo() {
|
|
2375
|
+
return {
|
|
2376
|
+
provider: this.provider,
|
|
2377
|
+
sessionId: this.id,
|
|
2378
|
+
model: this.modelId,
|
|
2379
|
+
modeId: this.modeId,
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
async getAvailableModes() {
|
|
2383
|
+
return OPENAI_COMPAT_MODES;
|
|
2384
|
+
}
|
|
2385
|
+
async getCurrentMode() {
|
|
2386
|
+
return this.modeId;
|
|
2387
|
+
}
|
|
2388
|
+
async setMode(modeId) {
|
|
2389
|
+
if (!VALID_MODE_IDS.has(modeId)) {
|
|
2390
|
+
throw new Error(`Unknown mode: ${modeId}`);
|
|
2391
|
+
}
|
|
2392
|
+
this.modeId = modeId;
|
|
2393
|
+
this.emit({
|
|
2394
|
+
type: "mode_changed",
|
|
2395
|
+
provider: this.provider,
|
|
2396
|
+
currentModeId: modeId,
|
|
2397
|
+
availableModes: OPENAI_COMPAT_MODES,
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
getPendingPermissions() {
|
|
2401
|
+
return [...this.pendingPermissions.values()].map((pending) => pending.request);
|
|
2402
|
+
}
|
|
2403
|
+
async respondToPermission(requestId, response) {
|
|
2404
|
+
const pending = this.pendingPermissions.get(requestId);
|
|
2405
|
+
if (!pending) {
|
|
2406
|
+
return undefined;
|
|
2407
|
+
}
|
|
2408
|
+
this.pendingPermissions.delete(requestId);
|
|
2409
|
+
this.emit({
|
|
2410
|
+
type: "permission_resolved",
|
|
2411
|
+
provider: this.provider,
|
|
2412
|
+
requestId,
|
|
2413
|
+
resolution: response,
|
|
2414
|
+
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
2415
|
+
});
|
|
2416
|
+
pending.resolve(response);
|
|
2417
|
+
return undefined;
|
|
2418
|
+
}
|
|
2419
|
+
describePersistence() {
|
|
2420
|
+
return {
|
|
2421
|
+
provider: this.provider,
|
|
2422
|
+
sessionId: this.id,
|
|
2423
|
+
metadata: {
|
|
2424
|
+
model: this.modelId,
|
|
2425
|
+
modeId: this.modeId,
|
|
2426
|
+
messages: this.messages,
|
|
2427
|
+
},
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
async setModel(modelId) {
|
|
2431
|
+
this.modelId = modelId;
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Rewind the daemon-owned conversation to just before the given user
|
|
2435
|
+
* message. The full conversation persists (no message cap), so any
|
|
2436
|
+
* previous user message is a valid rewind target — the not-found error
|
|
2437
|
+
* below only fires for a truly unknown messageId.
|
|
2438
|
+
*/
|
|
2439
|
+
async revertConversation(input) {
|
|
2440
|
+
if (this.activeTurn) {
|
|
2441
|
+
throw new Error(`${this.label} cannot rewind while a turn is running`);
|
|
2442
|
+
}
|
|
2443
|
+
const index = this.messages.findIndex((message) => message.role === "user" && message.messageId === input.messageId);
|
|
2444
|
+
if (index === -1) {
|
|
2445
|
+
throw new Error("Message not found in this session's conversation — it may predate rewind support or was trimmed from persisted history.");
|
|
2446
|
+
}
|
|
2447
|
+
const retained = sanitizeRestoredMessages(this.messages.slice(0, index));
|
|
2448
|
+
this.messages.length = 0;
|
|
2449
|
+
this.messages.push(...retained);
|
|
2450
|
+
this.rebuildEventHistory();
|
|
2451
|
+
this.lastContextTokens = null;
|
|
2452
|
+
}
|
|
2453
|
+
async interrupt() {
|
|
2454
|
+
const turn = this.activeTurn;
|
|
2455
|
+
if (!turn) {
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
turn.abort.abort();
|
|
2459
|
+
// A turn parked on a permission prompt has no in-flight fetch to abort —
|
|
2460
|
+
// deny the prompt so the loop unwinds and settles as canceled.
|
|
2461
|
+
this.failPendingPermissions();
|
|
2462
|
+
}
|
|
2463
|
+
async close() {
|
|
2464
|
+
await this.interrupt();
|
|
2465
|
+
await this.mcpManager?.close();
|
|
2466
|
+
this.listeners.clear();
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Retain what streamHistory replays: user messages, assistant messages
|
|
2470
|
+
* (coalesced per messageId so streamed deltas don't accumulate one event
|
|
2471
|
+
* each), reasoning (coalesced per contiguous block), and tool calls
|
|
2472
|
+
* (coalesced per callId so a call's running → completed/failed/canceled
|
|
2473
|
+
* transitions collapse to its latest status instead of replaying every
|
|
2474
|
+
* intermediate state). Usage and permission events aren't part of the
|
|
2475
|
+
* displayed transcript and are dropped. Copies are stored so coalescing
|
|
2476
|
+
* never mutates an event a listener already received.
|
|
2477
|
+
*/
|
|
2478
|
+
retainForHistory(event) {
|
|
2479
|
+
if (event.type !== "timeline") {
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const item = event.item;
|
|
2483
|
+
if (item.type === "user_message") {
|
|
2484
|
+
this.eventHistory.push({ ...event, item: { ...item } });
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
if (item.type === "tool_call") {
|
|
2488
|
+
const existingIdx = this.eventHistory.findIndex((candidate) => candidate.type === "timeline" &&
|
|
2489
|
+
candidate.item.type === "tool_call" &&
|
|
2490
|
+
candidate.item.callId === item.callId);
|
|
2491
|
+
const copy = { ...event, item: { ...item } };
|
|
2492
|
+
if (existingIdx >= 0) {
|
|
2493
|
+
this.eventHistory[existingIdx] = copy;
|
|
2494
|
+
}
|
|
2495
|
+
else {
|
|
2496
|
+
this.eventHistory.push(copy);
|
|
2497
|
+
}
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (item.type === "reasoning") {
|
|
2501
|
+
// Reasoning deltas stream contiguously within a round, so appending to
|
|
2502
|
+
// a trailing reasoning event coalesces one block per round; assistant
|
|
2503
|
+
// text or a tool call in between starts the next block naturally.
|
|
2504
|
+
const last = this.eventHistory[this.eventHistory.length - 1];
|
|
2505
|
+
if (last?.type === "timeline" && last.item.type === "reasoning") {
|
|
2506
|
+
last.item.text += item.text;
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
this.eventHistory.push({ ...event, item: { ...item } });
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
if (item.type !== "assistant_message") {
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
const last = this.eventHistory[this.eventHistory.length - 1];
|
|
2516
|
+
if (last?.type === "timeline" &&
|
|
2517
|
+
last.item.type === "assistant_message" &&
|
|
2518
|
+
last.item.messageId === item.messageId) {
|
|
2519
|
+
last.item.text += item.text;
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
this.eventHistory.push({ ...event, item: { ...item } });
|
|
2523
|
+
}
|
|
2524
|
+
emit(event) {
|
|
2525
|
+
this.retainForHistory(event);
|
|
2526
|
+
for (const listener of this.listeners) {
|
|
2527
|
+
try {
|
|
2528
|
+
listener(event);
|
|
2529
|
+
}
|
|
2530
|
+
catch (error) {
|
|
2531
|
+
this.logger?.warn({ err: error }, "OpenAI-compatible listener failed");
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
//# sourceMappingURL=openai-compat-agent.js.map
|