create-walle 0.9.21 → 0.9.23
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/README.md +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
package/template/wall-e/chat.js
CHANGED
|
@@ -6,12 +6,34 @@ const {
|
|
|
6
6
|
getDefaultClient,
|
|
7
7
|
getDefaultModelForProvider,
|
|
8
8
|
getDefaultProviderType,
|
|
9
|
+
getProviderRuntimeConfig,
|
|
9
10
|
resolveCompatibleModel,
|
|
11
|
+
withProviderMessageGuard,
|
|
10
12
|
} = require('./llm/client');
|
|
13
|
+
const { canonicalModelId: canonicalSessionModelId } = require('./llm/model-catalog');
|
|
11
14
|
const { routeLabel } = require('./llm/portkey');
|
|
12
15
|
const { executeLocalTool, LOCAL_TOOL_DEFINITIONS, resolveProjectPath } = require('./tools/local-tools');
|
|
16
|
+
const { buildAttachmentBlocks } = require('./chat/attachment-blocks');
|
|
17
|
+
const ctmContextClient = require('./memory/ctm-context-client');
|
|
18
|
+
const ctmOperationalContext = require('./memory/ctm-operational-context');
|
|
13
19
|
const slackMcp = require('./tools/slack-mcp');
|
|
14
20
|
const { estimateTokens, estimateMessagesTokens } = require('./context/token-counter');
|
|
21
|
+
|
|
22
|
+
// search_memories returns total_memories / total_slack for context. These were two
|
|
23
|
+
// count(*) scans of the memories table on EVERY search (a hot MCP path). The totals
|
|
24
|
+
// barely move between searches, so cache them briefly.
|
|
25
|
+
let _searchTotalsCache = { at: 0, total_memories: 0, total_slack: 0 };
|
|
26
|
+
const _SEARCH_TOTALS_TTL_MS = 60 * 1000;
|
|
27
|
+
function _searchTotals(db) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (now - _searchTotalsCache.at < _SEARCH_TOTALS_TTL_MS) return _searchTotalsCache;
|
|
30
|
+
try {
|
|
31
|
+
const total_memories = db.prepare('SELECT count(*) as c FROM memories').get().c;
|
|
32
|
+
const total_slack = db.prepare('SELECT count(*) as c FROM memories WHERE source = ?').get('slack').c;
|
|
33
|
+
_searchTotalsCache = { at: now, total_memories, total_slack };
|
|
34
|
+
} catch { /* keep last good cache on error */ }
|
|
35
|
+
return _searchTotalsCache;
|
|
36
|
+
}
|
|
15
37
|
const { shouldCompact, compactToolResult, compactMessages } = require('./context/compactor');
|
|
16
38
|
const { buildSystemPrompt } = require('./context/context-builder');
|
|
17
39
|
const {
|
|
@@ -29,9 +51,19 @@ const { sanitizeQuery } = require('./context/query-sanitizer');
|
|
|
29
51
|
const { checkPermission, recordDecision, configure: configurePermissions } = require('./tools/permission-checker');
|
|
30
52
|
const {
|
|
31
53
|
inputForExternalActionEnvelope,
|
|
54
|
+
isExternalActionTool,
|
|
32
55
|
reviewExternalAction,
|
|
33
56
|
} = require('./external-action-controller');
|
|
34
57
|
const { reviewExternalActionGateway } = require('./external-action-gateway');
|
|
58
|
+
const {
|
|
59
|
+
hasVerificationEvidence,
|
|
60
|
+
normalizeToolCallEvidence,
|
|
61
|
+
} = require('./coding/acceptance-contract');
|
|
62
|
+
const {
|
|
63
|
+
analyzeLocalPreviewClaims,
|
|
64
|
+
buildLocalPreviewVerificationNudge,
|
|
65
|
+
buildUnsupportedLocalPreviewReply,
|
|
66
|
+
} = require('./coding/local-preview-contract');
|
|
35
67
|
const { runShadow } = require('./eval/shadow');
|
|
36
68
|
const {
|
|
37
69
|
buildCodeReviewContextBlock,
|
|
@@ -40,6 +72,15 @@ const {
|
|
|
40
72
|
looksLikePrematureCodeReviewReply,
|
|
41
73
|
} = require('./chat/code-review-context');
|
|
42
74
|
const { createSessionRecorder } = require('./session-files');
|
|
75
|
+
const { normalizeContextMessagesForChat } = require('./chat/context-messages');
|
|
76
|
+
const { buildProviderMessagesForChat } = require('./chat/provider-messages');
|
|
77
|
+
const {
|
|
78
|
+
buildConversationFramePrompt,
|
|
79
|
+
buildConversationRuntime,
|
|
80
|
+
buildFrameRepairPrompt,
|
|
81
|
+
parseSessionMetadata,
|
|
82
|
+
validateFrameAnswer,
|
|
83
|
+
} = require('./chat/conversation-frame');
|
|
43
84
|
const {
|
|
44
85
|
decorateProviderError,
|
|
45
86
|
recordProviderFailureAlert,
|
|
@@ -49,8 +90,17 @@ const { extractTextToolCalls, normalizeToolCall } = require('./llm/text-tool-cal
|
|
|
49
90
|
const {
|
|
50
91
|
emitAgentRunContextWarnings,
|
|
51
92
|
isCodingAgentContext,
|
|
52
|
-
resolveAgentRunContext,
|
|
53
93
|
} = require('./runtime/agent-run-context');
|
|
94
|
+
const { classifyCodingTurnIntent } = require('./coding/coding-run-controller');
|
|
95
|
+
const {
|
|
96
|
+
createWallEToolRegistry,
|
|
97
|
+
executeRuntimeTool,
|
|
98
|
+
getRuntimeToolDefinitions,
|
|
99
|
+
RuntimeToolExecutor,
|
|
100
|
+
resolveWallERuntimeProfile,
|
|
101
|
+
runtimeToolContext,
|
|
102
|
+
} = require('./runtime/walle-runtime');
|
|
103
|
+
const { buildRuntimePromptManifest } = require('./runtime/prompt-manifest');
|
|
54
104
|
let _telemetry;
|
|
55
105
|
try { _telemetry = require('./telemetry'); } catch { _telemetry = { trackError() {}, track() {} }; }
|
|
56
106
|
let _embeddings;
|
|
@@ -62,6 +112,28 @@ function providerTypeOf(provider) {
|
|
|
62
112
|
return provider.type || provider.provider || provider.id || '';
|
|
63
113
|
}
|
|
64
114
|
|
|
115
|
+
// Native-tool-call providers never legitimately emit text-form tool markup.
|
|
116
|
+
// Anthropic (Claude) returns `tool_use` blocks; if an "anthropic" route returns
|
|
117
|
+
// DeepSeek-style DSML instead, the route is almost certainly NOT serving real
|
|
118
|
+
// Claude (e.g. a Portkey catalog slug mapped to a DSML-emitting backend). Warn
|
|
119
|
+
// once per (provider, model, format) so the misconfiguration is visible without
|
|
120
|
+
// log spam. Detection only — finalization already strips/recovers the markup.
|
|
121
|
+
const NATIVE_TOOL_CALL_PROVIDERS = new Set(['anthropic', 'google', 'openai']);
|
|
122
|
+
const _routeFormatMismatchWarned = new Set();
|
|
123
|
+
function warnOnTextToolCallFormatMismatch(providerType, model, format) {
|
|
124
|
+
const type = String(providerType || '').toLowerCase();
|
|
125
|
+
if (!format || !NATIVE_TOOL_CALL_PROVIDERS.has(type)) return;
|
|
126
|
+
const key = `${type}|${model || ''}|${format}`;
|
|
127
|
+
if (_routeFormatMismatchWarned.has(key)) return;
|
|
128
|
+
_routeFormatMismatchWarned.add(key);
|
|
129
|
+
console.warn(
|
|
130
|
+
`[chat] Route mismatch: provider "${type}" (model ${model || 'unknown'}) returned ` +
|
|
131
|
+
`text-form tool calls (${format}) instead of native tool_use. A genuine ${type} ` +
|
|
132
|
+
`model does not emit ${format}; this route is likely proxying to a different ` +
|
|
133
|
+
`backend (e.g. a misconfigured Portkey catalog slug). Verify the route's underlying model.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
65
137
|
function isDeepSeekV4Pro(provider, model) {
|
|
66
138
|
const type = providerTypeOf(provider).toLowerCase();
|
|
67
139
|
return (type === 'deepseek' || type.includes('deepseek'))
|
|
@@ -104,6 +176,10 @@ function looksLikePrematureToolFollowupReply(text) {
|
|
|
104
176
|
|| value.length > 500;
|
|
105
177
|
if (hasFinalShape) return false;
|
|
106
178
|
|
|
179
|
+
const deferredToolOffer = /\b(?:would you like me to|want me to|should i)\s+(?:look up|search|get|check|inspect|read|pull|fetch|find)\b/i.test(value)
|
|
180
|
+
&& /[??]/.test(value);
|
|
181
|
+
if (concise && deferredToolOffer) return true;
|
|
182
|
+
|
|
107
183
|
const progressCue = /\b(?:content inventory confirmed|inventory confirmed|analysis complete|context confirmed|now|next|then)\b/i.test(value);
|
|
108
184
|
const sequenceCue = /\b(?:now|next)\b[\s:,-—;]+[\s\S]{0,160}\bthen\b/i.test(value);
|
|
109
185
|
if (firstPersonFuture && executionFutureVerb && concise) return true;
|
|
@@ -112,6 +188,26 @@ function looksLikePrematureToolFollowupReply(text) {
|
|
|
112
188
|
return concise && progressCue && (sequenceCue || executionVerb);
|
|
113
189
|
}
|
|
114
190
|
|
|
191
|
+
function _chatToolCallHistoryForActionGuard(toolCalls = []) {
|
|
192
|
+
return (toolCalls || [])
|
|
193
|
+
.map((call) => {
|
|
194
|
+
const name = call?.name || call?.tool || '';
|
|
195
|
+
if (!name) return null;
|
|
196
|
+
return normalizeToolCallEvidence({ ...call, name });
|
|
197
|
+
})
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _chatNoActionContinuation(args) {
|
|
202
|
+
try {
|
|
203
|
+
const { getNoActionContinuation } = require('./coding-orchestrator');
|
|
204
|
+
return getNoActionContinuation(args);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn('[chat] Action completion guard unavailable:', err.message);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
115
211
|
function _hasExplicitWeatherLocation(message) {
|
|
116
212
|
const text = String(message || '').trim();
|
|
117
213
|
if (!text) return false;
|
|
@@ -272,7 +368,7 @@ function hasOwn(obj, key) {
|
|
|
272
368
|
}
|
|
273
369
|
|
|
274
370
|
function isWallECodingMode({ opts = {}, channel = '' } = {}) {
|
|
275
|
-
return isCodingAgentContext(
|
|
371
|
+
return isCodingAgentContext(resolveWallERuntimeProfile({ ...opts, channel }).context);
|
|
276
372
|
}
|
|
277
373
|
|
|
278
374
|
function resolveReasoningOptions({
|
|
@@ -305,36 +401,6 @@ function resolveReasoningOptions({
|
|
|
305
401
|
return out;
|
|
306
402
|
}
|
|
307
403
|
|
|
308
|
-
const PARALLEL_SAFE_CHAT_TOOLS = new Set([
|
|
309
|
-
'think',
|
|
310
|
-
'search_memories',
|
|
311
|
-
'lookup_person',
|
|
312
|
-
'list_mcp_tools',
|
|
313
|
-
'list_tasks',
|
|
314
|
-
'calendar_events',
|
|
315
|
-
'calendar_list',
|
|
316
|
-
'system_info',
|
|
317
|
-
'clipboard_read',
|
|
318
|
-
'mail_messages',
|
|
319
|
-
'mail_read',
|
|
320
|
-
'mail_search',
|
|
321
|
-
'read_project_file',
|
|
322
|
-
'search_project',
|
|
323
|
-
'read_file',
|
|
324
|
-
'search_files',
|
|
325
|
-
'web_fetch',
|
|
326
|
-
'glean_search',
|
|
327
|
-
'glean_people',
|
|
328
|
-
'glean_chat',
|
|
329
|
-
'glob',
|
|
330
|
-
'grep_files',
|
|
331
|
-
'list_directory',
|
|
332
|
-
]);
|
|
333
|
-
|
|
334
|
-
function _shouldRunToolCallsSequentially(toolCalls) {
|
|
335
|
-
return toolCalls.some((tu) => !PARALLEL_SAFE_CHAT_TOOLS.has(tu.name));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
404
|
let _codingOrchestrator;
|
|
339
405
|
function getCodingOrchestrator() {
|
|
340
406
|
if (!_codingOrchestrator) _codingOrchestrator = require('./coding-orchestrator');
|
|
@@ -521,90 +587,6 @@ function _requestedSkillProgressEvents(resolution = {}) {
|
|
|
521
587
|
});
|
|
522
588
|
}
|
|
523
589
|
|
|
524
|
-
/**
|
|
525
|
-
* Convert validated ParsedAttachments into the LLM content-block shape
|
|
526
|
-
* that the active provider expects. The provider adapter is responsible
|
|
527
|
-
* for any further reshaping (e.g. OpenAI's `image_url` envelope).
|
|
528
|
-
*
|
|
529
|
-
* Anthropic:
|
|
530
|
-
* image → { type:'image', source:{type:'base64', media_type, data} }
|
|
531
|
-
* pdf → { type:'document', source:{type:'base64', media_type:'application/pdf', data} }
|
|
532
|
-
* audio → throws (Anthropic vision API doesn't accept audio).
|
|
533
|
-
* OpenAI (vision/audio-capable models):
|
|
534
|
-
* image → internal {type:'image',source:...} — messagesToOpenAI
|
|
535
|
-
* rewrites to {type:'image_url',image_url:{url:'data:...'}}.
|
|
536
|
-
* audio → internal {type:'audio',source:...} — messagesToOpenAI
|
|
537
|
-
* rewrites to {type:'input_audio',input_audio:{data,format}}.
|
|
538
|
-
* pdf → throws (OpenAI doesn't natively support PDF in chat).
|
|
539
|
-
* Google Gemini:
|
|
540
|
-
* image/pdf/audio → {type:<kind>,source:{type:'base64',media_type,data}}
|
|
541
|
-
* — Gemini provider's messagesToGemini (TBD) rewrites to
|
|
542
|
-
* {inline_data:{mime_type,data}}. For now Phase 4 wires the
|
|
543
|
-
* internal shape only; audio reaches OpenAI today, Gemini
|
|
544
|
-
* lands when its adapter ships.
|
|
545
|
-
*
|
|
546
|
-
* @throws Error with .code='MULTIMODAL_NOT_AVAILABLE' when the active
|
|
547
|
-
* provider can't carry the requested attachment kinds.
|
|
548
|
-
*/
|
|
549
|
-
function buildAttachmentBlocks(providerType, attachments) {
|
|
550
|
-
if (!attachments || attachments.length === 0) return null;
|
|
551
|
-
|
|
552
|
-
// Internal canonical content-block shape (Anthropic-flavored). Each
|
|
553
|
-
// adapter's messages converter rewrites to its native shape on the
|
|
554
|
-
// way out. New attachment kinds add a `type` and optionally a
|
|
555
|
-
// `format` field for adapter convenience.
|
|
556
|
-
const toInternalBlock = (a) => {
|
|
557
|
-
const baseSrc = { type: 'base64', media_type: a.mediaType, data: a.base64 };
|
|
558
|
-
if (a.type === 'image') return { type: 'image', source: baseSrc };
|
|
559
|
-
if (a.type === 'pdf') return { type: 'document', source: { ...baseSrc, media_type: 'application/pdf' } };
|
|
560
|
-
if (a.type === 'audio') {
|
|
561
|
-
// OpenAI's input_audio API requires a `format` like 'wav'|'mp3'.
|
|
562
|
-
// Derive from the media type so the adapter can wire it up.
|
|
563
|
-
const fmt = a.mediaType.split('/')[1] || 'mp3';
|
|
564
|
-
return { type: 'audio', source: baseSrc, format: fmt };
|
|
565
|
-
}
|
|
566
|
-
const err = new Error(`Unknown attachment kind "${a.type}"`);
|
|
567
|
-
err.code = 'MULTIMODAL_NOT_AVAILABLE';
|
|
568
|
-
throw err;
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
if (providerType === 'anthropic') {
|
|
572
|
-
return attachments.map((a) => {
|
|
573
|
-
if (a.type === 'audio') {
|
|
574
|
-
const e = new Error('Anthropic chat does not accept audio attachments. Switch to OpenAI or Gemini for audio.');
|
|
575
|
-
e.code = 'MULTIMODAL_NOT_AVAILABLE';
|
|
576
|
-
throw e;
|
|
577
|
-
}
|
|
578
|
-
return toInternalBlock(a);
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (providerType === 'openai') {
|
|
583
|
-
return attachments.map((a) => {
|
|
584
|
-
if (a.type === 'pdf') {
|
|
585
|
-
const e = new Error('OpenAI chat does not natively support PDF attachments. Switch to Anthropic for PDF support.');
|
|
586
|
-
e.code = 'MULTIMODAL_NOT_AVAILABLE';
|
|
587
|
-
throw e;
|
|
588
|
-
}
|
|
589
|
-
return toInternalBlock(a); // image and audio
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (providerType === 'google' || providerType === 'gemini') {
|
|
594
|
-
// All three kinds are accepted; the Google adapter will rewrite to
|
|
595
|
-
// Gemini's `inline_data` shape on the way out.
|
|
596
|
-
return attachments.map(toInternalBlock);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Other providers (Ollama, MLX, etc.) reach this only when the
|
|
600
|
-
// capability resolver was bypassed or has a gap. Surface clearly.
|
|
601
|
-
const e = new Error(
|
|
602
|
-
`Attachments not supported on provider "${providerType}". Switch to a configured Anthropic/OpenAI/Gemini provider for this turn.`
|
|
603
|
-
);
|
|
604
|
-
e.code = 'MULTIMODAL_NOT_AVAILABLE';
|
|
605
|
-
throw e;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
590
|
/**
|
|
609
591
|
* Format a coding progress event as a human-readable chat message.
|
|
610
592
|
* Used by both inline execution and SSE streaming.
|
|
@@ -703,11 +685,12 @@ function _providerConfigFromRegistryRow(row) {
|
|
|
703
685
|
}
|
|
704
686
|
|
|
705
687
|
function _registryRouteFromRow(row, input, explicitProvider) {
|
|
688
|
+
const runtimeType = _providerRuntimeType(row) || row.provider_type || explicitProvider || null;
|
|
706
689
|
return {
|
|
707
690
|
input,
|
|
708
691
|
model: row.model_id,
|
|
709
|
-
provider:
|
|
710
|
-
providerConfig:
|
|
692
|
+
provider: runtimeType,
|
|
693
|
+
providerConfig: _providerRuntimeConfig(row),
|
|
711
694
|
registryId: row.id,
|
|
712
695
|
providerId: row.provider_id,
|
|
713
696
|
displayName: row.display_name,
|
|
@@ -734,19 +717,37 @@ function _ambiguousModelRoute(input, rows, explicitProvider) {
|
|
|
734
717
|
};
|
|
735
718
|
}
|
|
736
719
|
|
|
720
|
+
function _preferredRouteRowForBareModel(rows = [], explicitProvider) {
|
|
721
|
+
if (!rows.length) return null;
|
|
722
|
+
const type = explicitProvider || rows[0].provider_type;
|
|
723
|
+
const policy = (type && typeof brain.getProviderRoutePolicy === 'function')
|
|
724
|
+
? brain.getProviderRoutePolicy(type)
|
|
725
|
+
: 'auto';
|
|
726
|
+
if (typeof brain.sortModelProvidersByRoutePolicy !== 'function') return null;
|
|
727
|
+
const sorted = brain.sortModelProvidersByRoutePolicy(
|
|
728
|
+
rows.map((row) => ({ ...row, type: row.provider_type || row.type })),
|
|
729
|
+
policy,
|
|
730
|
+
);
|
|
731
|
+
return sorted[0] || null;
|
|
732
|
+
}
|
|
733
|
+
|
|
737
734
|
function _createChatProvider(type, config = {}, opts = {}) {
|
|
738
|
-
|
|
739
|
-
|
|
735
|
+
const provider = typeof opts.providerFactory === 'function'
|
|
736
|
+
? opts.providerFactory(type, config)
|
|
737
|
+
: createClient(type, config);
|
|
738
|
+
return withProviderMessageGuard(provider);
|
|
740
739
|
}
|
|
741
740
|
|
|
742
741
|
function _providerRuntimeType(row = {}) {
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
742
|
+
const type = row.type || row.provider_type;
|
|
743
|
+
if (type === 'anthropic' && row.auth_method === 'claude_cli') return 'claude-cli';
|
|
744
|
+
if (type === 'openai' && row.auth_method === 'codex_cli') return 'codex-cli';
|
|
745
|
+
return type;
|
|
746
746
|
}
|
|
747
747
|
|
|
748
748
|
function _providerRuntimeConfig(row = {}) {
|
|
749
|
-
|
|
749
|
+
const type = row.type || row.provider_type;
|
|
750
|
+
if (type === 'anthropic' && row.auth_method === 'oauth_proxy') {
|
|
750
751
|
return {
|
|
751
752
|
apiKey: 'oauth-proxy-placeholder',
|
|
752
753
|
baseUrl: `http://127.0.0.1:${process.env.OAUTH_PROXY_PORT || '3458'}`,
|
|
@@ -861,30 +862,35 @@ function _toolArtifactSummary(toolCalls = []) {
|
|
|
861
862
|
}
|
|
862
863
|
|
|
863
864
|
function _hasSuccessfulWrite(toolCalls = []) {
|
|
864
|
-
return (toolCalls || []).some(call =>
|
|
865
|
+
return (toolCalls || []).some(call => (
|
|
866
|
+
call
|
|
867
|
+
&& (call.tool === 'write_file' || call.name === 'write_file')
|
|
868
|
+
&& normalizeToolCallEvidence(call).ok === true
|
|
869
|
+
));
|
|
865
870
|
}
|
|
866
871
|
|
|
867
872
|
function _hasSuccessfulScreenshotAfterLastWrite(toolCalls = []) {
|
|
868
873
|
let lastWriteIndex = -1;
|
|
869
874
|
for (let i = 0; i < (toolCalls || []).length; i++) {
|
|
870
875
|
const call = toolCalls[i];
|
|
871
|
-
if (
|
|
876
|
+
if (
|
|
877
|
+
call
|
|
878
|
+
&& (call.tool === 'write_file' || call.name === 'write_file')
|
|
879
|
+
&& normalizeToolCallEvidence(call).ok === true
|
|
880
|
+
) {
|
|
881
|
+
lastWriteIndex = i;
|
|
882
|
+
}
|
|
872
883
|
}
|
|
873
884
|
if (lastWriteIndex < 0) return false;
|
|
874
|
-
return (toolCalls || []).
|
|
875
|
-
index > lastWriteIndex
|
|
876
|
-
&& call
|
|
877
|
-
&& !call.error
|
|
878
|
-
&& call.tool === 'browser_screenshot'
|
|
879
|
-
));
|
|
885
|
+
return hasVerificationEvidence(_chatToolCallHistoryForActionGuard((toolCalls || []).slice(lastWriteIndex + 1)));
|
|
880
886
|
}
|
|
881
887
|
|
|
882
888
|
function _postToolProgressFailureReply(toolCalls = []) {
|
|
883
889
|
if (_hasSuccessfulWrite(toolCalls)) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return `
|
|
890
|
+
if (_hasSuccessfulScreenshotAfterLastWrite(toolCalls)) {
|
|
891
|
+
return `Completed the requested changes.${_toolArtifactSummary(toolCalls)}\n\nVerified with tool evidence; detailed tool output is preserved in the activity panel.`;
|
|
892
|
+
}
|
|
893
|
+
return `I made file changes, but final answer generation failed before successful verification evidence was captured.${_toolArtifactSummary(toolCalls)}\n\nThe detailed tool outputs are preserved in the activity panel. Please treat this as incomplete until tests, build, check_url, or browser_screenshot succeeds.`;
|
|
888
894
|
}
|
|
889
895
|
return `I ran the requested tools, but final answer generation kept returning progress/tool-control text instead of a clean final answer.${_toolArtifactSummary(toolCalls)}\n\nThe detailed tool outputs are preserved in the activity panel.`;
|
|
890
896
|
}
|
|
@@ -1375,6 +1381,7 @@ async function _tryProviderFinalizationFallback({
|
|
|
1375
1381
|
const decorated = decorateProviderError(err, {
|
|
1376
1382
|
provider: fallback.providerType,
|
|
1377
1383
|
model: fallback.model,
|
|
1384
|
+
providerId: fallback.providerId || null,
|
|
1378
1385
|
});
|
|
1379
1386
|
recordProviderFailureAlert(decorated.providerError, brain);
|
|
1380
1387
|
console.warn('[chat] Finalization provider fallback failed, trying next candidate:', fallback.providerType, decorated.providerError.type);
|
|
@@ -1383,16 +1390,213 @@ async function _tryProviderFinalizationFallback({
|
|
|
1383
1390
|
return null;
|
|
1384
1391
|
}
|
|
1385
1392
|
|
|
1393
|
+
function _truncateFinalizationEvidence(value, maxChars) {
|
|
1394
|
+
const text = String(value || '').trim();
|
|
1395
|
+
if (text.length <= maxChars) return text;
|
|
1396
|
+
return text.slice(0, Math.max(0, maxChars - 80)).trimEnd() + '\n[... evidence truncated ...]';
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function _jsonForFinalizationEvidence(value, maxChars = 4000) {
|
|
1400
|
+
try {
|
|
1401
|
+
return _truncateFinalizationEvidence(JSON.stringify(value, null, 2), maxChars);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return _truncateFinalizationEvidence(String(value || ''), maxChars);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function _mailMessageLine(message = {}) {
|
|
1408
|
+
const parts = [];
|
|
1409
|
+
if (message.subject) parts.push(`Subject: ${message.subject}`);
|
|
1410
|
+
if (message.from) parts.push(`From: ${message.from}`);
|
|
1411
|
+
if (message.to) parts.push(`To: ${message.to}`);
|
|
1412
|
+
if (message.date) parts.push(`Date: ${message.date}`);
|
|
1413
|
+
if (message.messageId) parts.push(`Message ID: ${message.messageId}`);
|
|
1414
|
+
if (message.snippet) parts.push(`Snippet: ${message.snippet}`);
|
|
1415
|
+
return parts.join(' | ');
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function _finalizationEvidenceForToolCall(call, index) {
|
|
1419
|
+
const name = call?.name || call?.tool || 'tool';
|
|
1420
|
+
const result = call?.result;
|
|
1421
|
+
const evidence = normalizeToolCallEvidence(call);
|
|
1422
|
+
const ok = evidence.ok === true;
|
|
1423
|
+
const lines = [`Tool ${index + 1}: ${name} (${ok ? 'ok' : 'error'})`];
|
|
1424
|
+
if (call?.args || call?.input) {
|
|
1425
|
+
lines.push(`Input: ${_jsonForFinalizationEvidence(call.args || call.input, 1200)}`);
|
|
1426
|
+
}
|
|
1427
|
+
if (!result) return lines.join('\n');
|
|
1428
|
+
|
|
1429
|
+
if (name === 'mail_search') {
|
|
1430
|
+
if (result.count != null) lines.push(`Count: ${result.count}`);
|
|
1431
|
+
if (result.query) lines.push(`Query: ${result.query}`);
|
|
1432
|
+
if (Array.isArray(result.accounts) && result.accounts.length) {
|
|
1433
|
+
lines.push(`Accounts searched: ${result.accounts.join(', ')}`);
|
|
1434
|
+
}
|
|
1435
|
+
if (Array.isArray(result.messages) && result.messages.length) {
|
|
1436
|
+
lines.push('Messages:');
|
|
1437
|
+
for (const message of result.messages.slice(0, 6)) {
|
|
1438
|
+
lines.push(`- ${_mailMessageLine(message)}`);
|
|
1439
|
+
}
|
|
1440
|
+
if (result.messages.length > 6) lines.push(`- ... ${result.messages.length - 6} more message(s) omitted`);
|
|
1441
|
+
}
|
|
1442
|
+
if (result.error) lines.push(`Error: ${result.error}`);
|
|
1443
|
+
return _truncateFinalizationEvidence(lines.join('\n'), 6500);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (name === 'mail_read') {
|
|
1447
|
+
for (const field of ['account', 'mailbox', 'subject', 'from', 'to', 'cc', 'date', 'messageId', 'threadId']) {
|
|
1448
|
+
if (result[field]) lines.push(`${field}: ${result[field]}`);
|
|
1449
|
+
}
|
|
1450
|
+
if (result.content) {
|
|
1451
|
+
lines.push('Content:');
|
|
1452
|
+
lines.push(_truncateFinalizationEvidence(result.content, 10000));
|
|
1453
|
+
} else if (result.snippet) {
|
|
1454
|
+
lines.push(`Snippet: ${result.snippet}`);
|
|
1455
|
+
}
|
|
1456
|
+
if (result.error) lines.push(`Error: ${result.error}`);
|
|
1457
|
+
return _truncateFinalizationEvidence(lines.join('\n'), 12000);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (name === 'web_fetch') {
|
|
1461
|
+
if (result.url) lines.push(`URL: ${result.url}`);
|
|
1462
|
+
if (result.title) lines.push(`Title: ${result.title}`);
|
|
1463
|
+
if (result.error) lines.push(`Error: ${result.error}`);
|
|
1464
|
+
const body = result.text || result.content || result.body || result.markdown || result.html;
|
|
1465
|
+
if (body) {
|
|
1466
|
+
lines.push('Fetched content:');
|
|
1467
|
+
lines.push(_truncateFinalizationEvidence(body, 7000));
|
|
1468
|
+
}
|
|
1469
|
+
return _truncateFinalizationEvidence(lines.join('\n'), 8500);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (result.error) lines.push(`Error: ${result.error}`);
|
|
1473
|
+
lines.push(`Result: ${_jsonForFinalizationEvidence(result, 5000)}`);
|
|
1474
|
+
return _truncateFinalizationEvidence(lines.join('\n'), 6500);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function _buildEvidenceOnlyFinalizationMessages({ originalRequest, toolCalls = [] }) {
|
|
1478
|
+
const parts = [];
|
|
1479
|
+
let used = 0;
|
|
1480
|
+
const maxTotal = 22000;
|
|
1481
|
+
for (const [index, call] of toolCalls.entries()) {
|
|
1482
|
+
const block = _finalizationEvidenceForToolCall(call, index);
|
|
1483
|
+
if (!block.trim()) continue;
|
|
1484
|
+
if (used + block.length > maxTotal && parts.length > 0) {
|
|
1485
|
+
parts.push('[... additional tool evidence omitted to keep finalization focused ...]');
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
parts.push(block);
|
|
1489
|
+
used += block.length;
|
|
1490
|
+
}
|
|
1491
|
+
const evidence = parts.join('\n\n');
|
|
1492
|
+
return [{
|
|
1493
|
+
role: 'user',
|
|
1494
|
+
content: [
|
|
1495
|
+
'Original user request:',
|
|
1496
|
+
String(originalRequest || '').trim(),
|
|
1497
|
+
'',
|
|
1498
|
+
'Tool evidence already gathered:',
|
|
1499
|
+
evidence || 'No structured tool evidence is available.',
|
|
1500
|
+
'',
|
|
1501
|
+
'Write the final answer now. Use only this evidence. Do not mention tool calls, JSON, protocols, activity panels, or future inspection. If the request asks for drafts, write the drafts directly.',
|
|
1502
|
+
].join('\n'),
|
|
1503
|
+
}];
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async function _tryEvidenceOnlyFinalization({
|
|
1507
|
+
provider,
|
|
1508
|
+
model,
|
|
1509
|
+
originalRequest,
|
|
1510
|
+
toolCalls = [],
|
|
1511
|
+
opts = {},
|
|
1512
|
+
resetFinalizationTimeout,
|
|
1513
|
+
resolveCurrentReasoningOptions,
|
|
1514
|
+
signal,
|
|
1515
|
+
getSignal,
|
|
1516
|
+
} = {}) {
|
|
1517
|
+
if (typeof opts.finalizationFailureReply === 'string') return null;
|
|
1518
|
+
if (!provider || !Array.isArray(toolCalls) || toolCalls.length === 0) return null;
|
|
1519
|
+
try {
|
|
1520
|
+
resetFinalizationTimeout?.();
|
|
1521
|
+
const activeSignal = typeof getSignal === 'function' ? getSignal() : signal;
|
|
1522
|
+
const reasoning = typeof resolveCurrentReasoningOptions === 'function'
|
|
1523
|
+
? resolveCurrentReasoningOptions(provider, model)
|
|
1524
|
+
: {};
|
|
1525
|
+
const response = await provider.chat({
|
|
1526
|
+
model,
|
|
1527
|
+
maxTokens: Math.max(2048, Math.min(4096, Number(opts.finalizationMaxTokens) || 4096)),
|
|
1528
|
+
system: [
|
|
1529
|
+
'You are Wall-E final-answer synthesizer.',
|
|
1530
|
+
'The main agent has already gathered tool evidence.',
|
|
1531
|
+
'You cannot call tools and must not emit tool markup or progress notes.',
|
|
1532
|
+
'Answer the original user request directly from the supplied evidence.',
|
|
1533
|
+
].join(' '),
|
|
1534
|
+
messages: _buildEvidenceOnlyFinalizationMessages({ originalRequest, toolCalls }),
|
|
1535
|
+
tools: [],
|
|
1536
|
+
...reasoning,
|
|
1537
|
+
thinking: 'disabled',
|
|
1538
|
+
signal: activeSignal,
|
|
1539
|
+
});
|
|
1540
|
+
const parsed = extractTextToolCalls(response);
|
|
1541
|
+
const text = String(parsed.content || response.content || '').trim();
|
|
1542
|
+
if (text && !_finalizationResponseIsProtocol(parsed, text)) {
|
|
1543
|
+
try {
|
|
1544
|
+
_telemetry.track('chat_evidence_only_finalization', {
|
|
1545
|
+
status: 'success',
|
|
1546
|
+
provider: response.provider || provider.type || '',
|
|
1547
|
+
model: response.model || model || '',
|
|
1548
|
+
});
|
|
1549
|
+
} catch {}
|
|
1550
|
+
return {
|
|
1551
|
+
text,
|
|
1552
|
+
response,
|
|
1553
|
+
meta: {
|
|
1554
|
+
model: response.model || model,
|
|
1555
|
+
provider: response.provider || provider.type,
|
|
1556
|
+
usage: response.usage || null,
|
|
1557
|
+
stopReason: response.stopReason || null,
|
|
1558
|
+
evidenceOnlyFinalization: true,
|
|
1559
|
+
},
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
try {
|
|
1563
|
+
_telemetry.track('chat_evidence_only_finalization', {
|
|
1564
|
+
status: 'protocol_text',
|
|
1565
|
+
provider: response.provider || provider.type || '',
|
|
1566
|
+
model: response.model || model || '',
|
|
1567
|
+
});
|
|
1568
|
+
} catch {}
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
if (_isUserCancellation(opts, err)) throw _cancelledError();
|
|
1571
|
+
console.warn('[chat] Evidence-only finalization failed:', err.message);
|
|
1572
|
+
try {
|
|
1573
|
+
_telemetry.track('chat_evidence_only_finalization', {
|
|
1574
|
+
status: 'error',
|
|
1575
|
+
provider: provider.type || '',
|
|
1576
|
+
model: model || '',
|
|
1577
|
+
error_type: err.name || err.code || 'error',
|
|
1578
|
+
});
|
|
1579
|
+
} catch {}
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1386
1584
|
function _findChatProviderFallback(args) {
|
|
1387
1585
|
return (_listChatProviderFallbacks(args) || [])[0] || null;
|
|
1388
1586
|
}
|
|
1389
1587
|
|
|
1390
1588
|
function resolveModelSelection(model, explicitProvider) {
|
|
1391
1589
|
if (!model) {
|
|
1392
|
-
return {
|
|
1590
|
+
return {
|
|
1591
|
+
input: model,
|
|
1592
|
+
model: null,
|
|
1593
|
+
provider: explicitProvider || null,
|
|
1594
|
+
providerConfig: explicitProvider ? getProviderRuntimeConfig(explicitProvider) : null,
|
|
1595
|
+
registryId: null,
|
|
1596
|
+
};
|
|
1393
1597
|
}
|
|
1394
1598
|
|
|
1395
|
-
const
|
|
1599
|
+
const input = String(model);
|
|
1396
1600
|
try {
|
|
1397
1601
|
const exact = brain.getDb().prepare(`
|
|
1398
1602
|
SELECT
|
|
@@ -1412,11 +1616,12 @@ function resolveModelSelection(model, explicitProvider) {
|
|
|
1412
1616
|
AND mr.enabled = 1
|
|
1413
1617
|
AND mp.enabled = 1
|
|
1414
1618
|
LIMIT 1
|
|
1415
|
-
`).get(
|
|
1619
|
+
`).get(input);
|
|
1416
1620
|
if (exact) {
|
|
1417
|
-
return _registryRouteFromRow(exact,
|
|
1621
|
+
return _registryRouteFromRow(exact, input, explicitProvider);
|
|
1418
1622
|
}
|
|
1419
1623
|
|
|
1624
|
+
const raw = canonicalSessionModelId(input, explicitProvider || detectProviderForModel(input) || '');
|
|
1420
1625
|
const rows = brain.getDb().prepare(`
|
|
1421
1626
|
SELECT
|
|
1422
1627
|
mr.id,
|
|
@@ -1445,7 +1650,7 @@ function resolveModelSelection(model, explicitProvider) {
|
|
|
1445
1650
|
const compatibleModel = resolveCompatibleModel(row.model_id, explicitProvider);
|
|
1446
1651
|
if (compatibleModel !== row.model_id) {
|
|
1447
1652
|
return {
|
|
1448
|
-
input
|
|
1653
|
+
input,
|
|
1449
1654
|
model: compatibleModel,
|
|
1450
1655
|
provider: explicitProvider,
|
|
1451
1656
|
providerConfig: null,
|
|
@@ -1455,36 +1660,39 @@ function resolveModelSelection(model, explicitProvider) {
|
|
|
1455
1660
|
};
|
|
1456
1661
|
}
|
|
1457
1662
|
}
|
|
1458
|
-
return _registryRouteFromRow(row,
|
|
1663
|
+
return _registryRouteFromRow(row, input, explicitProvider);
|
|
1459
1664
|
}
|
|
1460
1665
|
|
|
1461
1666
|
if (rows.length > 1) {
|
|
1462
1667
|
const defaultRegistryId = (brain.getKv?.('walle_model_registry_id') || (explicitProvider ? brain.getKv?.(`walle_model_registry_${explicitProvider}`) : null) || '').trim();
|
|
1463
1668
|
const defaultRow = defaultRegistryId ? rows.find((row) => row.id === defaultRegistryId) : null;
|
|
1464
|
-
if (defaultRow) return _registryRouteFromRow(defaultRow,
|
|
1465
|
-
|
|
1669
|
+
if (defaultRow) return _registryRouteFromRow(defaultRow, input, explicitProvider);
|
|
1670
|
+
const preferredRow = _preferredRouteRowForBareModel(rows, explicitProvider);
|
|
1671
|
+
if (preferredRow) return _registryRouteFromRow(preferredRow, input, explicitProvider);
|
|
1672
|
+
return _ambiguousModelRoute(input, rows, explicitProvider);
|
|
1466
1673
|
}
|
|
1467
1674
|
} catch {}
|
|
1468
1675
|
|
|
1676
|
+
const raw = canonicalSessionModelId(input, explicitProvider || detectProviderForModel(input) || '');
|
|
1469
1677
|
const detectedProvider = detectProviderForModel(raw);
|
|
1470
1678
|
if (explicitProvider) {
|
|
1471
1679
|
const compatibleModel = resolveCompatibleModel(raw, explicitProvider);
|
|
1472
1680
|
return {
|
|
1473
|
-
input
|
|
1681
|
+
input,
|
|
1474
1682
|
model: compatibleModel,
|
|
1475
1683
|
provider: explicitProvider,
|
|
1476
|
-
providerConfig:
|
|
1684
|
+
providerConfig: getProviderRuntimeConfig(explicitProvider),
|
|
1477
1685
|
registryId: null,
|
|
1478
|
-
coercedFrom: compatibleModel ===
|
|
1686
|
+
coercedFrom: compatibleModel === input ? null : input,
|
|
1479
1687
|
detectedProvider,
|
|
1480
1688
|
};
|
|
1481
1689
|
}
|
|
1482
1690
|
|
|
1483
1691
|
return {
|
|
1484
|
-
input
|
|
1692
|
+
input,
|
|
1485
1693
|
model: raw,
|
|
1486
1694
|
provider: detectedProvider || null,
|
|
1487
|
-
providerConfig: null,
|
|
1695
|
+
providerConfig: detectedProvider ? getProviderRuntimeConfig(detectedProvider) : null,
|
|
1488
1696
|
registryId: null,
|
|
1489
1697
|
};
|
|
1490
1698
|
}
|
|
@@ -1571,8 +1779,9 @@ async function chat(message, opts = {}) {
|
|
|
1571
1779
|
let usedProvider = null;
|
|
1572
1780
|
const allToolCalls = [];
|
|
1573
1781
|
const channel = opts.channel || 'ctm';
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1782
|
+
const wallERuntimeProfile = resolveWallERuntimeProfile({ ...opts, channel });
|
|
1783
|
+
const agentRunContext = wallERuntimeProfile.context;
|
|
1784
|
+
emitAgentRunContextWarnings({ ...agentRunContext, warnings: wallERuntimeProfile.warnings }, { telemetry: _telemetry });
|
|
1576
1785
|
const wallECodingMode = isCodingAgentContext(agentRunContext);
|
|
1577
1786
|
|
|
1578
1787
|
// Scorecard-aware model selection (computed once per chat, used by all turns)
|
|
@@ -1590,6 +1799,11 @@ async function chat(message, opts = {}) {
|
|
|
1590
1799
|
.join(', ');
|
|
1591
1800
|
throw new Error(`Ambiguous model route for "${selectedRoute.input}". Choose a specific model route: ${choices}`);
|
|
1592
1801
|
}
|
|
1802
|
+
if (selectedRoute.model) {
|
|
1803
|
+
// Registry routes own the executable model id. The picker may pass an alias
|
|
1804
|
+
// or display id, but provider calls must use the route's canonical model.
|
|
1805
|
+
selectedModel = selectedRoute.model;
|
|
1806
|
+
}
|
|
1593
1807
|
const effectiveCwd = opts.cwd || opts.context?.cwd || opts.context?.projectPath || null;
|
|
1594
1808
|
|
|
1595
1809
|
// Plug-provider-routing Item B+E: log routing decision via
|
|
@@ -1618,6 +1832,7 @@ async function chat(message, opts = {}) {
|
|
|
1618
1832
|
const routingMessage = String(opts.routingMessage || opts.originalMessage || message || '');
|
|
1619
1833
|
const queryTopics = classifyTopics(routingMessage);
|
|
1620
1834
|
const intent = classifyIntent(queryTopics);
|
|
1835
|
+
const codingTurnIntent = classifyCodingTurnIntent(routingMessage);
|
|
1621
1836
|
|
|
1622
1837
|
// Item F (multi-agent deep-dive): conversation collapse policy.
|
|
1623
1838
|
// 'main' (default) keeps the historical 'default' bucket;
|
|
@@ -1631,6 +1846,7 @@ async function chat(message, opts = {}) {
|
|
|
1631
1846
|
brain,
|
|
1632
1847
|
});
|
|
1633
1848
|
const existingSession = brain.getSession(sessionId);
|
|
1849
|
+
const existingSessionMetadata = parseSessionMetadata(existingSession?.metadata);
|
|
1634
1850
|
const sessionRecorder = createSessionRecorder({
|
|
1635
1851
|
sessionId,
|
|
1636
1852
|
channel,
|
|
@@ -1640,6 +1856,8 @@ async function chat(message, opts = {}) {
|
|
|
1640
1856
|
source: opts.source || channel || 'chat',
|
|
1641
1857
|
model: selectedModel,
|
|
1642
1858
|
provider: selectedRoute.provider || opts.provider || null,
|
|
1859
|
+
runtimeProfile: wallERuntimeProfile.profileId,
|
|
1860
|
+
permissionProfile: wallERuntimeProfile.permissionProfile,
|
|
1643
1861
|
},
|
|
1644
1862
|
});
|
|
1645
1863
|
const recordSessionMessage = (role, content, extra) => {
|
|
@@ -1650,6 +1868,52 @@ async function chat(message, opts = {}) {
|
|
|
1650
1868
|
return null;
|
|
1651
1869
|
}
|
|
1652
1870
|
};
|
|
1871
|
+
const recordUsageLedger = (response, feature, extra = {}) => {
|
|
1872
|
+
if (!response?.usage) return null;
|
|
1873
|
+
try {
|
|
1874
|
+
const raw = response.raw && typeof response.raw === 'object' ? response.raw : {};
|
|
1875
|
+
const providerType = response.provider || extra.provider || usedProvider || targetProviderType || null;
|
|
1876
|
+
const modelId = response.model || extra.model || selectedModel || null;
|
|
1877
|
+
const routeProvider = selectedRoute?.provider || null;
|
|
1878
|
+
const routeModel = selectedRoute?.model || null;
|
|
1879
|
+
const routeMatches = !!selectedRoute
|
|
1880
|
+
&& (!providerType || !routeProvider || String(providerType).toLowerCase() === String(routeProvider).toLowerCase())
|
|
1881
|
+
&& (!modelId || !routeModel || String(modelId).toLowerCase() === String(routeModel).toLowerCase());
|
|
1882
|
+
const requestId = response.requestId
|
|
1883
|
+
|| response.responseId
|
|
1884
|
+
|| response.id
|
|
1885
|
+
|| raw.id
|
|
1886
|
+
|| raw.response_id
|
|
1887
|
+
|| raw.request_id
|
|
1888
|
+
|| null;
|
|
1889
|
+
return brain.recordModelUsage?.({
|
|
1890
|
+
source: 'wall-e.chat',
|
|
1891
|
+
feature,
|
|
1892
|
+
sessionId,
|
|
1893
|
+
providerType,
|
|
1894
|
+
providerId: extra.providerId || (routeMatches ? selectedRoute.providerId : null) || null,
|
|
1895
|
+
modelId,
|
|
1896
|
+
modelRegistryId: extra.modelRegistryId || (routeMatches ? selectedRoute.registryId : null) || null,
|
|
1897
|
+
gatewayType: extra.gatewayType || (routeMatches ? selectedRoute.gatewayType : null) || null,
|
|
1898
|
+
routeLabel: extra.routeLabel || (routeMatches ? selectedRoute.routeLabel : null) || null,
|
|
1899
|
+
requestId,
|
|
1900
|
+
usage: response.usage,
|
|
1901
|
+
latencyMs: response.latencyMs,
|
|
1902
|
+
stopReason: response.stopReason || null,
|
|
1903
|
+
metadata: {
|
|
1904
|
+
channel,
|
|
1905
|
+
taskType: opts.taskType || 'chat',
|
|
1906
|
+
turn: extra.turn ?? null,
|
|
1907
|
+
fallback: !!extra.fallback,
|
|
1908
|
+
repair: extra.repair || null,
|
|
1909
|
+
runtimeProfile: wallERuntimeProfile.profileId,
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
console.warn('[chat] Failed to record model usage ledger:', err.message);
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1653
1917
|
const progressSink = opts.onProgress || (() => {});
|
|
1654
1918
|
const onProgress = (event) => {
|
|
1655
1919
|
try { sessionRecorder.appendProgress(event); } catch (err) {
|
|
@@ -1657,6 +1921,16 @@ async function chat(message, opts = {}) {
|
|
|
1657
1921
|
}
|
|
1658
1922
|
return progressSink(event);
|
|
1659
1923
|
};
|
|
1924
|
+
const recordSessionFrame = (frame, extra = {}) => {
|
|
1925
|
+
try {
|
|
1926
|
+
return typeof sessionRecorder.appendFrame === 'function'
|
|
1927
|
+
? sessionRecorder.appendFrame(frame, extra)
|
|
1928
|
+
: sessionRecorder.appendProgress({ type: 'conversation_frame', frame, ...(extra || {}) });
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
console.error('[chat] Failed to append conversation frame:', err.message);
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1660
1934
|
const returnSystemReply = (text, meta = {}) => {
|
|
1661
1935
|
const persistStart = Date.now();
|
|
1662
1936
|
if (!opts._fallbackRetry) {
|
|
@@ -1688,7 +1962,11 @@ async function chat(message, opts = {}) {
|
|
|
1688
1962
|
metadata: JSON.stringify({ channel }),
|
|
1689
1963
|
});
|
|
1690
1964
|
}
|
|
1691
|
-
const assistantMessage = brain.insertChatMessage({
|
|
1965
|
+
const assistantMessage = brain.insertChatMessage({
|
|
1966
|
+
role: 'assistant', content: text, channel, session_id: sessionId,
|
|
1967
|
+
model_id: meta.model || null,
|
|
1968
|
+
model_provider: meta.provider || null,
|
|
1969
|
+
});
|
|
1692
1970
|
recordSessionMessage('assistant', text, {
|
|
1693
1971
|
dbMessageId: assistantMessage.id,
|
|
1694
1972
|
provider: meta.provider || 'system',
|
|
@@ -1740,6 +2018,35 @@ async function chat(message, opts = {}) {
|
|
|
1740
2018
|
});
|
|
1741
2019
|
}
|
|
1742
2020
|
|
|
2021
|
+
const runtimeFrameContextMessages = (() => {
|
|
2022
|
+
const override = normalizeContextMessagesForChat(opts.contextMessages, message);
|
|
2023
|
+
if (override.length > 0) return override;
|
|
2024
|
+
try {
|
|
2025
|
+
const recentChat = brain.listChatMessages({ session_id: sessionId, limit: 20 });
|
|
2026
|
+
return normalizeContextMessagesForChat(
|
|
2027
|
+
recentChat.map(m => ({ role: m.role, content: m.content })),
|
|
2028
|
+
message,
|
|
2029
|
+
);
|
|
2030
|
+
} catch {
|
|
2031
|
+
return normalizeContextMessagesForChat([], message);
|
|
2032
|
+
}
|
|
2033
|
+
})();
|
|
2034
|
+
const conversationRuntime = buildConversationRuntime({
|
|
2035
|
+
messages: runtimeFrameContextMessages,
|
|
2036
|
+
currentMessage: message,
|
|
2037
|
+
previousFrame: existingSessionMetadata.conversationFrame || existingSessionMetadata.activeFrame || null,
|
|
2038
|
+
channel,
|
|
2039
|
+
cwd: effectiveCwd || opts.cwd || process.cwd(),
|
|
2040
|
+
taskType: opts.taskType || 'chat',
|
|
2041
|
+
});
|
|
2042
|
+
const conversationFrameContextBlock = buildConversationFramePrompt(conversationRuntime.frame);
|
|
2043
|
+
if (conversationFrameContextBlock) {
|
|
2044
|
+
recordSessionFrame(conversationRuntime.frame, {
|
|
2045
|
+
reason: 'turn_start',
|
|
2046
|
+
validators: conversationRuntime.validators,
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1743
2050
|
let currentLocationSummary = null;
|
|
1744
2051
|
if (shouldUseLocationForMessage(routingMessage, queryTopics)) {
|
|
1745
2052
|
currentLocationSummary = await resolveLocationForMessage(opts.locationRequest || opts.req || null, brain, routingMessage, {
|
|
@@ -1841,7 +2148,7 @@ async function chat(message, opts = {}) {
|
|
|
1841
2148
|
intent,
|
|
1842
2149
|
topics: queryTopics,
|
|
1843
2150
|
currentLocation: currentLocationContext,
|
|
1844
|
-
}) + reviewContextBlock + codeReviewContextBlock + workspaceContextBlock + promptCapabilityContextBlock;
|
|
2151
|
+
}) + conversationFrameContextBlock + reviewContextBlock + codeReviewContextBlock + workspaceContextBlock + promptCapabilityContextBlock;
|
|
1845
2152
|
// Item D (multi-agent deep-dive): opt-in workspace directives override
|
|
1846
2153
|
// for the chat surface. Tutorial-style or persona-tweak directives can
|
|
1847
2154
|
// live in `wall-e/loops/chat.directives.md` (or
|
|
@@ -1851,11 +2158,14 @@ async function chat(message, opts = {}) {
|
|
|
1851
2158
|
const wallECodingScopeReminder = wallECodingMode
|
|
1852
2159
|
? '\n\n## Wall-E Coding Scope Reminder\nMatch the phase the user asked for. If the user asks to design, plan, analyze, review, or discuss without asking for changes, deliver that artifact and stop. If the user combines review/analyze/design with improve, polish, enhance, upgrade, fix, build, implement, refactor, code, edit, update, or change, then actually perform the requested change or start the coding agent; do not stop at a review-only answer.'
|
|
1853
2160
|
: '';
|
|
2161
|
+
const wallECodingContinuationReminder = wallECodingMode && codingTurnIntent.readOnly && !codingTurnIntent.expectsChange
|
|
2162
|
+
? '\n\n## Wall-E Coding Conversation Continuation\nThis turn is read-only or conversational. Do not start a coding task, do not call start_coding, and do not evaluate code-change completion. Use the existing conversation, supplied context, and any read-only tool results to answer directly. If the user asks to refine, update, or correct a prior summary, produce the updated summary now. Ask for more input only when the missing detail is truly required and say what evidence you checked.'
|
|
2163
|
+
: '';
|
|
1854
2164
|
const finalLanguageReminder = buildResponseLanguagePolicy({
|
|
1855
2165
|
heading: '## Final Response Language Reminder',
|
|
1856
2166
|
userMessage: message,
|
|
1857
2167
|
});
|
|
1858
|
-
const systemPrompt = `${augmentedSystemPrompt}${wallECodingScopeReminder}\n\n${finalLanguageReminder}`;
|
|
2168
|
+
const systemPrompt = `${augmentedSystemPrompt}${wallECodingScopeReminder}${wallECodingContinuationReminder}\n\n${finalLanguageReminder}`;
|
|
1859
2169
|
timings.promptBuildMs = Date.now() - promptStart;
|
|
1860
2170
|
|
|
1861
2171
|
// Expire old sessions occasionally (at most once per hour)
|
|
@@ -1875,14 +2185,22 @@ async function chat(message, opts = {}) {
|
|
|
1875
2185
|
let provider = _clientOverride || null;
|
|
1876
2186
|
if (!provider) {
|
|
1877
2187
|
const routeConfig = selectedRoute.providerConfig || {};
|
|
1878
|
-
const
|
|
1879
|
-
const hasSpecificConfig = !!(
|
|
2188
|
+
const suppliedProviderConfig = { ...routeConfig, ...(opts.providerConfig || {}) };
|
|
2189
|
+
const hasSpecificConfig = !!(
|
|
2190
|
+
suppliedProviderConfig.apiKey
|
|
2191
|
+
|| suppliedProviderConfig.baseUrl
|
|
2192
|
+
|| suppliedProviderConfig.customHeaders
|
|
2193
|
+
|| suppliedProviderConfig.authMode
|
|
2194
|
+
|| suppliedProviderConfig.refreshToken
|
|
2195
|
+
);
|
|
1880
2196
|
if (opts.providerFactory || targetProviderType !== defaultProviderType || opts.provider || hasSpecificConfig) {
|
|
2197
|
+
const providerConfig = getProviderRuntimeConfig(targetProviderType, suppliedProviderConfig);
|
|
1881
2198
|
provider = _createChatProvider(targetProviderType, providerConfig, opts);
|
|
1882
2199
|
} else {
|
|
1883
2200
|
provider = getDefaultClient();
|
|
1884
2201
|
}
|
|
1885
2202
|
}
|
|
2203
|
+
provider = withProviderMessageGuard(provider);
|
|
1886
2204
|
|
|
1887
2205
|
// Engine swap: if using ollama and user prefers MLX, transparently switch
|
|
1888
2206
|
if (!_clientOverride && (provider.type === 'ollama' || opts.provider === 'ollama')) {
|
|
@@ -1890,6 +2208,7 @@ async function chat(message, opts = {}) {
|
|
|
1890
2208
|
const localEngine = brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'local_engine'").get()?.value;
|
|
1891
2209
|
if (localEngine === 'mlx') {
|
|
1892
2210
|
provider = _createChatProvider('mlx', {}, opts);
|
|
2211
|
+
provider = withProviderMessageGuard(provider);
|
|
1893
2212
|
}
|
|
1894
2213
|
} catch (e) {
|
|
1895
2214
|
console.warn('[chat] MLX engine swap failed, staying on Ollama:', e.message);
|
|
@@ -1897,6 +2216,20 @@ async function chat(message, opts = {}) {
|
|
|
1897
2216
|
}
|
|
1898
2217
|
|
|
1899
2218
|
selectedModel = resolveCompatibleModel(selectedModel || getDefaultModelForProvider(targetProviderType), provider.type || targetProviderType);
|
|
2219
|
+
const primaryProviderErrorContext = (providerType, model) => {
|
|
2220
|
+
const resolvedProvider = providerType || targetProviderType || getDefaultProviderType();
|
|
2221
|
+
const resolvedModel = model || selectedModel;
|
|
2222
|
+
const routeApplies = selectedRoute
|
|
2223
|
+
&& (!selectedRoute.provider || selectedRoute.provider === resolvedProvider)
|
|
2224
|
+
&& (!selectedRoute.model || selectedRoute.model === resolvedModel);
|
|
2225
|
+
return {
|
|
2226
|
+
provider: resolvedProvider,
|
|
2227
|
+
model: resolvedModel,
|
|
2228
|
+
providerId: routeApplies ? (selectedRoute.providerId || null) : null,
|
|
2229
|
+
registryId: routeApplies ? (selectedRoute.registryId || null) : null,
|
|
2230
|
+
routeLabel: routeApplies ? (selectedRoute.routeLabel || null) : null,
|
|
2231
|
+
};
|
|
2232
|
+
};
|
|
1900
2233
|
|
|
1901
2234
|
const codeReviewFastPath = codeReviewRequested
|
|
1902
2235
|
&& isUsableCodeReviewSnapshot(codeReviewContextBlock)
|
|
@@ -1965,6 +2298,35 @@ async function chat(message, opts = {}) {
|
|
|
1965
2298
|
description: `Run one of my skills to fetch data or perform an action. PREFER skills over raw mcp_call when a matching skill exists — skills parse data correctly and store results in brain. Available skills: ${(() => { try { const { loadAllSkills } = require('./skills/skill-loader'); const bundled = loadAllSkills().map(s => s.name + ': ' + (s.description || '').slice(0, 60)); const db = brain.listSkills({ enabled: 1 }).map(s => s.name + ': ' + (s.description || '').slice(0, 60)); const all = [...new Set([...bundled, ...db])]; return all.join('; ') || 'none'; } catch { return 'unknown'; } })()}`,
|
|
1966
2299
|
input_schema: { type: 'object', properties: { skill_name: { type: 'string', description: 'Name of the skill to run' } }, required: ['skill_name'] },
|
|
1967
2300
|
},
|
|
2301
|
+
{
|
|
2302
|
+
name: 'ctm_context',
|
|
2303
|
+
description: 'Authoritative CTM operational/session-memory lookup for CTM primary, mobile phone URL, /m/, Dev Tunnel, Tailscale, and "CTM session memory" questions. Use this before generic memories for CTM remote-access answers.',
|
|
2304
|
+
input_schema: {
|
|
2305
|
+
type: 'object',
|
|
2306
|
+
properties: {
|
|
2307
|
+
query: { type: 'string', description: 'The CTM/mobile/session-memory question to resolve. Defaults to the user message.' },
|
|
2308
|
+
limit: { type: 'number', default: 5 },
|
|
2309
|
+
force: { type: 'boolean', default: true },
|
|
2310
|
+
},
|
|
2311
|
+
},
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
name: 'ctm_remote_access_status',
|
|
2315
|
+
description: 'Read current CTM setup/network status and return active phone URLs and tunnel state. Use for "what URL should my phone use?"',
|
|
2316
|
+
input_schema: { type: 'object', properties: {} },
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
name: 'ctm_session_search',
|
|
2320
|
+
description: 'Search cached CTM coding-session messages. Use for questions that explicitly ask for CTM session memory, prior coding sessions, or remembered CTM operational fixes.',
|
|
2321
|
+
input_schema: {
|
|
2322
|
+
type: 'object',
|
|
2323
|
+
properties: {
|
|
2324
|
+
query: { type: 'string', description: 'Search query' },
|
|
2325
|
+
limit: { type: 'number', default: 5 },
|
|
2326
|
+
},
|
|
2327
|
+
required: ['query'],
|
|
2328
|
+
},
|
|
2329
|
+
},
|
|
1968
2330
|
{
|
|
1969
2331
|
name: 'search_memories',
|
|
1970
2332
|
description: 'Hybrid search (BM25 + semantic vectors) across private/user memory. Use before public web search for remembered context, prior discussions, decisions, preferences, people, projects, tools, or Slack/email/calendar work context. Call MULTIPLE searches in ONE turn to batch them.',
|
|
@@ -2126,7 +2488,14 @@ async function chat(message, opts = {}) {
|
|
|
2126
2488
|
]);
|
|
2127
2489
|
|
|
2128
2490
|
// Intent-based tool filtering — fewer tools = fewer wrong choices
|
|
2129
|
-
const CONVERSATIONAL_TOOLS = new Set([
|
|
2491
|
+
const CONVERSATIONAL_TOOLS = new Set([
|
|
2492
|
+
'think',
|
|
2493
|
+
'remember_fact',
|
|
2494
|
+
'search_memories',
|
|
2495
|
+
'ctm_context',
|
|
2496
|
+
'ctm_remote_access_status',
|
|
2497
|
+
'ctm_session_search',
|
|
2498
|
+
]);
|
|
2130
2499
|
const DIRECT_ACTION_EXCLUDE = new Set([
|
|
2131
2500
|
'search_memories', 'lookup_person', 'remember_fact', 'think',
|
|
2132
2501
|
'pull_slack', 'slack_connect', 'slack_search', 'slack_read_channel', 'slack_send_message',
|
|
@@ -2148,6 +2517,7 @@ async function chat(message, opts = {}) {
|
|
|
2148
2517
|
|
|
2149
2518
|
function isCodingActionRequest() {
|
|
2150
2519
|
const text = String(routingMessage || '');
|
|
2520
|
+
if (codingTurnIntent.readOnly && !codingTurnIntent.expectsChange) return false;
|
|
2151
2521
|
if (CODING_AGENT_RE.test(text)) return true;
|
|
2152
2522
|
if (STRONG_CODE_CHANGE_RE.test(text)) return queryTopics.includes('technical') || CODE_TARGET_RE.test(text);
|
|
2153
2523
|
return GENERIC_CODE_CHANGE_RE.test(text) && CODE_TARGET_RE.test(text);
|
|
@@ -2194,6 +2564,20 @@ async function chat(message, opts = {}) {
|
|
|
2194
2564
|
return filtered;
|
|
2195
2565
|
}
|
|
2196
2566
|
|
|
2567
|
+
const chatToolRegistry = createWallEToolRegistry({
|
|
2568
|
+
runtimeProfile: wallERuntimeProfile,
|
|
2569
|
+
builtinTools: chatTools,
|
|
2570
|
+
prepareBuiltinArgs: (toolName, args) => normalizeToolCall({ name: toolName, input: args }).input,
|
|
2571
|
+
executeBuiltinTool: (toolName, args, ctx) => executeChatTool(toolName, args, ctx),
|
|
2572
|
+
unknownToolHandler: (toolName, args, ctx) => executeChatTool(toolName, args, ctx),
|
|
2573
|
+
});
|
|
2574
|
+
const chatToolContextBase = runtimeToolContext(wallERuntimeProfile, {
|
|
2575
|
+
provider: usedProvider || targetProviderType,
|
|
2576
|
+
model: selectedModel,
|
|
2577
|
+
sessionId,
|
|
2578
|
+
cwd: effectiveCwd || opts.cwd || process.cwd(),
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2197
2581
|
for (const event of requestedSkillEvents) onProgress(event);
|
|
2198
2582
|
|
|
2199
2583
|
// Execute a chat tool call
|
|
@@ -2220,14 +2604,16 @@ async function chat(message, opts = {}) {
|
|
|
2220
2604
|
if (effectiveCwd && input && typeof input === 'object') {
|
|
2221
2605
|
if (name === 'run_shell' && !input.cwd) {
|
|
2222
2606
|
input = { ...input, cwd: effectiveCwd };
|
|
2223
|
-
} else if (name === 'search_files'
|
|
2224
|
-
input = { ...input, directory: effectiveCwd };
|
|
2607
|
+
} else if (name === 'search_files') {
|
|
2608
|
+
input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
|
|
2225
2609
|
} else if (name === 'glob') {
|
|
2226
2610
|
input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
|
|
2227
2611
|
} else if (name === 'grep_files') {
|
|
2228
2612
|
input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
|
|
2229
2613
|
} else if ((name === 'read_file' || name === 'write_file' || name === 'edit_file') && !input.projectRoot) {
|
|
2230
2614
|
input = { ...input, projectRoot: effectiveCwd };
|
|
2615
|
+
} else if (name === 'start_static_server') {
|
|
2616
|
+
input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
|
|
2231
2617
|
} else if (name === 'list_directory') {
|
|
2232
2618
|
input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
|
|
2233
2619
|
}
|
|
@@ -2242,9 +2628,18 @@ async function chat(message, opts = {}) {
|
|
|
2242
2628
|
if (!externalActionReview.admitted) {
|
|
2243
2629
|
return externalActionReview.result;
|
|
2244
2630
|
}
|
|
2631
|
+
if (externalActionReview.envelope && isExternalActionTool(name)) {
|
|
2632
|
+
const executeExternalAction = typeof opts.externalActionExecutor === 'function'
|
|
2633
|
+
? opts.externalActionExecutor
|
|
2634
|
+
: executeLocalTool;
|
|
2635
|
+
return executeExternalAction(name, input, {
|
|
2636
|
+
envelope: externalActionReview.envelope,
|
|
2637
|
+
approval: externalActionReview.envelope.approval || null,
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2245
2640
|
|
|
2246
2641
|
// Permission check — skip for internal-only tools
|
|
2247
|
-
const SKIP_PERM_CHECK = new Set(['think', 'search_memories', 'lookup_person', 'remember_fact', 'list_mcp_tools', 'list_tasks', 'calendar_events', 'calendar_list', 'system_info', 'clipboard_read', 'mail_messages', 'mail_search', 'read_project_file', 'search_project', 'read_file', 'glob', 'grep_files', 'list_directory']);
|
|
2642
|
+
const SKIP_PERM_CHECK = new Set(['think', 'search_memories', 'ctm_context', 'ctm_remote_access_status', 'ctm_session_search', 'lookup_person', 'remember_fact', 'list_mcp_tools', 'list_tasks', 'calendar_events', 'calendar_list', 'system_info', 'clipboard_read', 'mail_messages', 'mail_search', 'read_project_file', 'search_project', 'read_file', 'glob', 'grep_files', 'list_directory', 'check_url', 'start_static_server', 'stop_static_server']);
|
|
2248
2643
|
if (!SKIP_PERM_CHECK.has(name)) {
|
|
2249
2644
|
const permCommand = name === 'run_shell' ? (input.command || '')
|
|
2250
2645
|
: name === 'write_file' || name === 'read_file' ? (input.file_path || '')
|
|
@@ -2336,6 +2731,39 @@ async function chat(message, opts = {}) {
|
|
|
2336
2731
|
return { error: err.message };
|
|
2337
2732
|
}
|
|
2338
2733
|
}
|
|
2734
|
+
if (name === 'ctm_context') {
|
|
2735
|
+
const query = String(input.query || routingMessage || message || '').trim();
|
|
2736
|
+
return ctmOperationalContext.lookupCtmOperationalContext({
|
|
2737
|
+
query,
|
|
2738
|
+
limit: input.limit || 5,
|
|
2739
|
+
force: input.force !== false,
|
|
2740
|
+
}, {
|
|
2741
|
+
timeoutMs: input.timeout_ms || 6000,
|
|
2742
|
+
cooldownMs: input.cooldown_ms || 0,
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
if (name === 'ctm_remote_access_status') {
|
|
2746
|
+
const status = await ctmContextClient.getRemoteAccessStatus({}, {
|
|
2747
|
+
timeoutMs: input.timeout_ms || 6000,
|
|
2748
|
+
cooldownMs: input.cooldown_ms || 0,
|
|
2749
|
+
});
|
|
2750
|
+
return {
|
|
2751
|
+
ok: !status.reason,
|
|
2752
|
+
source: status.source || 'ctm-api',
|
|
2753
|
+
reason: status.reason || '',
|
|
2754
|
+
api_transport: status.api_transport || null,
|
|
2755
|
+
remote_access: status.reason ? null : ctmOperationalContext.summarizeRemoteAccess(status),
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
if (name === 'ctm_session_search') {
|
|
2759
|
+
return ctmContextClient.searchSessions({
|
|
2760
|
+
query: input.query,
|
|
2761
|
+
limit: input.limit || 5,
|
|
2762
|
+
}, {
|
|
2763
|
+
timeoutMs: input.timeout_ms || 6000,
|
|
2764
|
+
cooldownMs: input.cooldown_ms || 0,
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2339
2767
|
if (name === 'search_memories') {
|
|
2340
2768
|
const sanitizedInput = { ...input, query: sanitizeQuery(input.query) || input.query };
|
|
2341
2769
|
// Hybrid search: FTS5/LIKE + vector similarity (if available)
|
|
@@ -2422,16 +2850,15 @@ async function chat(message, opts = {}) {
|
|
|
2422
2850
|
}
|
|
2423
2851
|
}
|
|
2424
2852
|
|
|
2425
|
-
// Touch accessed memories for importance decay tracking
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
}
|
|
2853
|
+
// Touch accessed memories for importance decay tracking — one batched UPDATE
|
|
2854
|
+
// instead of an N+1 of write-locked statements (one per result row).
|
|
2855
|
+
try { brain.touchMemories(results.map(m => m.id)); } catch {}
|
|
2429
2856
|
|
|
2430
|
-
const
|
|
2857
|
+
const totals = _searchTotals(db);
|
|
2431
2858
|
return {
|
|
2432
2859
|
count: results.length,
|
|
2433
|
-
total_memories:
|
|
2434
|
-
total_slack:
|
|
2860
|
+
total_memories: totals.total_memories,
|
|
2861
|
+
total_slack: totals.total_slack,
|
|
2435
2862
|
search_method: searchMethod,
|
|
2436
2863
|
memories: results.map(m => ({
|
|
2437
2864
|
source: m.source,
|
|
@@ -2585,6 +3012,17 @@ async function chat(message, opts = {}) {
|
|
|
2585
3012
|
return { created: true, id: result.id, title: input.title, status: 'pending', due_at: input.due_at || 'immediate' };
|
|
2586
3013
|
}
|
|
2587
3014
|
if (name === 'start_coding') {
|
|
3015
|
+
const requestedCodingIntent = classifyCodingTurnIntent(input?.request || '');
|
|
3016
|
+
if (requestedCodingIntent.readOnly && !requestedCodingIntent.expectsChange) {
|
|
3017
|
+
return {
|
|
3018
|
+
success: true,
|
|
3019
|
+
skipped: true,
|
|
3020
|
+
read_only: true,
|
|
3021
|
+
intent: requestedCodingIntent,
|
|
3022
|
+
message: 'This is a conversational/read-only follow-up, not a coding task. Answer from the existing conversation and context; do not run a coding task or append a no-code-changes blocker.',
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2588
3026
|
ensureBrainInit();
|
|
2589
3027
|
|
|
2590
3028
|
// Inline execution with progress streaming (default)
|
|
@@ -2649,9 +3087,18 @@ async function chat(message, opts = {}) {
|
|
|
2649
3087
|
progress: progressLog.join('\n'),
|
|
2650
3088
|
};
|
|
2651
3089
|
} catch (err) {
|
|
3090
|
+
// Classify provider/LLM failures so a coding task that couldn't reach the model
|
|
3091
|
+
// reports a clear message ("AI provider network error: …could not reach the
|
|
3092
|
+
// provider endpoint…") instead of a raw "fetch failed".
|
|
3093
|
+
let friendly = (err && err.message) || 'Coding task failed';
|
|
3094
|
+
try {
|
|
3095
|
+
const { decorateProviderError } = require('./llm/provider-error');
|
|
3096
|
+
const pe = decorateProviderError(err, { model: (input && input.model) || undefined }).providerError;
|
|
3097
|
+
if (pe && pe.userMessage) friendly = pe.userMessage;
|
|
3098
|
+
} catch {}
|
|
2652
3099
|
return {
|
|
2653
3100
|
success: false,
|
|
2654
|
-
error:
|
|
3101
|
+
error: friendly,
|
|
2655
3102
|
progress: progressLog.join('\n'),
|
|
2656
3103
|
};
|
|
2657
3104
|
}
|
|
@@ -2815,7 +3262,10 @@ async function chat(message, opts = {}) {
|
|
|
2815
3262
|
// Load recent chat history, resuming from compacted state if available
|
|
2816
3263
|
const chatSessionId = sessionId;
|
|
2817
3264
|
let historyMessages;
|
|
2818
|
-
|
|
3265
|
+
const contextOverride = normalizeContextMessagesForChat(opts.contextMessages, message);
|
|
3266
|
+
if (contextOverride.length > 0) {
|
|
3267
|
+
historyMessages = contextOverride;
|
|
3268
|
+
} else if (existingSession?.compacted_messages) {
|
|
2819
3269
|
// Resume from compacted state + recent messages
|
|
2820
3270
|
try {
|
|
2821
3271
|
const compacted = JSON.parse(existingSession.compacted_messages);
|
|
@@ -2831,36 +3281,17 @@ async function chat(message, opts = {}) {
|
|
|
2831
3281
|
const recentChat = brain.listChatMessages({ session_id: chatSessionId, limit: 10 });
|
|
2832
3282
|
historyMessages = recentChat.map(m => ({ role: m.role, content: m.content }));
|
|
2833
3283
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
// string content. Replace its `content` with a content-block array so
|
|
2846
|
-
// the LLM actually sees the image(s).
|
|
2847
|
-
if (_attachmentBlocks && messages.length > 0) {
|
|
2848
|
-
// Walk back to the last user-role entry (should be the one at -1
|
|
2849
|
-
// for normal flows; defensive scan handles compacted resume edge).
|
|
2850
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2851
|
-
if (messages[i].role === 'user') {
|
|
2852
|
-
const text = typeof messages[i].content === 'string' ? messages[i].content : message;
|
|
2853
|
-
messages[i] = {
|
|
2854
|
-
role: 'user',
|
|
2855
|
-
content: [
|
|
2856
|
-
{ type: 'text', text },
|
|
2857
|
-
..._attachmentBlocks,
|
|
2858
|
-
],
|
|
2859
|
-
};
|
|
2860
|
-
break;
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
3284
|
+
// Provider-boundary assembly is the authority for the current turn.
|
|
3285
|
+
// Persistence happens before the model call, but a DB reread can still be
|
|
3286
|
+
// stale or compacted. Always reassert the accepted user input here so no
|
|
3287
|
+
// provider can receive an empty message list.
|
|
3288
|
+
const messages = buildProviderMessagesForChat({
|
|
3289
|
+
historyMessages,
|
|
3290
|
+
currentMessage: message,
|
|
3291
|
+
attachmentBlocks: _attachmentBlocks,
|
|
3292
|
+
provider: provider.type || targetProviderType,
|
|
3293
|
+
model: selectedModel,
|
|
3294
|
+
});
|
|
2864
3295
|
let finalText = '';
|
|
2865
3296
|
let lastTurnText = ''; // Only the last turn's text (prevents error accumulation across turns)
|
|
2866
3297
|
let lastTurn = 0;
|
|
@@ -2868,6 +3299,8 @@ async function chat(message, opts = {}) {
|
|
|
2868
3299
|
let forceFinalNoTools = false;
|
|
2869
3300
|
let toolLimitNoticeSent = false;
|
|
2870
3301
|
let postToolProgressContinuationCount = 0;
|
|
3302
|
+
let actionCompletionContinuationCount = 0;
|
|
3303
|
+
let localPreviewVerificationContinuationCount = 0;
|
|
2871
3304
|
|
|
2872
3305
|
// Adaptive limits based on intent classification
|
|
2873
3306
|
const INTENT_LIMITS = {
|
|
@@ -2996,13 +3429,36 @@ async function chat(message, opts = {}) {
|
|
|
2996
3429
|
|
|
2997
3430
|
resetTurnTimeout();
|
|
2998
3431
|
const { withRetry } = require('./llm/retry');
|
|
2999
|
-
const toolsForTurn = (() => {
|
|
3432
|
+
const toolsForTurn = await (async () => {
|
|
3000
3433
|
if (codeReviewFastPath || forceFinalNoTools) return [];
|
|
3001
|
-
|
|
3434
|
+
const registryTools = await getRuntimeToolDefinitions(chatToolRegistry, wallERuntimeProfile, {
|
|
3435
|
+
...chatToolContextBase,
|
|
3436
|
+
provider: usedProvider || targetProviderType,
|
|
3437
|
+
model: selectedModel,
|
|
3438
|
+
});
|
|
3439
|
+
let tools = opts.allowedTools ? registryTools.filter(t => opts.allowedTools.includes(t.name)) : registryTools;
|
|
3002
3440
|
tools = filterToolsForIntent(tools, intent);
|
|
3003
3441
|
if (channel === 'task' || channel === 'eval') tools = tools.filter(t => !TASK_CHANNEL_EXCLUDE.has(t.name));
|
|
3004
3442
|
return tools;
|
|
3005
3443
|
})();
|
|
3444
|
+
const runtimePromptManifest = buildRuntimePromptManifest({
|
|
3445
|
+
runtimeProfile: wallERuntimeProfile,
|
|
3446
|
+
systemPrompt,
|
|
3447
|
+
userTask: message,
|
|
3448
|
+
messages,
|
|
3449
|
+
tools: toolsForTurn,
|
|
3450
|
+
provider: usedProvider || targetProviderType,
|
|
3451
|
+
model: selectedModel,
|
|
3452
|
+
metadata: { channel, turn },
|
|
3453
|
+
});
|
|
3454
|
+
try {
|
|
3455
|
+
_telemetry.track('prompt_manifest', {
|
|
3456
|
+
runtime_profile: wallERuntimeProfile.profileId,
|
|
3457
|
+
stable_sections: runtimePromptManifest.stableSectionCount,
|
|
3458
|
+
dynamic_sections: runtimePromptManifest.dynamicSectionCount,
|
|
3459
|
+
token_estimate: runtimePromptManifest.tokenEstimate,
|
|
3460
|
+
});
|
|
3461
|
+
} catch {}
|
|
3006
3462
|
const primaryRetries = (_allowChatProviderFallback(opts)
|
|
3007
3463
|
&& _allowCrossProviderFallbackForTurn({
|
|
3008
3464
|
opts,
|
|
@@ -3037,8 +3493,7 @@ async function chat(message, opts = {}) {
|
|
|
3037
3493
|
if (registeredProv) providerAvailability.recordFailure(registeredProv.providerId, llmErr.message);
|
|
3038
3494
|
} catch {}
|
|
3039
3495
|
const decorated = decorateProviderError(llmErr, {
|
|
3040
|
-
|
|
3041
|
-
model: selectedModel,
|
|
3496
|
+
...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
|
|
3042
3497
|
});
|
|
3043
3498
|
const allowCrossProviderFallback = _allowCrossProviderFallbackForTurn({
|
|
3044
3499
|
opts,
|
|
@@ -3060,7 +3515,7 @@ async function chat(message, opts = {}) {
|
|
|
3060
3515
|
let lastFallbackDecorated = null;
|
|
3061
3516
|
for (const fallback of fallbacks) {
|
|
3062
3517
|
if (attemptedProviderTypes.has(fallback.providerType) || attemptedProviderTypes.has(fallback.provider?.type)) continue;
|
|
3063
|
-
provider = fallback.provider;
|
|
3518
|
+
provider = withProviderMessageGuard(fallback.provider);
|
|
3064
3519
|
selectedModel = fallback.model;
|
|
3065
3520
|
usedProvider = fallback.provider.type || fallback.providerType;
|
|
3066
3521
|
attemptedProviderTypes.add(fallback.providerType);
|
|
@@ -3098,6 +3553,7 @@ async function chat(message, opts = {}) {
|
|
|
3098
3553
|
const fallbackDecorated = decorateProviderError(fallbackErr, {
|
|
3099
3554
|
provider: fallback.providerType,
|
|
3100
3555
|
model: fallback.model,
|
|
3556
|
+
providerId: fallback.providerId || null,
|
|
3101
3557
|
});
|
|
3102
3558
|
lastFallbackDecorated = fallbackDecorated;
|
|
3103
3559
|
recordProviderFailureAlert(fallbackDecorated.providerError, brain);
|
|
@@ -3153,6 +3609,11 @@ async function chat(message, opts = {}) {
|
|
|
3153
3609
|
|
|
3154
3610
|
const textToolResponse = extractTextToolCalls(response);
|
|
3155
3611
|
if (textToolResponse.textToolCallFormat) {
|
|
3612
|
+
warnOnTextToolCallFormatMismatch(
|
|
3613
|
+
response.provider || usedProvider || targetProviderType,
|
|
3614
|
+
response.model || selectedModel,
|
|
3615
|
+
textToolResponse.textToolCallFormat,
|
|
3616
|
+
);
|
|
3156
3617
|
const allowedToolNames = new Set((Array.isArray(toolsForTurn) ? toolsForTurn : []).map((tool) => tool.name));
|
|
3157
3618
|
const allowedToolCalls = textToolResponse.toolCalls.filter((call) => allowedToolNames.has(call.name));
|
|
3158
3619
|
response = { ...textToolResponse, toolCalls: allowedToolCalls };
|
|
@@ -3175,6 +3636,7 @@ async function chat(message, opts = {}) {
|
|
|
3175
3636
|
if (response.usage) {
|
|
3176
3637
|
totalInputTokens += response.usage.input || 0;
|
|
3177
3638
|
totalOutputTokens += response.usage.output || 0;
|
|
3639
|
+
recordUsageLedger(response, 'turn', { turn });
|
|
3178
3640
|
}
|
|
3179
3641
|
if (!usedModel) { usedModel = response.model; usedProvider = response.provider; }
|
|
3180
3642
|
|
|
@@ -3266,6 +3728,84 @@ async function chat(message, opts = {}) {
|
|
|
3266
3728
|
console.log('[chat] Premature post-tool progress text — continuing tool loop');
|
|
3267
3729
|
continue;
|
|
3268
3730
|
}
|
|
3731
|
+
|
|
3732
|
+
const shouldRunActionCompletionGuard = (wallECodingMode || isCodingActionRequest())
|
|
3733
|
+
&& !forceFinalNoTools
|
|
3734
|
+
&& !(codingTurnIntent.readOnly && !codingTurnIntent.expectsChange);
|
|
3735
|
+
if (shouldRunActionCompletionGuard) {
|
|
3736
|
+
let hasTurnBudgetForActionContinuation = turn + 1 < allowedTurns;
|
|
3737
|
+
if (!hasTurnBudgetForActionContinuation) {
|
|
3738
|
+
hasTurnBudgetForActionContinuation = extendCodingTurnBudget('action completion guard');
|
|
3739
|
+
}
|
|
3740
|
+
const toolsAvailableForActionContinuation = Array.isArray(toolsForTurn)
|
|
3741
|
+
&& toolsForTurn.length > 0
|
|
3742
|
+
&& toolCallCount < MAX_TOOL_CALLS
|
|
3743
|
+
&& hasTurnBudgetForActionContinuation;
|
|
3744
|
+
const actionContinuation = _chatNoActionContinuation({
|
|
3745
|
+
prompt: routingMessage || message,
|
|
3746
|
+
content: finalText,
|
|
3747
|
+
toolCallHistory: _chatToolCallHistoryForActionGuard(allToolCalls),
|
|
3748
|
+
toolsAvailable: toolsAvailableForActionContinuation,
|
|
3749
|
+
nudges: actionCompletionContinuationCount,
|
|
3750
|
+
maxNudges: wallECodingMode ? 3 : 1,
|
|
3751
|
+
cwd: effectiveCwd || opts.cwd || process.cwd(),
|
|
3752
|
+
codingIntent: codingTurnIntent,
|
|
3753
|
+
});
|
|
3754
|
+
if (actionContinuation?.action === 'continue') {
|
|
3755
|
+
actionCompletionContinuationCount += 1;
|
|
3756
|
+
const assistantHistoryContent = [];
|
|
3757
|
+
if (response.reasoningContent && typeof response.reasoningContent === 'string') {
|
|
3758
|
+
assistantHistoryContent.push({ type: 'reasoning', text: response.reasoningContent });
|
|
3759
|
+
}
|
|
3760
|
+
if (finalText) assistantHistoryContent.push({ type: 'text', text: finalText });
|
|
3761
|
+
messages.push({
|
|
3762
|
+
role: 'assistant',
|
|
3763
|
+
content: assistantHistoryContent.length > 0 ? assistantHistoryContent : finalText,
|
|
3764
|
+
});
|
|
3765
|
+
messages.push({
|
|
3766
|
+
role: 'user',
|
|
3767
|
+
content: `${actionContinuation.message}\n` +
|
|
3768
|
+
'If the target project is outside the current working directory, do not run broad filesystem scans. Use start_coding with cwd set to the explicit target project directory, or use project-scoped file tools against that directory. For website/UI/UX requests, finish only after concrete edits and verification evidence.',
|
|
3769
|
+
});
|
|
3770
|
+
console.log('[chat] Action completion guard — continuing tool loop:', actionContinuation.reason);
|
|
3771
|
+
continue;
|
|
3772
|
+
}
|
|
3773
|
+
if (actionContinuation?.action === 'fail') {
|
|
3774
|
+
finalText = `I could not complete the requested code change: ${actionContinuation.reason}`;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
const localPreviewClaim = analyzeLocalPreviewClaims(finalText, allToolCalls);
|
|
3778
|
+
if (!forceFinalNoTools
|
|
3779
|
+
&& localPreviewClaim.hasPositiveClaim
|
|
3780
|
+
&& !localPreviewClaim.ok
|
|
3781
|
+
&& localPreviewVerificationContinuationCount < 2) {
|
|
3782
|
+
let hasTurnBudgetForLocalPreviewVerification = turn + 1 < allowedTurns;
|
|
3783
|
+
if (!hasTurnBudgetForLocalPreviewVerification) {
|
|
3784
|
+
hasTurnBudgetForLocalPreviewVerification = extendCodingTurnBudget('local preview verification guard');
|
|
3785
|
+
}
|
|
3786
|
+
const toolsAvailableForLocalPreviewVerification = Array.isArray(toolsForTurn)
|
|
3787
|
+
&& toolsForTurn.length > 0
|
|
3788
|
+
&& toolCallCount < MAX_TOOL_CALLS
|
|
3789
|
+
&& hasTurnBudgetForLocalPreviewVerification;
|
|
3790
|
+
if (toolsAvailableForLocalPreviewVerification) {
|
|
3791
|
+
localPreviewVerificationContinuationCount += 1;
|
|
3792
|
+
const assistantHistoryContent = [];
|
|
3793
|
+
if (response.reasoningContent && typeof response.reasoningContent === 'string') {
|
|
3794
|
+
assistantHistoryContent.push({ type: 'reasoning', text: response.reasoningContent });
|
|
3795
|
+
}
|
|
3796
|
+
if (finalText) assistantHistoryContent.push({ type: 'text', text: finalText });
|
|
3797
|
+
messages.push({
|
|
3798
|
+
role: 'assistant',
|
|
3799
|
+
content: assistantHistoryContent.length > 0 ? assistantHistoryContent : finalText,
|
|
3800
|
+
});
|
|
3801
|
+
messages.push({
|
|
3802
|
+
role: 'user',
|
|
3803
|
+
content: buildLocalPreviewVerificationNudge(localPreviewClaim),
|
|
3804
|
+
});
|
|
3805
|
+
console.warn('[chat] Local preview claim lacked tool evidence; continuing tool loop');
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3269
3809
|
finalResponseMeta = {
|
|
3270
3810
|
model: response.model || selectedModel,
|
|
3271
3811
|
provider: response.provider || usedProvider || targetProviderType,
|
|
@@ -3280,6 +3820,12 @@ async function chat(message, opts = {}) {
|
|
|
3280
3820
|
for (const tu of response.toolCalls) {
|
|
3281
3821
|
const summary = tu.name === 'search_memories'
|
|
3282
3822
|
? `Searching: "${tu.input.query}"${tu.input.source ? ` (${tu.input.source})` : ''}`
|
|
3823
|
+
: tu.name === 'ctm_context'
|
|
3824
|
+
? 'Checking CTM operational context...'
|
|
3825
|
+
: tu.name === 'ctm_remote_access_status'
|
|
3826
|
+
? 'Checking CTM phone access...'
|
|
3827
|
+
: tu.name === 'ctm_session_search'
|
|
3828
|
+
? `Searching CTM sessions: "${tu.input.query}"`
|
|
3283
3829
|
: tu.name === 'think'
|
|
3284
3830
|
? 'Analyzing evidence...'
|
|
3285
3831
|
: tu.name === 'lookup_person'
|
|
@@ -3338,7 +3884,13 @@ async function chat(message, opts = {}) {
|
|
|
3338
3884
|
const executeToolCall = async (tu) => {
|
|
3339
3885
|
const t0 = Date.now();
|
|
3340
3886
|
console.log('[chat] Tool call:', tu.name, JSON.stringify(tu.input).slice(0, 150));
|
|
3341
|
-
const result = await
|
|
3887
|
+
const result = await executeRuntimeTool(chatToolRegistry, wallERuntimeProfile, tu.name, tu.input, {
|
|
3888
|
+
...chatToolContextBase,
|
|
3889
|
+
provider: usedProvider || targetProviderType,
|
|
3890
|
+
model: selectedModel,
|
|
3891
|
+
sessionId,
|
|
3892
|
+
cwd: effectiveCwd || opts.cwd || process.cwd(),
|
|
3893
|
+
});
|
|
3342
3894
|
const resultStr = JSON.stringify(result);
|
|
3343
3895
|
const elapsed = Date.now() - t0;
|
|
3344
3896
|
timings.toolMs += elapsed;
|
|
@@ -3346,18 +3898,25 @@ async function chat(message, opts = {}) {
|
|
|
3346
3898
|
// Per-tool-call telemetry: name + status + latency only. No tool args
|
|
3347
3899
|
// or result content (those can carry user data); error_type is the
|
|
3348
3900
|
// top-level shape only, not the message.
|
|
3901
|
+
const evidence = normalizeToolCallEvidence({ name: tu.name, input: tu.input }, result);
|
|
3902
|
+
const isError = evidence.ok === false;
|
|
3349
3903
|
try {
|
|
3350
|
-
const isError = !!(result && result.error);
|
|
3351
3904
|
_telemetry.track('tool_call', {
|
|
3352
3905
|
name: tu.name,
|
|
3353
3906
|
status: isError ? 'error' : 'success',
|
|
3354
3907
|
duration_ms: elapsed,
|
|
3355
|
-
error_type: isError ? (typeof result
|
|
3908
|
+
error_type: isError ? (typeof result?.error === 'string' ? 'message' : (result?.auth_required ? 'auth_required' : 'object')) : undefined,
|
|
3356
3909
|
});
|
|
3357
3910
|
} catch {}
|
|
3358
3911
|
// Emit tool completion with result summary
|
|
3359
3912
|
const resultSummary = tu.name === 'search_memories'
|
|
3360
3913
|
? `Found ${result.count || 0} results (${result.search_method || 'search'})`
|
|
3914
|
+
: tu.name === 'ctm_context'
|
|
3915
|
+
? `CTM context ${result.ok ? 'available' : 'unavailable'}`
|
|
3916
|
+
: tu.name === 'ctm_remote_access_status'
|
|
3917
|
+
? `CTM phone access ${result.ok ? 'available' : 'unavailable'}`
|
|
3918
|
+
: tu.name === 'ctm_session_search'
|
|
3919
|
+
? `Found ${(result.results || []).length} CTM session result(s)`
|
|
3361
3920
|
: tu.name === 'think'
|
|
3362
3921
|
? 'Done thinking'
|
|
3363
3922
|
: `Completed in ${elapsed}ms`;
|
|
@@ -3371,23 +3930,62 @@ async function chat(message, opts = {}) {
|
|
|
3371
3930
|
name: tu.name,
|
|
3372
3931
|
result: _progressToolResultPayload(result, resultStr),
|
|
3373
3932
|
summary: resultSummary,
|
|
3933
|
+
error: isError ? (result?.error || result?.stderr || 'Tool returned unsuccessful evidence') : null,
|
|
3374
3934
|
fullSizeBytes: Buffer.byteLength(resultStr, 'utf8'),
|
|
3375
3935
|
});
|
|
3376
3936
|
return {
|
|
3377
3937
|
message: { type: 'tool_result', tool_use_id: tu.id, content: compactedResult },
|
|
3378
|
-
call: {
|
|
3938
|
+
call: {
|
|
3939
|
+
id: tu.id,
|
|
3940
|
+
tool: tu.name,
|
|
3941
|
+
name: tu.name,
|
|
3942
|
+
args: tu.input,
|
|
3943
|
+
input: tu.input,
|
|
3944
|
+
result,
|
|
3945
|
+
ok: evidence.ok === true,
|
|
3946
|
+
result_summary: resultSummary,
|
|
3947
|
+
error: isError ? (result?.error || true) : false,
|
|
3948
|
+
},
|
|
3379
3949
|
};
|
|
3380
3950
|
};
|
|
3381
3951
|
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3952
|
+
const runtimeToolExecutor = new RuntimeToolExecutor({
|
|
3953
|
+
registry: chatToolRegistry,
|
|
3954
|
+
runtimeProfile: wallERuntimeProfile,
|
|
3955
|
+
executeToolCall,
|
|
3956
|
+
});
|
|
3957
|
+
const toolExecutionRows = await runtimeToolExecutor.run(response.toolCalls, {
|
|
3958
|
+
...chatToolContextBase,
|
|
3959
|
+
provider: usedProvider || targetProviderType,
|
|
3960
|
+
model: selectedModel,
|
|
3961
|
+
sessionId,
|
|
3962
|
+
cwd: effectiveCwd || opts.cwd || process.cwd(),
|
|
3963
|
+
});
|
|
3964
|
+
const toolOutputs = toolExecutionRows.map((row) => {
|
|
3965
|
+
if (row.rawResult?.message && row.rawResult?.call) return row.rawResult;
|
|
3966
|
+
const call = row.call || {};
|
|
3967
|
+
const envelope = row.envelope || {};
|
|
3968
|
+
const result = envelope.outputForModel || row.result || { error: envelope.error || 'Tool execution failed' };
|
|
3969
|
+
const content = typeof result === 'string' ? result : JSON.stringify(result);
|
|
3970
|
+
return {
|
|
3971
|
+
message: {
|
|
3972
|
+
type: 'tool_result',
|
|
3973
|
+
tool_use_id: call.id,
|
|
3974
|
+
content,
|
|
3975
|
+
is_error: row.ok === false,
|
|
3976
|
+
},
|
|
3977
|
+
call: {
|
|
3978
|
+
id: call.id,
|
|
3979
|
+
tool: call.name,
|
|
3980
|
+
name: call.name,
|
|
3981
|
+
args: call.input,
|
|
3982
|
+
input: call.input,
|
|
3983
|
+
result,
|
|
3984
|
+
ok: row.ok === true,
|
|
3985
|
+
error: row.ok === true ? false : (envelope.error || true),
|
|
3986
|
+
},
|
|
3987
|
+
};
|
|
3988
|
+
});
|
|
3391
3989
|
|
|
3392
3990
|
for (const output of toolOutputs) allToolCalls.push(output.call);
|
|
3393
3991
|
const toolResults = toolOutputs.map((output) => output.message);
|
|
@@ -3516,6 +4114,7 @@ async function chat(message, opts = {}) {
|
|
|
3516
4114
|
if (repairResponse.usage) {
|
|
3517
4115
|
totalInputTokens += repairResponse.usage.input || 0;
|
|
3518
4116
|
totalOutputTokens += repairResponse.usage.output || 0;
|
|
4117
|
+
recordUsageLedger(repairResponse, 'code_review_repair', { repair: 'code_review' });
|
|
3519
4118
|
}
|
|
3520
4119
|
timings.repairMs += Date.now() - repairStart;
|
|
3521
4120
|
if (!usedModel) { usedModel = repairResponse.model; usedProvider = repairResponse.provider; }
|
|
@@ -3538,8 +4137,7 @@ async function chat(message, opts = {}) {
|
|
|
3538
4137
|
console.error('[chat] Code-review repair call failed:', repairErr.message);
|
|
3539
4138
|
try {
|
|
3540
4139
|
const decorated = decorateProviderError(repairErr, {
|
|
3541
|
-
|
|
3542
|
-
model: selectedModel,
|
|
4140
|
+
...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
|
|
3543
4141
|
});
|
|
3544
4142
|
recordProviderFailureAlert(decorated.providerError, brain);
|
|
3545
4143
|
} catch {}
|
|
@@ -3572,6 +4170,7 @@ async function chat(message, opts = {}) {
|
|
|
3572
4170
|
if (repairResponse.usage) {
|
|
3573
4171
|
totalInputTokens += repairResponse.usage.input || 0;
|
|
3574
4172
|
totalOutputTokens += repairResponse.usage.output || 0;
|
|
4173
|
+
recordUsageLedger(repairResponse, 'post_tool_repair', { repair: 'tool_followup' });
|
|
3575
4174
|
}
|
|
3576
4175
|
timings.repairMs += Date.now() - repairStart;
|
|
3577
4176
|
if (!usedModel) { usedModel = repairResponse.model; usedProvider = repairResponse.provider; }
|
|
@@ -3597,14 +4196,23 @@ async function chat(message, opts = {}) {
|
|
|
3597
4196
|
console.error('[chat] Post-tool repair call failed:', repairErr.message);
|
|
3598
4197
|
try {
|
|
3599
4198
|
const decorated = decorateProviderError(repairErr, {
|
|
3600
|
-
|
|
3601
|
-
model: selectedModel,
|
|
4199
|
+
...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
|
|
3602
4200
|
});
|
|
3603
4201
|
recordProviderFailureAlert(decorated.providerError, brain);
|
|
3604
4202
|
} catch {}
|
|
3605
4203
|
}
|
|
3606
4204
|
if (repairFailed && hasPrematureToolControlText(null, finalText)) {
|
|
3607
|
-
const
|
|
4205
|
+
const evidenceFallback = await _tryEvidenceOnlyFinalization({
|
|
4206
|
+
provider,
|
|
4207
|
+
model: selectedModel,
|
|
4208
|
+
originalRequest: routingMessage || message,
|
|
4209
|
+
toolCalls: allToolCalls,
|
|
4210
|
+
opts,
|
|
4211
|
+
resetFinalizationTimeout,
|
|
4212
|
+
resolveCurrentReasoningOptions,
|
|
4213
|
+
getSignal: () => controller.signal,
|
|
4214
|
+
});
|
|
4215
|
+
const fallback = evidenceFallback || await _tryProviderFinalizationFallback({
|
|
3608
4216
|
messages,
|
|
3609
4217
|
systemPrompt,
|
|
3610
4218
|
attemptedProviderTypes,
|
|
@@ -3623,13 +4231,19 @@ async function chat(message, opts = {}) {
|
|
|
3623
4231
|
if (fallback.response?.usage) {
|
|
3624
4232
|
totalInputTokens += fallback.response.usage.input || 0;
|
|
3625
4233
|
totalOutputTokens += fallback.response.usage.output || 0;
|
|
4234
|
+
recordUsageLedger(fallback.response, evidenceFallback ? 'tool_followup_evidence_only' : 'tool_followup_provider_fallback', {
|
|
4235
|
+
fallback: true,
|
|
4236
|
+
repair: evidenceFallback ? 'tool_followup_evidence_only' : 'tool_followup_provider_fallback',
|
|
4237
|
+
provider: fallback.meta?.provider || null,
|
|
4238
|
+
model: fallback.meta?.model || null,
|
|
4239
|
+
});
|
|
3626
4240
|
}
|
|
3627
4241
|
usedModel = fallback.meta.model || usedModel;
|
|
3628
4242
|
usedProvider = fallback.meta.provider || usedProvider;
|
|
3629
4243
|
finalResponseMeta = {
|
|
3630
4244
|
...(finalResponseMeta || {}),
|
|
3631
4245
|
...fallback.meta,
|
|
3632
|
-
repair: 'tool_followup_provider_fallback',
|
|
4246
|
+
repair: evidenceFallback ? 'tool_followup_evidence_only' : 'tool_followup_provider_fallback',
|
|
3633
4247
|
};
|
|
3634
4248
|
} else {
|
|
3635
4249
|
finalText = _postToolFinalizationFailureReply(opts, allToolCalls);
|
|
@@ -3667,14 +4281,25 @@ async function chat(message, opts = {}) {
|
|
|
3667
4281
|
if (summaryResponse.usage) {
|
|
3668
4282
|
totalInputTokens += summaryResponse.usage.input || 0;
|
|
3669
4283
|
totalOutputTokens += summaryResponse.usage.output || 0;
|
|
4284
|
+
recordUsageLedger(summaryResponse, 'summary_fallback', { repair: 'summary' });
|
|
3670
4285
|
}
|
|
3671
4286
|
const summaryTextResponse = extractTextToolCalls(summaryResponse);
|
|
3672
4287
|
const summaryText = summaryTextResponse.content || '';
|
|
3673
4288
|
if (summaryText && !hasPrematureToolControlText(summaryTextResponse, summaryText)) {
|
|
3674
4289
|
finalText = summaryText;
|
|
3675
4290
|
} else {
|
|
3676
|
-
console.warn('[chat] Summary fallback returned progress/tool-control text; trying
|
|
3677
|
-
const
|
|
4291
|
+
console.warn('[chat] Summary fallback returned progress/tool-control text; trying evidence-only finalization');
|
|
4292
|
+
const evidenceFallback = await _tryEvidenceOnlyFinalization({
|
|
4293
|
+
provider,
|
|
4294
|
+
model: selectedModel,
|
|
4295
|
+
originalRequest: routingMessage || message,
|
|
4296
|
+
toolCalls: allToolCalls,
|
|
4297
|
+
opts,
|
|
4298
|
+
resetFinalizationTimeout,
|
|
4299
|
+
resolveCurrentReasoningOptions,
|
|
4300
|
+
getSignal: () => controller.signal,
|
|
4301
|
+
});
|
|
4302
|
+
const fallback = evidenceFallback || await _tryProviderFinalizationFallback({
|
|
3678
4303
|
messages,
|
|
3679
4304
|
systemPrompt,
|
|
3680
4305
|
attemptedProviderTypes,
|
|
@@ -3693,6 +4318,12 @@ async function chat(message, opts = {}) {
|
|
|
3693
4318
|
if (fallback.response?.usage) {
|
|
3694
4319
|
totalInputTokens += fallback.response.usage.input || 0;
|
|
3695
4320
|
totalOutputTokens += fallback.response.usage.output || 0;
|
|
4321
|
+
recordUsageLedger(fallback.response, evidenceFallback ? 'summary_evidence_only' : 'summary_provider_fallback', {
|
|
4322
|
+
fallback: true,
|
|
4323
|
+
repair: evidenceFallback ? 'summary_evidence_only' : 'summary_provider_fallback',
|
|
4324
|
+
provider: fallback.meta?.provider || null,
|
|
4325
|
+
model: fallback.meta?.model || null,
|
|
4326
|
+
});
|
|
3696
4327
|
}
|
|
3697
4328
|
usedModel = fallback.meta.model || usedModel;
|
|
3698
4329
|
usedProvider = fallback.meta.provider || usedProvider;
|
|
@@ -3722,8 +4353,7 @@ async function chat(message, opts = {}) {
|
|
|
3722
4353
|
let summaryProviderError = null;
|
|
3723
4354
|
try {
|
|
3724
4355
|
const decorated = decorateProviderError(summaryErr, {
|
|
3725
|
-
|
|
3726
|
-
model: selectedModel,
|
|
4356
|
+
...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
|
|
3727
4357
|
});
|
|
3728
4358
|
summaryProviderError = decorated.providerError;
|
|
3729
4359
|
recordProviderFailureAlert(summaryProviderError, brain);
|
|
@@ -3736,7 +4366,19 @@ async function chat(message, opts = {}) {
|
|
|
3736
4366
|
const summaryFailureText = summaryProviderError
|
|
3737
4367
|
? `${summaryProviderError.title}: ${summaryProviderError.userMessage}`
|
|
3738
4368
|
: `Summary generation failed: ${summaryErr.message}`;
|
|
3739
|
-
const
|
|
4369
|
+
const evidenceFallback = allToolCalls.length > 0
|
|
4370
|
+
? await _tryEvidenceOnlyFinalization({
|
|
4371
|
+
provider,
|
|
4372
|
+
model: selectedModel,
|
|
4373
|
+
originalRequest: routingMessage || message,
|
|
4374
|
+
toolCalls: allToolCalls,
|
|
4375
|
+
opts,
|
|
4376
|
+
resetFinalizationTimeout,
|
|
4377
|
+
resolveCurrentReasoningOptions,
|
|
4378
|
+
getSignal: () => controller.signal,
|
|
4379
|
+
})
|
|
4380
|
+
: null;
|
|
4381
|
+
const fallback = evidenceFallback || (allToolCalls.length > 0
|
|
3740
4382
|
? await _tryProviderFinalizationFallback({
|
|
3741
4383
|
messages,
|
|
3742
4384
|
systemPrompt,
|
|
@@ -3750,13 +4392,19 @@ async function chat(message, opts = {}) {
|
|
|
3750
4392
|
fromProvider: usedProvider || targetProviderType,
|
|
3751
4393
|
fromModel: usedModel || selectedModel,
|
|
3752
4394
|
})
|
|
3753
|
-
: null;
|
|
4395
|
+
: null);
|
|
3754
4396
|
if (fallback) {
|
|
3755
4397
|
finalText = fallback.text;
|
|
3756
4398
|
lastTurnText = finalText;
|
|
3757
4399
|
if (fallback.response?.usage) {
|
|
3758
4400
|
totalInputTokens += fallback.response.usage.input || 0;
|
|
3759
4401
|
totalOutputTokens += fallback.response.usage.output || 0;
|
|
4402
|
+
recordUsageLedger(fallback.response, evidenceFallback ? 'summary_error_evidence_only' : 'summary_error_provider_fallback', {
|
|
4403
|
+
fallback: true,
|
|
4404
|
+
repair: evidenceFallback ? 'summary_error_evidence_only' : 'summary_error_provider_fallback',
|
|
4405
|
+
provider: fallback.meta?.provider || null,
|
|
4406
|
+
model: fallback.meta?.model || null,
|
|
4407
|
+
});
|
|
3760
4408
|
}
|
|
3761
4409
|
usedModel = fallback.meta.model || usedModel;
|
|
3762
4410
|
usedProvider = fallback.meta.provider || usedProvider;
|
|
@@ -3838,6 +4486,91 @@ async function chat(message, opts = {}) {
|
|
|
3838
4486
|
console.error('[chat] Self-critique error:', critiqueErr.message);
|
|
3839
4487
|
}
|
|
3840
4488
|
|
|
4489
|
+
const frameValidation = validateFrameAnswer(text, conversationRuntime.frame);
|
|
4490
|
+
if (!frameValidation.ok && !opts.disableConversationFrameRepair) {
|
|
4491
|
+
try {
|
|
4492
|
+
console.warn('[chat] Conversation frame validation failed; forcing scoped final-answer repair:', frameValidation.violations.map(v => v.code).join(','));
|
|
4493
|
+
resetFinalizationTimeout();
|
|
4494
|
+
const repairStart = Date.now();
|
|
4495
|
+
const frameRepairResponse = await provider.chat({
|
|
4496
|
+
model: selectedModel,
|
|
4497
|
+
maxTokens: Math.min(2048, Math.max(512, estimateTokens(text) + 512)),
|
|
4498
|
+
system: systemPrompt,
|
|
4499
|
+
messages: [
|
|
4500
|
+
...messages,
|
|
4501
|
+
{ role: 'assistant', content: text },
|
|
4502
|
+
{
|
|
4503
|
+
role: 'user',
|
|
4504
|
+
content: buildFrameRepairPrompt({
|
|
4505
|
+
frame: conversationRuntime.frame,
|
|
4506
|
+
violations: frameValidation.violations,
|
|
4507
|
+
originalUserMessage: message,
|
|
4508
|
+
}),
|
|
4509
|
+
},
|
|
4510
|
+
],
|
|
4511
|
+
tools: [],
|
|
4512
|
+
thinking: 'disabled',
|
|
4513
|
+
signal: controller.signal,
|
|
4514
|
+
});
|
|
4515
|
+
if (frameRepairResponse.usage) {
|
|
4516
|
+
totalInputTokens += frameRepairResponse.usage.input || 0;
|
|
4517
|
+
totalOutputTokens += frameRepairResponse.usage.output || 0;
|
|
4518
|
+
recordUsageLedger(frameRepairResponse, 'conversation_frame_repair', { repair: 'conversation_frame' });
|
|
4519
|
+
}
|
|
4520
|
+
timings.repairMs += Date.now() - repairStart;
|
|
4521
|
+
const frameRepairSanitization = _sanitizeProtocolFinalText(frameRepairResponse.content || '', opts, allToolCalls);
|
|
4522
|
+
const repairedText = frameRepairSanitization.text || '';
|
|
4523
|
+
const repairedValidation = validateFrameAnswer(repairedText, conversationRuntime.frame);
|
|
4524
|
+
if (repairedText && !frameRepairSanitization.finalizationFailed && repairedValidation.ok) {
|
|
4525
|
+
text = repairedText;
|
|
4526
|
+
lastTurnText = text;
|
|
4527
|
+
finalResponseMeta = {
|
|
4528
|
+
...(finalResponseMeta || {}),
|
|
4529
|
+
model: frameRepairResponse.model || selectedModel,
|
|
4530
|
+
provider: frameRepairResponse.provider || usedProvider || targetProviderType,
|
|
4531
|
+
usage: frameRepairResponse.usage || finalResponseMeta?.usage || null,
|
|
4532
|
+
stopReason: frameRepairResponse.stopReason || finalResponseMeta?.stopReason || null,
|
|
4533
|
+
conversationFrameRepair: true,
|
|
4534
|
+
conversationFrameValidation: {
|
|
4535
|
+
ok: true,
|
|
4536
|
+
repairedFrom: frameValidation.violations.map(v => v.code),
|
|
4537
|
+
},
|
|
4538
|
+
...(frameRepairSanitization.changed ? { conversationFrameRepairProtocolSanitized: true } : {}),
|
|
4539
|
+
};
|
|
4540
|
+
} else {
|
|
4541
|
+
console.warn('[chat] Conversation frame repair did not satisfy validation; keeping original final response');
|
|
4542
|
+
finalResponseMeta = {
|
|
4543
|
+
...(finalResponseMeta || {}),
|
|
4544
|
+
conversationFrameValidation: {
|
|
4545
|
+
ok: false,
|
|
4546
|
+
violations: frameValidation.violations.map(v => ({ code: v.code, message: v.message })),
|
|
4547
|
+
repairOk: false,
|
|
4548
|
+
repairedViolations: repairedValidation.violations.map(v => ({ code: v.code, message: v.message })),
|
|
4549
|
+
},
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
4552
|
+
} catch (repairErr) {
|
|
4553
|
+
if (_isUserCancellation(opts, repairErr)) throw _cancelledError();
|
|
4554
|
+
console.error('[chat] Conversation frame repair call failed:', repairErr.message);
|
|
4555
|
+
finalResponseMeta = {
|
|
4556
|
+
...(finalResponseMeta || {}),
|
|
4557
|
+
conversationFrameValidation: {
|
|
4558
|
+
ok: false,
|
|
4559
|
+
violations: frameValidation.violations.map(v => ({ code: v.code, message: v.message })),
|
|
4560
|
+
repairError: repairErr.message,
|
|
4561
|
+
},
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
} else {
|
|
4565
|
+
finalResponseMeta = {
|
|
4566
|
+
...(finalResponseMeta || {}),
|
|
4567
|
+
conversationFrameValidation: {
|
|
4568
|
+
ok: true,
|
|
4569
|
+
validators: conversationRuntime.validators,
|
|
4570
|
+
},
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
|
|
3841
4574
|
const languageMismatch = detectResponseLanguageMismatch(message, text);
|
|
3842
4575
|
if (languageMismatch && !opts.disableLanguageRepair) {
|
|
3843
4576
|
try {
|
|
@@ -3860,6 +4593,7 @@ async function chat(message, opts = {}) {
|
|
|
3860
4593
|
if (languageRepairResponse.usage) {
|
|
3861
4594
|
totalInputTokens += languageRepairResponse.usage.input || 0;
|
|
3862
4595
|
totalOutputTokens += languageRepairResponse.usage.output || 0;
|
|
4596
|
+
recordUsageLedger(languageRepairResponse, 'language_repair', { repair: 'language' });
|
|
3863
4597
|
}
|
|
3864
4598
|
timings.repairMs += Date.now() - repairStart;
|
|
3865
4599
|
const languageRepairSanitization = _sanitizeProtocolFinalText(languageRepairResponse.content || '', opts, allToolCalls);
|
|
@@ -3898,15 +4632,34 @@ async function chat(message, opts = {}) {
|
|
|
3898
4632
|
};
|
|
3899
4633
|
}
|
|
3900
4634
|
|
|
4635
|
+
const finalLocalPreviewClaim = analyzeLocalPreviewClaims(text, allToolCalls);
|
|
4636
|
+
if (finalLocalPreviewClaim.hasPositiveClaim && !finalLocalPreviewClaim.ok) {
|
|
4637
|
+
console.warn('[chat] Blocking unsupported local preview claim before persistence');
|
|
4638
|
+
text = buildUnsupportedLocalPreviewReply(finalLocalPreviewClaim);
|
|
4639
|
+
lastTurnText = text;
|
|
4640
|
+
finalResponseMeta = {
|
|
4641
|
+
...(finalResponseMeta || {}),
|
|
4642
|
+
localPreviewClaimBlocked: true,
|
|
4643
|
+
unsupportedLocalPreviewUrls: finalLocalPreviewClaim.unsupportedUrls.map((url) => url.raw),
|
|
4644
|
+
};
|
|
4645
|
+
}
|
|
4646
|
+
|
|
3901
4647
|
// Save enriched session state
|
|
3902
4648
|
try {
|
|
3903
4649
|
const metadata = {
|
|
4650
|
+
...existingSessionMetadata,
|
|
3904
4651
|
lastTopic: message.slice(0, 200),
|
|
3905
4652
|
turnCount: lastTurn + 1,
|
|
3906
4653
|
toolsUsed: [...new Set(messages
|
|
3907
4654
|
.filter(m => m.role === 'assistant' && Array.isArray(m.content))
|
|
3908
4655
|
.flatMap(m => m.content.filter(b => b.type === 'tool_use').map(b => b.name))
|
|
3909
4656
|
)],
|
|
4657
|
+
conversationFrame: conversationRuntime.frame,
|
|
4658
|
+
conversationRuntime: {
|
|
4659
|
+
schemaVersion: conversationRuntime.schemaVersion,
|
|
4660
|
+
validators: conversationRuntime.validators,
|
|
4661
|
+
lastValidation: finalResponseMeta?.conversationFrameValidation || null,
|
|
4662
|
+
},
|
|
3910
4663
|
};
|
|
3911
4664
|
brain.upsertSession({
|
|
3912
4665
|
id: sessionId, channel,
|
|
@@ -3919,9 +4672,16 @@ async function chat(message, opts = {}) {
|
|
|
3919
4672
|
console.error('[chat] Failed to save session:', sessionErr.message);
|
|
3920
4673
|
}
|
|
3921
4674
|
|
|
3922
|
-
// Save assistant response (user message was already saved before calling Claude)
|
|
4675
|
+
// Save assistant response (user message was already saved before calling Claude).
|
|
4676
|
+
// Record which model/provider actually produced this turn (finalResponseMeta is
|
|
4677
|
+
// the synthesis turn's meta; fall back to the last used/selected model) so the
|
|
4678
|
+
// conversation log can attribute it instead of showing a blank model.
|
|
3923
4679
|
const persistStart = Date.now();
|
|
3924
|
-
const assistantMessage = brain.insertChatMessage({
|
|
4680
|
+
const assistantMessage = brain.insertChatMessage({
|
|
4681
|
+
role: 'assistant', content: text, channel, session_id: sessionId,
|
|
4682
|
+
model_id: finalResponseMeta?.model || usedModel || selectedModel || null,
|
|
4683
|
+
model_provider: finalResponseMeta?.provider || usedProvider || targetProviderType || null,
|
|
4684
|
+
});
|
|
3925
4685
|
recordSessionMessage('assistant', text, {
|
|
3926
4686
|
...(finalResponseMeta || {}),
|
|
3927
4687
|
dbMessageId: assistantMessage.id,
|
|
@@ -3961,7 +4721,7 @@ async function chat(message, opts = {}) {
|
|
|
3961
4721
|
timings.persistMs += Date.now() - persistStart;
|
|
3962
4722
|
|
|
3963
4723
|
const latencyMs = Date.now() - chatStartTime;
|
|
3964
|
-
const cost = calculateCost(usedModel, totalInputTokens, totalOutputTokens);
|
|
4724
|
+
const cost = calculateCost(usedModel, totalInputTokens, totalOutputTokens, usedProvider);
|
|
3965
4725
|
|
|
3966
4726
|
// Telemetry (anonymous — no message content)
|
|
3967
4727
|
try {
|
|
@@ -4079,7 +4839,16 @@ async function chat(message, opts = {}) {
|
|
|
4079
4839
|
}
|
|
4080
4840
|
}
|
|
4081
4841
|
|
|
4082
|
-
function calculateCost(model, inputTokens, outputTokens) {
|
|
4842
|
+
function calculateCost(model, inputTokens, outputTokens, providerType) {
|
|
4843
|
+
try {
|
|
4844
|
+
const estimate = brain.estimateModelUsageCost?.({
|
|
4845
|
+
providerType,
|
|
4846
|
+
modelId: model,
|
|
4847
|
+
inputTokens,
|
|
4848
|
+
outputTokens,
|
|
4849
|
+
});
|
|
4850
|
+
if (Number.isFinite(estimate?.costUsd)) return estimate.costUsd;
|
|
4851
|
+
} catch {}
|
|
4083
4852
|
const pricing = {
|
|
4084
4853
|
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
4085
4854
|
'claude-opus-4-6': { input: 15, output: 75 },
|