@project-ava-supernova/core 0.2.71
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/dist/agent/agent.d.ts +623 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/agent.js +3558 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/claims-auditor.d.ts +54 -0
- package/dist/agent/claims-auditor.d.ts.map +1 -0
- package/dist/agent/claims-auditor.js +125 -0
- package/dist/agent/claims-auditor.js.map +1 -0
- package/dist/agent/context-continuity.d.ts +125 -0
- package/dist/agent/context-continuity.d.ts.map +1 -0
- package/dist/agent/context-continuity.js +231 -0
- package/dist/agent/context-continuity.js.map +1 -0
- package/dist/agent/conversation.d.ts +26 -0
- package/dist/agent/conversation.d.ts.map +1 -0
- package/dist/agent/conversation.js +86 -0
- package/dist/agent/conversation.js.map +1 -0
- package/dist/agent/design-reinjection.d.ts +93 -0
- package/dist/agent/design-reinjection.d.ts.map +1 -0
- package/dist/agent/design-reinjection.js +235 -0
- package/dist/agent/design-reinjection.js.map +1 -0
- package/dist/agent/error-loop-detector.d.ts +59 -0
- package/dist/agent/error-loop-detector.d.ts.map +1 -0
- package/dist/agent/error-loop-detector.js +124 -0
- package/dist/agent/error-loop-detector.js.map +1 -0
- package/dist/agent/fresh-eyes.d.ts +52 -0
- package/dist/agent/fresh-eyes.d.ts.map +1 -0
- package/dist/agent/fresh-eyes.js +118 -0
- package/dist/agent/fresh-eyes.js.map +1 -0
- package/dist/agent/intent-classifier.d.ts +51 -0
- package/dist/agent/intent-classifier.d.ts.map +1 -0
- package/dist/agent/intent-classifier.js +125 -0
- package/dist/agent/intent-classifier.js.map +1 -0
- package/dist/agent/post-edit-verify.d.ts +79 -0
- package/dist/agent/post-edit-verify.d.ts.map +1 -0
- package/dist/agent/post-edit-verify.js +143 -0
- package/dist/agent/post-edit-verify.js.map +1 -0
- package/dist/agent/recover-written-calls.d.ts +90 -0
- package/dist/agent/recover-written-calls.d.ts.map +1 -0
- package/dist/agent/recover-written-calls.js +257 -0
- package/dist/agent/recover-written-calls.js.map +1 -0
- package/dist/agent/social-craft.d.ts +76 -0
- package/dist/agent/social-craft.d.ts.map +1 -0
- package/dist/agent/social-craft.js +210 -0
- package/dist/agent/social-craft.js.map +1 -0
- package/dist/agent/stop-command.d.ts +23 -0
- package/dist/agent/stop-command.d.ts.map +1 -0
- package/dist/agent/stop-command.js +59 -0
- package/dist/agent/stop-command.js.map +1 -0
- package/dist/agent/system-prompt.d.ts +125 -0
- package/dist/agent/system-prompt.d.ts.map +1 -0
- package/dist/agent/system-prompt.js +1334 -0
- package/dist/agent/system-prompt.js.map +1 -0
- package/dist/agent/task-classifier.d.ts +50 -0
- package/dist/agent/task-classifier.d.ts.map +1 -0
- package/dist/agent/task-classifier.js +187 -0
- package/dist/agent/task-classifier.js.map +1 -0
- package/dist/agent/text-tool-parser.d.ts +34 -0
- package/dist/agent/text-tool-parser.d.ts.map +1 -0
- package/dist/agent/text-tool-parser.js +96 -0
- package/dist/agent/text-tool-parser.js.map +1 -0
- package/dist/agent/vision-bridge.d.ts +29 -0
- package/dist/agent/vision-bridge.d.ts.map +1 -0
- package/dist/agent/vision-bridge.js +143 -0
- package/dist/agent/vision-bridge.js.map +1 -0
- package/dist/audit/cost.d.ts +18 -0
- package/dist/audit/cost.d.ts.map +1 -0
- package/dist/audit/cost.js +170 -0
- package/dist/audit/cost.js.map +1 -0
- package/dist/audit/export.d.ts +30 -0
- package/dist/audit/export.d.ts.map +1 -0
- package/dist/audit/export.js +155 -0
- package/dist/audit/export.js.map +1 -0
- package/dist/audit/index.d.ts +11 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/integrity.d.ts +7 -0
- package/dist/audit/integrity.d.ts.map +1 -0
- package/dist/audit/integrity.js +78 -0
- package/dist/audit/integrity.js.map +1 -0
- package/dist/audit/logger.d.ts +18 -0
- package/dist/audit/logger.d.ts.map +1 -0
- package/dist/audit/logger.js +113 -0
- package/dist/audit/logger.js.map +1 -0
- package/dist/audit/patterns.d.ts +34 -0
- package/dist/audit/patterns.d.ts.map +1 -0
- package/dist/audit/patterns.js +104 -0
- package/dist/audit/patterns.js.map +1 -0
- package/dist/audit/security.d.ts +8 -0
- package/dist/audit/security.d.ts.map +1 -0
- package/dist/audit/security.js +56 -0
- package/dist/audit/security.js.map +1 -0
- package/dist/audit/summary.d.ts +22 -0
- package/dist/audit/summary.d.ts.map +1 -0
- package/dist/audit/summary.js +51 -0
- package/dist/audit/summary.js.map +1 -0
- package/dist/audit/types.d.ts +104 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +11 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/auto/aurora-router.d.ts +71 -0
- package/dist/auto/aurora-router.d.ts.map +1 -0
- package/dist/auto/aurora-router.js +138 -0
- package/dist/auto/aurora-router.js.map +1 -0
- package/dist/auto/auto-coordinator.d.ts +232 -0
- package/dist/auto/auto-coordinator.d.ts.map +1 -0
- package/dist/auto/auto-coordinator.js +1260 -0
- package/dist/auto/auto-coordinator.js.map +1 -0
- package/dist/auto/brief-generator.d.ts +10 -0
- package/dist/auto/brief-generator.d.ts.map +1 -0
- package/dist/auto/brief-generator.js +67 -0
- package/dist/auto/brief-generator.js.map +1 -0
- package/dist/auto/changes-summary.d.ts +19 -0
- package/dist/auto/changes-summary.d.ts.map +1 -0
- package/dist/auto/changes-summary.js +45 -0
- package/dist/auto/changes-summary.js.map +1 -0
- package/dist/auto/context-tracker.d.ts +27 -0
- package/dist/auto/context-tracker.d.ts.map +1 -0
- package/dist/auto/context-tracker.js +56 -0
- package/dist/auto/context-tracker.js.map +1 -0
- package/dist/auto/coordinator-model.d.ts +31 -0
- package/dist/auto/coordinator-model.d.ts.map +1 -0
- package/dist/auto/coordinator-model.js +68 -0
- package/dist/auto/coordinator-model.js.map +1 -0
- package/dist/auto/index.d.ts +12 -0
- package/dist/auto/index.d.ts.map +1 -0
- package/dist/auto/index.js +11 -0
- package/dist/auto/index.js.map +1 -0
- package/dist/auto/intent-gate.d.ts +85 -0
- package/dist/auto/intent-gate.d.ts.map +1 -0
- package/dist/auto/intent-gate.js +247 -0
- package/dist/auto/intent-gate.js.map +1 -0
- package/dist/auto/longxiang-router.d.ts +123 -0
- package/dist/auto/longxiang-router.d.ts.map +1 -0
- package/dist/auto/longxiang-router.js +184 -0
- package/dist/auto/longxiang-router.js.map +1 -0
- package/dist/auto/model-router.d.ts +42 -0
- package/dist/auto/model-router.d.ts.map +1 -0
- package/dist/auto/model-router.js +208 -0
- package/dist/auto/model-router.js.map +1 -0
- package/dist/auto/routing-modes.d.ts +33 -0
- package/dist/auto/routing-modes.d.ts.map +1 -0
- package/dist/auto/routing-modes.js +34 -0
- package/dist/auto/routing-modes.js.map +1 -0
- package/dist/auto/supernova-router.d.ts +50 -0
- package/dist/auto/supernova-router.d.ts.map +1 -0
- package/dist/auto/supernova-router.js +116 -0
- package/dist/auto/supernova-router.js.map +1 -0
- package/dist/auto/task-classifier.d.ts +10 -0
- package/dist/auto/task-classifier.d.ts.map +1 -0
- package/dist/auto/task-classifier.js +116 -0
- package/dist/auto/task-classifier.js.map +1 -0
- package/dist/auto/task-executor.d.ts +86 -0
- package/dist/auto/task-executor.d.ts.map +1 -0
- package/dist/auto/task-executor.js +271 -0
- package/dist/auto/task-executor.js.map +1 -0
- package/dist/auto/types.d.ts +57 -0
- package/dist/auto/types.d.ts.map +1 -0
- package/dist/auto/types.js +2 -0
- package/dist/auto/types.js.map +1 -0
- package/dist/awareness/tick-engine.d.ts +73 -0
- package/dist/awareness/tick-engine.d.ts.map +1 -0
- package/dist/awareness/tick-engine.js +156 -0
- package/dist/awareness/tick-engine.js.map +1 -0
- package/dist/billing/credits.d.ts +213 -0
- package/dist/billing/credits.d.ts.map +1 -0
- package/dist/billing/credits.js +535 -0
- package/dist/billing/credits.js.map +1 -0
- package/dist/billing/meter.d.ts +43 -0
- package/dist/billing/meter.d.ts.map +1 -0
- package/dist/billing/meter.js +67 -0
- package/dist/billing/meter.js.map +1 -0
- package/dist/billing/plans.d.ts +34 -0
- package/dist/billing/plans.d.ts.map +1 -0
- package/dist/billing/plans.js +108 -0
- package/dist/billing/plans.js.map +1 -0
- package/dist/briefing/briefing-engine.d.ts +51 -0
- package/dist/briefing/briefing-engine.d.ts.map +1 -0
- package/dist/briefing/briefing-engine.js +289 -0
- package/dist/briefing/briefing-engine.js.map +1 -0
- package/dist/briefing/index.d.ts +3 -0
- package/dist/briefing/index.d.ts.map +1 -0
- package/dist/briefing/index.js +2 -0
- package/dist/briefing/index.js.map +1 -0
- package/dist/briefing/types.d.ts +49 -0
- package/dist/briefing/types.d.ts.map +1 -0
- package/dist/briefing/types.js +8 -0
- package/dist/briefing/types.js.map +1 -0
- package/dist/checkpoint/checkpoint-manager.d.ts +33 -0
- package/dist/checkpoint/checkpoint-manager.d.ts.map +1 -0
- package/dist/checkpoint/checkpoint-manager.js +144 -0
- package/dist/checkpoint/checkpoint-manager.js.map +1 -0
- package/dist/config/config.d.ts +10 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +169 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/personality.d.ts +32 -0
- package/dist/config/personality.d.ts.map +1 -0
- package/dist/config/personality.js +96 -0
- package/dist/config/personality.js.map +1 -0
- package/dist/config/project-config.d.ts +17 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/config/project-config.js +38 -0
- package/dist/config/project-config.js.map +1 -0
- package/dist/config/project.d.ts +155 -0
- package/dist/config/project.d.ts.map +1 -0
- package/dist/config/project.js +746 -0
- package/dist/config/project.js.map +1 -0
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +11 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/constants.d.ts +13 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +15 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/dates.d.ts +35 -0
- package/dist/core/dates.d.ts.map +1 -0
- package/dist/core/dates.js +45 -0
- package/dist/core/dates.js.map +1 -0
- package/dist/core/errors.d.ts +27 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +172 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/file-lock.d.ts +22 -0
- package/dist/core/file-lock.d.ts.map +1 -0
- package/dist/core/file-lock.js +120 -0
- package/dist/core/file-lock.js.map +1 -0
- package/dist/core/logger.d.ts +9 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +32 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/types.d.ts +157 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +13 -0
- package/dist/core/types.js.map +1 -0
- package/dist/creative/index.d.ts +50 -0
- package/dist/creative/index.d.ts.map +1 -0
- package/dist/creative/index.js +178 -0
- package/dist/creative/index.js.map +1 -0
- package/dist/dataset/capture.d.ts +15 -0
- package/dist/dataset/capture.d.ts.map +1 -0
- package/dist/dataset/capture.js +203 -0
- package/dist/dataset/capture.js.map +1 -0
- package/dist/dataset/config.d.ts +33 -0
- package/dist/dataset/config.d.ts.map +1 -0
- package/dist/dataset/config.js +60 -0
- package/dist/dataset/config.js.map +1 -0
- package/dist/dataset/consumer.d.ts +33 -0
- package/dist/dataset/consumer.d.ts.map +1 -0
- package/dist/dataset/consumer.js +97 -0
- package/dist/dataset/consumer.js.map +1 -0
- package/dist/dataset/emitter.d.ts +153 -0
- package/dist/dataset/emitter.d.ts.map +1 -0
- package/dist/dataset/emitter.js +157 -0
- package/dist/dataset/emitter.js.map +1 -0
- package/dist/dataset/events.d.ts +360 -0
- package/dist/dataset/events.d.ts.map +1 -0
- package/dist/dataset/events.js +30 -0
- package/dist/dataset/events.js.map +1 -0
- package/dist/dataset/generation-emit.d.ts +72 -0
- package/dist/dataset/generation-emit.d.ts.map +1 -0
- package/dist/dataset/generation-emit.js +94 -0
- package/dist/dataset/generation-emit.js.map +1 -0
- package/dist/dataset/redactor.d.ts +22 -0
- package/dist/dataset/redactor.d.ts.map +1 -0
- package/dist/dataset/redactor.js +68 -0
- package/dist/dataset/redactor.js.map +1 -0
- package/dist/dataset/routing.d.ts +17 -0
- package/dist/dataset/routing.d.ts.map +1 -0
- package/dist/dataset/routing.js +77 -0
- package/dist/dataset/routing.js.map +1 -0
- package/dist/dataset/summarizers.d.ts +47 -0
- package/dist/dataset/summarizers.d.ts.map +1 -0
- package/dist/dataset/summarizers.js +98 -0
- package/dist/dataset/summarizers.js.map +1 -0
- package/dist/dataset/verification.d.ts +29 -0
- package/dist/dataset/verification.d.ts.map +1 -0
- package/dist/dataset/verification.js +70 -0
- package/dist/dataset/verification.js.map +1 -0
- package/dist/desktop/budget.d.ts +66 -0
- package/dist/desktop/budget.d.ts.map +1 -0
- package/dist/desktop/budget.js +159 -0
- package/dist/desktop/budget.js.map +1 -0
- package/dist/desktop/capability.d.ts +28 -0
- package/dist/desktop/capability.d.ts.map +1 -0
- package/dist/desktop/capability.js +39 -0
- package/dist/desktop/capability.js.map +1 -0
- package/dist/desktop/conductor.d.ts +104 -0
- package/dist/desktop/conductor.d.ts.map +1 -0
- package/dist/desktop/conductor.js +876 -0
- package/dist/desktop/conductor.js.map +1 -0
- package/dist/desktop/fork-points.d.ts +56 -0
- package/dist/desktop/fork-points.d.ts.map +1 -0
- package/dist/desktop/fork-points.js +138 -0
- package/dist/desktop/fork-points.js.map +1 -0
- package/dist/desktop/index.d.ts +22 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/index.js +21 -0
- package/dist/desktop/index.js.map +1 -0
- package/dist/desktop/perception.d.ts +24 -0
- package/dist/desktop/perception.d.ts.map +1 -0
- package/dist/desktop/perception.js +106 -0
- package/dist/desktop/perception.js.map +1 -0
- package/dist/desktop/personas.d.ts +30 -0
- package/dist/desktop/personas.d.ts.map +1 -0
- package/dist/desktop/personas.js +206 -0
- package/dist/desktop/personas.js.map +1 -0
- package/dist/desktop/safety.d.ts +86 -0
- package/dist/desktop/safety.d.ts.map +1 -0
- package/dist/desktop/safety.js +272 -0
- package/dist/desktop/safety.js.map +1 -0
- package/dist/desktop/screen-key.d.ts +54 -0
- package/dist/desktop/screen-key.d.ts.map +1 -0
- package/dist/desktop/screen-key.js +187 -0
- package/dist/desktop/screen-key.js.map +1 -0
- package/dist/desktop/types.d.ts +141 -0
- package/dist/desktop/types.d.ts.map +1 -0
- package/dist/desktop/types.js +8 -0
- package/dist/desktop/types.js.map +1 -0
- package/dist/docs/adapter.d.ts +71 -0
- package/dist/docs/adapter.d.ts.map +1 -0
- package/dist/docs/adapter.js +75 -0
- package/dist/docs/adapter.js.map +1 -0
- package/dist/docs/content/concepts.d.ts +3 -0
- package/dist/docs/content/concepts.d.ts.map +1 -0
- package/dist/docs/content/concepts.js +304 -0
- package/dist/docs/content/concepts.js.map +1 -0
- package/dist/docs/content/features.d.ts +3 -0
- package/dist/docs/content/features.d.ts.map +1 -0
- package/dist/docs/content/features.js +275 -0
- package/dist/docs/content/features.js.map +1 -0
- package/dist/docs/content/index.d.ts +3 -0
- package/dist/docs/content/index.d.ts.map +1 -0
- package/dist/docs/content/index.js +15 -0
- package/dist/docs/content/index.js.map +1 -0
- package/dist/docs/content/reference.d.ts +3 -0
- package/dist/docs/content/reference.d.ts.map +1 -0
- package/dist/docs/content/reference.js +220 -0
- package/dist/docs/content/reference.js.map +1 -0
- package/dist/docs/content/start.d.ts +3 -0
- package/dist/docs/content/start.d.ts.map +1 -0
- package/dist/docs/content/start.js +203 -0
- package/dist/docs/content/start.js.map +1 -0
- package/dist/docs/content/troubleshooting.d.ts +3 -0
- package/dist/docs/content/troubleshooting.d.ts.map +1 -0
- package/dist/docs/content/troubleshooting.js +98 -0
- package/dist/docs/content/troubleshooting.js.map +1 -0
- package/dist/docs/corpus.d.ts +7 -0
- package/dist/docs/corpus.d.ts.map +1 -0
- package/dist/docs/corpus.js +18 -0
- package/dist/docs/corpus.js.map +1 -0
- package/dist/docs/data/capabilities.d.ts +24 -0
- package/dist/docs/data/capabilities.d.ts.map +1 -0
- package/dist/docs/data/capabilities.js +63 -0
- package/dist/docs/data/capabilities.js.map +1 -0
- package/dist/docs/data/modes.d.ts +11 -0
- package/dist/docs/data/modes.d.ts.map +1 -0
- package/dist/docs/data/modes.js +60 -0
- package/dist/docs/data/modes.js.map +1 -0
- package/dist/docs/data/permissions.d.ts +12 -0
- package/dist/docs/data/permissions.d.ts.map +1 -0
- package/dist/docs/data/permissions.js +62 -0
- package/dist/docs/data/permissions.js.map +1 -0
- package/dist/docs/data/personas.d.ts +13 -0
- package/dist/docs/data/personas.d.ts.map +1 -0
- package/dist/docs/data/personas.js +42 -0
- package/dist/docs/data/personas.js.map +1 -0
- package/dist/docs/data/providers.d.ts +21 -0
- package/dist/docs/data/providers.d.ts.map +1 -0
- package/dist/docs/data/providers.js +210 -0
- package/dist/docs/data/providers.js.map +1 -0
- package/dist/docs/data/shortcuts.d.ts +9 -0
- package/dist/docs/data/shortcuts.d.ts.map +1 -0
- package/dist/docs/data/shortcuts.js +18 -0
- package/dist/docs/data/shortcuts.js.map +1 -0
- package/dist/docs/data/tasks.d.ts +17 -0
- package/dist/docs/data/tasks.d.ts.map +1 -0
- package/dist/docs/data/tasks.js +64 -0
- package/dist/docs/data/tasks.js.map +1 -0
- package/dist/docs/data/tools.d.ts +12 -0
- package/dist/docs/data/tools.d.ts.map +1 -0
- package/dist/docs/data/tools.js +100 -0
- package/dist/docs/data/tools.js.map +1 -0
- package/dist/docs/filter.d.ts +35 -0
- package/dist/docs/filter.d.ts.map +1 -0
- package/dist/docs/filter.js +68 -0
- package/dist/docs/filter.js.map +1 -0
- package/dist/docs/i18n/translations.d.ts +2 -0
- package/dist/docs/i18n/translations.d.ts.map +1 -0
- package/dist/docs/i18n/translations.js +4 -0
- package/dist/docs/i18n/translations.js.map +1 -0
- package/dist/docs/i18n.d.ts +15 -0
- package/dist/docs/i18n.d.ts.map +1 -0
- package/dist/docs/i18n.js +88 -0
- package/dist/docs/i18n.js.map +1 -0
- package/dist/docs/index.d.ts +16 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +15 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/docs/product-knowledge.d.ts +45 -0
- package/dist/docs/product-knowledge.d.ts.map +1 -0
- package/dist/docs/product-knowledge.js +147 -0
- package/dist/docs/product-knowledge.js.map +1 -0
- package/dist/docs/types.d.ts +138 -0
- package/dist/docs/types.d.ts.map +1 -0
- package/dist/docs/types.js +14 -0
- package/dist/docs/types.js.map +1 -0
- package/dist/events/event-detector.d.ts +39 -0
- package/dist/events/event-detector.d.ts.map +1 -0
- package/dist/events/event-detector.js +223 -0
- package/dist/events/event-detector.js.map +1 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +2 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +8 -0
- package/dist/events/types.js.map +1 -0
- package/dist/exercises/index.d.ts +249 -0
- package/dist/exercises/index.d.ts.map +1 -0
- package/dist/exercises/index.js +181 -0
- package/dist/exercises/index.js.map +1 -0
- package/dist/health/aisles.d.ts +33 -0
- package/dist/health/aisles.d.ts.map +1 -0
- package/dist/health/aisles.js +417 -0
- package/dist/health/aisles.js.map +1 -0
- package/dist/health/cooking.d.ts +13 -0
- package/dist/health/cooking.d.ts.map +1 -0
- package/dist/health/cooking.js +19 -0
- package/dist/health/cooking.js.map +1 -0
- package/dist/health/duplicate.d.ts +47 -0
- package/dist/health/duplicate.d.ts.map +1 -0
- package/dist/health/duplicate.js +136 -0
- package/dist/health/duplicate.js.map +1 -0
- package/dist/health/index.d.ts +7 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +10 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/node-store.d.ts +30 -0
- package/dist/health/node-store.d.ts.map +1 -0
- package/dist/health/node-store.js +250 -0
- package/dist/health/node-store.js.map +1 -0
- package/dist/health/prep.d.ts +86 -0
- package/dist/health/prep.d.ts.map +1 -0
- package/dist/health/prep.js +152 -0
- package/dist/health/prep.js.map +1 -0
- package/dist/health/profile-fields.d.ts +46 -0
- package/dist/health/profile-fields.d.ts.map +1 -0
- package/dist/health/profile-fields.js +194 -0
- package/dist/health/profile-fields.js.map +1 -0
- package/dist/health/session-store.d.ts +30 -0
- package/dist/health/session-store.d.ts.map +1 -0
- package/dist/health/session-store.js +20 -0
- package/dist/health/session-store.js.map +1 -0
- package/dist/health/session-summary.d.ts +10 -0
- package/dist/health/session-summary.d.ts.map +1 -0
- package/dist/health/session-summary.js +106 -0
- package/dist/health/session-summary.js.map +1 -0
- package/dist/health/session-types.d.ts +74 -0
- package/dist/health/session-types.d.ts.map +1 -0
- package/dist/health/session-types.js +53 -0
- package/dist/health/session-types.js.map +1 -0
- package/dist/health/shopping-list.d.ts +226 -0
- package/dist/health/shopping-list.d.ts.map +1 -0
- package/dist/health/shopping-list.js +432 -0
- package/dist/health/shopping-list.js.map +1 -0
- package/dist/health/starters.d.ts +155 -0
- package/dist/health/starters.d.ts.map +1 -0
- package/dist/health/starters.js +270 -0
- package/dist/health/starters.js.map +1 -0
- package/dist/health/store.d.ts +59 -0
- package/dist/health/store.d.ts.map +1 -0
- package/dist/health/store.js +17 -0
- package/dist/health/store.js.map +1 -0
- package/dist/health/types.d.ts +213 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +11 -0
- package/dist/health/types.js.map +1 -0
- package/dist/history/conversation-title.d.ts +67 -0
- package/dist/history/conversation-title.d.ts.map +1 -0
- package/dist/history/conversation-title.js +184 -0
- package/dist/history/conversation-title.js.map +1 -0
- package/dist/history/history-manager.d.ts +50 -0
- package/dist/history/history-manager.d.ts.map +1 -0
- package/dist/history/history-manager.js +168 -0
- package/dist/history/history-manager.js.map +1 -0
- package/dist/history/storage.d.ts +41 -0
- package/dist/history/storage.d.ts.map +1 -0
- package/dist/history/storage.js +133 -0
- package/dist/history/storage.js.map +1 -0
- package/dist/i18n/index.d.ts +41 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +188 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/keep-english.d.ts +12 -0
- package/dist/i18n/keep-english.d.ts.map +1 -0
- package/dist/i18n/keep-english.js +351 -0
- package/dist/i18n/keep-english.js.map +1 -0
- package/dist/i18n/locales/ar.d.ts +4 -0
- package/dist/i18n/locales/ar.d.ts.map +1 -0
- package/dist/i18n/locales/ar.js +4143 -0
- package/dist/i18n/locales/ar.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +4 -0
- package/dist/i18n/locales/de.d.ts.map +1 -0
- package/dist/i18n/locales/de.js +4133 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/en.d.ts +4068 -0
- package/dist/i18n/locales/en.d.ts.map +1 -0
- package/dist/i18n/locales/en.js +4347 -0
- package/dist/i18n/locales/en.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +4 -0
- package/dist/i18n/locales/es.d.ts.map +1 -0
- package/dist/i18n/locales/es.js +4128 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +4 -0
- package/dist/i18n/locales/fr.d.ts.map +1 -0
- package/dist/i18n/locales/fr.js +4140 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hi.d.ts +4 -0
- package/dist/i18n/locales/hi.d.ts.map +1 -0
- package/dist/i18n/locales/hi.js +4143 -0
- package/dist/i18n/locales/hi.js.map +1 -0
- package/dist/i18n/locales/id.d.ts +4 -0
- package/dist/i18n/locales/id.d.ts.map +1 -0
- package/dist/i18n/locales/id.js +4142 -0
- package/dist/i18n/locales/id.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +4 -0
- package/dist/i18n/locales/it.d.ts.map +1 -0
- package/dist/i18n/locales/it.js +4143 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/ja.d.ts +4 -0
- package/dist/i18n/locales/ja.d.ts.map +1 -0
- package/dist/i18n/locales/ja.js +4144 -0
- package/dist/i18n/locales/ja.js.map +1 -0
- package/dist/i18n/locales/ko.d.ts +4 -0
- package/dist/i18n/locales/ko.d.ts.map +1 -0
- package/dist/i18n/locales/ko.js +4143 -0
- package/dist/i18n/locales/ko.js.map +1 -0
- package/dist/i18n/locales/nl.d.ts +4 -0
- package/dist/i18n/locales/nl.d.ts.map +1 -0
- package/dist/i18n/locales/nl.js +4143 -0
- package/dist/i18n/locales/nl.js.map +1 -0
- package/dist/i18n/locales/pl.d.ts +4 -0
- package/dist/i18n/locales/pl.d.ts.map +1 -0
- package/dist/i18n/locales/pl.js +4133 -0
- package/dist/i18n/locales/pl.js.map +1 -0
- package/dist/i18n/locales/pt.d.ts +4 -0
- package/dist/i18n/locales/pt.d.ts.map +1 -0
- package/dist/i18n/locales/pt.js +4141 -0
- package/dist/i18n/locales/pt.js.map +1 -0
- package/dist/i18n/locales/ru.d.ts +4 -0
- package/dist/i18n/locales/ru.d.ts.map +1 -0
- package/dist/i18n/locales/ru.js +4143 -0
- package/dist/i18n/locales/ru.js.map +1 -0
- package/dist/i18n/locales/th.d.ts +4 -0
- package/dist/i18n/locales/th.d.ts.map +1 -0
- package/dist/i18n/locales/th.js +4143 -0
- package/dist/i18n/locales/th.js.map +1 -0
- package/dist/i18n/locales/tr.d.ts +4 -0
- package/dist/i18n/locales/tr.d.ts.map +1 -0
- package/dist/i18n/locales/tr.js +4143 -0
- package/dist/i18n/locales/tr.js.map +1 -0
- package/dist/i18n/locales/uk.d.ts +4 -0
- package/dist/i18n/locales/uk.d.ts.map +1 -0
- package/dist/i18n/locales/uk.js +4133 -0
- package/dist/i18n/locales/uk.js.map +1 -0
- package/dist/i18n/locales/vi.d.ts +4 -0
- package/dist/i18n/locales/vi.d.ts.map +1 -0
- package/dist/i18n/locales/vi.js +4143 -0
- package/dist/i18n/locales/vi.js.map +1 -0
- package/dist/i18n/locales/zh-CN.d.ts +4 -0
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
- package/dist/i18n/locales/zh-CN.js +4133 -0
- package/dist/i18n/locales/zh-CN.js.map +1 -0
- package/dist/i18n/locales/zh-TW.d.ts +4 -0
- package/dist/i18n/locales/zh-TW.d.ts.map +1 -0
- package/dist/i18n/locales/zh-TW.js +4143 -0
- package/dist/i18n/locales/zh-TW.js.map +1 -0
- package/dist/i18n/types.d.ts +5 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +46 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/project-indexer.d.ts +66 -0
- package/dist/indexer/project-indexer.d.ts.map +1 -0
- package/dist/indexer/project-indexer.js +574 -0
- package/dist/indexer/project-indexer.js.map +1 -0
- package/dist/indexer/symbol-indexer.d.ts +56 -0
- package/dist/indexer/symbol-indexer.d.ts.map +1 -0
- package/dist/indexer/symbol-indexer.js +579 -0
- package/dist/indexer/symbol-indexer.js.map +1 -0
- package/dist/journal/journal-manager.d.ts +143 -0
- package/dist/journal/journal-manager.d.ts.map +1 -0
- package/dist/journal/journal-manager.js +472 -0
- package/dist/journal/journal-manager.js.map +1 -0
- package/dist/journal/platform-sync.d.ts +17 -0
- package/dist/journal/platform-sync.d.ts.map +1 -0
- package/dist/journal/platform-sync.js +21 -0
- package/dist/journal/platform-sync.js.map +1 -0
- package/dist/journal/types.d.ts +78 -0
- package/dist/journal/types.d.ts.map +1 -0
- package/dist/journal/types.js +74 -0
- package/dist/journal/types.js.map +1 -0
- package/dist/knowledge/index.d.ts +2 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +2 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/learning/index.d.ts +6 -0
- package/dist/learning/index.d.ts.map +1 -0
- package/dist/learning/index.js +9 -0
- package/dist/learning/index.js.map +1 -0
- package/dist/learning/learner-context.d.ts +11 -0
- package/dist/learning/learner-context.d.ts.map +1 -0
- package/dist/learning/learner-context.js +57 -0
- package/dist/learning/learner-context.js.map +1 -0
- package/dist/learning/library-fork.d.ts +33 -0
- package/dist/learning/library-fork.d.ts.map +1 -0
- package/dist/learning/library-fork.js +92 -0
- package/dist/learning/library-fork.js.map +1 -0
- package/dist/learning/progression-export.d.ts +4 -0
- package/dist/learning/progression-export.d.ts.map +1 -0
- package/dist/learning/progression-export.js +21 -0
- package/dist/learning/progression-export.js.map +1 -0
- package/dist/learning/progression-markdown.d.ts +14 -0
- package/dist/learning/progression-markdown.d.ts.map +1 -0
- package/dist/learning/progression-markdown.js +73 -0
- package/dist/learning/progression-markdown.js.map +1 -0
- package/dist/learning/progression.d.ts +65 -0
- package/dist/learning/progression.d.ts.map +1 -0
- package/dist/learning/progression.js +186 -0
- package/dist/learning/progression.js.map +1 -0
- package/dist/memory/ambient-capture.d.ts +26 -0
- package/dist/memory/ambient-capture.d.ts.map +1 -0
- package/dist/memory/ambient-capture.js +174 -0
- package/dist/memory/ambient-capture.js.map +1 -0
- package/dist/memory/auto-extract.d.ts +54 -0
- package/dist/memory/auto-extract.d.ts.map +1 -0
- package/dist/memory/auto-extract.js +499 -0
- package/dist/memory/auto-extract.js.map +1 -0
- package/dist/memory/config.d.ts +47 -0
- package/dist/memory/config.d.ts.map +1 -0
- package/dist/memory/config.js +52 -0
- package/dist/memory/config.js.map +1 -0
- package/dist/memory/consolidation.d.ts +60 -0
- package/dist/memory/consolidation.d.ts.map +1 -0
- package/dist/memory/consolidation.js +311 -0
- package/dist/memory/consolidation.js.map +1 -0
- package/dist/memory/embedding-service.d.ts +50 -0
- package/dist/memory/embedding-service.d.ts.map +1 -0
- package/dist/memory/embedding-service.js +86 -0
- package/dist/memory/embedding-service.js.map +1 -0
- package/dist/memory/embedding-store.d.ts +31 -0
- package/dist/memory/embedding-store.d.ts.map +1 -0
- package/dist/memory/embedding-store.js +109 -0
- package/dist/memory/embedding-store.js.map +1 -0
- package/dist/memory/graph-engine.d.ts +145 -0
- package/dist/memory/graph-engine.d.ts.map +1 -0
- package/dist/memory/graph-engine.js +628 -0
- package/dist/memory/graph-engine.js.map +1 -0
- package/dist/memory/insights.d.ts +39 -0
- package/dist/memory/insights.d.ts.map +1 -0
- package/dist/memory/insights.js +138 -0
- package/dist/memory/insights.js.map +1 -0
- package/dist/memory/memory-agent.d.ts +93 -0
- package/dist/memory/memory-agent.d.ts.map +1 -0
- package/dist/memory/memory-agent.js +475 -0
- package/dist/memory/memory-agent.js.map +1 -0
- package/dist/memory/memory-manager.d.ts +232 -0
- package/dist/memory/memory-manager.d.ts.map +1 -0
- package/dist/memory/memory-manager.js +1633 -0
- package/dist/memory/memory-manager.js.map +1 -0
- package/dist/memory/migration-v3.d.ts +22 -0
- package/dist/memory/migration-v3.d.ts.map +1 -0
- package/dist/memory/migration-v3.js +193 -0
- package/dist/memory/migration-v3.js.map +1 -0
- package/dist/memory/mode-recall.d.ts +20 -0
- package/dist/memory/mode-recall.d.ts.map +1 -0
- package/dist/memory/mode-recall.js +104 -0
- package/dist/memory/mode-recall.js.map +1 -0
- package/dist/memory/patterns.d.ts +92 -0
- package/dist/memory/patterns.d.ts.map +1 -0
- package/dist/memory/patterns.js +389 -0
- package/dist/memory/patterns.js.map +1 -0
- package/dist/memory/platform-sync.d.ts +54 -0
- package/dist/memory/platform-sync.d.ts.map +1 -0
- package/dist/memory/platform-sync.js +176 -0
- package/dist/memory/platform-sync.js.map +1 -0
- package/dist/memory/procedural.d.ts +47 -0
- package/dist/memory/procedural.d.ts.map +1 -0
- package/dist/memory/procedural.js +182 -0
- package/dist/memory/procedural.js.map +1 -0
- package/dist/memory/project-brain.d.ts +27 -0
- package/dist/memory/project-brain.d.ts.map +1 -0
- package/dist/memory/project-brain.js +158 -0
- package/dist/memory/project-brain.js.map +1 -0
- package/dist/memory/self-improvement.d.ts +45 -0
- package/dist/memory/self-improvement.d.ts.map +1 -0
- package/dist/memory/self-improvement.js +178 -0
- package/dist/memory/self-improvement.js.map +1 -0
- package/dist/memory/tfidf.d.ts +71 -0
- package/dist/memory/tfidf.d.ts.map +1 -0
- package/dist/memory/tfidf.js +223 -0
- package/dist/memory/tfidf.js.map +1 -0
- package/dist/memory/training-export.d.ts +51 -0
- package/dist/memory/training-export.d.ts.map +1 -0
- package/dist/memory/training-export.js +122 -0
- package/dist/memory/training-export.js.map +1 -0
- package/dist/memory/types.d.ts +380 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +99 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/news/index.d.ts +163 -0
- package/dist/news/index.d.ts.map +1 -0
- package/dist/news/index.js +184 -0
- package/dist/news/index.js.map +1 -0
- package/dist/papers/arxiv-client.d.ts +21 -0
- package/dist/papers/arxiv-client.d.ts.map +1 -0
- package/dist/papers/arxiv-client.js +130 -0
- package/dist/papers/arxiv-client.js.map +1 -0
- package/dist/papers/crossref-client.d.ts +3 -0
- package/dist/papers/crossref-client.d.ts.map +1 -0
- package/dist/papers/crossref-client.js +96 -0
- package/dist/papers/crossref-client.js.map +1 -0
- package/dist/papers/discipline.d.ts +19 -0
- package/dist/papers/discipline.d.ts.map +1 -0
- package/dist/papers/discipline.js +96 -0
- package/dist/papers/discipline.js.map +1 -0
- package/dist/papers/index.d.ts +10 -0
- package/dist/papers/index.d.ts.map +1 -0
- package/dist/papers/index.js +7 -0
- package/dist/papers/index.js.map +1 -0
- package/dist/papers/openalex-client.d.ts +13 -0
- package/dist/papers/openalex-client.d.ts.map +1 -0
- package/dist/papers/openalex-client.js +206 -0
- package/dist/papers/openalex-client.js.map +1 -0
- package/dist/papers/paper-fetcher.d.ts +37 -0
- package/dist/papers/paper-fetcher.d.ts.map +1 -0
- package/dist/papers/paper-fetcher.js +75 -0
- package/dist/papers/paper-fetcher.js.map +1 -0
- package/dist/papers/types.d.ts +80 -0
- package/dist/papers/types.d.ts.map +1 -0
- package/dist/papers/types.js +25 -0
- package/dist/papers/types.js.map +1 -0
- package/dist/personas/conductor.d.ts +184 -0
- package/dist/personas/conductor.d.ts.map +1 -0
- package/dist/personas/conductor.js +871 -0
- package/dist/personas/conductor.js.map +1 -0
- package/dist/personas/definitions.d.ts +48 -0
- package/dist/personas/definitions.d.ts.map +1 -0
- package/dist/personas/definitions.js +828 -0
- package/dist/personas/definitions.js.map +1 -0
- package/dist/personas/index.d.ts +4 -0
- package/dist/personas/index.d.ts.map +1 -0
- package/dist/personas/index.js +3 -0
- package/dist/personas/index.js.map +1 -0
- package/dist/personas/types.d.ts +148 -0
- package/dist/personas/types.d.ts.map +1 -0
- package/dist/personas/types.js +5 -0
- package/dist/personas/types.js.map +1 -0
- package/dist/personas/verification-matrix.d.ts +44 -0
- package/dist/personas/verification-matrix.d.ts.map +1 -0
- package/dist/personas/verification-matrix.js +232 -0
- package/dist/personas/verification-matrix.js.map +1 -0
- package/dist/portability/bundle.d.ts +80 -0
- package/dist/portability/bundle.d.ts.map +1 -0
- package/dist/portability/bundle.js +155 -0
- package/dist/portability/bundle.js.map +1 -0
- package/dist/portability/crypto.d.ts +42 -0
- package/dist/portability/crypto.d.ts.map +1 -0
- package/dist/portability/crypto.js +89 -0
- package/dist/portability/crypto.js.map +1 -0
- package/dist/portability/data-types.d.ts +54 -0
- package/dist/portability/data-types.d.ts.map +1 -0
- package/dist/portability/data-types.js +281 -0
- package/dist/portability/data-types.js.map +1 -0
- package/dist/portability/index.d.ts +35 -0
- package/dist/portability/index.d.ts.map +1 -0
- package/dist/portability/index.js +41 -0
- package/dist/portability/index.js.map +1 -0
- package/dist/providers/base-provider.d.ts +65 -0
- package/dist/providers/base-provider.d.ts.map +1 -0
- package/dist/providers/base-provider.js +305 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/catalog.d.ts +25 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +53 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/deepseek/index.d.ts +9 -0
- package/dist/providers/deepseek/index.d.ts.map +1 -0
- package/dist/providers/deepseek/index.js +19 -0
- package/dist/providers/deepseek/index.js.map +1 -0
- package/dist/providers/deepseek/models.d.ts +3 -0
- package/dist/providers/deepseek/models.d.ts.map +1 -0
- package/dist/providers/deepseek/models.js +56 -0
- package/dist/providers/deepseek/models.js.map +1 -0
- package/dist/providers/generic/index.d.ts +25 -0
- package/dist/providers/generic/index.d.ts.map +1 -0
- package/dist/providers/generic/index.js +51 -0
- package/dist/providers/generic/index.js.map +1 -0
- package/dist/providers/generic/models.d.ts +3 -0
- package/dist/providers/generic/models.d.ts.map +1 -0
- package/dist/providers/generic/models.js +13 -0
- package/dist/providers/generic/models.js.map +1 -0
- package/dist/providers/health-tracker.d.ts +44 -0
- package/dist/providers/health-tracker.d.ts.map +1 -0
- package/dist/providers/health-tracker.js +110 -0
- package/dist/providers/health-tracker.js.map +1 -0
- package/dist/providers/kimi/index.d.ts +9 -0
- package/dist/providers/kimi/index.d.ts.map +1 -0
- package/dist/providers/kimi/index.js +13 -0
- package/dist/providers/kimi/index.js.map +1 -0
- package/dist/providers/kimi/models.d.ts +3 -0
- package/dist/providers/kimi/models.d.ts.map +1 -0
- package/dist/providers/kimi/models.js +60 -0
- package/dist/providers/kimi/models.js.map +1 -0
- package/dist/providers/minimax/index.d.ts +9 -0
- package/dist/providers/minimax/index.d.ts.map +1 -0
- package/dist/providers/minimax/index.js +13 -0
- package/dist/providers/minimax/index.js.map +1 -0
- package/dist/providers/minimax/models.d.ts +3 -0
- package/dist/providers/minimax/models.d.ts.map +1 -0
- package/dist/providers/minimax/models.js +65 -0
- package/dist/providers/minimax/models.js.map +1 -0
- package/dist/providers/mistral/index.d.ts +11 -0
- package/dist/providers/mistral/index.d.ts.map +1 -0
- package/dist/providers/mistral/index.js +24 -0
- package/dist/providers/mistral/index.js.map +1 -0
- package/dist/providers/mistral/models.d.ts +3 -0
- package/dist/providers/mistral/models.d.ts.map +1 -0
- package/dist/providers/mistral/models.js +65 -0
- package/dist/providers/mistral/models.js.map +1 -0
- package/dist/providers/nvidia/index.d.ts +16 -0
- package/dist/providers/nvidia/index.d.ts.map +1 -0
- package/dist/providers/nvidia/index.js +20 -0
- package/dist/providers/nvidia/index.js.map +1 -0
- package/dist/providers/nvidia/models.d.ts +3 -0
- package/dist/providers/nvidia/models.d.ts.map +1 -0
- package/dist/providers/nvidia/models.js +56 -0
- package/dist/providers/nvidia/models.js.map +1 -0
- package/dist/providers/platform/index.d.ts +10 -0
- package/dist/providers/platform/index.d.ts.map +1 -0
- package/dist/providers/platform/index.js +16 -0
- package/dist/providers/platform/index.js.map +1 -0
- package/dist/providers/platform/models.d.ts +3 -0
- package/dist/providers/platform/models.d.ts.map +1 -0
- package/dist/providers/platform/models.js +233 -0
- package/dist/providers/platform/models.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +44 -0
- package/dist/providers/provider-registry.d.ts.map +1 -0
- package/dist/providers/provider-registry.js +197 -0
- package/dist/providers/provider-registry.js.map +1 -0
- package/dist/providers/qwen/index.d.ts +9 -0
- package/dist/providers/qwen/index.d.ts.map +1 -0
- package/dist/providers/qwen/index.js +13 -0
- package/dist/providers/qwen/index.js.map +1 -0
- package/dist/providers/qwen/models.d.ts +3 -0
- package/dist/providers/qwen/models.d.ts.map +1 -0
- package/dist/providers/qwen/models.js +164 -0
- package/dist/providers/qwen/models.js.map +1 -0
- package/dist/providers/request-shaping/index.d.ts +29 -0
- package/dist/providers/request-shaping/index.d.ts.map +1 -0
- package/dist/providers/request-shaping/index.js +31 -0
- package/dist/providers/request-shaping/index.js.map +1 -0
- package/dist/providers/request-shaping/messages.d.ts +61 -0
- package/dist/providers/request-shaping/messages.d.ts.map +1 -0
- package/dist/providers/request-shaping/messages.js +114 -0
- package/dist/providers/request-shaping/messages.js.map +1 -0
- package/dist/providers/request-shaping/model-ids.d.ts +44 -0
- package/dist/providers/request-shaping/model-ids.d.ts.map +1 -0
- package/dist/providers/request-shaping/model-ids.js +96 -0
- package/dist/providers/request-shaping/model-ids.js.map +1 -0
- package/dist/providers/request-shaping/params.d.ts +38 -0
- package/dist/providers/request-shaping/params.d.ts.map +1 -0
- package/dist/providers/request-shaping/params.js +79 -0
- package/dist/providers/request-shaping/params.js.map +1 -0
- package/dist/providers/resilient-provider.d.ts +44 -0
- package/dist/providers/resilient-provider.d.ts.map +1 -0
- package/dist/providers/resilient-provider.js +141 -0
- package/dist/providers/resilient-provider.js.map +1 -0
- package/dist/providers/tencent/index.d.ts +19 -0
- package/dist/providers/tencent/index.d.ts.map +1 -0
- package/dist/providers/tencent/index.js +50 -0
- package/dist/providers/tencent/index.js.map +1 -0
- package/dist/providers/tencent/models.d.ts +3 -0
- package/dist/providers/tencent/models.d.ts.map +1 -0
- package/dist/providers/tencent/models.js +29 -0
- package/dist/providers/tencent/models.js.map +1 -0
- package/dist/providers/types.d.ts +98 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/xiaomi/index.d.ts +9 -0
- package/dist/providers/xiaomi/index.d.ts.map +1 -0
- package/dist/providers/xiaomi/index.js +16 -0
- package/dist/providers/xiaomi/index.js.map +1 -0
- package/dist/providers/xiaomi/models.d.ts +3 -0
- package/dist/providers/xiaomi/models.d.ts.map +1 -0
- package/dist/providers/xiaomi/models.js +37 -0
- package/dist/providers/xiaomi/models.js.map +1 -0
- package/dist/providers/zhipu/index.d.ts +11 -0
- package/dist/providers/zhipu/index.d.ts.map +1 -0
- package/dist/providers/zhipu/index.js +44 -0
- package/dist/providers/zhipu/index.js.map +1 -0
- package/dist/providers/zhipu/models.d.ts +3 -0
- package/dist/providers/zhipu/models.d.ts.map +1 -0
- package/dist/providers/zhipu/models.js +37 -0
- package/dist/providers/zhipu/models.js.map +1 -0
- package/dist/recipes/index.d.ts +215 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +232 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/remote/client.d.ts +67 -0
- package/dist/remote/client.d.ts.map +1 -0
- package/dist/remote/client.js +202 -0
- package/dist/remote/client.js.map +1 -0
- package/dist/remote/index.d.ts +9 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +7 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/schema.d.ts +152 -0
- package/dist/remote/schema.d.ts.map +1 -0
- package/dist/remote/schema.js +26 -0
- package/dist/remote/schema.js.map +1 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +5 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/scanner.d.ts +67 -0
- package/dist/security/scanner.d.ts.map +1 -0
- package/dist/security/scanner.js +422 -0
- package/dist/security/scanner.js.map +1 -0
- package/dist/social/day-plan.d.ts +38 -0
- package/dist/social/day-plan.d.ts.map +1 -0
- package/dist/social/day-plan.js +16 -0
- package/dist/social/day-plan.js.map +1 -0
- package/dist/social/index.d.ts +290 -0
- package/dist/social/index.d.ts.map +1 -0
- package/dist/social/index.js +96 -0
- package/dist/social/index.js.map +1 -0
- package/dist/tasks/platform-sync.d.ts +22 -0
- package/dist/tasks/platform-sync.d.ts.map +1 -0
- package/dist/tasks/platform-sync.js +132 -0
- package/dist/tasks/platform-sync.js.map +1 -0
- package/dist/tasks/task-manager.d.ts +164 -0
- package/dist/tasks/task-manager.d.ts.map +1 -0
- package/dist/tasks/task-manager.js +643 -0
- package/dist/tasks/task-manager.js.map +1 -0
- package/dist/tasks/types.d.ts +105 -0
- package/dist/tasks/types.d.ts.map +1 -0
- package/dist/tasks/types.js +23 -0
- package/dist/tasks/types.js.map +1 -0
- package/dist/tools/analyze-architecture.d.ts +17 -0
- package/dist/tools/analyze-architecture.d.ts.map +1 -0
- package/dist/tools/analyze-architecture.js +316 -0
- package/dist/tools/analyze-architecture.js.map +1 -0
- package/dist/tools/apply-plan.d.ts +12 -0
- package/dist/tools/apply-plan.d.ts.map +1 -0
- package/dist/tools/apply-plan.js +119 -0
- package/dist/tools/apply-plan.js.map +1 -0
- package/dist/tools/ask-user.d.ts +11 -0
- package/dist/tools/ask-user.d.ts.map +1 -0
- package/dist/tools/ask-user.js +32 -0
- package/dist/tools/ask-user.js.map +1 -0
- package/dist/tools/audit-dependencies.d.ts +13 -0
- package/dist/tools/audit-dependencies.d.ts.map +1 -0
- package/dist/tools/audit-dependencies.js +156 -0
- package/dist/tools/audit-dependencies.js.map +1 -0
- package/dist/tools/authoring/doc-model.d.ts +128 -0
- package/dist/tools/authoring/doc-model.d.ts.map +1 -0
- package/dist/tools/authoring/doc-model.js +82 -0
- package/dist/tools/authoring/doc-model.js.map +1 -0
- package/dist/tools/authoring/frontmatter.d.ts +51 -0
- package/dist/tools/authoring/frontmatter.d.ts.map +1 -0
- package/dist/tools/authoring/frontmatter.js +203 -0
- package/dist/tools/authoring/frontmatter.js.map +1 -0
- package/dist/tools/authoring/image-size.d.ts +17 -0
- package/dist/tools/authoring/image-size.d.ts.map +1 -0
- package/dist/tools/authoring/image-size.js +47 -0
- package/dist/tools/authoring/image-size.js.map +1 -0
- package/dist/tools/authoring/index.d.ts +34 -0
- package/dist/tools/authoring/index.d.ts.map +1 -0
- package/dist/tools/authoring/index.js +44 -0
- package/dist/tools/authoring/index.js.map +1 -0
- package/dist/tools/authoring/md-parse.d.ts +18 -0
- package/dist/tools/authoring/md-parse.d.ts.map +1 -0
- package/dist/tools/authoring/md-parse.js +262 -0
- package/dist/tools/authoring/md-parse.js.map +1 -0
- package/dist/tools/authoring/render-docx.d.ts +18 -0
- package/dist/tools/authoring/render-docx.d.ts.map +1 -0
- package/dist/tools/authoring/render-docx.js +272 -0
- package/dist/tools/authoring/render-docx.js.map +1 -0
- package/dist/tools/authoring/render-md.d.ts +13 -0
- package/dist/tools/authoring/render-md.d.ts.map +1 -0
- package/dist/tools/authoring/render-md.js +139 -0
- package/dist/tools/authoring/render-md.js.map +1 -0
- package/dist/tools/authoring/render-pdf.d.ts +16 -0
- package/dist/tools/authoring/render-pdf.d.ts.map +1 -0
- package/dist/tools/authoring/render-pdf.js +298 -0
- package/dist/tools/authoring/render-pdf.js.map +1 -0
- package/dist/tools/authoring/sections.d.ts +35 -0
- package/dist/tools/authoring/sections.d.ts.map +1 -0
- package/dist/tools/authoring/sections.js +81 -0
- package/dist/tools/authoring/sections.js.map +1 -0
- package/dist/tools/authoring/templates/builtins.d.ts +11 -0
- package/dist/tools/authoring/templates/builtins.d.ts.map +1 -0
- package/dist/tools/authoring/templates/builtins.js +478 -0
- package/dist/tools/authoring/templates/builtins.js.map +1 -0
- package/dist/tools/authoring/templates/index.d.ts +15 -0
- package/dist/tools/authoring/templates/index.d.ts.map +1 -0
- package/dist/tools/authoring/templates/index.js +25 -0
- package/dist/tools/authoring/templates/index.js.map +1 -0
- package/dist/tools/authoring/templates/store.d.ts +15 -0
- package/dist/tools/authoring/templates/store.d.ts.map +1 -0
- package/dist/tools/authoring/templates/store.js +63 -0
- package/dist/tools/authoring/templates/store.js.map +1 -0
- package/dist/tools/authoring/templates/template-model.d.ts +44 -0
- package/dist/tools/authoring/templates/template-model.d.ts.map +1 -0
- package/dist/tools/authoring/templates/template-model.js +39 -0
- package/dist/tools/authoring/templates/template-model.js.map +1 -0
- package/dist/tools/bash.d.ts +34 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +353 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/benchmark.d.ts +12 -0
- package/dist/tools/benchmark.d.ts.map +1 -0
- package/dist/tools/benchmark.js +147 -0
- package/dist/tools/benchmark.js.map +1 -0
- package/dist/tools/browse-library.d.ts +12 -0
- package/dist/tools/browse-library.d.ts.map +1 -0
- package/dist/tools/browse-library.js +188 -0
- package/dist/tools/browse-library.js.map +1 -0
- package/dist/tools/browser-click.d.ts +12 -0
- package/dist/tools/browser-click.d.ts.map +1 -0
- package/dist/tools/browser-click.js +58 -0
- package/dist/tools/browser-click.js.map +1 -0
- package/dist/tools/browser-close.d.ts +11 -0
- package/dist/tools/browser-close.d.ts.map +1 -0
- package/dist/tools/browser-close.js +27 -0
- package/dist/tools/browser-close.js.map +1 -0
- package/dist/tools/browser-navigate.d.ts +12 -0
- package/dist/tools/browser-navigate.d.ts.map +1 -0
- package/dist/tools/browser-navigate.js +66 -0
- package/dist/tools/browser-navigate.js.map +1 -0
- package/dist/tools/browser-snapshot.d.ts +11 -0
- package/dist/tools/browser-snapshot.d.ts.map +1 -0
- package/dist/tools/browser-snapshot.js +56 -0
- package/dist/tools/browser-snapshot.js.map +1 -0
- package/dist/tools/browser-type.d.ts +12 -0
- package/dist/tools/browser-type.d.ts.map +1 -0
- package/dist/tools/browser-type.js +68 -0
- package/dist/tools/browser-type.js.map +1 -0
- package/dist/tools/browser.d.ts +29 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +307 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/conversation-recall.d.ts +43 -0
- package/dist/tools/conversation-recall.d.ts.map +1 -0
- package/dist/tools/conversation-recall.js +177 -0
- package/dist/tools/conversation-recall.js.map +1 -0
- package/dist/tools/creative-asset-sync.d.ts +33 -0
- package/dist/tools/creative-asset-sync.d.ts.map +1 -0
- package/dist/tools/creative-asset-sync.js +39 -0
- package/dist/tools/creative-asset-sync.js.map +1 -0
- package/dist/tools/curator.d.ts +34 -0
- package/dist/tools/curator.d.ts.map +1 -0
- package/dist/tools/curator.js +191 -0
- package/dist/tools/curator.js.map +1 -0
- package/dist/tools/database-query.d.ts +11 -0
- package/dist/tools/database-query.d.ts.map +1 -0
- package/dist/tools/database-query.js +228 -0
- package/dist/tools/database-query.js.map +1 -0
- package/dist/tools/day-plan.d.ts +27 -0
- package/dist/tools/day-plan.d.ts.map +1 -0
- package/dist/tools/day-plan.js +207 -0
- package/dist/tools/day-plan.js.map +1 -0
- package/dist/tools/debug-logs.d.ts +13 -0
- package/dist/tools/debug-logs.d.ts.map +1 -0
- package/dist/tools/debug-logs.js +141 -0
- package/dist/tools/debug-logs.js.map +1 -0
- package/dist/tools/deploy-manifest.d.ts +37 -0
- package/dist/tools/deploy-manifest.d.ts.map +1 -0
- package/dist/tools/deploy-manifest.js +71 -0
- package/dist/tools/deploy-manifest.js.map +1 -0
- package/dist/tools/design-studio-tools.d.ts +83 -0
- package/dist/tools/design-studio-tools.d.ts.map +1 -0
- package/dist/tools/design-studio-tools.js +761 -0
- package/dist/tools/design-studio-tools.js.map +1 -0
- package/dist/tools/desktop-click-by-name.d.ts +12 -0
- package/dist/tools/desktop-click-by-name.d.ts.map +1 -0
- package/dist/tools/desktop-click-by-name.js +64 -0
- package/dist/tools/desktop-click-by-name.js.map +1 -0
- package/dist/tools/desktop-focus-window.d.ts +12 -0
- package/dist/tools/desktop-focus-window.d.ts.map +1 -0
- package/dist/tools/desktop-focus-window.js +59 -0
- package/dist/tools/desktop-focus-window.js.map +1 -0
- package/dist/tools/desktop-key-press.d.ts +12 -0
- package/dist/tools/desktop-key-press.d.ts.map +1 -0
- package/dist/tools/desktop-key-press.js +73 -0
- package/dist/tools/desktop-key-press.js.map +1 -0
- package/dist/tools/desktop-launch-app.d.ts +12 -0
- package/dist/tools/desktop-launch-app.d.ts.map +1 -0
- package/dist/tools/desktop-launch-app.js +58 -0
- package/dist/tools/desktop-launch-app.js.map +1 -0
- package/dist/tools/desktop-list-elements.d.ts +11 -0
- package/dist/tools/desktop-list-elements.d.ts.map +1 -0
- package/dist/tools/desktop-list-elements.js +65 -0
- package/dist/tools/desktop-list-elements.js.map +1 -0
- package/dist/tools/desktop-plan-approve.d.ts +30 -0
- package/dist/tools/desktop-plan-approve.d.ts.map +1 -0
- package/dist/tools/desktop-plan-approve.js +135 -0
- package/dist/tools/desktop-plan-approve.js.map +1 -0
- package/dist/tools/desktop-providers.d.ts +144 -0
- package/dist/tools/desktop-providers.d.ts.map +1 -0
- package/dist/tools/desktop-providers.js +12 -0
- package/dist/tools/desktop-providers.js.map +1 -0
- package/dist/tools/desktop-safety-gate.d.ts +84 -0
- package/dist/tools/desktop-safety-gate.d.ts.map +1 -0
- package/dist/tools/desktop-safety-gate.js +171 -0
- package/dist/tools/desktop-safety-gate.js.map +1 -0
- package/dist/tools/desktop-tools.d.ts +22 -0
- package/dist/tools/desktop-tools.d.ts.map +1 -0
- package/dist/tools/desktop-tools.js +71 -0
- package/dist/tools/desktop-tools.js.map +1 -0
- package/dist/tools/desktop-type.d.ts +12 -0
- package/dist/tools/desktop-type.d.ts.map +1 -0
- package/dist/tools/desktop-type.js +63 -0
- package/dist/tools/desktop-type.js.map +1 -0
- package/dist/tools/detect-language.d.ts +11 -0
- package/dist/tools/detect-language.d.ts.map +1 -0
- package/dist/tools/detect-language.js +235 -0
- package/dist/tools/detect-language.js.map +1 -0
- package/dist/tools/discover-news.d.ts +11 -0
- package/dist/tools/discover-news.d.ts.map +1 -0
- package/dist/tools/discover-news.js +261 -0
- package/dist/tools/discover-news.js.map +1 -0
- package/dist/tools/doc-generate.d.ts +17 -0
- package/dist/tools/doc-generate.d.ts.map +1 -0
- package/dist/tools/doc-generate.js +222 -0
- package/dist/tools/doc-generate.js.map +1 -0
- package/dist/tools/docs-lookup.d.ts +38 -0
- package/dist/tools/docs-lookup.d.ts.map +1 -0
- package/dist/tools/docs-lookup.js +174 -0
- package/dist/tools/docs-lookup.js.map +1 -0
- package/dist/tools/document-author.d.ts +40 -0
- package/dist/tools/document-author.d.ts.map +1 -0
- package/dist/tools/document-author.js +465 -0
- package/dist/tools/document-author.js.map +1 -0
- package/dist/tools/document-manage.d.ts +43 -0
- package/dist/tools/document-manage.d.ts.map +1 -0
- package/dist/tools/document-manage.js +665 -0
- package/dist/tools/document-manage.js.map +1 -0
- package/dist/tools/document-styling.d.ts +132 -0
- package/dist/tools/document-styling.d.ts.map +1 -0
- package/dist/tools/document-styling.js +360 -0
- package/dist/tools/document-styling.js.map +1 -0
- package/dist/tools/document-templates.d.ts +32 -0
- package/dist/tools/document-templates.d.ts.map +1 -0
- package/dist/tools/document-templates.js +95 -0
- package/dist/tools/document-templates.js.map +1 -0
- package/dist/tools/email-draft.d.ts +20 -0
- package/dist/tools/email-draft.d.ts.map +1 -0
- package/dist/tools/email-draft.js +191 -0
- package/dist/tools/email-draft.js.map +1 -0
- package/dist/tools/env-write.d.ts +32 -0
- package/dist/tools/env-write.d.ts.map +1 -0
- package/dist/tools/env-write.js +145 -0
- package/dist/tools/env-write.js.map +1 -0
- package/dist/tools/error-guidance.d.ts +38 -0
- package/dist/tools/error-guidance.d.ts.map +1 -0
- package/dist/tools/error-guidance.js +261 -0
- package/dist/tools/error-guidance.js.map +1 -0
- package/dist/tools/exercise-repair.d.ts +89 -0
- package/dist/tools/exercise-repair.d.ts.map +1 -0
- package/dist/tools/exercise-repair.js +350 -0
- package/dist/tools/exercise-repair.js.map +1 -0
- package/dist/tools/fact-check.d.ts +24 -0
- package/dist/tools/fact-check.d.ts.map +1 -0
- package/dist/tools/fact-check.js +78 -0
- package/dist/tools/fact-check.js.map +1 -0
- package/dist/tools/file-edit.d.ts +11 -0
- package/dist/tools/file-edit.d.ts.map +1 -0
- package/dist/tools/file-edit.js +166 -0
- package/dist/tools/file-edit.js.map +1 -0
- package/dist/tools/file-read.d.ts +12 -0
- package/dist/tools/file-read.d.ts.map +1 -0
- package/dist/tools/file-read.js +98 -0
- package/dist/tools/file-read.js.map +1 -0
- package/dist/tools/file-write.d.ts +13 -0
- package/dist/tools/file-write.d.ts.map +1 -0
- package/dist/tools/file-write.js +163 -0
- package/dist/tools/file-write.js.map +1 -0
- package/dist/tools/find-symbol.d.ts +13 -0
- package/dist/tools/find-symbol.d.ts.map +1 -0
- package/dist/tools/find-symbol.js +144 -0
- package/dist/tools/find-symbol.js.map +1 -0
- package/dist/tools/generate-image.d.ts +19 -0
- package/dist/tools/generate-image.d.ts.map +1 -0
- package/dist/tools/generate-image.js +438 -0
- package/dist/tools/generate-image.js.map +1 -0
- package/dist/tools/generate-video.d.ts +21 -0
- package/dist/tools/generate-video.d.ts.map +1 -0
- package/dist/tools/generate-video.js +197 -0
- package/dist/tools/generate-video.js.map +1 -0
- package/dist/tools/generate-voice.d.ts +13 -0
- package/dist/tools/generate-voice.d.ts.map +1 -0
- package/dist/tools/generate-voice.js +156 -0
- package/dist/tools/generate-voice.js.map +1 -0
- package/dist/tools/generation-manager.d.ts +75 -0
- package/dist/tools/generation-manager.d.ts.map +1 -0
- package/dist/tools/generation-manager.js +150 -0
- package/dist/tools/generation-manager.js.map +1 -0
- package/dist/tools/get-datetime.d.ts +11 -0
- package/dist/tools/get-datetime.d.ts.map +1 -0
- package/dist/tools/get-datetime.js +59 -0
- package/dist/tools/get-datetime.js.map +1 -0
- package/dist/tools/git-commit.d.ts +13 -0
- package/dist/tools/git-commit.d.ts.map +1 -0
- package/dist/tools/git-commit.js +136 -0
- package/dist/tools/git-commit.js.map +1 -0
- package/dist/tools/git-create-pr.d.ts +13 -0
- package/dist/tools/git-create-pr.d.ts.map +1 -0
- package/dist/tools/git-create-pr.js +134 -0
- package/dist/tools/git-create-pr.js.map +1 -0
- package/dist/tools/git-diff.d.ts +12 -0
- package/dist/tools/git-diff.d.ts.map +1 -0
- package/dist/tools/git-diff.js +134 -0
- package/dist/tools/git-diff.js.map +1 -0
- package/dist/tools/git.d.ts +11 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/git.js +89 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/glob.d.ts +11 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +64 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +11 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +131 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/health-catalogue-search.d.ts +11 -0
- package/dist/tools/health-catalogue-search.d.ts.map +1 -0
- package/dist/tools/health-catalogue-search.js +179 -0
- package/dist/tools/health-catalogue-search.js.map +1 -0
- package/dist/tools/health-plan-create.d.ts +24 -0
- package/dist/tools/health-plan-create.d.ts.map +1 -0
- package/dist/tools/health-plan-create.js +325 -0
- package/dist/tools/health-plan-create.js.map +1 -0
- package/dist/tools/health-plan-delete.d.ts +11 -0
- package/dist/tools/health-plan-delete.d.ts.map +1 -0
- package/dist/tools/health-plan-delete.js +97 -0
- package/dist/tools/health-plan-delete.js.map +1 -0
- package/dist/tools/health-plan-list.d.ts +11 -0
- package/dist/tools/health-plan-list.d.ts.map +1 -0
- package/dist/tools/health-plan-list.js +123 -0
- package/dist/tools/health-plan-list.js.map +1 -0
- package/dist/tools/health-plan-update-day.d.ts +18 -0
- package/dist/tools/health-plan-update-day.d.ts.map +1 -0
- package/dist/tools/health-plan-update-day.js +134 -0
- package/dist/tools/health-plan-update-day.js.map +1 -0
- package/dist/tools/health-plan-update.d.ts +11 -0
- package/dist/tools/health-plan-update.d.ts.map +1 -0
- package/dist/tools/health-plan-update.js +103 -0
- package/dist/tools/health-plan-update.js.map +1 -0
- package/dist/tools/health-profile-ask.d.ts +21 -0
- package/dist/tools/health-profile-ask.d.ts.map +1 -0
- package/dist/tools/health-profile-ask.js +50 -0
- package/dist/tools/health-profile-ask.js.map +1 -0
- package/dist/tools/http-request.d.ts +12 -0
- package/dist/tools/http-request.d.ts.map +1 -0
- package/dist/tools/http-request.js +329 -0
- package/dist/tools/http-request.js.map +1 -0
- package/dist/tools/journal.d.ts +17 -0
- package/dist/tools/journal.d.ts.map +1 -0
- package/dist/tools/journal.js +159 -0
- package/dist/tools/journal.js.map +1 -0
- package/dist/tools/learning.d.ts +154 -0
- package/dist/tools/learning.d.ts.map +1 -0
- package/dist/tools/learning.js +1344 -0
- package/dist/tools/learning.js.map +1 -0
- package/dist/tools/list-directory.d.ts +11 -0
- package/dist/tools/list-directory.d.ts.map +1 -0
- package/dist/tools/list-directory.js +95 -0
- package/dist/tools/list-directory.js.map +1 -0
- package/dist/tools/memory-delete.d.ts +11 -0
- package/dist/tools/memory-delete.d.ts.map +1 -0
- package/dist/tools/memory-delete.js +54 -0
- package/dist/tools/memory-delete.js.map +1 -0
- package/dist/tools/memory-recall.d.ts +11 -0
- package/dist/tools/memory-recall.d.ts.map +1 -0
- package/dist/tools/memory-recall.js +101 -0
- package/dist/tools/memory-recall.js.map +1 -0
- package/dist/tools/memory-save.d.ts +11 -0
- package/dist/tools/memory-save.d.ts.map +1 -0
- package/dist/tools/memory-save.js +110 -0
- package/dist/tools/memory-save.js.map +1 -0
- package/dist/tools/memory-update.d.ts +11 -0
- package/dist/tools/memory-update.d.ts.map +1 -0
- package/dist/tools/memory-update.js +88 -0
- package/dist/tools/memory-update.js.map +1 -0
- package/dist/tools/news.d.ts +11 -0
- package/dist/tools/news.d.ts.map +1 -0
- package/dist/tools/news.js +137 -0
- package/dist/tools/news.js.map +1 -0
- package/dist/tools/open-design-studio.d.ts +20 -0
- package/dist/tools/open-design-studio.d.ts.map +1 -0
- package/dist/tools/open-design-studio.js +45 -0
- package/dist/tools/open-design-studio.js.map +1 -0
- package/dist/tools/open-health-room.d.ts +19 -0
- package/dist/tools/open-health-room.d.ts.map +1 -0
- package/dist/tools/open-health-room.js +44 -0
- package/dist/tools/open-health-room.js.map +1 -0
- package/dist/tools/open-learning-room.d.ts +20 -0
- package/dist/tools/open-learning-room.d.ts.map +1 -0
- package/dist/tools/open-learning-room.js +44 -0
- package/dist/tools/open-learning-room.js.map +1 -0
- package/dist/tools/paper-fetch.d.ts +11 -0
- package/dist/tools/paper-fetch.d.ts.map +1 -0
- package/dist/tools/paper-fetch.js +236 -0
- package/dist/tools/paper-fetch.js.map +1 -0
- package/dist/tools/post-performance.d.ts +17 -0
- package/dist/tools/post-performance.d.ts.map +1 -0
- package/dist/tools/post-performance.js +58 -0
- package/dist/tools/post-performance.js.map +1 -0
- package/dist/tools/present-plan.d.ts +34 -0
- package/dist/tools/present-plan.d.ts.map +1 -0
- package/dist/tools/present-plan.js +117 -0
- package/dist/tools/present-plan.js.map +1 -0
- package/dist/tools/project-index.d.ts +11 -0
- package/dist/tools/project-index.d.ts.map +1 -0
- package/dist/tools/project-index.js +88 -0
- package/dist/tools/project-index.js.map +1 -0
- package/dist/tools/propose-hooks.d.ts +17 -0
- package/dist/tools/propose-hooks.d.ts.map +1 -0
- package/dist/tools/propose-hooks.js +71 -0
- package/dist/tools/propose-hooks.js.map +1 -0
- package/dist/tools/propose-tool.d.ts +11 -0
- package/dist/tools/propose-tool.d.ts.map +1 -0
- package/dist/tools/propose-tool.js +155 -0
- package/dist/tools/propose-tool.js.map +1 -0
- package/dist/tools/recipe-repair.d.ts +116 -0
- package/dist/tools/recipe-repair.d.ts.map +1 -0
- package/dist/tools/recipe-repair.js +404 -0
- package/dist/tools/recipe-repair.js.map +1 -0
- package/dist/tools/record-machine-rule.d.ts +24 -0
- package/dist/tools/record-machine-rule.d.ts.map +1 -0
- package/dist/tools/record-machine-rule.js +60 -0
- package/dist/tools/record-machine-rule.js.map +1 -0
- package/dist/tools/release-notes.d.ts +11 -0
- package/dist/tools/release-notes.d.ts.map +1 -0
- package/dist/tools/release-notes.js +59 -0
- package/dist/tools/release-notes.js.map +1 -0
- package/dist/tools/remove-background.d.ts +15 -0
- package/dist/tools/remove-background.d.ts.map +1 -0
- package/dist/tools/remove-background.js +117 -0
- package/dist/tools/remove-background.js.map +1 -0
- package/dist/tools/report-generate.d.ts +30 -0
- package/dist/tools/report-generate.d.ts.map +1 -0
- package/dist/tools/report-generate.js +325 -0
- package/dist/tools/report-generate.js.map +1 -0
- package/dist/tools/research-post.d.ts +20 -0
- package/dist/tools/research-post.d.ts.map +1 -0
- package/dist/tools/research-post.js +87 -0
- package/dist/tools/research-post.js.map +1 -0
- package/dist/tools/research-story.d.ts +29 -0
- package/dist/tools/research-story.d.ts.map +1 -0
- package/dist/tools/research-story.js +78 -0
- package/dist/tools/research-story.js.map +1 -0
- package/dist/tools/rollback.d.ts +11 -0
- package/dist/tools/rollback.d.ts.map +1 -0
- package/dist/tools/rollback.js +81 -0
- package/dist/tools/rollback.js.map +1 -0
- package/dist/tools/scan-industry.d.ts +19 -0
- package/dist/tools/scan-industry.d.ts.map +1 -0
- package/dist/tools/scan-industry.js +82 -0
- package/dist/tools/scan-industry.js.map +1 -0
- package/dist/tools/screenshot.d.ts +11 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +82 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/secret-request.d.ts +24 -0
- package/dist/tools/secret-request.d.ts.map +1 -0
- package/dist/tools/secret-request.js +75 -0
- package/dist/tools/secret-request.js.map +1 -0
- package/dist/tools/security.d.ts +26 -0
- package/dist/tools/security.d.ts.map +1 -0
- package/dist/tools/security.js +78 -0
- package/dist/tools/security.js.map +1 -0
- package/dist/tools/self-inspect.d.ts +34 -0
- package/dist/tools/self-inspect.d.ts.map +1 -0
- package/dist/tools/self-inspect.js +427 -0
- package/dist/tools/self-inspect.js.map +1 -0
- package/dist/tools/suggest-beats.d.ts +17 -0
- package/dist/tools/suggest-beats.d.ts.map +1 -0
- package/dist/tools/suggest-beats.js +71 -0
- package/dist/tools/suggest-beats.js.map +1 -0
- package/dist/tools/suggest-stories.d.ts +26 -0
- package/dist/tools/suggest-stories.d.ts.map +1 -0
- package/dist/tools/suggest-stories.js +102 -0
- package/dist/tools/suggest-stories.js.map +1 -0
- package/dist/tools/support-request.d.ts +11 -0
- package/dist/tools/support-request.d.ts.map +1 -0
- package/dist/tools/support-request.js +110 -0
- package/dist/tools/support-request.js.map +1 -0
- package/dist/tools/switch-mode.d.ts +21 -0
- package/dist/tools/switch-mode.d.ts.map +1 -0
- package/dist/tools/switch-mode.js +57 -0
- package/dist/tools/switch-mode.js.map +1 -0
- package/dist/tools/task-manage.d.ts +16 -0
- package/dist/tools/task-manage.d.ts.map +1 -0
- package/dist/tools/task-manage.js +256 -0
- package/dist/tools/task-manage.js.map +1 -0
- package/dist/tools/task-suggest.d.ts +27 -0
- package/dist/tools/task-suggest.d.ts.map +1 -0
- package/dist/tools/task-suggest.js +70 -0
- package/dist/tools/task-suggest.js.map +1 -0
- package/dist/tools/test-generate.d.ts +20 -0
- package/dist/tools/test-generate.d.ts.map +1 -0
- package/dist/tools/test-generate.js +259 -0
- package/dist/tools/test-generate.js.map +1 -0
- package/dist/tools/test-run.d.ts +13 -0
- package/dist/tools/test-run.d.ts.map +1 -0
- package/dist/tools/test-run.js +177 -0
- package/dist/tools/test-run.js.map +1 -0
- package/dist/tools/todo-write.d.ts +11 -0
- package/dist/tools/todo-write.d.ts.map +1 -0
- package/dist/tools/todo-write.js +70 -0
- package/dist/tools/todo-write.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +84 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +728 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/types.d.ts +128 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/verification-trust.d.ts +39 -0
- package/dist/tools/verification-trust.d.ts.map +1 -0
- package/dist/tools/verification-trust.js +135 -0
- package/dist/tools/verification-trust.js.map +1 -0
- package/dist/tools/verify-change.d.ts +11 -0
- package/dist/tools/verify-change.d.ts.map +1 -0
- package/dist/tools/verify-change.js +775 -0
- package/dist/tools/verify-change.js.map +1 -0
- package/dist/tools/weather.d.ts +11 -0
- package/dist/tools/weather.d.ts.map +1 -0
- package/dist/tools/weather.js +96 -0
- package/dist/tools/weather.js.map +1 -0
- package/dist/tools/web-search.d.ts +12 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +137 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tools/write-article.d.ts +33 -0
- package/dist/tools/write-article.d.ts.map +1 -0
- package/dist/tools/write-article.js +206 -0
- package/dist/tools/write-article.js.map +1 -0
- package/dist/tools/write-exercise.d.ts +31 -0
- package/dist/tools/write-exercise.d.ts.map +1 -0
- package/dist/tools/write-exercise.js +278 -0
- package/dist/tools/write-exercise.js.map +1 -0
- package/dist/tools/write-post-image.d.ts +31 -0
- package/dist/tools/write-post-image.d.ts.map +1 -0
- package/dist/tools/write-post-image.js +102 -0
- package/dist/tools/write-post-image.js.map +1 -0
- package/dist/tools/write-post.d.ts +20 -0
- package/dist/tools/write-post.d.ts.map +1 -0
- package/dist/tools/write-post.js +113 -0
- package/dist/tools/write-post.js.map +1 -0
- package/dist/tools/write-recipe.d.ts +29 -0
- package/dist/tools/write-recipe.d.ts.map +1 -0
- package/dist/tools/write-recipe.js +262 -0
- package/dist/tools/write-recipe.js.map +1 -0
- package/dist/tools/write-video-post.d.ts +25 -0
- package/dist/tools/write-video-post.d.ts.map +1 -0
- package/dist/tools/write-video-post.js +238 -0
- package/dist/tools/write-video-post.js.map +1 -0
- package/dist/tools/write-voiceover.d.ts +29 -0
- package/dist/tools/write-voiceover.d.ts.map +1 -0
- package/dist/tools/write-voiceover.js +98 -0
- package/dist/tools/write-voiceover.js.map +1 -0
- package/dist/workflows/index.d.ts +4 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/types.d.ts +112 -0
- package/dist/workflows/types.d.ts.map +1 -0
- package/dist/workflows/types.js +10 -0
- package/dist/workflows/types.js.map +1 -0
- package/dist/workflows/workflow-manager.d.ts +54 -0
- package/dist/workflows/workflow-manager.d.ts.map +1 -0
- package/dist/workflows/workflow-manager.js +277 -0
- package/dist/workflows/workflow-manager.js.map +1 -0
- package/package.json +223 -0
|
@@ -0,0 +1,3558 @@
|
|
|
1
|
+
import { recoverWrittenToolCalls, WrittenCallStreamFilter } from './recover-written-calls.js';
|
|
2
|
+
import { getTextContent } from '../core/types.js';
|
|
3
|
+
import { MAX_TOOL_CALL_ITERATIONS, ITERATION_WARNING_THRESHOLD } from '../core/constants.js';
|
|
4
|
+
import { t } from '../i18n/index.js';
|
|
5
|
+
import { logger } from '../core/logger.js';
|
|
6
|
+
import { buildToolPrompt, parseToolCalls, formatToolResult } from './text-tool-parser.js';
|
|
7
|
+
import { bridgeImagesForTextModel } from './vision-bridge.js';
|
|
8
|
+
import { auditClaims } from './claims-auditor.js';
|
|
9
|
+
import { autoExtractAndSave } from '../memory/auto-extract.js';
|
|
10
|
+
import { maybeBuildDesignReinjection, isUIFilePath as isUIFilePathLocal } from './design-reinjection.js';
|
|
11
|
+
import { isStopCommand } from './stop-command.js';
|
|
12
|
+
import { findOriginalUserTaskIndex, formatSessionTasksBlock, buildCompressionContinuationHeader, buildVerbatimUserTurnsBlock, extractStructuredFields, trimMessageBody, OLD_MESSAGE_BODY_MAX_CHARS, isMetaPrefix, } from './context-continuity.js';
|
|
13
|
+
import { classifyTaskComplexity, formatDirectnessHint, COMPLEXITY_BUDGETS, } from './task-classifier.js';
|
|
14
|
+
import { avaEvents, withTrajectory, withChildTrajectory, getTrajectory } from '../dataset/emitter.js';
|
|
15
|
+
import { chargeCredits, extractUsage } from '../billing/meter.js';
|
|
16
|
+
import { summarizeToolArgs, summarizeToolResult, summarizeChainOutcome, categorizeToolPurpose } from '../dataset/summarizers.js';
|
|
17
|
+
import { pickVerificationTools, categorizeCorrection, VERIFICATION_TOOLS } from '../dataset/verification.js';
|
|
18
|
+
import { matchToolError } from '../tools/error-guidance.js';
|
|
19
|
+
import { recordEditFromTool, pendingFilesAtClosure, runPendingVerify, buildVerifyFailureNudge, } from './post-edit-verify.js';
|
|
20
|
+
import { signatureForFailure, recordFailure, shouldEscalateFreshEyes, markFreshEyesEscalated, describeFailureLoop, } from './error-loop-detector.js';
|
|
21
|
+
import { runFreshEyesReview, buildFreshEyesContext } from './fresh-eyes.js';
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { PNG } from 'pngjs';
|
|
24
|
+
// ─── Image downsampling ────────────────────────────────────────────────────
|
|
25
|
+
// Screenshots at native resolution are the single biggest token sink in
|
|
26
|
+
// vision-heavy sessions. A 1920×1080 full-page PNG encodes to ~100KB base64
|
|
27
|
+
// = ~25K tokens. Re-sent across 10 turns = 250K tokens for one image.
|
|
28
|
+
// Downsampling to max 1024px preserves all semantic information the model
|
|
29
|
+
// needs (layout, hierarchy, colour, typography visibility) while cutting
|
|
30
|
+
// the byte cost by 60-80%. Nearest-neighbor sampling is fine — this is not
|
|
31
|
+
// photo restoration, it's context for reasoning.
|
|
32
|
+
const IMAGE_MAX_DIMENSION = 1024;
|
|
33
|
+
function downsampleScreenshotBase64(base64) {
|
|
34
|
+
try {
|
|
35
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
36
|
+
const src = PNG.sync.read(buffer);
|
|
37
|
+
const maxCurrent = Math.max(src.width, src.height);
|
|
38
|
+
if (maxCurrent <= IMAGE_MAX_DIMENSION)
|
|
39
|
+
return base64;
|
|
40
|
+
const scale = IMAGE_MAX_DIMENSION / maxCurrent;
|
|
41
|
+
const newW = Math.max(1, Math.round(src.width * scale));
|
|
42
|
+
const newH = Math.max(1, Math.round(src.height * scale));
|
|
43
|
+
const dst = new PNG({ width: newW, height: newH });
|
|
44
|
+
for (let y = 0; y < newH; y++) {
|
|
45
|
+
const srcY = Math.min(src.height - 1, Math.floor(y / scale));
|
|
46
|
+
for (let x = 0; x < newW; x++) {
|
|
47
|
+
const srcX = Math.min(src.width - 1, Math.floor(x / scale));
|
|
48
|
+
const srcIdx = (src.width * srcY + srcX) << 2;
|
|
49
|
+
const dstIdx = (newW * y + x) << 2;
|
|
50
|
+
dst.data[dstIdx] = src.data[srcIdx];
|
|
51
|
+
dst.data[dstIdx + 1] = src.data[srcIdx + 1];
|
|
52
|
+
dst.data[dstIdx + 2] = src.data[srcIdx + 2];
|
|
53
|
+
dst.data[dstIdx + 3] = src.data[srcIdx + 3];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return PNG.sync.write(dst).toString('base64');
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// If decode/resize fails for any reason, fall back to the original.
|
|
60
|
+
// Never block the vision pipeline on a resize failure.
|
|
61
|
+
logger.debug(`[agent] Image downsample failed, using original: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
return base64;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ─── Mode-aware tool filtering ──────────────────────────────────────────────
|
|
66
|
+
// When a non-work mode is active, restrict the tool schema sent to the model
|
|
67
|
+
// so it can only call tools listed in that mode's system prompt.
|
|
68
|
+
// Without this, the model sees all tools in the schema and ignores text restrictions.
|
|
69
|
+
// ─── Continuation-stall detection ──────────────────────────────────────────
|
|
70
|
+
// Identifies assistant responses that narrate intent ("Let me rewrite the
|
|
71
|
+
// sidebar...") but terminate without making any tool calls. These are worse
|
|
72
|
+
// than empty responses because the user sees a promise that never gets
|
|
73
|
+
// fulfilled. Detected via prefix matching on common narration patterns.
|
|
74
|
+
//
|
|
75
|
+
// False positives (real closures that look like stalls) are preferable to
|
|
76
|
+
// false negatives (stalls that slip through) because the cost of a redundant
|
|
77
|
+
// "continue" nudge is small while the cost of invisible stalled work is
|
|
78
|
+
// catastrophic for UX.
|
|
79
|
+
const STALL_PREFIX_PATTERNS = [
|
|
80
|
+
'let me ',
|
|
81
|
+
"i'll ",
|
|
82
|
+
'i will ',
|
|
83
|
+
"i'm going to ",
|
|
84
|
+
'i am going to ',
|
|
85
|
+
'first, let me ',
|
|
86
|
+
'first, i',
|
|
87
|
+
'now let me ',
|
|
88
|
+
"now i'll ",
|
|
89
|
+
'okay, let me ',
|
|
90
|
+
'ok, let me ',
|
|
91
|
+
'right, let me ',
|
|
92
|
+
'alright, let me ',
|
|
93
|
+
'starting the ',
|
|
94
|
+
'starting with ',
|
|
95
|
+
'beginning the ',
|
|
96
|
+
"let's ",
|
|
97
|
+
];
|
|
98
|
+
function looksLikeContinuationStall(content) {
|
|
99
|
+
const trimmed = content.trim().toLowerCase();
|
|
100
|
+
if (trimmed.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
// Long responses are probably genuine explanations, not stalls
|
|
103
|
+
if (trimmed.length > 500)
|
|
104
|
+
return false;
|
|
105
|
+
// Check known continuation-narration prefixes
|
|
106
|
+
for (const prefix of STALL_PREFIX_PATTERNS) {
|
|
107
|
+
if (trimmed.startsWith(prefix))
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Detect context drift: the model produced a greeting or social response
|
|
114
|
+
* after a turn with tool usage. This happens when attention on the original
|
|
115
|
+
* task fades under the weight of many file reads / tool results and the
|
|
116
|
+
* model defaults to a safe social response instead of summarising findings.
|
|
117
|
+
*
|
|
118
|
+
* Only fires when the turn had 3+ tool calls AND the response is short
|
|
119
|
+
* and contains greeting patterns. A greeting in a zero-tool turn is fine
|
|
120
|
+
* (that's just a Chat-mode response).
|
|
121
|
+
*/
|
|
122
|
+
const GREETING_PATTERNS = [
|
|
123
|
+
/\bhey\b/i, /\bhello\b/i, /\bhi\b/i, /\bgood\s+(?:morning|afternoon|evening)\b/i,
|
|
124
|
+
/\bhow(?:'s| is) your (?:day|morning|evening|afternoon)\b/i,
|
|
125
|
+
/\bhow are you\b/i, /\bwhat(?:'s| is) up\b/i, /\bnice to (?:see|hear|meet)\b/i,
|
|
126
|
+
];
|
|
127
|
+
function looksLikePostToolDrift(content, toolCallCount) {
|
|
128
|
+
if (toolCallCount < 3)
|
|
129
|
+
return false; // Only relevant after real tool usage
|
|
130
|
+
const trimmed = content.trim();
|
|
131
|
+
if (trimmed.length > 200)
|
|
132
|
+
return false; // Short response after many tools = suspicious
|
|
133
|
+
const lower = trimmed.toLowerCase();
|
|
134
|
+
return GREETING_PATTERNS.some(p => p.test(lower));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Desktop-automation + browser-control tools. Two separate jobs ride on this
|
|
138
|
+
* one list, which is why it's exported rather than local:
|
|
139
|
+
*
|
|
140
|
+
* 1. MODE GATING (any surface) — they're only valid in desktop mode, so the
|
|
141
|
+
* no-prefix default path filters them out. Otherwise the model hallucinates
|
|
142
|
+
* desktop_click_by_name mid-coding-session and the safety gate eats the noise.
|
|
143
|
+
*
|
|
144
|
+
* 2. MARKETPLACE COMPLIANCE (extension only) — Microsoft blocked this extension
|
|
145
|
+
* over exactly these tools and required their removal to reinstate it
|
|
146
|
+
* (v0.48.1, 2026-04-21). The extension host passes this list to
|
|
147
|
+
* registerBuiltins({ exclude }) so they are NEVER CONSTRUCTED there.
|
|
148
|
+
*
|
|
149
|
+
* Until 2026-07-17 only job 1 existed, and job 2 was believed done but wasn't:
|
|
150
|
+
* the tools were registered on every surface and the only thing standing
|
|
151
|
+
* between a marketplace user and a desktop_* schema was mode detection, which
|
|
152
|
+
* keys off a literal '[Desktop Automation Mode]' prefix in the user's own
|
|
153
|
+
* message text and had no idea which surface it was running on. They couldn't
|
|
154
|
+
* actually drive anything (the extension supplies no uiaProvider/inputProvider),
|
|
155
|
+
* but "inert" is not the promise we made to MS.
|
|
156
|
+
*
|
|
157
|
+
* Keep this list as the ONE definition. If a desktop_* or browser_* tool is
|
|
158
|
+
* added to the registry and not added here, it ships to the marketplace.
|
|
159
|
+
*/
|
|
160
|
+
export const DESKTOP_TOOL_NAMES = [
|
|
161
|
+
'desktop_plan_approve',
|
|
162
|
+
'desktop_launch_app',
|
|
163
|
+
'desktop_list_elements',
|
|
164
|
+
'desktop_click_by_name',
|
|
165
|
+
'desktop_focus_window',
|
|
166
|
+
'desktop_type',
|
|
167
|
+
'desktop_key_press',
|
|
168
|
+
'browser_navigate',
|
|
169
|
+
'browser_snapshot',
|
|
170
|
+
'browser_click',
|
|
171
|
+
'browser_type',
|
|
172
|
+
'browser_close',
|
|
173
|
+
];
|
|
174
|
+
const DESKTOP_ONLY_TOOLS = new Set(DESKTOP_TOOL_NAMES);
|
|
175
|
+
/**
|
|
176
|
+
* Tools every mode gets, unioned in on top of MODE_ALLOWED_TOOLS.
|
|
177
|
+
*
|
|
178
|
+
* `self_inspect` is read-only by construction — it reads Ava's own source and
|
|
179
|
+
* her deploy state. It cannot write a file, run a command, or spend anything;
|
|
180
|
+
* changing the code is a thing the operator does directly, never Ava. So there
|
|
181
|
+
* is no mode where "don't let her read her own source" is the right answer.
|
|
182
|
+
*
|
|
183
|
+
* It was previously allowed in `work` and `plan` only, which meant that in the
|
|
184
|
+
* other ten modes the schema filter removed it and she'd correctly say she
|
|
185
|
+
* didn't have it — including in Chat, where "what can you do?" is exactly the
|
|
186
|
+
* question you'd ask, and which the README promises she answers by reading her
|
|
187
|
+
* own source.
|
|
188
|
+
*
|
|
189
|
+
* This is a union rather than twelve list edits on purpose: a new mode gets it
|
|
190
|
+
* automatically, so it can't rot back out the way it did the first time.
|
|
191
|
+
*
|
|
192
|
+
* conversation_recall joins it for the same reason. The system prompt tells Ava
|
|
193
|
+
* every turn to "call conversation_recall to read the real transcript instead
|
|
194
|
+
* of guessing" — but it was in no mode's list, so the filter dropped it in every
|
|
195
|
+
* mode that carries a prefix (which the dashboard always sends). The exact-recall
|
|
196
|
+
* backstop was unreachable on the main surface. Read-only: it reads the current
|
|
197
|
+
* run's transcript off sharedState, writes nothing.
|
|
198
|
+
*/
|
|
199
|
+
const ALWAYS_ALLOWED_TOOLS = new Set(['self_inspect', 'conversation_recall']);
|
|
200
|
+
const MODE_ALLOWED_TOOLS = {
|
|
201
|
+
// Work mode — the bread-and-butter coding surface. Ships every turn
|
|
202
|
+
// to users writing code, so the schema list is the single biggest
|
|
203
|
+
// per-turn token line item. Keep what a coder actually reaches for;
|
|
204
|
+
// push anything that belongs to a different state-of-thought out.
|
|
205
|
+
// Cross-mode asks (image generation, email drafts, weather) still
|
|
206
|
+
// work — Ava calls switch_mode. The friction of one switch is worth
|
|
207
|
+
// the 2-3K tokens saved on every single coding turn.
|
|
208
|
+
//
|
|
209
|
+
// Deliberately OUT:
|
|
210
|
+
// - journal_write / learning / weather / news — Chat / Teach.
|
|
211
|
+
// - email_draft / report_generate / document_manage /
|
|
212
|
+
// document_templates — office work, rarely mixed with coding.
|
|
213
|
+
// - memory_delete — destructive, never a normal-flow tool.
|
|
214
|
+
work: new Set([
|
|
215
|
+
// File operations
|
|
216
|
+
'read', 'write', 'edit',
|
|
217
|
+
// Search
|
|
218
|
+
'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
|
|
219
|
+
// Shell
|
|
220
|
+
'bash',
|
|
221
|
+
// The build's own check. The pre-closure guard constructs this tool
|
|
222
|
+
// directly rather than asking for it, so verify has always run — but a
|
|
223
|
+
// mode that edits files should be able to ASK for it too, and a name
|
|
224
|
+
// absent from the list it belongs on is how the next audit gets it wrong.
|
|
225
|
+
'verify_change',
|
|
226
|
+
// Git
|
|
227
|
+
'git_status', 'git_diff', 'rollback', 'git_commit', 'git_create_pr',
|
|
228
|
+
// Web
|
|
229
|
+
'web_search', 'http_request', 'browser',
|
|
230
|
+
// Creative Studio — making an asset happens in the Studio, not inline
|
|
231
|
+
// here. This gate used to list 'generate_image' / 'generate_video' /
|
|
232
|
+
// but the registry only ever builds the design_* tools,
|
|
233
|
+
// so those three names resolved to nothing — dead entries. The handoff
|
|
234
|
+
// (open_design_studio, further down) and browse_library are what work mode
|
|
235
|
+
// actually needs: point at the Studio to make, read the library to reuse.
|
|
236
|
+
'browse_library',
|
|
237
|
+
'remove_background',
|
|
238
|
+
// Data
|
|
239
|
+
'database_query',
|
|
240
|
+
// Memory — delete is out (rare, destructive)
|
|
241
|
+
'memory_save', 'memory_recall', 'memory_update',
|
|
242
|
+
// Planning / tasks
|
|
243
|
+
'present_plan', 'todo_write', 'task_manage', 'task_suggest', 'apply_plan',
|
|
244
|
+
// Testing
|
|
245
|
+
'test_run', 'test_generate',
|
|
246
|
+
// Architecture / docs gen
|
|
247
|
+
'analyze_architecture', 'doc_generate',
|
|
248
|
+
// Security audits
|
|
249
|
+
'audit_dependencies',
|
|
250
|
+
// Performance
|
|
251
|
+
'benchmark',
|
|
252
|
+
// Debug
|
|
253
|
+
'debug_logs',
|
|
254
|
+
// Interaction
|
|
255
|
+
'ask_user', 'support_request',
|
|
256
|
+
// Secret vault → project. The prompt tells Ava to call secret_request for a
|
|
257
|
+
// {{secret:<id>}} handle, then env_write to put a granted key into the
|
|
258
|
+
// project's gitignored .env — the host swaps the real value in at write
|
|
259
|
+
// time so she never sees it. Both were missing from every build surface
|
|
260
|
+
// (secret_request was desktop-only, env_write in no mode), so the flow the
|
|
261
|
+
// prompt describes couldn't run. Confirmation-gated writes; env_write
|
|
262
|
+
// refuses any non-gitignored target.
|
|
263
|
+
'secret_request', 'env_write',
|
|
264
|
+
// Self
|
|
265
|
+
'docs_lookup', 'propose_tool', 'self_inspect', 'release_notes',
|
|
266
|
+
// Taste specialist — fresh-context Curator for design/voice/microcopy
|
|
267
|
+
// calls that would otherwise degrade under cognitive load.
|
|
268
|
+
'curator',
|
|
269
|
+
// Utility
|
|
270
|
+
'get_datetime', 'detect_language',
|
|
271
|
+
// Hand a fitness/meal plan request off to the focused Health room, a
|
|
272
|
+
// learn-a-topic request off to the focused Learning room, or an icon /
|
|
273
|
+
// on-brand asset request off to the focused Design Studio.
|
|
274
|
+
'open_health_room', 'open_learning_room', 'open_design_studio',
|
|
275
|
+
// Mode switch
|
|
276
|
+
'switch_mode',
|
|
277
|
+
]),
|
|
278
|
+
plan: new Set([
|
|
279
|
+
// Read + nav
|
|
280
|
+
'read', 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
|
|
281
|
+
// Research surface — http_request/browser/news added so the
|
|
282
|
+
// coordinator-direct path (the most common Plan flow) has the same
|
|
283
|
+
// research tools the Researcher persona gets in orchestrated mode.
|
|
284
|
+
'web_search', 'http_request', 'browser', 'news',
|
|
285
|
+
// Memory + planning + analysis
|
|
286
|
+
'memory_save', 'memory_recall', 'present_plan', 'analyze_architecture',
|
|
287
|
+
// Reference / introspection (already used; advertised below now too)
|
|
288
|
+
'docs_lookup', 'self_inspect',
|
|
289
|
+
// Taste specialist for design/voice/microcopy decisions
|
|
290
|
+
'curator',
|
|
291
|
+
// Interaction + utilities
|
|
292
|
+
'ask_user', 'get_datetime', 'detect_language',
|
|
293
|
+
'switch_mode',
|
|
294
|
+
]),
|
|
295
|
+
chat: new Set([
|
|
296
|
+
'web_search', 'memory_save', 'memory_recall', 'memory_update', 'journal_write',
|
|
297
|
+
// todo_write for Ava's own session steps. task_suggest is her DEFAULT for a
|
|
298
|
+
// task-worthy thing she notices (a tap-to-add card); task_manage is for when
|
|
299
|
+
// the user explicitly says "add X to my list" — create directly then.
|
|
300
|
+
'todo_write', 'task_suggest', 'task_manage',
|
|
301
|
+
'get_datetime', 'weather', 'news', 'ask_user',
|
|
302
|
+
// Hand a fitness/meal plan request off to the focused Health room, a
|
|
303
|
+
// learn-a-topic request off to the focused Learning room, or an icon /
|
|
304
|
+
// on-brand asset request off to the focused Design Studio.
|
|
305
|
+
'open_health_room', 'open_learning_room', 'open_design_studio',
|
|
306
|
+
'switch_mode',
|
|
307
|
+
]),
|
|
308
|
+
brainstorm: new Set([
|
|
309
|
+
// Research signals (web + news) so coordinator-direct ideation has the
|
|
310
|
+
// same research surface the orchestrated team gets via IDEATION_TOOLS.
|
|
311
|
+
'web_search', 'http_request', 'browser', 'news',
|
|
312
|
+
// Memory — update is in for refining accumulated ideas across sessions.
|
|
313
|
+
'memory_save', 'memory_recall', 'memory_update',
|
|
314
|
+
// Output shape
|
|
315
|
+
'present_plan', 'journal_write', 'todo_write',
|
|
316
|
+
// Taste specialist for naming/voice/microcopy decisions
|
|
317
|
+
'curator',
|
|
318
|
+
'ask_user', 'get_datetime',
|
|
319
|
+
'switch_mode',
|
|
320
|
+
]),
|
|
321
|
+
teach: new Set([
|
|
322
|
+
// Reading + project navigation
|
|
323
|
+
'read', 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
|
|
324
|
+
// Writing — needed because the Tutor system prompt says "create sample
|
|
325
|
+
// files" and runs live code examples. file_write/file_edit + bash only.
|
|
326
|
+
// Git commit/PR tools intentionally OUT — wrong blast radius for a
|
|
327
|
+
// teaching session.
|
|
328
|
+
'write', 'edit', 'bash',
|
|
329
|
+
// Web (verify facts, fetch docs, browse references)
|
|
330
|
+
'web_search', 'http_request', 'browser',
|
|
331
|
+
// Memory — update is in so the learner profile evolves across sessions
|
|
332
|
+
'memory_save', 'memory_recall', 'memory_update',
|
|
333
|
+
// Journal — Ava reflects on the learner + their progress in her own voice
|
|
334
|
+
'journal_write',
|
|
335
|
+
// Learning subsystem
|
|
336
|
+
'learning_create', 'learning_teach', 'learning_progress',
|
|
337
|
+
// Scientific Papers library — "Read with Ava" hands her an arXiv/DOI paper
|
|
338
|
+
// to explain. The tool's own description says "use this in Teach mode", and
|
|
339
|
+
// this was the one mode that didn't allow it, so the button pointed at a
|
|
340
|
+
// tool she couldn't reach.
|
|
341
|
+
'paper_fetch_full_text',
|
|
342
|
+
// Interaction + utilities
|
|
343
|
+
'ask_user', 'get_datetime', 'detect_language',
|
|
344
|
+
'switch_mode',
|
|
345
|
+
]),
|
|
346
|
+
security: new Set([
|
|
347
|
+
'read', 'glob', 'grep', 'list_directory', 'find_symbol', 'project_index',
|
|
348
|
+
'bash', 'git_status', 'git_diff', 'web_search', 'analyze_architecture',
|
|
349
|
+
'audit_dependencies', 'debug_logs', 'memory_save', 'memory_recall',
|
|
350
|
+
'test_run', 'ask_user',
|
|
351
|
+
// get_datetime was missing here and nowhere else — eleven of the twelve
|
|
352
|
+
// rooms had it. Security is the room where it matters most: whether an
|
|
353
|
+
// advisory predates the version you run, whether a dependency is actually
|
|
354
|
+
// behind, how old an unpatched CVE is. Without it she dates from her
|
|
355
|
+
// training cutoff, which is not a vague answer but a confident wrong one,
|
|
356
|
+
// and it gets more wrong every month the model ages.
|
|
357
|
+
'get_datetime',
|
|
358
|
+
'switch_mode',
|
|
359
|
+
]),
|
|
360
|
+
// Health Room — Ava focused entirely on the user's health & fitness. Same
|
|
361
|
+
// Ava, health-scoped: the plan + catalogue tools are her kit, memory carries
|
|
362
|
+
// the relationship, web_search backs facts. Coding / file / shell tools are
|
|
363
|
+
// deliberately OUT — this room composes plans from the real catalogue, it
|
|
364
|
+
// doesn't touch the codebase.
|
|
365
|
+
health: new Set([
|
|
366
|
+
// health_plan_list before create: activating archives another plan of the
|
|
367
|
+
// same type, and she cannot warn about what she cannot see.
|
|
368
|
+
// health_plan_update was missing from this set while the room prompt told
|
|
369
|
+
// her to use it for "make that draft active" — so the only tool she could
|
|
370
|
+
// reach was create, which is precisely how the library ended up with two
|
|
371
|
+
// copies of the same week, one draft and one active on the wrong day.
|
|
372
|
+
'health_plan_list', 'health_plan_create', 'health_plan_update',
|
|
373
|
+
'health_plan_update_day', 'health_plan_delete', 'health_catalogue_search',
|
|
374
|
+
'health_profile_ask',
|
|
375
|
+
'memory_save', 'memory_recall', 'memory_update',
|
|
376
|
+
// Journal — Ava reflects on the person + their health journey in her voice
|
|
377
|
+
'journal_write',
|
|
378
|
+
'web_search', 'ask_user', 'get_datetime',
|
|
379
|
+
'switch_mode',
|
|
380
|
+
]),
|
|
381
|
+
// Design Studio — Ava the Design Architect, focused entirely on making the
|
|
382
|
+
// user on-brand icons. Same Ava, design-scoped: the shape/generate/brand/save
|
|
383
|
+
// tools are her kit, memory carries their taste, journal her read on their eye.
|
|
384
|
+
// Coding / file / shell tools are deliberately OUT — this room makes assets,
|
|
385
|
+
// it doesn't touch the codebase. Scope is icons for now.
|
|
386
|
+
design: new Set([
|
|
387
|
+
'design_find_shape', 'design_generate_icon', 'design_generate_set',
|
|
388
|
+
'design_generate_video', 'design_generate_image', 'design_generate_voice',
|
|
389
|
+
'design_generate_logo', 'design_explore_logos', 'design_brand_kit', 'design_save',
|
|
390
|
+
'memory_save', 'memory_recall', 'memory_update',
|
|
391
|
+
// Journal — Ava reflects on the person + their taste in her own voice
|
|
392
|
+
'journal_write',
|
|
393
|
+
// Research — ground a look in current design references / trends before authoring it
|
|
394
|
+
'web_search',
|
|
395
|
+
'ask_user', 'get_datetime',
|
|
396
|
+
'switch_mode',
|
|
397
|
+
]),
|
|
398
|
+
// Social Studio — Ava as the social-media & marketing lead, driving the
|
|
399
|
+
// Posts floor. Same Ava, marketing-scoped: research/hook/write/performance
|
|
400
|
+
// tools are her kit, docs_lookup grounds every product claim, generate_image
|
|
401
|
+
// makes visuals, memory carries the mission's voice, journal her read on the
|
|
402
|
+
// brand's public presence. Coding / file / shell tools are deliberately OUT —
|
|
403
|
+
// this room ships posts, it doesn't touch the codebase.
|
|
404
|
+
social: new Set([
|
|
405
|
+
'research_post', 'propose_hooks', 'write_post', 'post_performance', 'suggest_beats',
|
|
406
|
+
// The day plan. She and the operator agree the day together, so she
|
|
407
|
+
// needs to read it (items may already be carried from yesterday),
|
|
408
|
+
// write what was agreed, and tick what she can verify. Registering
|
|
409
|
+
// the tools was not enough on its own — see write_video_post below,
|
|
410
|
+
// which sat unusable in exactly this way while she correctly reported
|
|
411
|
+
// it was not in her toolset. This Set is the real gate.
|
|
412
|
+
'day_plan_read', 'day_plan_write', 'day_plan_item_status',
|
|
413
|
+
// Short-form video — the Video Posts room. Registering the tool in the
|
|
414
|
+
// builtins was not enough: this Set is the real gate, and while it was
|
|
415
|
+
// missing she reported "write_video_post isn't in my toolset" and pointed
|
|
416
|
+
// people at the Design Studio instead. She was reading her list correctly.
|
|
417
|
+
'write_video_post',
|
|
418
|
+
// A post's picture is part of the post. Made here, not via a trip to the
|
|
419
|
+
// Design Studio — she cannot finish a post she is not allowed to illustrate.
|
|
420
|
+
'write_post_image',
|
|
421
|
+
// Our own catalogue, read-only. A food or fitness post should be about a
|
|
422
|
+
// dish or a movement we actually have, with the real name, ingredients and
|
|
423
|
+
// method — and the video store already pulls OUR photography for the hero
|
|
424
|
+
// image, so the copy has to match the picture it is paired with. Without
|
|
425
|
+
// these she told users the recipe catalogue "lives in a different part of
|
|
426
|
+
// the system that I can't query from this room", which was true and is the
|
|
427
|
+
// same gap write_video_post had: registered in the builtins, missing here.
|
|
428
|
+
//
|
|
429
|
+
// Authoring stays out. Recipes are written at the Pantry desk and
|
|
430
|
+
// exercises in the Gym; this room reads the shelf, it does not stock it.
|
|
431
|
+
'find_recipe', 'read_recipe', 'find_exercise', 'read_exercise',
|
|
432
|
+
// Industry radar — what AI leaders/labs actually said this week (sourced)
|
|
433
|
+
'scan_industry',
|
|
434
|
+
// Ground every product claim in the real docs, never training memory
|
|
435
|
+
'docs_lookup', 'release_notes',
|
|
436
|
+
// Visuals for posts are made in the Creative Studio, not inline. Point the
|
|
437
|
+
// user there (open_design_studio) and reuse what they already have
|
|
438
|
+
// (browse_library). 'generate_image' used to sit here — a name the registry
|
|
439
|
+
// never builds — so a post's visual silently did nothing.
|
|
440
|
+
'open_design_studio', 'browse_library',
|
|
441
|
+
// A voiceover ON ITS OWN, not attached to a clip — audio is its own
|
|
442
|
+
// deliverable (a read for footage they already have), and routing it
|
|
443
|
+
// through a video they don't want burns a video generation.
|
|
444
|
+
//
|
|
445
|
+
// NOT design_generate_voice: that tool speaks through `designControl`, the
|
|
446
|
+
// Design Studio's canvas channel, which this surface never mounts. Adding
|
|
447
|
+
// it here would have registered a tool that answers "no canvas" every time.
|
|
448
|
+
// write_voiceover carries its own store, the way write_video_post does.
|
|
449
|
+
'write_voiceover',
|
|
450
|
+
// Research to make the angle current
|
|
451
|
+
'web_search',
|
|
452
|
+
// Memory carries the mission voice; journal her read on the brand
|
|
453
|
+
'memory_save', 'memory_recall', 'memory_update', 'journal_write',
|
|
454
|
+
'ask_user', 'get_datetime',
|
|
455
|
+
'switch_mode',
|
|
456
|
+
]),
|
|
457
|
+
// Newsroom — Ava as Correspondent. She reads what outlets published, stands
|
|
458
|
+
// the story up, and writes her OWN account with the receipts attached.
|
|
459
|
+
//
|
|
460
|
+
// The kit is small on purpose. web_search is deliberately OUT: an open web
|
|
461
|
+
// search returns blogs, forums and SEO sludge, and once that is in the corpus
|
|
462
|
+
// a quote "verifies" against a source that was never journalism. The news
|
|
463
|
+
// index is the only door in, so the evidence write_article checks against is
|
|
464
|
+
// evidence from a publisher. Coding / file / shell tools are out entirely.
|
|
465
|
+
news: new Set([
|
|
466
|
+
'discover_news', 'suggest_stories', 'research_story', 'fact_check', 'write_article',
|
|
467
|
+
// Header images are authored in the Creative Studio, never a lifted press
|
|
468
|
+
// photo — point the user there rather than generating inline. 'generate_image'
|
|
469
|
+
// used to be here, but the registry never builds that name.
|
|
470
|
+
'open_design_studio', 'browse_library',
|
|
471
|
+
// Continuity: running stories, corrections owed, what she has already covered.
|
|
472
|
+
'memory_save', 'memory_recall', 'memory_update', 'journal_write',
|
|
473
|
+
'ask_user', 'get_datetime',
|
|
474
|
+
'switch_mode',
|
|
475
|
+
]),
|
|
476
|
+
// Write mode — the author's surface. Markdown is the editable source;
|
|
477
|
+
// Word/PDF are exports. Ships the authoring tool + the supporting cast a
|
|
478
|
+
// writer reaches for (research, images for covers, the file ops the .md
|
|
479
|
+
// lives in, memory for house style/templates). Coding tools stay out — this
|
|
480
|
+
// is writing, not building.
|
|
481
|
+
write: new Set([
|
|
482
|
+
// Authoring
|
|
483
|
+
'document_author', 'document_manage', 'report_generate', 'email_draft',
|
|
484
|
+
// The .md source lives on disk
|
|
485
|
+
'read', 'write', 'edit', 'glob', 'grep', 'list_directory',
|
|
486
|
+
// Research to ground the writing
|
|
487
|
+
'web_search', 'http_request', 'browser',
|
|
488
|
+
// Illustrations / covers are made in the Creative Studio — hand off there
|
|
489
|
+
// and reuse existing assets. 'generate_image' used to sit here (a name the
|
|
490
|
+
// registry never builds), which is why write mode's "cover image she
|
|
491
|
+
// generates herself" never actually generated one. remove_background stays:
|
|
492
|
+
// it's a one-shot edit utility, not creative generation.
|
|
493
|
+
'open_design_studio', 'browse_library', 'remove_background',
|
|
494
|
+
// Memory — house style, saved templates, continuity across a long piece
|
|
495
|
+
'memory_save', 'memory_recall', 'memory_update',
|
|
496
|
+
// Light planning for long documents
|
|
497
|
+
'present_plan', 'todo_write',
|
|
498
|
+
// Utility + interaction
|
|
499
|
+
'get_datetime', 'detect_language', 'ask_user', 'support_request',
|
|
500
|
+
'docs_lookup', 'curator',
|
|
501
|
+
// Mode switch
|
|
502
|
+
'switch_mode',
|
|
503
|
+
]),
|
|
504
|
+
// Desktop Automation mode. Two layers of hands:
|
|
505
|
+
// - desktop_launch_app to open apps (denylist-scoped — no shell)
|
|
506
|
+
// - desktop_* for UIA-tree targeting of native windows
|
|
507
|
+
// - browser_* for driving the visible Ava Chromium via DOM
|
|
508
|
+
// File-editing and coordinate-based native input are deliberately absent
|
|
509
|
+
// here — file changes go through Work mode, and native UIA input is the
|
|
510
|
+
// stable targeting layer. `bash` is intentionally OUT: it's too broad
|
|
511
|
+
// for this surface and gives the model an escape hatch we don't want.
|
|
512
|
+
desktop: new Set([
|
|
513
|
+
// Trajectory-level plan approval (one card, many steps)
|
|
514
|
+
'desktop_plan_approve',
|
|
515
|
+
// Launch apps — scoped, no shell interpreter
|
|
516
|
+
'desktop_launch_app',
|
|
517
|
+
// Native desktop via UIA tree — stable selectors, not pixel coords.
|
|
518
|
+
// No desktop_screenshot or desktop_click_xy: vision + coordinate
|
|
519
|
+
// guessing is a failure mode. Ava must use tree/DOM-based targeting.
|
|
520
|
+
'desktop_list_elements', 'desktop_click_by_name', 'desktop_focus_window',
|
|
521
|
+
'desktop_type', 'desktop_key_press',
|
|
522
|
+
// Persist a standing rule the user wants obeyed on this machine forever
|
|
523
|
+
'record_machine_rule',
|
|
524
|
+
// Browser automation via Playwright DOM — visible Chromium, stable.
|
|
525
|
+
'browser_navigate', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_close',
|
|
526
|
+
// Light support
|
|
527
|
+
'web_search', 'memory_recall', 'ask_user', 'get_datetime',
|
|
528
|
+
'switch_mode',
|
|
529
|
+
// Capability-style secret grant — required so Ava can fetch a
|
|
530
|
+
// {{secret:<id>}} handle when the safety gate blocks typing into
|
|
531
|
+
// a sensitive field. Without this in the allowlist the fix below
|
|
532
|
+
// would be a dead end — Ava would be told to call secret_request
|
|
533
|
+
// but the registry would reject the call for being out-of-mode.
|
|
534
|
+
'secret_request',
|
|
535
|
+
]),
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* The mode this turn is in, read from the tag the surfaces prepend.
|
|
539
|
+
*
|
|
540
|
+
* Exported because the AutoCoordinator had grown a second copy of this that
|
|
541
|
+
* sniffed the SYSTEM prompt for the string `'Plan mode'`. The marker is
|
|
542
|
+
* `[Plan Mode]`, on the user message, with a capital M — so the copy matched
|
|
543
|
+
* nothing and answered `'work'` for all seven modes, every turn, since it was
|
|
544
|
+
* written. One fact, two detectors, and the quiet one was wrong: the same
|
|
545
|
+
* shape as the dead tool names in the mode allowlists.
|
|
546
|
+
*/
|
|
547
|
+
export function detectModeFromMessages(messages) {
|
|
548
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
549
|
+
const msg = messages[i];
|
|
550
|
+
if (msg.role !== 'user')
|
|
551
|
+
continue;
|
|
552
|
+
const text = getTextContent(msg.content);
|
|
553
|
+
if (text.startsWith('[Internal Planning'))
|
|
554
|
+
continue;
|
|
555
|
+
if (text.startsWith('[Plan Mode]'))
|
|
556
|
+
return 'plan';
|
|
557
|
+
if (text.startsWith('[Chat Mode]'))
|
|
558
|
+
return 'chat';
|
|
559
|
+
if (text.startsWith('[Brainstorm Mode]'))
|
|
560
|
+
return 'brainstorm';
|
|
561
|
+
if (text.startsWith('[Write Mode]'))
|
|
562
|
+
return 'write';
|
|
563
|
+
if (text.startsWith('[Teach Mode]'))
|
|
564
|
+
return 'teach';
|
|
565
|
+
if (text.startsWith('[Security Audit Mode]'))
|
|
566
|
+
return 'security';
|
|
567
|
+
if (text.startsWith('[Desktop Automation Mode]'))
|
|
568
|
+
return 'desktop';
|
|
569
|
+
if (text.startsWith('[Health Room]'))
|
|
570
|
+
return 'health';
|
|
571
|
+
if (text.startsWith('[Design Studio]'))
|
|
572
|
+
return 'design';
|
|
573
|
+
if (text.startsWith('[Social Studio]'))
|
|
574
|
+
return 'social';
|
|
575
|
+
if (text.startsWith('[Newsroom]'))
|
|
576
|
+
return 'news';
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Can this mode change files?
|
|
583
|
+
*
|
|
584
|
+
* Read from `MODE_ALLOWED_TOOLS` rather than a list of its own. A mode that is
|
|
585
|
+
* not handed `write` or `edit` is read-only by design, and nothing downstream
|
|
586
|
+
* should be able to grant it more than the mode itself has.
|
|
587
|
+
*
|
|
588
|
+
* Exists because the AutoCoordinator's Builder hand-off had no idea what mode
|
|
589
|
+
* it was in. Seen live 2026-08-19: a Plan-mode turn produced a plan card, wrote
|
|
590
|
+
* eleven todos, announced "Builder dispatched — executing 11 tasks", and started
|
|
591
|
+
* editing an Unreal project. Plan mode is read-only — it cannot open a file to
|
|
592
|
+
* write it — and the orchestrator went around that by spawning agents that
|
|
593
|
+
* could. The operator's words were "why are you coding when did i say to do
|
|
594
|
+
* anything but plan".
|
|
595
|
+
*
|
|
596
|
+
* An unknown mode returns true: work mode carries no prefix, so callers that
|
|
597
|
+
* default to it must not be silently blocked.
|
|
598
|
+
*/
|
|
599
|
+
/**
|
|
600
|
+
* The hard ceiling on what a persona may be handed in a READ-ONLY mode.
|
|
601
|
+
*
|
|
602
|
+
* The conductor scopes each persona by its own `allowedTools`, taken from the
|
|
603
|
+
* full registry — the mode allowlist is not consulted at all. So a mode's
|
|
604
|
+
* read-only guarantee held only for as long as nobody put a write-capable
|
|
605
|
+
* persona on its team, which is a promise about a roster rather than about the
|
|
606
|
+
* mode. Audited 2026-08-19: Plan's team happened to be clean, and the
|
|
607
|
+
* guarantee was one persona away from being false.
|
|
608
|
+
*
|
|
609
|
+
* Returns null for a mode that can edit files, deliberately. Work's allowlist
|
|
610
|
+
* is known to be stale and has never applied (it has no prefix, so the filter
|
|
611
|
+
* never runs) — clamping the Builder to it here would enforce a list nobody
|
|
612
|
+
* has checked, and would land as a breakage dressed as a tightening.
|
|
613
|
+
*/
|
|
614
|
+
export function readOnlyModeToolCeiling(mode) {
|
|
615
|
+
if (!mode || modeCanEditFiles(mode))
|
|
616
|
+
return null;
|
|
617
|
+
const allowed = MODE_ALLOWED_TOOLS[mode];
|
|
618
|
+
if (!allowed)
|
|
619
|
+
return null;
|
|
620
|
+
return new Set([...allowed, ...ALWAYS_ALLOWED_TOOLS]);
|
|
621
|
+
}
|
|
622
|
+
export function modeCanEditFiles(mode) {
|
|
623
|
+
if (!mode)
|
|
624
|
+
return true;
|
|
625
|
+
const allowed = MODE_ALLOWED_TOOLS[mode];
|
|
626
|
+
if (!allowed)
|
|
627
|
+
return true;
|
|
628
|
+
return allowed.has('write') || allowed.has('edit');
|
|
629
|
+
}
|
|
630
|
+
// ─── Agent ───────────────────────────────────────────────────────────────────
|
|
631
|
+
export class Agent {
|
|
632
|
+
provider;
|
|
633
|
+
model;
|
|
634
|
+
// Vision bridge — a dedicated vision-capable provider+model (e.g. Qwen Omni)
|
|
635
|
+
// used to DESCRIBE images when the main coordinator is text-only (DeepSeek,
|
|
636
|
+
// Mistral Codestral, etc.), so it can "see" the image as text. Optional; when
|
|
637
|
+
// absent, text-only models fall back to a "switch model" note.
|
|
638
|
+
visionProvider;
|
|
639
|
+
visionModel;
|
|
640
|
+
// Image descriptions cached per session (keyed by the image data URL) so a
|
|
641
|
+
// text-only coordinator doesn't re-run the vision model on every turn.
|
|
642
|
+
visionDescriptionCache = new Map();
|
|
643
|
+
toolRegistry;
|
|
644
|
+
toolContext;
|
|
645
|
+
pendingInterjections = [];
|
|
646
|
+
// Graceful-pause flag — set by requestPause(), checked at each loop
|
|
647
|
+
// boundary. A "pause" finishes the current step then exits cleanly, unlike
|
|
648
|
+
// the abort signal which is an immediate hard stop.
|
|
649
|
+
pauseRequested = false;
|
|
650
|
+
// Verifying tools that ran this run() (name + success), for the soft
|
|
651
|
+
// honesty gate (claims-auditor) at final-answer time. Reset per run.
|
|
652
|
+
runToolEvidence = [];
|
|
653
|
+
/**
|
|
654
|
+
* The id of the turn currently running — one per run(), shared by every
|
|
655
|
+
* model call the turn makes, sent to our platform as X-Ava-Turn-Id.
|
|
656
|
+
*
|
|
657
|
+
* A tool-using turn calls the model repeatedly, and each call was landing in
|
|
658
|
+
* usage_logs as its own row with nothing connecting them. So a turn where
|
|
659
|
+
* the third call failed and the fourth succeeded was indistinguishable from
|
|
660
|
+
* four separate turns: retries invisible, and no way to tell a price change
|
|
661
|
+
* from a behaviour change. This is what makes cost-per-outcome answerable
|
|
662
|
+
* rather than just cost-per-call.
|
|
663
|
+
*/
|
|
664
|
+
runTurnId;
|
|
665
|
+
// Did the soft honesty gate flag an unbacked factual claim this run?
|
|
666
|
+
// Set by the claims-auditor branch; read by the verification_evidence
|
|
667
|
+
// dataset emit in run()'s finally. Reset per run.
|
|
668
|
+
runClaimFlagged = false;
|
|
669
|
+
// Latest claims-auditor result this run (set at answer finalization).
|
|
670
|
+
// Read by the verify-or-restate guard in runInner. Reset per run.
|
|
671
|
+
lastAudit = null;
|
|
672
|
+
// Did the honesty gate already fire its one verify-or-restate re-prompt
|
|
673
|
+
// this run? Caps the active loop at a single attempt. Reset per run.
|
|
674
|
+
honestyVerifyAttempted = false;
|
|
675
|
+
/** Build the verify-or-restate re-prompt for the honesty gate. Maps the
|
|
676
|
+
* claim to the tool that would actually check it, and offers the honest
|
|
677
|
+
* alternative (restate without asserting). Internal — never shown as text. */
|
|
678
|
+
buildHonestyVerifyNudge(audit) {
|
|
679
|
+
const claim = audit.claims[0] ?? 'a completion/state claim';
|
|
680
|
+
const how = audit.tier === 'critical'
|
|
681
|
+
? 'run a real check (audit_dependencies, a scan, or grep for the actual pattern)'
|
|
682
|
+
: 'run the tool that checks it (test_run for tests, bash or git_diff for code changes, http_request or browser for an endpoint, file_read for a file)';
|
|
683
|
+
return `[Honesty check — you stated "${claim}" but ran no tool that verifies it this turn. Do ONE of two things now, no exceptions: (a) ${how}, then report the actual result; or (b) restate without asserting it as done — e.g. "I changed X, but haven't verified it yet." Do not repeat the unbacked claim as fact.]`;
|
|
684
|
+
}
|
|
685
|
+
_inThinkTag = false;
|
|
686
|
+
// ─── Exploration budget tracking (token-cost discipline) ────────────────
|
|
687
|
+
// Per-run state: the task classification and how many read-only tool calls
|
|
688
|
+
// the agent has made before its first write-capable call. When the count
|
|
689
|
+
// exceeds the budget for the current task complexity, a soft nudge is
|
|
690
|
+
// injected into the next LLM call ("you're stalling — commit to a
|
|
691
|
+
// direction"). Reset on each Agent.run() call.
|
|
692
|
+
currentTaskComplexity = 'moderate';
|
|
693
|
+
readCountBeforeFirstWrite = 0;
|
|
694
|
+
hasWrittenInThisRun = false;
|
|
695
|
+
explorationNudgeFired = false;
|
|
696
|
+
// Design re-injection state — tracks last re-injection turn and file mtimes
|
|
697
|
+
// so we don't re-read the same design files 20 times in a single session.
|
|
698
|
+
designReinjectionTurn = 0;
|
|
699
|
+
designReinjectionLastTurn = -Infinity;
|
|
700
|
+
designReinjectionLastMtimes = new Map();
|
|
701
|
+
/** Which surface this Agent is running in (cli/extension/ide/companion). */
|
|
702
|
+
surface;
|
|
703
|
+
/** Stable session UUID — one per Agent instance unless caller overrides. */
|
|
704
|
+
sessionId;
|
|
705
|
+
/**
|
|
706
|
+
* Loop-prevention master switch. When false, the pre-closure verify
|
|
707
|
+
* guard skips the verify_change call and falls straight through to the
|
|
708
|
+
* existing closure logic — the agent behaves exactly like it did before
|
|
709
|
+
* post-edit-verify shipped. Used as an emergency off switch and a way
|
|
710
|
+
* for power users who hate any extra LLM round-trips to opt out. The
|
|
711
|
+
* `recordEditFromTool` post-tool hook still runs (it's free) so the
|
|
712
|
+
* trajectory has the data if the flag flips on mid-session.
|
|
713
|
+
*
|
|
714
|
+
* Default true — the guard catches real bugs (build-broken closures,
|
|
715
|
+
* stuck-loop credit burn) and the cost is bounded (one verify pass +
|
|
716
|
+
* at most one fresh-eyes call per turn).
|
|
717
|
+
*/
|
|
718
|
+
loopPreventionEnabled;
|
|
719
|
+
/**
|
|
720
|
+
* Recovery hook that returns the new messages accumulated during the
|
|
721
|
+
* currently-running Agent.run() call. Set at the top of run(),
|
|
722
|
+
* cleared (left as a stale closure) when run() exits — the next run
|
|
723
|
+
* overwrites it. Lets the caller reach into a cancelled run and
|
|
724
|
+
* persist Ava's partial work (completed tool calls, streamed
|
|
725
|
+
* assistant text) before the AbortError throws her out, instead of
|
|
726
|
+
* losing all of it because run() never reached its return statement.
|
|
727
|
+
*
|
|
728
|
+
* Without this, pressing Stop mid-task discarded every tool call and
|
|
729
|
+
* file edit Ava had completed — the next user message saw a
|
|
730
|
+
* conversation history with a gap where her work used to be, and
|
|
731
|
+
* Ava had no memory of what she'd done. See cancelRun() in the host.
|
|
732
|
+
*/
|
|
733
|
+
currentRunRecoveryHook = null;
|
|
734
|
+
/**
|
|
735
|
+
* Consecutive identical failures, per tool+arguments, within one run.
|
|
736
|
+
*
|
|
737
|
+
* A tool that fails the SAME WAY on the same arguments is not a retry
|
|
738
|
+
* situation — nothing has changed, so the next attempt cannot go differently.
|
|
739
|
+
* Observed in the wild: health_plan_create failed seven times running while
|
|
740
|
+
* the model cheerfully called it again each time, spending credits per turn,
|
|
741
|
+
* until the operator hit Stop. Nothing counted the repeats and nothing told
|
|
742
|
+
* anyone.
|
|
743
|
+
*
|
|
744
|
+
* Keyed on tool + arguments so a genuine retry with DIFFERENT arguments is
|
|
745
|
+
* untouched — changing the call is exactly the productive response to a
|
|
746
|
+
* failure, and this must not punish it.
|
|
747
|
+
*/
|
|
748
|
+
repeatedToolFailures = new Map();
|
|
749
|
+
/**
|
|
750
|
+
* Trajectory metadata from the previous Agent.run() in this session.
|
|
751
|
+
* Used to attach `correction_received` events to the trajectory the
|
|
752
|
+
* user is correcting, and to record whether that prior trajectory
|
|
753
|
+
* had verified before answering. Reset across agent instances.
|
|
754
|
+
*/
|
|
755
|
+
lastTrajectoryMetadata = null;
|
|
756
|
+
/**
|
|
757
|
+
* The mode of the previous Agent.run() in this session. Used to fire
|
|
758
|
+
* `mode_switch` dataset events when the user changes modes between
|
|
759
|
+
* turns (e.g. switches from Work to Plan via the [Plan Mode] prefix).
|
|
760
|
+
*/
|
|
761
|
+
lastDetectedMode = null;
|
|
762
|
+
constructor(opts) {
|
|
763
|
+
this.provider = opts.provider;
|
|
764
|
+
this.model = opts.model;
|
|
765
|
+
this.visionProvider = opts.visionProvider;
|
|
766
|
+
this.visionModel = opts.visionModel;
|
|
767
|
+
this.toolRegistry = opts.toolRegistry;
|
|
768
|
+
const sf = opts.surface ?? 'cli';
|
|
769
|
+
this.toolContext = {
|
|
770
|
+
cwd: opts.cwd,
|
|
771
|
+
sharedState: opts.sharedState,
|
|
772
|
+
secretGranter: opts.secretGranter,
|
|
773
|
+
// Map the host's AvaSurface to the docs Surface ('extension' -> 'ext') so
|
|
774
|
+
// surface-aware tools (docs_lookup) know where Ava is running.
|
|
775
|
+
surface: sf === 'extension' ? 'ext' : sf,
|
|
776
|
+
};
|
|
777
|
+
this.surface = sf;
|
|
778
|
+
this.sessionId = opts.sessionId ?? randomUUID();
|
|
779
|
+
this.loopPreventionEnabled = opts.loopPreventionEnabled ?? true;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Return the new messages accumulated by the currently-running (or
|
|
783
|
+
* just-aborted) Agent.run() call. Used by the host's cancellation
|
|
784
|
+
* path to persist Ava's partial work — completed tool calls, streamed
|
|
785
|
+
* assistant text — before the conversation history is updated with
|
|
786
|
+
* the stop marker. Returns an empty array if no run has happened.
|
|
787
|
+
*
|
|
788
|
+
* Safe to call after run() has exited normally — the hook still
|
|
789
|
+
* returns a snapshot of the run that just finished.
|
|
790
|
+
*/
|
|
791
|
+
getCurrentRunPartialMessages() {
|
|
792
|
+
return this.currentRunRecoveryHook?.() ?? [];
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Update the working directory used by all tool executions.
|
|
796
|
+
* Called when the user opens a different project folder mid-session.
|
|
797
|
+
*/
|
|
798
|
+
setCwd(cwd) {
|
|
799
|
+
this.toolContext.cwd = cwd;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Run a one-shot completion — single prompt, no tools, no streaming,
|
|
803
|
+
* timeout-bounded. Returns the assistant's trimmed text content, or
|
|
804
|
+
* null on error / timeout / empty response.
|
|
805
|
+
*
|
|
806
|
+
* Intended for utility callers that need a quick model round-trip
|
|
807
|
+
* without spinning up the full agent loop — e.g. the extension host's
|
|
808
|
+
* auto-journal reflection that writes a 2–4 sentence session summary
|
|
809
|
+
* after a completed turn. Cheap: one LLM call, no tool schemas, small
|
|
810
|
+
* max_tokens.
|
|
811
|
+
*
|
|
812
|
+
* Errors (network, provider, parse, timeout) all collapse to null so
|
|
813
|
+
* the caller can fall back gracefully without try/catch plumbing.
|
|
814
|
+
*/
|
|
815
|
+
async completeOneShot(prompt, opts) {
|
|
816
|
+
const maxTokens = opts?.maxTokens ?? 200;
|
|
817
|
+
const timeoutMs = opts?.timeoutMs ?? 10_000;
|
|
818
|
+
try {
|
|
819
|
+
const response = await Promise.race([
|
|
820
|
+
this.provider.createCompletion({
|
|
821
|
+
model: this.model.id,
|
|
822
|
+
messages: [{ role: 'user', content: prompt }],
|
|
823
|
+
max_tokens: maxTokens,
|
|
824
|
+
}),
|
|
825
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)),
|
|
826
|
+
]);
|
|
827
|
+
if (!response)
|
|
828
|
+
return null;
|
|
829
|
+
const choice = response.choices?.[0];
|
|
830
|
+
const content = choice?.message?.content;
|
|
831
|
+
if (typeof content !== 'string')
|
|
832
|
+
return null;
|
|
833
|
+
const trimmed = content.trim();
|
|
834
|
+
return trimmed || null;
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Inject a user message mid-run. The message will be appended to the
|
|
842
|
+
* conversation between the current and next agent iteration, allowing
|
|
843
|
+
* the user to steer, add context, or redirect without cancelling.
|
|
844
|
+
*/
|
|
845
|
+
inject(message) {
|
|
846
|
+
// Guard against empty or whitespace-only injections.
|
|
847
|
+
//
|
|
848
|
+
// Without this, any caller that accidentally passes an empty string
|
|
849
|
+
// (missing translation key, race condition on a programmatic send,
|
|
850
|
+
// stale callback, IPC edge case) ends up appending an empty user
|
|
851
|
+
// message into the conversation mid-run — and the model reasonably
|
|
852
|
+
// responds "did you send something?" to a blank turn. That's the
|
|
853
|
+
// "blonde moment" failure mode: not attention drift, just an empty
|
|
854
|
+
// turn being treated as a real one.
|
|
855
|
+
//
|
|
856
|
+
// Drop silently with a debug log so bugs upstream stay visible in
|
|
857
|
+
// logs but don't manifest as weird agent behaviour to the user.
|
|
858
|
+
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
859
|
+
logger.debug('[agent] inject() called with empty/invalid message — dropped');
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
this.pendingInterjections.push(message);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Request a graceful pause. Unlike the abort signal (a hard, immediate
|
|
866
|
+
* stop), this lets the current step finish and then exits the loop cleanly
|
|
867
|
+
* at the next boundary — so a typed "wait"/"pause" never rips Ava out
|
|
868
|
+
* mid-write. The conversation keeps everything completed; the user's next
|
|
869
|
+
* message continues from there.
|
|
870
|
+
*/
|
|
871
|
+
requestPause() {
|
|
872
|
+
this.pauseRequested = true;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Drop every queued interjection without processing it.
|
|
876
|
+
*
|
|
877
|
+
* Called from the extension's cancel/stop handler so user injections
|
|
878
|
+
* queued during the aborted task don't silently replay as the first
|
|
879
|
+
* message of the next run. Without this, "stop" + "new task" would
|
|
880
|
+
* carry the last typed-but-not-yet-processed interjection into the
|
|
881
|
+
* fresh turn and feel like the old task was still alive.
|
|
882
|
+
*/
|
|
883
|
+
clearPendingInterjections() {
|
|
884
|
+
if (this.pendingInterjections.length > 0) {
|
|
885
|
+
logger.debug(`[agent] clearPendingInterjections — dropped ${this.pendingInterjections.length} queued message(s)`);
|
|
886
|
+
this.pendingInterjections.length = 0;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* True when at least one interjection is queued waiting to be consumed.
|
|
891
|
+
* Used by Conductor orchestration (passed via the orchestrate options'
|
|
892
|
+
* `hasPendingInjection` callback) to break out of the blocking persona
|
|
893
|
+
* loop early when the user sends a message — without this poll, the
|
|
894
|
+
* Conductor can hold the main Agent loop for 10–60 seconds on a full
|
|
895
|
+
* team and silently drop the injection when control returns.
|
|
896
|
+
*/
|
|
897
|
+
hasPendingInterjections() {
|
|
898
|
+
return this.pendingInterjections.length > 0;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Run one agent turn against the given conversation history.
|
|
902
|
+
*
|
|
903
|
+
* Contract: `messages` is the CALLER'S current conversation history
|
|
904
|
+
* — system prompt + user turns + assistant turns + tool calls/results,
|
|
905
|
+
* ending with the user's latest message to act on. The agent uses this
|
|
906
|
+
* as read-only input to build its own working context (which may be
|
|
907
|
+
* compressed, truncated, or trimmed internally for token economy);
|
|
908
|
+
* none of those transforms leak back to the caller.
|
|
909
|
+
*
|
|
910
|
+
* Returns ONLY the new messages produced by this turn — assistant
|
|
911
|
+
* replies, tool results, mid-turn user interjections. The caller is
|
|
912
|
+
* responsible for appending them to its conversation
|
|
913
|
+
* (`conversation.appendMessages(result)`).
|
|
914
|
+
*
|
|
915
|
+
* Before the Option 2 refactor (commit trail leading up to this) the
|
|
916
|
+
* return value was the full history plus the new turn's messages, and
|
|
917
|
+
* callers did `conversation.setMessages(result)` — which meant
|
|
918
|
+
* compression's destructive transforms could leak across the
|
|
919
|
+
* conversation boundary and silently clear user scrollback. Returning
|
|
920
|
+
* only new messages makes that class of bug impossible by construction.
|
|
921
|
+
*/
|
|
922
|
+
/**
|
|
923
|
+
* Mode detection with the surface rule applied.
|
|
924
|
+
*
|
|
925
|
+
* `detectModeFromMessages` keys off a literal prefix in the USER's own
|
|
926
|
+
* message text ('[Desktop Automation Mode]'), and knows nothing about which
|
|
927
|
+
* surface it's running on. On the VS Code extension, desktop mode does not
|
|
928
|
+
* exist: Microsoft blocked us over the desktop/browser tools and required
|
|
929
|
+
* their removal to reinstate (v0.48.1). Without this, a marketplace user
|
|
930
|
+
* could type the prefix by hand and pull desktop_* into the turn — schemas,
|
|
931
|
+
* personas, mode-switch events, the lot.
|
|
932
|
+
*
|
|
933
|
+
* This is the second of two locks. The first is the extension host excluding
|
|
934
|
+
* DESKTOP_TOOL_NAMES at registerBuiltins, so the tools are never constructed
|
|
935
|
+
* there at all. Two locks, because we already broke this promise once by
|
|
936
|
+
* assuming one implicit one was enough.
|
|
937
|
+
*/
|
|
938
|
+
detectModeForSurface(messages) {
|
|
939
|
+
const mode = detectModeFromMessages(messages);
|
|
940
|
+
if (mode === 'desktop' && this.toolContext.sharedState?.clientSurface === 'extension') {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
return mode;
|
|
944
|
+
}
|
|
945
|
+
async run(messages, onEvent, signal) {
|
|
946
|
+
// Open a dataset trajectory for the entire run. Every avaEvents.emit()
|
|
947
|
+
// inside (sync or async, in this method or any helper it calls) inherits
|
|
948
|
+
// this envelope. trajectory_id is auto-generated; the consumer in
|
|
949
|
+
// packages/core/src/dataset/consumer.ts only writes if a user has
|
|
950
|
+
// explicitly opted in via ~/.ava/datasets/config.json — defaults are
|
|
951
|
+
// all-off so this scope opens but emits nothing for unconsenting users.
|
|
952
|
+
const detectedMode = (this.detectModeForSurface(messages) ?? 'work');
|
|
953
|
+
const previousMode = this.lastDetectedMode;
|
|
954
|
+
this.lastDetectedMode = detectedMode;
|
|
955
|
+
// One id for this whole turn, however many model calls it takes.
|
|
956
|
+
// Generated here rather than server-side because only the agent knows
|
|
957
|
+
// where a turn begins — to the platform route, each call is just another
|
|
958
|
+
// HTTP request.
|
|
959
|
+
this.runTurnId = randomUUID();
|
|
960
|
+
// Reset per-run tool evidence for the honesty gate (claims-auditor).
|
|
961
|
+
this.runToolEvidence = [];
|
|
962
|
+
this.runClaimFlagged = false;
|
|
963
|
+
this.lastAudit = null;
|
|
964
|
+
this.honestyVerifyAttempted = false;
|
|
965
|
+
// If we're nested inside an outer trajectory (e.g. AutoCoordinator
|
|
966
|
+
// wrapped its own run), open a child trajectory so the chain is
|
|
967
|
+
// preserved via parent_trajectory_id. Otherwise open a fresh root.
|
|
968
|
+
const openTrajectory = getTrajectory()
|
|
969
|
+
? withChildTrajectory
|
|
970
|
+
: withTrajectory;
|
|
971
|
+
return openTrajectory({
|
|
972
|
+
session_id: this.sessionId,
|
|
973
|
+
surface: this.surface,
|
|
974
|
+
mode: detectedMode,
|
|
975
|
+
model_id: this.model.id,
|
|
976
|
+
}, async () => {
|
|
977
|
+
const traj = getTrajectory();
|
|
978
|
+
let finalContent = null;
|
|
979
|
+
// ── Dataset event: mode switched between turns ──────────────
|
|
980
|
+
// Mode is detected per-run from message prefixes ([Plan Mode]
|
|
981
|
+
// etc.). When this run's mode differs from the prior one, the
|
|
982
|
+
// user explicitly switched modes — capture that as a distinct
|
|
983
|
+
// event so we can train on the conditions under which mode
|
|
984
|
+
// shifts happen.
|
|
985
|
+
if (previousMode && previousMode !== detectedMode) {
|
|
986
|
+
avaEvents.emit('mode_switch', {
|
|
987
|
+
from_mode: previousMode,
|
|
988
|
+
to_mode: detectedMode,
|
|
989
|
+
trigger: 'user_prefix',
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
// ── Dataset event: did the user just correct the prior turn? ──
|
|
993
|
+
// Fires at the START of the new trajectory, references the
|
|
994
|
+
// previous trajectory's id so training-time joins know which
|
|
995
|
+
// response was the wrong one.
|
|
996
|
+
const latestUser = this.findLatestNonMetaUserMessage(messages);
|
|
997
|
+
if (latestUser && this.lastTrajectoryMetadata) {
|
|
998
|
+
const correctionKind = categorizeCorrection(latestUser);
|
|
999
|
+
if (correctionKind) {
|
|
1000
|
+
avaEvents.emit('correction_received', {
|
|
1001
|
+
corrected_trajectory_id: this.lastTrajectoryMetadata.trajectory_id,
|
|
1002
|
+
original_verification: this.lastTrajectoryMetadata.verified,
|
|
1003
|
+
correction_signature: correctionKind,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
const result = await this.runInner(messages, onEvent, signal);
|
|
1009
|
+
// Best-effort: pull the final assistant text for the chain-complete
|
|
1010
|
+
// summary. This is shape-only (word count, not content).
|
|
1011
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
1012
|
+
if (result[i].role === 'assistant') {
|
|
1013
|
+
finalContent = getTextContent(result[i].content);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
finally {
|
|
1020
|
+
// ── Dataset event: did Ava verify before answering? ──────
|
|
1021
|
+
const verifTools = pickVerificationTools(traj.toolsSoFar);
|
|
1022
|
+
const responseWords = finalContent
|
|
1023
|
+
? finalContent.trim().split(/\s+/).filter(Boolean).length
|
|
1024
|
+
: 0;
|
|
1025
|
+
avaEvents.emit('verification_decision', {
|
|
1026
|
+
verified: verifTools.length > 0,
|
|
1027
|
+
verification_tools_used: verifTools,
|
|
1028
|
+
response_word_count: responseWords,
|
|
1029
|
+
// Question signature is mode + question-mark presence — never
|
|
1030
|
+
// raw text. The mode is already on the envelope so this is a
|
|
1031
|
+
// small additional categorisation.
|
|
1032
|
+
question_signature: latestUser && /\?/.test(latestUser) ? 'question' : 'imperative',
|
|
1033
|
+
});
|
|
1034
|
+
// ── Dataset event: did the evidence-gathering actually succeed? ──
|
|
1035
|
+
// Complements verification_decision (which only says verification
|
|
1036
|
+
// was *attempted*). This is the verifiability signal — whether the
|
|
1037
|
+
// verify tools came back ok and whether the honesty gate flagged
|
|
1038
|
+
// an unbacked claim. Shape-only counts/booleans from runToolEvidence.
|
|
1039
|
+
const verifyEvidence = this.runToolEvidence.filter((e) => VERIFICATION_TOOLS.has(e.name));
|
|
1040
|
+
avaEvents.emit('verification_evidence', {
|
|
1041
|
+
verify_tool_calls: verifyEvidence.length,
|
|
1042
|
+
verify_tool_successes: verifyEvidence.filter((e) => e.ok).length,
|
|
1043
|
+
distinct_verify_tools: new Set(verifyEvidence.map((e) => e.name)).size,
|
|
1044
|
+
verified_before_response: verifyEvidence.length > 0,
|
|
1045
|
+
claim_flagged: this.runClaimFlagged,
|
|
1046
|
+
});
|
|
1047
|
+
avaEvents.emit('tool_chain_complete', {
|
|
1048
|
+
tool_count: traj.toolsSoFar.length,
|
|
1049
|
+
total_duration_ms: Date.now() - traj.startedAt,
|
|
1050
|
+
outcome: traj.outcome ?? 'task_completed',
|
|
1051
|
+
outcome_summary: summarizeChainOutcome(finalContent),
|
|
1052
|
+
});
|
|
1053
|
+
// ── Dataset event: continuation nudge outcome ───────────────
|
|
1054
|
+
// If a stall was detected and a nudge fired during this run,
|
|
1055
|
+
// report whether the nudge actually got things back on track.
|
|
1056
|
+
// Optimistic default (recovered=true) is set at fire time and
|
|
1057
|
+
// flipped to false by the fallback-exhausted branch.
|
|
1058
|
+
if (traj.pendingStallEventId) {
|
|
1059
|
+
avaEvents.emit('continuation_nudge_fired', {
|
|
1060
|
+
stall_event_id: traj.pendingStallEventId,
|
|
1061
|
+
nudge_action: 'forcing-prompt',
|
|
1062
|
+
recovered: traj.nudgeRecovered ?? true,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
// Stash this trajectory's metadata so the NEXT run can attach
|
|
1066
|
+
// correction_received events to it if the user pushes back.
|
|
1067
|
+
this.lastTrajectoryMetadata = {
|
|
1068
|
+
trajectory_id: traj.trajectory_id,
|
|
1069
|
+
verified: verifTools.length > 0,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
async runInner(messages, onEvent, signal) {
|
|
1075
|
+
// ─── History preservation across destructive context transforms ────────
|
|
1076
|
+
//
|
|
1077
|
+
// This function treats `messages` as a working context — the array
|
|
1078
|
+
// that gets sent to the model each iteration. Several transforms in
|
|
1079
|
+
// the loop below mutate `messages` destructively: compressContext()
|
|
1080
|
+
// replaces older turns with a summary, truncateMessages() drops
|
|
1081
|
+
// messages from the start to stay under the context window. These
|
|
1082
|
+
// mutations are correct for model context (saving tokens) but
|
|
1083
|
+
// WRONG for user history — if the return value reflects the
|
|
1084
|
+
// compressed state, the caller persists a lossy transcript to disk
|
|
1085
|
+
// and the user's scrollback disappears on next load.
|
|
1086
|
+
//
|
|
1087
|
+
// Fix: track what the user's history actually is separately from
|
|
1088
|
+
// the model's working context. `realEvents` collects messages
|
|
1089
|
+
// genuinely added this turn — assistant replies, tool results,
|
|
1090
|
+
// interjections. Destructive transforms intercept first so anything
|
|
1091
|
+
// between the last snapshot point and the transform gets absorbed
|
|
1092
|
+
// into realEvents before the transform mutates messages out from
|
|
1093
|
+
// under us. `isMetaPrefix` filters synthetic user-role injections
|
|
1094
|
+
// (iteration warnings, compression continuation headers,
|
|
1095
|
+
// task-re-injection blocks) so only real events land here.
|
|
1096
|
+
//
|
|
1097
|
+
// Agent.run returns only realEvents — the caller appends them to
|
|
1098
|
+
// its canonical conversation. Compression and truncation are now
|
|
1099
|
+
// strictly internal to the working context and cannot cross the
|
|
1100
|
+
// conversation boundary.
|
|
1101
|
+
const realEvents = [];
|
|
1102
|
+
let lastSnapshotOffset = messages.length;
|
|
1103
|
+
/** Pick up new non-meta messages since the last snapshot point. */
|
|
1104
|
+
const absorbSinceLastSnapshot = () => {
|
|
1105
|
+
if (messages.length > lastSnapshotOffset) {
|
|
1106
|
+
for (let i = lastSnapshotOffset; i < messages.length; i++) {
|
|
1107
|
+
const m = messages[i];
|
|
1108
|
+
const text = getTextContent(m.content);
|
|
1109
|
+
if (typeof text === 'string' && text.length > 0 && isMetaPrefix(text))
|
|
1110
|
+
continue;
|
|
1111
|
+
realEvents.push(m);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
/**
|
|
1116
|
+
* Return ONLY the new messages produced this turn. Caller appends
|
|
1117
|
+
* to their conversation. See the Agent.run() docstring for why this
|
|
1118
|
+
* is the return shape — compression's destructive transforms cannot
|
|
1119
|
+
* leak across the conversation boundary when the agent explicitly
|
|
1120
|
+
* returns "what was new" rather than "what the full context now
|
|
1121
|
+
* looks like after compression."
|
|
1122
|
+
*/
|
|
1123
|
+
const finalHistory = () => {
|
|
1124
|
+
absorbSinceLastSnapshot();
|
|
1125
|
+
return [...realEvents];
|
|
1126
|
+
};
|
|
1127
|
+
// Expose finalHistory as the recovery hook so cancelRun() in the host
|
|
1128
|
+
// can pull whatever Ava had accumulated when the user pressed Stop.
|
|
1129
|
+
// Reassigned every run; the previous run's reference becomes stale
|
|
1130
|
+
// (closes over a dead realEvents) but is never read again.
|
|
1131
|
+
this.currentRunRecoveryHook = finalHistory;
|
|
1132
|
+
this.repeatedToolFailures.clear();
|
|
1133
|
+
// ─── Recoverability backstop for compression ───────────────────────────
|
|
1134
|
+
// Stash the uncompressed transcript as it stands at the START of this turn
|
|
1135
|
+
// so the conversation_recall tool can read it. `messages` here is the full
|
|
1136
|
+
// canonical history the caller passed in; the compression/truncation
|
|
1137
|
+
// transforms below only ever REASSIGN the local `messages` variable (they
|
|
1138
|
+
// build new arrays, never mutate in place), so a shallow copy taken now
|
|
1139
|
+
// stays lossless for the whole turn even after the working context is
|
|
1140
|
+
// compressed. This is what makes "Ava lost context" structurally
|
|
1141
|
+
// impossible: the summary is the fast path, this is the source of truth.
|
|
1142
|
+
if (this.toolContext.sharedState) {
|
|
1143
|
+
this.toolContext.sharedState.recallTranscript = [...messages];
|
|
1144
|
+
}
|
|
1145
|
+
// ─── Stop-command detection ────────────────────────────────────────────
|
|
1146
|
+
// If the user's latest message is an explicit stop command ("stop",
|
|
1147
|
+
// "halt", "leave it", "don't touch", "how dare you i said stop", etc.),
|
|
1148
|
+
// DO NOT start a new task. Acknowledge and return immediately. The
|
|
1149
|
+
// previous run was already aborted by the signal; this new turn should
|
|
1150
|
+
// not re-engage with the work the user told us to leave alone.
|
|
1151
|
+
//
|
|
1152
|
+
// This is the architectural enforcement of Rule 10. The prompt rule
|
|
1153
|
+
// tells the model to stop; this code ensures the agent loop doesn't
|
|
1154
|
+
// even give the model a chance to decide otherwise.
|
|
1155
|
+
const earlyUserMsg = this.findLatestNonMetaUserMessage(messages);
|
|
1156
|
+
if (earlyUserMsg) {
|
|
1157
|
+
// A typed stop/pause directive aimed at Ava ("stop", "leave it", "wait",
|
|
1158
|
+
// "hold on") halts here, so the model never gets a chance to re-engage
|
|
1159
|
+
// work the user told us to drop. Shared with the mid-run path via
|
|
1160
|
+
// isStopCommand so a typed halt behaves identically whenever it arrives.
|
|
1161
|
+
if (isStopCommand(earlyUserMsg)) {
|
|
1162
|
+
const stopResponse = {
|
|
1163
|
+
role: 'assistant',
|
|
1164
|
+
content: 'Stopped. Not touching anything else. Let me know when you want to continue.',
|
|
1165
|
+
};
|
|
1166
|
+
messages = [...messages, stopResponse];
|
|
1167
|
+
onEvent({ type: 'stream_start' });
|
|
1168
|
+
onEvent({ type: 'stream_delta', content: stopResponse.content });
|
|
1169
|
+
onEvent({ type: 'stream_end', message: stopResponse });
|
|
1170
|
+
onEvent({ type: 'done', finalMessage: stopResponse });
|
|
1171
|
+
return finalHistory();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
// ─── Post-stop context restriction ─────────────────────────────────────
|
|
1175
|
+
// When the user pressed Stop and has now sent a new message, the full
|
|
1176
|
+
// prior conversation (could be 150K+ tokens) is still in `messages`.
|
|
1177
|
+
// Sending all of that back to the model means it draws on the prior
|
|
1178
|
+
// task context and continues the work the user asked us to stop.
|
|
1179
|
+
//
|
|
1180
|
+
// Fix: for the first turn after a stop marker, strip everything
|
|
1181
|
+
// between the system message and the marker. Keep:
|
|
1182
|
+
// - system prompt (with marker content merged in as a directive)
|
|
1183
|
+
// - the new user message (the only non-meta user message after the marker)
|
|
1184
|
+
//
|
|
1185
|
+
// After this turn, normal accumulation resumes. Subsequent turns see
|
|
1186
|
+
// the post-stop conversation as a fresh sub-thread — no leakage from
|
|
1187
|
+
// the terminated task.
|
|
1188
|
+
//
|
|
1189
|
+
// Pairs with the intent gate (Fix E): if the post-stop message is
|
|
1190
|
+
// short/conversational, the intent gate will disable tools too.
|
|
1191
|
+
// Combined effect: user presses Stop → types something → Ava responds
|
|
1192
|
+
// on the user's actual terms, not the prior task's terms.
|
|
1193
|
+
messages = this.maybeRestrictPostStopContext(messages);
|
|
1194
|
+
// ─── Classify this task for directness discipline ─────────────────────
|
|
1195
|
+
// Find the latest non-meta user message and run the lightweight
|
|
1196
|
+
// classifier. The result sets the exploration budget for this run and
|
|
1197
|
+
// is injected into the system prompt as a directness hint so Ava knows
|
|
1198
|
+
// up front how aggressively to scope her work.
|
|
1199
|
+
//
|
|
1200
|
+
// Reset exploration budget state on each run — it's per-task, not
|
|
1201
|
+
// per-session.
|
|
1202
|
+
this.readCountBeforeFirstWrite = 0;
|
|
1203
|
+
this.hasWrittenInThisRun = false;
|
|
1204
|
+
this.explorationNudgeFired = false;
|
|
1205
|
+
// Closure fallback state — if the agent exits the main loop with an
|
|
1206
|
+
// empty final assistant message, we try once more with a forcing
|
|
1207
|
+
// "one-sentence summary" nudge. Prevents the "she didn't say anything
|
|
1208
|
+
// to close out" failure where the model terminates cleanly but leaves
|
|
1209
|
+
// the user staring at a wall of tool calls with no visible confirmation.
|
|
1210
|
+
let closureFallbackAttempted = false;
|
|
1211
|
+
// Pre-closure verify guard — bounded to MAX_CLOSURE_VERIFY cycles per run.
|
|
1212
|
+
// One cycle isn't enough: when the first verify fails and the model fixes
|
|
1213
|
+
// it, that *recovery* edit must itself be re-verified — otherwise the fix
|
|
1214
|
+
// sails through unchecked. A small cap (not a single boolean) lets the fix
|
|
1215
|
+
// be checked while still guaranteeing exit so an unfixable verify can't
|
|
1216
|
+
// trap a turn. The fresh-eyes/signature escalation still fires on repeated
|
|
1217
|
+
// same-cause failures within these cycles.
|
|
1218
|
+
const MAX_CLOSURE_VERIFY = 3;
|
|
1219
|
+
let closureVerifyCount = 0;
|
|
1220
|
+
const latestUserMessage = this.findLatestNonMetaUserMessage(messages);
|
|
1221
|
+
if (latestUserMessage) {
|
|
1222
|
+
const classification = classifyTaskComplexity(latestUserMessage);
|
|
1223
|
+
this.currentTaskComplexity = classification.complexity;
|
|
1224
|
+
logger.debug(`[agent] Task classified as ${classification.complexity} (${classification.confidence} confidence) — ${classification.reasoning}`);
|
|
1225
|
+
// Merge directness hint into the first system message. This keeps the
|
|
1226
|
+
// hint anchored to the session identity rather than floating as a
|
|
1227
|
+
// separate message that could be compressed away.
|
|
1228
|
+
const hint = formatDirectnessHint(classification);
|
|
1229
|
+
messages = this.appendToSystemMessage(messages, `\n\n${hint}`);
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
// No user task — default to moderate budget just in case.
|
|
1233
|
+
this.currentTaskComplexity = 'moderate';
|
|
1234
|
+
}
|
|
1235
|
+
// Detect mode early — needed for tool filtering downstream.
|
|
1236
|
+
const detectedMode = this.detectModeForSurface(messages);
|
|
1237
|
+
// Knowledge-pack auto-activation removed in v0.59.2. The 12 builtin
|
|
1238
|
+
// domain packs (marketing, finance, devops, etc.) added ~750-1000
|
|
1239
|
+
// tokens of static framework guidance per matched keyword, which
|
|
1240
|
+
// frontier models like Qwen 3.7 Plus / DeepSeek V4 Pro / Mistral
|
|
1241
|
+
// Large 3 already cover from training. After the chat-tier
|
|
1242
|
+
// rebalance, that silent injection started bumping ~1-credit chat
|
|
1243
|
+
// turns into the next bracket without the user asking for it.
|
|
1244
|
+
// Net: small lift on rare turns, opaque cost on every match.
|
|
1245
|
+
// Removed wholesale; the desktop-automation knowledge under the
|
|
1246
|
+
// same module survives because it's genuine model-novel content.
|
|
1247
|
+
// ─── Intent nudge (Qwen Flash classifier) ─────────────────────────────
|
|
1248
|
+
// Soft preference, not a hard gate. Classifies the user's message as
|
|
1249
|
+
// task/conversational/ambiguous and injects a brief guidance nudge
|
|
1250
|
+
// into the system prompt. Tools remain available in all cases — the
|
|
1251
|
+
// nudge shapes the default response style, but the model retains
|
|
1252
|
+
// judgment to call tools when the request clearly warrants action.
|
|
1253
|
+
//
|
|
1254
|
+
// Why soft instead of hard: hard blocks fail catastrophically when
|
|
1255
|
+
// the classifier is wrong (model can't use tools on a real task,
|
|
1256
|
+
// users see "Tools are disabled this turn" leaking into output).
|
|
1257
|
+
// Soft nudges fail gracefully — false positives waste a few tokens,
|
|
1258
|
+
// false negatives still let the model do the right thing.
|
|
1259
|
+
//
|
|
1260
|
+
// The nudge wording is intentionally generic and non-recitable so
|
|
1261
|
+
// the model won't quote it back to users.
|
|
1262
|
+
let userIntent = 'task';
|
|
1263
|
+
const intentClassifier = this.toolContext.sharedState?.intentClassifier;
|
|
1264
|
+
if (intentClassifier && latestUserMessage) {
|
|
1265
|
+
try {
|
|
1266
|
+
userIntent = await intentClassifier.classify(latestUserMessage);
|
|
1267
|
+
logger.info(`[agent] Intent classified as '${userIntent}' for message: "${latestUserMessage.slice(0, 80)}"`);
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
userIntent = 'task';
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const useNativeTools = this.model.supportsToolCalls !== false;
|
|
1274
|
+
const allSchemas = this.toolRegistry.getSchemas();
|
|
1275
|
+
// Mode-aware filtering: restrict tool schemas to only those allowed in
|
|
1276
|
+
// the active mode. When the user message lacks an explicit mode prefix
|
|
1277
|
+
// (default work / code path on both surfaces), the per-mode allowlist
|
|
1278
|
+
// can't apply — but we still need to keep desktop-only tools out of
|
|
1279
|
+
// non-desktop turns, otherwise the model can hallucinate desktop_*
|
|
1280
|
+
// calls in code mode and the gate noise leaks into a coding turn.
|
|
1281
|
+
const modeAllowed = detectedMode ? MODE_ALLOWED_TOOLS[detectedMode] : null;
|
|
1282
|
+
let filteredSchemas;
|
|
1283
|
+
if (modeAllowed) {
|
|
1284
|
+
filteredSchemas = allSchemas.filter(s => modeAllowed.has(s.function.name) || ALWAYS_ALLOWED_TOOLS.has(s.function.name));
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
// No prefix detected. Filter only the desktop-only tools so the
|
|
1288
|
+
// surgical leak closes without tightening any other tool the user
|
|
1289
|
+
// may have been calling in unprefixed turns (journal_write, weather,
|
|
1290
|
+
// etc. — those stay available exactly as before).
|
|
1291
|
+
filteredSchemas = allSchemas.filter(s => !DESKTOP_ONLY_TOOLS.has(s.function.name));
|
|
1292
|
+
}
|
|
1293
|
+
// Tools always available when the model supports them. Intent shapes
|
|
1294
|
+
// the response style via the nudge below, not via schema removal.
|
|
1295
|
+
const toolSchemas = useNativeTools ? filteredSchemas : [];
|
|
1296
|
+
if (userIntent === 'conversational') {
|
|
1297
|
+
// Brief, generic guidance. Not framed as a command so the model
|
|
1298
|
+
// internalises it rather than quoting it back to the user.
|
|
1299
|
+
messages = this.appendToSystemMessage(messages, `\n\nStyle note: this turn's message reads conversational. Lead with a worded reply. Reach for tools only if the request clearly requires concrete action on files, commands, or the project.`);
|
|
1300
|
+
}
|
|
1301
|
+
else if (userIntent === 'ambiguous') {
|
|
1302
|
+
messages = this.appendToSystemMessage(messages, `\n\nStyle note: this turn's message is ambiguous in intent. If you are not sure what action is wanted, ask a short clarifying question before acting. Tools remain available if action is clearly warranted.`);
|
|
1303
|
+
}
|
|
1304
|
+
logger.info(`[agent] Starting run: model=${this.model.id} supportsToolCalls=${useNativeTools} toolSchemas=${toolSchemas.length} intent=${userIntent}${detectedMode ? ` mode=${detectedMode}` : ''}`);
|
|
1305
|
+
// For models without native tool_calls, inject tool descriptions into the system prompt
|
|
1306
|
+
if (!useNativeTools && filteredSchemas.length > 0) {
|
|
1307
|
+
const toolPrompt = buildToolPrompt(filteredSchemas);
|
|
1308
|
+
const firstMsg = messages[0];
|
|
1309
|
+
if (firstMsg?.role === 'system') {
|
|
1310
|
+
messages = [
|
|
1311
|
+
{ ...firstMsg, content: firstMsg.content + '\n\n' + toolPrompt },
|
|
1312
|
+
...messages.slice(1),
|
|
1313
|
+
];
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
messages = [
|
|
1317
|
+
{ role: 'system', content: toolPrompt },
|
|
1318
|
+
...messages,
|
|
1319
|
+
];
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
// Pass signal to tool execution context so tools (esp. bash) can be cancelled
|
|
1323
|
+
const runContext = { ...this.toolContext, signal };
|
|
1324
|
+
let iterations = 0;
|
|
1325
|
+
this.pauseRequested = false; // clear any stale pause from a prior run
|
|
1326
|
+
this._inThinkTag = false;
|
|
1327
|
+
let warningInjected = false;
|
|
1328
|
+
let lastToolName = null;
|
|
1329
|
+
let repeatCount = 0;
|
|
1330
|
+
const MAX_SAME_TOOL_REPEATS = 3;
|
|
1331
|
+
while (iterations < MAX_TOOL_CALL_ITERATIONS) {
|
|
1332
|
+
iterations++;
|
|
1333
|
+
logger.debug(`[agent] ── Iteration ${iterations}/${MAX_TOOL_CALL_ITERATIONS} ── messages=${messages.length}`);
|
|
1334
|
+
// ── Sliding Window — compress old messages when context is genuinely full ─
|
|
1335
|
+
//
|
|
1336
|
+
// Previously this fired whenever non-system message count exceeded
|
|
1337
|
+
// 30, regardless of token usage. A single task with tool-use can
|
|
1338
|
+
// produce 30+ messages in 3-5 user turns (each turn = user message +
|
|
1339
|
+
// assistant messages + tool results). That made compression fire at
|
|
1340
|
+
// ~2% token usage, destabilising the conversation every few turns
|
|
1341
|
+
// and causing Ava to lose context mid-task.
|
|
1342
|
+
//
|
|
1343
|
+
// New rule: the window compresses only when BOTH conditions are
|
|
1344
|
+
// true — message count is very high AND estimated tokens cross a
|
|
1345
|
+
// meaningful threshold. Pure message count is no longer a trigger.
|
|
1346
|
+
// Token-based thresholds compress when there's a real reason to,
|
|
1347
|
+
// not on a schedule. The absolute token check below (at 70% of
|
|
1348
|
+
// context) is the primary gate; this one is a secondary safety net
|
|
1349
|
+
// for pathological cases with tons of tiny messages.
|
|
1350
|
+
const WINDOW_MAX = 120; // Only extreme message counts hit this path
|
|
1351
|
+
const WINDOW_KEEP = 24; // Keep more recent context when it does
|
|
1352
|
+
const nonSystem = messages.filter(m => m.role !== 'system');
|
|
1353
|
+
const estimatedTokensForWindow = this.estimateTokenCount(messages);
|
|
1354
|
+
const windowTokenFloor = Math.floor(this.model.contextWindow * 0.5);
|
|
1355
|
+
if (nonSystem.length > WINDOW_MAX && estimatedTokensForWindow > windowTokenFloor) {
|
|
1356
|
+
const systemMsgs = messages.filter(m => m.role === 'system');
|
|
1357
|
+
const pinnedIdxFull = findOriginalUserTaskIndex(messages);
|
|
1358
|
+
const pinnedMsg = pinnedIdxFull !== -1 ? messages[pinnedIdxFull] : null;
|
|
1359
|
+
const toCompress = nonSystem.slice(0, nonSystem.length - WINDOW_KEEP);
|
|
1360
|
+
const toKeep = nonSystem.slice(nonSystem.length - WINDOW_KEEP);
|
|
1361
|
+
// Reference-equality check — cast to Message[] because toKeep's type
|
|
1362
|
+
// is narrowed by the system filter and doesn't accept Message directly.
|
|
1363
|
+
const pinnedInKeep = pinnedMsg ? toKeep.indexOf(pinnedMsg) !== -1 : false;
|
|
1364
|
+
// Memory policy: conversation content does NOT get saved to memory
|
|
1365
|
+
// here. Compression is a working-state operation — the summary
|
|
1366
|
+
// belongs in the conversation history (persisted per-conversation
|
|
1367
|
+
// in ~/.ava/history/*.json), not in user or project memory.
|
|
1368
|
+
//
|
|
1369
|
+
// Previously this block called autoExtractAndSave on the compressed
|
|
1370
|
+
// messages AND saved the raw summary as a "[Session context]"
|
|
1371
|
+
// project memory entry. Both were category errors: memory should
|
|
1372
|
+
// be distilled, durable facts about the user or project — not
|
|
1373
|
+
// conversation transcripts that get re-injected on later turns and
|
|
1374
|
+
// create a self-referential feedback loop.
|
|
1375
|
+
//
|
|
1376
|
+
// If something in the compressed context was worth remembering,
|
|
1377
|
+
// the model already had the chance to call memory_save during the
|
|
1378
|
+
// turn that produced it. Ambient extraction from compressed logs
|
|
1379
|
+
// is not the mechanism for durable memory.
|
|
1380
|
+
// Rebuild messages: system (with compression note merged in) + pinned
|
|
1381
|
+
// original task (if not already in the kept window) + recent messages.
|
|
1382
|
+
// Merging the compression note into the first system message avoids
|
|
1383
|
+
// Qwen's "system must be at beginning" error.
|
|
1384
|
+
const fixedKeep = this.fixToolPairing(toKeep);
|
|
1385
|
+
const compressionNote = [
|
|
1386
|
+
`[${toCompress.length} earlier messages compressed out of your working context. Your active task is still in flight — continue from where you left off. Do NOT treat this as a new conversation.]`,
|
|
1387
|
+
'The full transcript of those messages is still on record.',
|
|
1388
|
+
'If the user references something from earlier — or you need an exact detail, decision, path or value — call conversation_recall to read it from the real transcript instead of guessing.',
|
|
1389
|
+
'Do NOT say you don\'t have context — recall it first. Do NOT greet the user.',
|
|
1390
|
+
].join(' ');
|
|
1391
|
+
// Session tasks re-injection — same pattern as compressContext()
|
|
1392
|
+
let slidingTaskBlock = null;
|
|
1393
|
+
try {
|
|
1394
|
+
const tm = this.toolContext.sharedState?.taskManager;
|
|
1395
|
+
if (tm && typeof tm.getSessionTasks === 'function') {
|
|
1396
|
+
const block = formatSessionTasksBlock(tm.getSessionTasks());
|
|
1397
|
+
if (block)
|
|
1398
|
+
slidingTaskBlock = { role: 'user', content: block };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
catch { /* non-critical */ }
|
|
1402
|
+
// Pinned original task: previously this re-injected the original
|
|
1403
|
+
// user message VERBATIM as a user-role message. The model saw
|
|
1404
|
+
// what looked like a freshly-sent user turn and responded to it
|
|
1405
|
+
// as if it were new input — the classic "acts on the initial
|
|
1406
|
+
// message again" bug after compression. Fix: fold the original
|
|
1407
|
+
// task text into the system prompt's compression note as a
|
|
1408
|
+
// reference ("the user's original ask was X"), never as a
|
|
1409
|
+
// replayed user turn. The model knows the task context without
|
|
1410
|
+
// interpreting the replay as a new request.
|
|
1411
|
+
let pinnedNote = '';
|
|
1412
|
+
if (pinnedMsg && !pinnedInKeep) {
|
|
1413
|
+
const pinnedText = getTextContent(pinnedMsg.content);
|
|
1414
|
+
if (pinnedText) {
|
|
1415
|
+
pinnedNote = `\n\n[Original request at session start] "${pinnedText.slice(0, 800)}" — this is context for what the user initially asked. You were already in the middle of working on this; continue from where you left off. Do NOT treat this as a new request.`;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const tail = slidingTaskBlock ? [slidingTaskBlock, ...fixedKeep] : fixedKeep;
|
|
1419
|
+
const mergedNote = compressionNote + pinnedNote;
|
|
1420
|
+
if (systemMsgs.length > 0) {
|
|
1421
|
+
const primary = systemMsgs[0];
|
|
1422
|
+
const mergedSystem = { ...primary, content: (typeof primary.content === 'string' ? primary.content : '') + '\n\n' + mergedNote };
|
|
1423
|
+
messages = [mergedSystem, ...tail];
|
|
1424
|
+
}
|
|
1425
|
+
else {
|
|
1426
|
+
messages = [
|
|
1427
|
+
{ role: 'system', content: mergedNote },
|
|
1428
|
+
...tail,
|
|
1429
|
+
];
|
|
1430
|
+
}
|
|
1431
|
+
// Notify UI about compression (uses 'info' event type)
|
|
1432
|
+
logger.debug(`[agent] Sliding window: compressed ${toCompress.length} messages, kept ${fixedKeep.length}`);
|
|
1433
|
+
}
|
|
1434
|
+
// Check for cancellation before each iteration
|
|
1435
|
+
if (signal?.aborted) {
|
|
1436
|
+
onEvent({ type: 'done', finalMessage: { role: 'assistant', content: null } });
|
|
1437
|
+
return finalHistory();
|
|
1438
|
+
}
|
|
1439
|
+
// Check for user interjections — messages the user sent mid-run.
|
|
1440
|
+
// Frame them neutrally: enough signal that this arrived WHILE Ava was
|
|
1441
|
+
// working (so she folds it into the current task instead of treating it
|
|
1442
|
+
// as a brand-new request) without the corrective tone of the old
|
|
1443
|
+
// "[User interjection]:" prefix, which primed her to read questions as
|
|
1444
|
+
// criticism and apologise instead of answering. The UI event still
|
|
1445
|
+
// carries the raw text — the frame is for the model only.
|
|
1446
|
+
while (this.pendingInterjections.length > 0) {
|
|
1447
|
+
const interjection = this.pendingInterjections.shift();
|
|
1448
|
+
messages = [
|
|
1449
|
+
...messages,
|
|
1450
|
+
{
|
|
1451
|
+
role: 'user',
|
|
1452
|
+
content: `[The user added this while you were working — take it into account and carry on]: ${interjection}`,
|
|
1453
|
+
},
|
|
1454
|
+
];
|
|
1455
|
+
onEvent({ type: 'interjection', content: interjection });
|
|
1456
|
+
}
|
|
1457
|
+
// Graceful pause — the user typed "wait"/"pause" mid-run. We're at a
|
|
1458
|
+
// clean step boundary (the previous step's tools have finished), so end
|
|
1459
|
+
// the turn here rather than aborting mid-step. Everything completed
|
|
1460
|
+
// stays in the conversation; the user's next message continues from
|
|
1461
|
+
// here. Distinct from the hard stop (abort signal), which is immediate.
|
|
1462
|
+
if (this.pauseRequested) {
|
|
1463
|
+
this.pauseRequested = false;
|
|
1464
|
+
logger.info('[agent] Graceful pause requested — halting at step boundary');
|
|
1465
|
+
onEvent({ type: 'done', finalMessage: { role: 'assistant', content: null } });
|
|
1466
|
+
return finalHistory();
|
|
1467
|
+
}
|
|
1468
|
+
iterations++;
|
|
1469
|
+
// Warn the model when approaching the iteration limit
|
|
1470
|
+
// Injected as a user-role message to avoid Qwen's "system must be at beginning" error
|
|
1471
|
+
const remaining = MAX_TOOL_CALL_ITERATIONS - iterations;
|
|
1472
|
+
if (!warningInjected && remaining <= ITERATION_WARNING_THRESHOLD) {
|
|
1473
|
+
warningInjected = true;
|
|
1474
|
+
messages = [
|
|
1475
|
+
...messages,
|
|
1476
|
+
{
|
|
1477
|
+
role: 'user',
|
|
1478
|
+
content: `[System notice]: ${t('error.msg.iteration_warning', { remaining: String(remaining) })}`,
|
|
1479
|
+
},
|
|
1480
|
+
];
|
|
1481
|
+
}
|
|
1482
|
+
// Trim old tool results to save tokens — but ONLY when context is
|
|
1483
|
+
// genuinely filling up. Trimming on raw message count (regardless of how
|
|
1484
|
+
// much window is free) crushed files the model still needed on multi-file
|
|
1485
|
+
// tasks: it would lose a file read a few turns ago, re-read it, lose
|
|
1486
|
+
// another, and loop forever without ever reaching an edit. Gate on the
|
|
1487
|
+
// token budget so we keep full tool results while there's plenty of room
|
|
1488
|
+
// (the common case) and only collapse them once we cross half the window.
|
|
1489
|
+
const trimThreshold = Math.floor(this.model.contextWindow * 0.5);
|
|
1490
|
+
if (this.estimateTokenCount(messages) > trimThreshold) {
|
|
1491
|
+
messages = this.trimOldToolResults(messages);
|
|
1492
|
+
}
|
|
1493
|
+
// Auto-compress at 70% of the model's context window, capped at
|
|
1494
|
+
// 400K tokens as an absolute ceiling.
|
|
1495
|
+
//
|
|
1496
|
+
// The 70% ratio is preserved for small-to-mid-sized context models
|
|
1497
|
+
// (128K → ~90K trigger, 256K → ~180K trigger) because the author
|
|
1498
|
+
// deliberately chose it to avoid compression thrash: each
|
|
1499
|
+
// compression pass is an LLM call that can destabilise a session
|
|
1500
|
+
// ("acts on the initial message again" regression). Firing too
|
|
1501
|
+
// eagerly is worse than firing late.
|
|
1502
|
+
//
|
|
1503
|
+
// The 400K ceiling fixes the 1M-context degenerate case — at 70%
|
|
1504
|
+
// of 1M every turn would send up to 700K tokens before anything
|
|
1505
|
+
// got summarised. On managed Qwen Plus ($0.20 / $1.20 per 1M),
|
|
1506
|
+
// that's ~$0.14 per turn of raw input cost, and 700K of context
|
|
1507
|
+
// slows every response substantially. Capping at 400K means one
|
|
1508
|
+
// earlier compression pass vs. carrying an extra 300K per turn
|
|
1509
|
+
// for 20+ turns.
|
|
1510
|
+
//
|
|
1511
|
+
// Math: trigger = min(contextWindow × 0.7, 400_000)
|
|
1512
|
+
const maxInputTokens = Math.min(Math.floor(this.model.contextWindow * 0.7), 400_000);
|
|
1513
|
+
const estimatedTotal = this.estimateTokenCount(messages);
|
|
1514
|
+
// Emit context usage so UIs can show a progress bar
|
|
1515
|
+
const contextPercent = Math.round((estimatedTotal / this.model.contextWindow) * 100);
|
|
1516
|
+
onEvent({
|
|
1517
|
+
type: 'context_usage',
|
|
1518
|
+
context: { used: estimatedTotal, limit: this.model.contextWindow, percent: contextPercent },
|
|
1519
|
+
});
|
|
1520
|
+
// Auto-compress only when we've crossed the 70% threshold AND the
|
|
1521
|
+
// conversation is long enough that compression has something to
|
|
1522
|
+
// work with (< 6 messages means there's nothing meaningful to
|
|
1523
|
+
// summarise — just skip).
|
|
1524
|
+
if (estimatedTotal > maxInputTokens && messages.length >= 6) {
|
|
1525
|
+
// Destructive transform — absorb any real events added since the
|
|
1526
|
+
// last snapshot point BEFORE the transform mutates messages, then
|
|
1527
|
+
// reset the offset so further additions are tracked from the new
|
|
1528
|
+
// post-compression length.
|
|
1529
|
+
absorbSinceLastSnapshot();
|
|
1530
|
+
const msgsBeforeCompress = messages.length;
|
|
1531
|
+
messages = await this.compressContext(messages, onEvent, signal);
|
|
1532
|
+
lastSnapshotOffset = messages.length;
|
|
1533
|
+
// ── Dataset event: context compression fired ────────────────────
|
|
1534
|
+
avaEvents.emit('context_compression', {
|
|
1535
|
+
operation: 'compress',
|
|
1536
|
+
messages_before: msgsBeforeCompress,
|
|
1537
|
+
messages_after: messages.length,
|
|
1538
|
+
tokens_before: estimatedTotal,
|
|
1539
|
+
token_budget: maxInputTokens,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
// Still over budget? Fall back to truncation.
|
|
1543
|
+
//
|
|
1544
|
+
// Previously this emitted a user-facing error telling them to
|
|
1545
|
+
// "Consider starting a new chat for best results" — which was both
|
|
1546
|
+
// misleading (compression is routine, not an error) and risky (if
|
|
1547
|
+
// the agent ever saw that wording in its own context, it could
|
|
1548
|
+
// interpret "start a new chat" as instruction and reset its
|
|
1549
|
+
// behaviour, which is exactly the "she acted like it was a new
|
|
1550
|
+
// chat" failure mode we're fixing).
|
|
1551
|
+
//
|
|
1552
|
+
// Now it emits a neutral info message that doesn't prompt the user
|
|
1553
|
+
// or the agent to abandon the session. The agent's active task
|
|
1554
|
+
// state is preserved via the pinned original user task, the
|
|
1555
|
+
// re-injected session tasks block, and the continuation-first
|
|
1556
|
+
// compression header elsewhere in this file.
|
|
1557
|
+
const preCount = messages.length;
|
|
1558
|
+
// Truncation is also destructive — same snapshot/reset pattern.
|
|
1559
|
+
absorbSinceLastSnapshot();
|
|
1560
|
+
messages = this.truncateMessages(messages, maxInputTokens);
|
|
1561
|
+
lastSnapshotOffset = messages.length;
|
|
1562
|
+
const dropped = preCount - messages.length;
|
|
1563
|
+
if (dropped > 0) {
|
|
1564
|
+
// ── Dataset event: fell back to truncation ──────────────────────
|
|
1565
|
+
avaEvents.emit('context_compression', {
|
|
1566
|
+
operation: 'truncate',
|
|
1567
|
+
messages_before: preCount,
|
|
1568
|
+
messages_after: messages.length,
|
|
1569
|
+
tokens_before: estimatedTotal,
|
|
1570
|
+
token_budget: maxInputTokens,
|
|
1571
|
+
});
|
|
1572
|
+
onEvent({
|
|
1573
|
+
type: 'error',
|
|
1574
|
+
error: Object.assign(new Error(`Context compressed: ${dropped} older messages summarised to memory. Continuing your current task.`), { code: 'context_compressed' }),
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
// ── Sanitize messages for model compatibility ──────────────────────────
|
|
1578
|
+
const filteredMessages = !useNativeTools
|
|
1579
|
+
? messages.filter((m) => m.role !== 'tool') // Drop any stray tool messages in text mode
|
|
1580
|
+
: messages;
|
|
1581
|
+
// Vision bridge — when the coordinator can't see images but a vision
|
|
1582
|
+
// provider is configured (e.g. Supernova/DeepSeek with Qwen Omni), describe
|
|
1583
|
+
// every image FIRST (async, cached per image) so the model gets the image
|
|
1584
|
+
// as text instead of a "switch model" nag. This is what lets DeepSeek "see".
|
|
1585
|
+
// No-op for vision-capable models. See agent/vision-bridge.ts.
|
|
1586
|
+
const bridgedMessages = await bridgeImagesForTextModel(filteredMessages, this.model, this.visionProvider, this.visionModel, this.visionDescriptionCache);
|
|
1587
|
+
let sanitizedMessages = bridgedMessages.map((m) => {
|
|
1588
|
+
let msg = m;
|
|
1589
|
+
// Strip empty tool_calls arrays from assistant messages. Qwen
|
|
1590
|
+
// rejects `tool_calls: []` with a 400 error — the field must be
|
|
1591
|
+
// either omitted or non-empty. Upstream mutations (mode blocking,
|
|
1592
|
+
// budget enforcement, text-parser fallbacks) can leave an empty
|
|
1593
|
+
// array on the message; this is the architectural guard at the
|
|
1594
|
+
// API boundary so any future code path that reintroduces the bug
|
|
1595
|
+
// gets caught here before it reaches the provider.
|
|
1596
|
+
if (msg.role === 'assistant') {
|
|
1597
|
+
const asst = msg;
|
|
1598
|
+
if (Array.isArray(asst.tool_calls) && asst.tool_calls.length === 0) {
|
|
1599
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1600
|
+
const { tool_calls: _empty, ...rest } = asst;
|
|
1601
|
+
msg = rest;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
// Text-based tool mode: strip tool_calls from assistant messages
|
|
1605
|
+
// The model doesn't understand these fields — they're our internal bookkeeping
|
|
1606
|
+
if (!useNativeTools && msg.role === 'assistant' && msg.tool_calls) {
|
|
1607
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1608
|
+
const { tool_calls: _tc, ...rest } = msg;
|
|
1609
|
+
msg = rest;
|
|
1610
|
+
}
|
|
1611
|
+
// Handle reasoning_content based on model capability:
|
|
1612
|
+
// - Thinking models (DeepSeek Reasoner, etc.): KEEP — required for multi-turn
|
|
1613
|
+
// - Non-thinking models: STRIP — providers reject it as input
|
|
1614
|
+
if (msg.role === 'assistant' && 'reasoning_content' in msg) {
|
|
1615
|
+
const aMsg = msg;
|
|
1616
|
+
if (this.model.supportsThinking) {
|
|
1617
|
+
if (aMsg.reasoning_content && !aMsg.content) {
|
|
1618
|
+
return { ...aMsg, content: '' };
|
|
1619
|
+
}
|
|
1620
|
+
return msg;
|
|
1621
|
+
}
|
|
1622
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1623
|
+
const { reasoning_content: _rc, ...rest } = aMsg;
|
|
1624
|
+
return rest;
|
|
1625
|
+
}
|
|
1626
|
+
return msg;
|
|
1627
|
+
});
|
|
1628
|
+
// Ensure all messages have string content and strip ANSI escape codes
|
|
1629
|
+
// Qwen rejects content: null and ANSI codes with 400 Bad Request
|
|
1630
|
+
sanitizedMessages = sanitizedMessages.map(m => {
|
|
1631
|
+
if (m.content === null || m.content === undefined) {
|
|
1632
|
+
return { ...m, content: '' };
|
|
1633
|
+
}
|
|
1634
|
+
if (typeof m.content === 'string') {
|
|
1635
|
+
// Strip all ANSI escape sequences and control characters that APIs reject.
|
|
1636
|
+
// no-control-regex is off for this block on purpose: matching control
|
|
1637
|
+
// characters IS the job here, and removing them from the pattern would
|
|
1638
|
+
// stop the sanitiser doing anything.
|
|
1639
|
+
/* eslint-disable no-control-regex */
|
|
1640
|
+
const cleaned = m.content
|
|
1641
|
+
.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // Standard ANSI escape codes
|
|
1642
|
+
.replace(/\u001b\][^\u0007]*\u0007/g, '') // OSC sequences
|
|
1643
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, ''); // Control chars (keep \n \r \t)
|
|
1644
|
+
/* eslint-enable no-control-regex */
|
|
1645
|
+
if (cleaned !== m.content)
|
|
1646
|
+
return { ...m, content: cleaned };
|
|
1647
|
+
}
|
|
1648
|
+
return m;
|
|
1649
|
+
});
|
|
1650
|
+
// Age out old content before sending — biggest token lever in the
|
|
1651
|
+
// agent loop. Without this, every screenshot and every verbose tool
|
|
1652
|
+
// result stays in full fidelity for the rest of the session, costing
|
|
1653
|
+
// 20-50K tokens per image × turns remaining and 1-5K tokens per
|
|
1654
|
+
// stale tool result × turns remaining. The model's prior reasoning
|
|
1655
|
+
// about these is preserved in the assistant messages; the raw
|
|
1656
|
+
// payload almost never adds value after 2-3 turns.
|
|
1657
|
+
sanitizedMessages = this.ageHistoryContent(sanitizedMessages);
|
|
1658
|
+
// Fix orphaned tool messages before sending — prevents 400 errors
|
|
1659
|
+
sanitizedMessages = this.fixToolPairing(sanitizedMessages);
|
|
1660
|
+
// Guard against 413: check estimated body size and truncate if too large
|
|
1661
|
+
// Most APIs reject bodies over 4MB. Target 3MB to leave headroom.
|
|
1662
|
+
const MAX_BODY_BYTES = 3 * 1024 * 1024;
|
|
1663
|
+
let finalMessages = sanitizedMessages;
|
|
1664
|
+
const estimatedSize = JSON.stringify(sanitizedMessages).length;
|
|
1665
|
+
if (estimatedSize > MAX_BODY_BYTES) {
|
|
1666
|
+
logger.warn(`[agent] Request body too large (${(estimatedSize / 1024 / 1024).toFixed(1)}MB). Truncating tool results and old messages.`);
|
|
1667
|
+
// First pass: truncate large tool results (keep first 500 chars)
|
|
1668
|
+
finalMessages = finalMessages.map(m => {
|
|
1669
|
+
if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 500) {
|
|
1670
|
+
return { ...m, content: m.content.slice(0, 500) + '\n\n[Output truncated — original was ' + m.content.length + ' chars]' };
|
|
1671
|
+
}
|
|
1672
|
+
return m;
|
|
1673
|
+
});
|
|
1674
|
+
// Second pass: if still too large, drop oldest messages (keep system + last 20)
|
|
1675
|
+
if (JSON.stringify(finalMessages).length > MAX_BODY_BYTES) {
|
|
1676
|
+
const systemMsg = finalMessages.find(m => m.role === 'system');
|
|
1677
|
+
const nonSystem = finalMessages.filter(m => m.role !== 'system');
|
|
1678
|
+
const dropped = nonSystem.slice(0, -20);
|
|
1679
|
+
const kept = nonSystem.slice(-20);
|
|
1680
|
+
// Fix orphaned tool messages after truncation
|
|
1681
|
+
const fixedKept = this.fixToolPairing(kept);
|
|
1682
|
+
finalMessages = systemMsg ? [systemMsg, ...fixedKept] : fixedKept;
|
|
1683
|
+
logger.warn(`[agent] Aggressive truncation: kept system + last ${fixedKept.length} messages, dropped ${dropped.length}`);
|
|
1684
|
+
// Memory policy: dropped conversation context does NOT get saved
|
|
1685
|
+
// to memory. It lives in the conversation history file on disk
|
|
1686
|
+
// (per-conversation, persistent) and that's where it belongs.
|
|
1687
|
+
// Previously this saved a concatenated transcript of dropped
|
|
1688
|
+
// messages to 'global' memory under a 'session' category —
|
|
1689
|
+
// exactly the conversation-into-memory leak we are cutting.
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const request = {
|
|
1693
|
+
model: this.model.id,
|
|
1694
|
+
messages: finalMessages,
|
|
1695
|
+
tools: toolSchemas.length > 0 ? toolSchemas : undefined,
|
|
1696
|
+
tool_choice: toolSchemas.length > 0 ? 'auto' : undefined,
|
|
1697
|
+
stream: true,
|
|
1698
|
+
// Every iteration of this loop is another call serving the SAME user
|
|
1699
|
+
// turn. Tagging them all with one id is what lets a turn be costed as
|
|
1700
|
+
// an outcome rather than as N unrelated calls.
|
|
1701
|
+
turnId: this.runTurnId,
|
|
1702
|
+
};
|
|
1703
|
+
let assistantMessage;
|
|
1704
|
+
let promptTokens;
|
|
1705
|
+
let streamInterrupted;
|
|
1706
|
+
const estimatedInput = this.estimateTokenCount(messages);
|
|
1707
|
+
logger.debug(`[agent] Calling streamResponse (est. ${estimatedInput} input tokens, model context: ${this.model.contextWindow})`);
|
|
1708
|
+
try {
|
|
1709
|
+
const streamResult = await this.streamResponse(request, onEvent, signal);
|
|
1710
|
+
assistantMessage = streamResult.message;
|
|
1711
|
+
promptTokens = streamResult.promptTokens;
|
|
1712
|
+
streamInterrupted = streamResult.interrupted === true;
|
|
1713
|
+
logger.debug(`[agent] streamResponse returned: content=${assistantMessage.content?.length ?? 0} chars, tool_calls=${assistantMessage.tool_calls?.length ?? 0}, promptTokens=${promptTokens}${streamInterrupted ? ' (INTERRUPTED by injection)' : ''}`);
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
logger.error(`[agent] streamResponse THREW: ${error instanceof Error ? error.message : String(error)}`);
|
|
1717
|
+
// Surface the error through the event system so CLI/extension handle it consistently
|
|
1718
|
+
onEvent({ type: 'error', error: error instanceof Error ? error : new Error(String(error)) });
|
|
1719
|
+
// Always emit done so UI clears isStreaming/isThinking
|
|
1720
|
+
onEvent({ type: 'done', finalMessage: { role: 'assistant', content: '' } });
|
|
1721
|
+
return finalHistory();
|
|
1722
|
+
}
|
|
1723
|
+
// Mid-stream injection happened. We aborted the provider request
|
|
1724
|
+
// before tool_calls could start. Preserve any partial text in the
|
|
1725
|
+
// transcript (keeps UI/history honest — user saw it) but skip both
|
|
1726
|
+
// the text-based tool parser (partial <tool_call> blocks would
|
|
1727
|
+
// mis-parse) and the full tool-execution path. The outer loop will
|
|
1728
|
+
// drain pendingInterjections at the top of the next iteration and
|
|
1729
|
+
// send a fresh request to the model.
|
|
1730
|
+
if (streamInterrupted) {
|
|
1731
|
+
const hasText = typeof assistantMessage.content === 'string' && assistantMessage.content.trim().length > 0;
|
|
1732
|
+
if (hasText) {
|
|
1733
|
+
messages = [...messages, {
|
|
1734
|
+
role: 'assistant',
|
|
1735
|
+
content: assistantMessage.content,
|
|
1736
|
+
...(assistantMessage.reasoning_content ? { reasoning_content: assistantMessage.reasoning_content } : {}),
|
|
1737
|
+
}];
|
|
1738
|
+
}
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
// Text-based tool parsing: extract <tool_call> blocks from the model's text
|
|
1742
|
+
if (!useNativeTools && assistantMessage.content) {
|
|
1743
|
+
const { toolCalls: parsedCalls, cleanText } = parseToolCalls(assistantMessage.content);
|
|
1744
|
+
if (parsedCalls.length > 0) {
|
|
1745
|
+
logger.debug(`[agent] Parsed ${parsedCalls.length} tool calls from text output`);
|
|
1746
|
+
assistantMessage = {
|
|
1747
|
+
...assistantMessage,
|
|
1748
|
+
content: cleanText || null,
|
|
1749
|
+
tool_calls: parsedCalls,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
messages = [...messages, assistantMessage];
|
|
1754
|
+
// NOTE: Do NOT truncate here — tool results haven't been appended yet.
|
|
1755
|
+
// Truncation between assistant tool_calls and tool results breaks the
|
|
1756
|
+
// message ordering that models require. Truncation happens after tool
|
|
1757
|
+
// results are appended, at the top of the next loop iteration.
|
|
1758
|
+
//
|
|
1759
|
+
// The truncation call that used to sit here was disabled with
|
|
1760
|
+
// `if (false && ...)` rather than removed. Deleted now: the note above
|
|
1761
|
+
// is the part worth keeping, and dead code behind a constant false is
|
|
1762
|
+
// code that still has to compile, still gets read as if it might run,
|
|
1763
|
+
// and cannot be tested.
|
|
1764
|
+
// If cancelled during streaming, stop immediately
|
|
1765
|
+
if (signal?.aborted) {
|
|
1766
|
+
onEvent({ type: 'done', finalMessage: assistantMessage });
|
|
1767
|
+
return finalHistory();
|
|
1768
|
+
}
|
|
1769
|
+
// ─── A written tool call is still a tool call ──────────────────────
|
|
1770
|
+
// Some models emit <present_plan>{…}</present_plan> as TEXT instead of
|
|
1771
|
+
// calling the tool, even with native schemas offered. Left alone the user
|
|
1772
|
+
// gets raw JSON where a plan card belongs, and the turn closes cleanly
|
|
1773
|
+
// because the agent sees no tool_calls. Recovery is reliable where
|
|
1774
|
+
// instructing the model is not — the models that do this are the ones
|
|
1775
|
+
// least likely to follow an instruction about it.
|
|
1776
|
+
if ((!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) &&
|
|
1777
|
+
typeof assistantMessage.content === 'string') {
|
|
1778
|
+
const offered = new Set(toolSchemas.map((t) => t.function.name));
|
|
1779
|
+
const recovered = recoverWrittenToolCalls(assistantMessage.content, offered);
|
|
1780
|
+
if (recovered.calls.length > 0) {
|
|
1781
|
+
logger.warn(`[agent] Recovered ${recovered.calls.length} written tool call(s) from text: ` +
|
|
1782
|
+
recovered.calls.map((c) => c.function.name).join(', '));
|
|
1783
|
+
assistantMessage.tool_calls = recovered.calls;
|
|
1784
|
+
assistantMessage.content = recovered.text;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
1788
|
+
logger.debug(`[agent] No tool_calls in response. content=${(assistantMessage.content ?? '').length} chars, reasoning=${(assistantMessage.reasoning_content ?? '').length} chars`);
|
|
1789
|
+
// ─── Pre-closure verify guard (universal) ─────────────────────
|
|
1790
|
+
// If the trajectory has unverified file edits, run verify_change
|
|
1791
|
+
// before allowing the turn to end. On pass, files move to
|
|
1792
|
+
// verifiedFiles and closure proceeds. On fail, inject the
|
|
1793
|
+
// failure report (or a fresh-eyes review if we've looped on
|
|
1794
|
+
// the same root cause) as user-role context and re-enter the
|
|
1795
|
+
// loop. Bounded to MAX_CLOSURE_VERIFY cycles per run via
|
|
1796
|
+
// closureVerifyCount — re-verifies recovery fixes, still guaranteed
|
|
1797
|
+
// to exit so an unfixable verify can't trap a turn.
|
|
1798
|
+
//
|
|
1799
|
+
// Lives in the universal agent loop, NOT AutoCoordinator, so
|
|
1800
|
+
// single-model BYOK chats get the same enforcement orchestrated
|
|
1801
|
+
// modes have always had.
|
|
1802
|
+
const trajForVerify = getTrajectory();
|
|
1803
|
+
const pendingFiles = trajForVerify ? pendingFilesAtClosure(trajForVerify) : null;
|
|
1804
|
+
if (this.loopPreventionEnabled &&
|
|
1805
|
+
pendingFiles &&
|
|
1806
|
+
pendingFiles.length > 0 &&
|
|
1807
|
+
closureVerifyCount < MAX_CLOSURE_VERIFY &&
|
|
1808
|
+
!signal?.aborted &&
|
|
1809
|
+
trajForVerify) {
|
|
1810
|
+
closureVerifyCount++;
|
|
1811
|
+
logger.debug(`[agent] Pre-closure verify on ${pendingFiles.length} pending file(s)`);
|
|
1812
|
+
onEvent({ type: 'verify_started', files: pendingFiles });
|
|
1813
|
+
const verifyResult = await runPendingVerify(trajForVerify, this.toolContext);
|
|
1814
|
+
if (!verifyResult.passed) {
|
|
1815
|
+
onEvent({ type: 'verify_failed', files: verifyResult.files, output: verifyResult.output });
|
|
1816
|
+
// Record signature + check loop threshold; fall through to
|
|
1817
|
+
// a normal nudge or, if we've been spinning, a fresh-eyes
|
|
1818
|
+
// independent second opinion.
|
|
1819
|
+
const sig = signatureForFailure(verifyResult.output, verifyResult.files);
|
|
1820
|
+
recordFailure(trajForVerify, sig, 'verify');
|
|
1821
|
+
let nudgeContent;
|
|
1822
|
+
if (shouldEscalateFreshEyes(trajForVerify, sig)) {
|
|
1823
|
+
markFreshEyesEscalated(trajForVerify);
|
|
1824
|
+
logger.info(`[agent] Fresh-eyes escalation triggered — signature ${sig.slice(0, 60)}... has recurred`);
|
|
1825
|
+
onEvent({ type: 'fresh_eyes_started', signature: sig.slice(0, 60) });
|
|
1826
|
+
const firstUserMsg = messages.find((m) => m.role === 'user');
|
|
1827
|
+
const originalTask = typeof firstUserMsg?.content === 'string' ? firstUserMsg.content : '';
|
|
1828
|
+
const review = await runFreshEyesReview({
|
|
1829
|
+
provider: this.provider,
|
|
1830
|
+
modelId: this.model.id,
|
|
1831
|
+
originalTask,
|
|
1832
|
+
files: verifyResult.files,
|
|
1833
|
+
cwd: this.toolContext.cwd,
|
|
1834
|
+
failureSummary: describeFailureLoop(trajForVerify),
|
|
1835
|
+
lastFailureReport: verifyResult.output,
|
|
1836
|
+
signal,
|
|
1837
|
+
});
|
|
1838
|
+
onEvent({ type: 'fresh_eyes_complete' });
|
|
1839
|
+
// Credit-fairness signal — by the time fresh-eyes has fired,
|
|
1840
|
+
// the user has paid for ≥3 same-signature failures + the
|
|
1841
|
+
// fresh-eyes call itself. Flag this turn as refund-eligible
|
|
1842
|
+
// so the backend can decide whether to credit the user
|
|
1843
|
+
// back. Token estimate: fresh-eyes max_tokens (800) plus
|
|
1844
|
+
// the prompt budget it builds (capped at ~10K input via
|
|
1845
|
+
// file/report/task budgets in fresh-eyes.ts). Conservative
|
|
1846
|
+
// 11_000 is the worst-case ceiling, not a measured spend —
|
|
1847
|
+
// backend should still cross-reference its own usage rows
|
|
1848
|
+
// for the authoritative number.
|
|
1849
|
+
onEvent({
|
|
1850
|
+
type: 'loop_refund_eligible',
|
|
1851
|
+
signature: sig.slice(0, 60),
|
|
1852
|
+
tokensInRecovery: 11_000,
|
|
1853
|
+
reason: 'fresh-eyes review fired — same-signature failure recurred 3+ times',
|
|
1854
|
+
});
|
|
1855
|
+
nudgeContent = buildFreshEyesContext(review);
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
nudgeContent = buildVerifyFailureNudge(verifyResult.output, verifyResult.files);
|
|
1859
|
+
}
|
|
1860
|
+
// Drop the closure attempt and re-prompt with the failure
|
|
1861
|
+
// context — gives the model real diagnostic info to act on
|
|
1862
|
+
// instead of letting it declare done with broken code.
|
|
1863
|
+
messages = messages.slice(0, -1);
|
|
1864
|
+
messages = [...messages, { role: 'user', content: nudgeContent }];
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
onEvent({ type: 'verify_passed', files: verifyResult.files });
|
|
1868
|
+
// Pass — fall through to existing closure logic.
|
|
1869
|
+
}
|
|
1870
|
+
// ─── Closure fallback ─────────────────────────────────────────
|
|
1871
|
+
// Detect two failure modes where the turn terminates without
|
|
1872
|
+
// actually doing visible work:
|
|
1873
|
+
//
|
|
1874
|
+
// 1. Empty close — the model finished cleanly but produced zero
|
|
1875
|
+
// visible content. User sees silence after tool calls.
|
|
1876
|
+
//
|
|
1877
|
+
// 2. Continuation stall — the model produced text like "Let me
|
|
1878
|
+
// rewrite the sidebar" but terminated with no tool_calls.
|
|
1879
|
+
// It narrated intent but never acted. This is arguably worse
|
|
1880
|
+
// than an empty close because the user sees a promise that
|
|
1881
|
+
// never gets fulfilled.
|
|
1882
|
+
//
|
|
1883
|
+
// Both cases share the same fix: drop the stalled message, inject
|
|
1884
|
+
// a forcing nudge, re-enter the loop for one more iteration.
|
|
1885
|
+
// Guarded by closureFallbackAttempted so we never loop more than
|
|
1886
|
+
// once per run. If the nudged response is ALSO stalled, fall
|
|
1887
|
+
// through to the hardcoded "Done" substitution.
|
|
1888
|
+
const contentText = typeof assistantMessage.content === 'string'
|
|
1889
|
+
? assistantMessage.content.trim()
|
|
1890
|
+
: '';
|
|
1891
|
+
const isEmptyClose = contentText.length === 0;
|
|
1892
|
+
const isContinuationStall = !isEmptyClose && looksLikeContinuationStall(contentText);
|
|
1893
|
+
// Count tool calls in this run so far (for drift detection)
|
|
1894
|
+
const runToolCallCount = messages.filter(m => m.role === 'assistant' && m.tool_calls?.length > 0)
|
|
1895
|
+
.reduce((sum, m) => sum + (m.tool_calls?.length ?? 0), 0);
|
|
1896
|
+
const isPostToolDrift = !isEmptyClose && !isContinuationStall && looksLikePostToolDrift(contentText, runToolCallCount);
|
|
1897
|
+
if ((isEmptyClose || isContinuationStall || isPostToolDrift) && !closureFallbackAttempted) {
|
|
1898
|
+
closureFallbackAttempted = true;
|
|
1899
|
+
const reason = isEmptyClose ? 'empty final message'
|
|
1900
|
+
: isContinuationStall ? 'continuation stall (narrated intent without acting)'
|
|
1901
|
+
: 'post-tool drift (greeting/social response after tool usage)';
|
|
1902
|
+
logger.debug(`[agent] Closure fallback: ${reason}, re-prompting`);
|
|
1903
|
+
// ── Dataset event: stall detected + nudge will fire ─────────
|
|
1904
|
+
// Stash the stall event_id on the trajectory so the run
|
|
1905
|
+
// wrapper's finally can emit continuation_nudge_fired with
|
|
1906
|
+
// an accurate `recovered` flag once we know whether the
|
|
1907
|
+
// nudge worked.
|
|
1908
|
+
const stallPattern = isEmptyClose ? 'empty-close'
|
|
1909
|
+
: isContinuationStall ? 'continuation-narration'
|
|
1910
|
+
: 'post-tool-drift';
|
|
1911
|
+
const stallEventId = avaEvents.emit('continuation_stall_detected', {
|
|
1912
|
+
response_summary: `${stallPattern}, ${contentText.length}ch`,
|
|
1913
|
+
stall_pattern: stallPattern,
|
|
1914
|
+
});
|
|
1915
|
+
const stallTraj = getTrajectory();
|
|
1916
|
+
if (stallTraj) {
|
|
1917
|
+
stallTraj.pendingStallEventId = stallEventId;
|
|
1918
|
+
// Optimistic default — the wrapper's finally flips this to
|
|
1919
|
+
// false if we hit the fallback-exhausted branch below.
|
|
1920
|
+
stallTraj.nudgeRecovered = true;
|
|
1921
|
+
}
|
|
1922
|
+
// Drop the stalled assistant message from history
|
|
1923
|
+
messages = messages.slice(0, -1);
|
|
1924
|
+
// Inject the appropriate forcing nudge
|
|
1925
|
+
const nudgeContent = isEmptyClose
|
|
1926
|
+
? '[Closure check — your previous response was empty. The user needs visible confirmation that you finished. Write ONE short sentence summarising what you just did in this turn. Example: "Done — sidebar.tsx updated with the new palette." or "Fixed the missing habitId arg on line 71 of App.tsx." No tool calls. Just one sentence of text. This is the minimum required to close out a turn.]'
|
|
1927
|
+
: isPostToolDrift
|
|
1928
|
+
? `[Context drift detected — you just used ${runToolCallCount} tools (reading files, searching, etc.) but then produced a greeting/social response instead of summarising your findings. You were in the middle of a task. The user did NOT change the subject — your attention drifted under the weight of all those tool results. Go back to the ORIGINAL task. Summarise what you found in the files you just read, present your plan, or continue working. Never produce a greeting after research.]`
|
|
1929
|
+
: `[Continuation check — you said "${contentText.slice(0, 120)}${contentText.length > 120 ? '…' : ''}" but then stopped without making any tool calls. You NARRATED intent but never acted on it. The user sees a promise that never got fulfilled — the worst possible UX. Do the work NOW in this response: make the actual tool calls to accomplish what you said you would. If the work genuinely can't be done, explain clearly why ("I can't X because Y"). Silence or another narration loop is not acceptable — either act or explain, no middle ground.]`;
|
|
1930
|
+
messages = [
|
|
1931
|
+
...messages,
|
|
1932
|
+
{ role: 'user', content: nudgeContent },
|
|
1933
|
+
];
|
|
1934
|
+
// Loop back for one more streaming call — the nudge will force
|
|
1935
|
+
// either tool calls or a clear explanation. Normal flow resumes
|
|
1936
|
+
// from there.
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
// If we already tried the closure fallback and STILL got a stall,
|
|
1940
|
+
// substitute a hardcoded "Done." so the user sees something rather
|
|
1941
|
+
// than a blank turn or a broken promise. This is belt-and-braces —
|
|
1942
|
+
// the prompt rule should catch most cases, the fallback nudge
|
|
1943
|
+
// catches more, and this final substitution catches the remaining
|
|
1944
|
+
// edge cases where the model is genuinely broken on closure.
|
|
1945
|
+
if ((isEmptyClose || isContinuationStall) && closureFallbackAttempted) {
|
|
1946
|
+
logger.warn('[agent] Closure fallback exhausted — substituting hardcoded "Done."');
|
|
1947
|
+
// Mark the nudge as failed for the upcoming nudge_fired emit.
|
|
1948
|
+
const exhaustedTraj = getTrajectory();
|
|
1949
|
+
if (exhaustedTraj)
|
|
1950
|
+
exhaustedTraj.nudgeRecovered = false;
|
|
1951
|
+
const substitute = isContinuationStall
|
|
1952
|
+
? contentText + ' [Agent stalled — closure fallback substituted this message.]'
|
|
1953
|
+
: 'Done.';
|
|
1954
|
+
assistantMessage = {
|
|
1955
|
+
...assistantMessage,
|
|
1956
|
+
content: substitute,
|
|
1957
|
+
};
|
|
1958
|
+
messages = [...messages.slice(0, -1), assistantMessage];
|
|
1959
|
+
}
|
|
1960
|
+
// Surface empty responses — model returned nothing visible to the user
|
|
1961
|
+
// (kept for the edge case where both content AND reasoning are empty
|
|
1962
|
+
// even after the closure fallback — genuinely broken model output)
|
|
1963
|
+
if (!assistantMessage.content && !assistantMessage.reasoning_content) {
|
|
1964
|
+
onEvent({
|
|
1965
|
+
type: 'error',
|
|
1966
|
+
error: new Error(t('error.msg.empty_response')),
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
// ─── Honesty gate: verify-or-restate (active, every-model) ─────
|
|
1970
|
+
// A high-stakes completion/security claim with no verifying tool
|
|
1971
|
+
// behind it doesn't get to close on a guess. Re-prompt once to
|
|
1972
|
+
// verify (call the right tool) or restate without the claim —
|
|
1973
|
+
// mirrors the pre-closure file-verify guard above. Bounded to one
|
|
1974
|
+
// attempt per run; if it still can't back it, the claims-auditor's
|
|
1975
|
+
// deterministic caveat floor has already annotated the reply.
|
|
1976
|
+
const honestyAudit = this.lastAudit;
|
|
1977
|
+
if (this.loopPreventionEnabled &&
|
|
1978
|
+
!this.honestyVerifyAttempted &&
|
|
1979
|
+
honestyAudit?.flagged &&
|
|
1980
|
+
(honestyAudit.tier === 'high' || honestyAudit.tier === 'critical') &&
|
|
1981
|
+
!signal?.aborted &&
|
|
1982
|
+
typeof assistantMessage.content === 'string' &&
|
|
1983
|
+
assistantMessage.content.trim().length > 0) {
|
|
1984
|
+
this.honestyVerifyAttempted = true;
|
|
1985
|
+
logger.debug(`[agent] Honesty gate: unbacked ${honestyAudit.tier} claim — re-prompting to verify or restate`);
|
|
1986
|
+
const nudge = this.buildHonestyVerifyNudge(honestyAudit);
|
|
1987
|
+
messages = messages.slice(0, -1);
|
|
1988
|
+
messages = [...messages, { role: 'user', content: nudge }];
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
// Memory extraction — runs post-turn, extracts genuinely durable
|
|
1992
|
+
// user/project facts (name, preferences, decisions, architecture).
|
|
1993
|
+
// Bounded by the Memory Agent's regex + single LLM call.
|
|
1994
|
+
this.extractMemoriesFromRun(messages, runContext);
|
|
1995
|
+
// Ambient hot-path writers DISABLED.
|
|
1996
|
+
// The following used to fire on every turn, each reading the
|
|
1997
|
+
// conversation and deriving persistent state from it:
|
|
1998
|
+
// captureInteraction(messages) — dataset capture
|
|
1999
|
+
// this.feedProceduralObserver(...) — v3 procedural learning
|
|
2000
|
+
// this.saveGraphState(runContext) — v3 graph persistence
|
|
2001
|
+
//
|
|
2002
|
+
// Collectively they were saving conversation-shaped content into
|
|
2003
|
+
// memory, creating a feedback loop where earlier turns' text got
|
|
2004
|
+
// re-injected on later turns and re-saved. They also ran 3+
|
|
2005
|
+
// concurrent writers against the shared conversation state,
|
|
2006
|
+
// which is the likely source of the empty-tool-call corruption
|
|
2007
|
+
// we've been seeing in longer sessions.
|
|
2008
|
+
//
|
|
2009
|
+
// These capabilities are not deleted — they belong in a
|
|
2010
|
+
// session-end or scheduled background job that reads memory
|
|
2011
|
+
// (distilled facts only), not the live conversation. Wiring
|
|
2012
|
+
// that up is follow-up work; for now, the hot path stays clean.
|
|
2013
|
+
onEvent({ type: 'done', finalMessage: assistantMessage });
|
|
2014
|
+
return finalHistory();
|
|
2015
|
+
}
|
|
2016
|
+
logger.debug(`[agent] Got ${assistantMessage.tool_calls.length} tool_calls: ${assistantMessage.tool_calls.map((tc) => tc.function.name).join(', ')}`);
|
|
2017
|
+
// ── Repeated tool-call detection ───────────────────────────────────────
|
|
2018
|
+
// If the model calls the same tool with the same arguments 3+ times, break the loop.
|
|
2019
|
+
// Different arguments = different call = not a loop (e.g. list_directory on different paths).
|
|
2020
|
+
const currentToolSig = assistantMessage.tool_calls.map((tc) => `${tc.function.name}:${tc.function.arguments}`).sort().join(',');
|
|
2021
|
+
if (currentToolSig === lastToolName) {
|
|
2022
|
+
repeatCount++;
|
|
2023
|
+
if (repeatCount >= MAX_SAME_TOOL_REPEATS) {
|
|
2024
|
+
logger.warn(`[agent] HARD STOP: ${currentToolSig} called ${repeatCount + 1} times consecutively`);
|
|
2025
|
+
const stopMsg = `Stopped: ${currentToolSig} was called ${repeatCount + 1} times in a row and kept failing. Try a different approach or start a new chat.`;
|
|
2026
|
+
onEvent({
|
|
2027
|
+
type: 'error',
|
|
2028
|
+
error: Object.assign(new Error(stopMsg), { code: 'tool_loop_stopped' }),
|
|
2029
|
+
});
|
|
2030
|
+
onEvent({
|
|
2031
|
+
type: 'done',
|
|
2032
|
+
finalMessage: { role: 'assistant', content: stopMsg },
|
|
2033
|
+
});
|
|
2034
|
+
const trajLoopStop = getTrajectory();
|
|
2035
|
+
if (trajLoopStop)
|
|
2036
|
+
trajLoopStop.outcome = 'hit_loop_limit';
|
|
2037
|
+
return finalHistory();
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
lastToolName = currentToolSig;
|
|
2042
|
+
repeatCount = 0;
|
|
2043
|
+
}
|
|
2044
|
+
// ── Mode enforcement: block tools not allowed in the active mode ────
|
|
2045
|
+
// ALWAYS_ALLOWED_TOOLS is honoured here as well as in the schema filter
|
|
2046
|
+
// above. The two must agree: offering a schema and then blocking the call
|
|
2047
|
+
// gives the model a tool it can see and cannot use, which reads to the
|
|
2048
|
+
// user as Ava being broken rather than restricted.
|
|
2049
|
+
if (modeAllowed) {
|
|
2050
|
+
const isAllowed = (name) => modeAllowed.has(name) || ALWAYS_ALLOWED_TOOLS.has(name);
|
|
2051
|
+
const blocked = assistantMessage.tool_calls.filter((tc) => !isAllowed(tc.function.name));
|
|
2052
|
+
if (blocked.length > 0) {
|
|
2053
|
+
const blockedNames = blocked.map((tc) => tc.function.name).join(', ');
|
|
2054
|
+
logger.warn(`[agent] Mode ${detectedMode} blocked tools: ${blockedNames}`);
|
|
2055
|
+
// assistantMessage is already in `messages` (pushed unconditionally
|
|
2056
|
+
// earlier in the loop). The original code pushed it again inside
|
|
2057
|
+
// the for-loop, producing duplicate assistant turns in history
|
|
2058
|
+
// (one extra copy per blocked tool). Just push the tool results.
|
|
2059
|
+
for (const tc of blocked) {
|
|
2060
|
+
messages.push({
|
|
2061
|
+
role: 'tool',
|
|
2062
|
+
content: `Tool "${tc.function.name}" is not available in ${detectedMode} mode. This mode is read-only — use work mode (>>) to make changes.`,
|
|
2063
|
+
tool_call_id: tc.id,
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
// Remove blocked calls, keep allowed ones.
|
|
2067
|
+
assistantMessage.tool_calls = assistantMessage.tool_calls.filter((tc) => isAllowed(tc.function.name));
|
|
2068
|
+
if (assistantMessage.tool_calls.length === 0) {
|
|
2069
|
+
// Delete the field entirely — Qwen rejects `tool_calls: []`.
|
|
2070
|
+
// Since assistantMessage is a reference already in `messages`,
|
|
2071
|
+
// the deletion propagates to the history.
|
|
2072
|
+
delete assistantMessage.tool_calls;
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
// ── Hard exploration budget enforcement ──────────────────────────────
|
|
2078
|
+
// Soft nudge (maybeExplorationBudgetNudge) fires once as guidance,
|
|
2079
|
+
// but the model can ignore it and keep reading. This block is the
|
|
2080
|
+
// architectural floor: if reads-before-first-write exceed 2× the
|
|
2081
|
+
// task-complexity budget, read-only calls are refused at the agent
|
|
2082
|
+
// loop BEFORE they execute. The model receives a tool result telling
|
|
2083
|
+
// it exactly why and what to do next. This prevents the "100 reads,
|
|
2084
|
+
// still no code" failure mode that burns hundreds of thousands of
|
|
2085
|
+
// tokens with nothing to show.
|
|
2086
|
+
if (!this.hasWrittenInThisRun) {
|
|
2087
|
+
const hardBudget = COMPLEXITY_BUDGETS[this.currentTaskComplexity];
|
|
2088
|
+
const hardCap = hardBudget.readCapBeforeFirstWrite * 2;
|
|
2089
|
+
const projectedReads = this.readCountBeforeFirstWrite
|
|
2090
|
+
+ assistantMessage.tool_calls.filter((tc) => this.isReadOnlyToolCall(tc.function.name)).length;
|
|
2091
|
+
if (projectedReads > hardCap) {
|
|
2092
|
+
const blockedReads = assistantMessage.tool_calls.filter((tc) => this.isReadOnlyToolCall(tc.function.name));
|
|
2093
|
+
if (blockedReads.length > 0) {
|
|
2094
|
+
logger.warn(`[agent] HARD BUDGET BLOCK: ${this.readCountBeforeFirstWrite} reads already done, ${blockedReads.length} more would exceed ${hardCap} cap for ${this.currentTaskComplexity} task`);
|
|
2095
|
+
// Note: assistantMessage is already in `messages` (pushed
|
|
2096
|
+
// unconditionally earlier in the loop). Do not push it again
|
|
2097
|
+
// — that would produce a duplicate assistant turn.
|
|
2098
|
+
const refusalBody = [
|
|
2099
|
+
`Read budget hard-limit exceeded.`,
|
|
2100
|
+
``,
|
|
2101
|
+
`You've made ${this.readCountBeforeFirstWrite} read-only tool calls without a single write on a ${this.currentTaskComplexity} task (hard cap: ${hardCap}).`,
|
|
2102
|
+
``,
|
|
2103
|
+
`Further reads are blocked until you either:`,
|
|
2104
|
+
` 1. Commit to a write — pick the most likely correct change and make it. You have enough context.`,
|
|
2105
|
+
` 2. Explicitly re-scope — if this task is genuinely architectural, say so in your next response ("this is bigger than it looked because...") and the budget resets.`,
|
|
2106
|
+
``,
|
|
2107
|
+
`Stalling on context-gathering is the failure mode. Act or explain.`,
|
|
2108
|
+
].join('\n');
|
|
2109
|
+
for (const tc of blockedReads) {
|
|
2110
|
+
messages.push({
|
|
2111
|
+
role: 'tool',
|
|
2112
|
+
content: refusalBody,
|
|
2113
|
+
tool_call_id: tc.id,
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
// Remove blocked reads, keep any non-read tool calls (writes, etc.)
|
|
2117
|
+
assistantMessage.tool_calls = assistantMessage.tool_calls.filter((tc) => !blockedReads.some(b => b.id === tc.id));
|
|
2118
|
+
if (assistantMessage.tool_calls.length === 0) {
|
|
2119
|
+
// Delete the field entirely — Qwen rejects `tool_calls: []`.
|
|
2120
|
+
delete assistantMessage.tool_calls;
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
// ── Parallel tool execution ──────────────────────────────────────────
|
|
2127
|
+
// Partition tool calls: confirmation-required run sequentially first,
|
|
2128
|
+
// auto-approved tools run in parallel after for speed.
|
|
2129
|
+
const confirmCalls = [];
|
|
2130
|
+
const autoCalls = [];
|
|
2131
|
+
for (const tc of assistantMessage.tool_calls) {
|
|
2132
|
+
const tool = this.toolRegistry.getTool(tc.function.name);
|
|
2133
|
+
if (tool && this.toolRegistry.needsConfirmation(tool)) {
|
|
2134
|
+
confirmCalls.push(tc);
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
autoCalls.push(tc);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
// Phase 1: Confirmation-required tools (sequential — user must approve each)
|
|
2141
|
+
for (const toolCall of confirmCalls) {
|
|
2142
|
+
if (signal?.aborted) {
|
|
2143
|
+
onEvent({ type: 'done', finalMessage: assistantMessage });
|
|
2144
|
+
return finalHistory();
|
|
2145
|
+
}
|
|
2146
|
+
// Auto-checkpoint before write/dangerous tools
|
|
2147
|
+
const toolDef = this.toolRegistry.getTool(toolCall.function.name);
|
|
2148
|
+
if (toolDef && (toolDef.riskLevel === 'write' || toolDef.riskLevel === 'dangerous')) {
|
|
2149
|
+
const cp = runContext.sharedState?.checkpointManager;
|
|
2150
|
+
if (cp && !cp.hasActiveCheckpoint()) {
|
|
2151
|
+
try {
|
|
2152
|
+
await cp.createCheckpoint();
|
|
2153
|
+
}
|
|
2154
|
+
catch { /* best-effort */ }
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
messages = await this.executeToolCall(toolCall, runContext, onEvent, messages, useNativeTools);
|
|
2158
|
+
}
|
|
2159
|
+
// Phase 2: Auto-approved tools (parallel via Promise.allSettled)
|
|
2160
|
+
if (autoCalls.length > 0) {
|
|
2161
|
+
if (signal?.aborted) {
|
|
2162
|
+
onEvent({ type: 'done', finalMessage: assistantMessage });
|
|
2163
|
+
return finalHistory();
|
|
2164
|
+
}
|
|
2165
|
+
// Auto-checkpoint if any auto-approved tool is write/dangerous
|
|
2166
|
+
const hasRiskyAuto = autoCalls.some(tc => {
|
|
2167
|
+
const td = this.toolRegistry.getTool(tc.function.name);
|
|
2168
|
+
return td && (td.riskLevel === 'write' || td.riskLevel === 'dangerous');
|
|
2169
|
+
});
|
|
2170
|
+
if (hasRiskyAuto) {
|
|
2171
|
+
const cp = runContext.sharedState?.checkpointManager;
|
|
2172
|
+
if (cp && !cp.hasActiveCheckpoint()) {
|
|
2173
|
+
try {
|
|
2174
|
+
await cp.createCheckpoint();
|
|
2175
|
+
}
|
|
2176
|
+
catch { /* best-effort */ }
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// Fire all start events
|
|
2180
|
+
for (const tc of autoCalls) {
|
|
2181
|
+
onEvent({ type: 'tool_call_start', toolCall: tc });
|
|
2182
|
+
}
|
|
2183
|
+
// Execute all in parallel
|
|
2184
|
+
const results = await Promise.allSettled(autoCalls.map(async (tc) => {
|
|
2185
|
+
let parsedArgs;
|
|
2186
|
+
try {
|
|
2187
|
+
parsedArgs = JSON.parse(tc.function.arguments);
|
|
2188
|
+
}
|
|
2189
|
+
catch {
|
|
2190
|
+
parsedArgs = {};
|
|
2191
|
+
}
|
|
2192
|
+
const ctx = {
|
|
2193
|
+
...runContext,
|
|
2194
|
+
// Thread the model's tool_call ID so any confirmation handler
|
|
2195
|
+
// (auto tools should never trigger one, but this is defensive)
|
|
2196
|
+
// can match cards to the exact tool call.
|
|
2197
|
+
toolCallId: tc.id,
|
|
2198
|
+
onOutput: (data) => {
|
|
2199
|
+
onEvent({ type: 'tool_call_partial', toolCallId: tc.id, data });
|
|
2200
|
+
},
|
|
2201
|
+
};
|
|
2202
|
+
return this.executeToolWithCapture(tc.function.name, parsedArgs, ctx);
|
|
2203
|
+
}));
|
|
2204
|
+
// Append results in order (API requires tool messages match tool_call order)
|
|
2205
|
+
for (let i = 0; i < autoCalls.length; i++) {
|
|
2206
|
+
const toolCall = autoCalls[i];
|
|
2207
|
+
const settled = results[i];
|
|
2208
|
+
const result = settled.status === 'fulfilled'
|
|
2209
|
+
? settled.value
|
|
2210
|
+
: { success: false, output: `Tool failed: ${settled.reason}`, metadata: undefined };
|
|
2211
|
+
onEvent({
|
|
2212
|
+
type: 'tool_call_end',
|
|
2213
|
+
toolCall,
|
|
2214
|
+
result: result.output,
|
|
2215
|
+
success: result.success,
|
|
2216
|
+
metadata: result.metadata,
|
|
2217
|
+
});
|
|
2218
|
+
if (useNativeTools) {
|
|
2219
|
+
messages = [
|
|
2220
|
+
...messages,
|
|
2221
|
+
{
|
|
2222
|
+
role: 'tool',
|
|
2223
|
+
tool_call_id: toolCall.id,
|
|
2224
|
+
content: result.output,
|
|
2225
|
+
},
|
|
2226
|
+
];
|
|
2227
|
+
}
|
|
2228
|
+
else {
|
|
2229
|
+
// Text-based mode: send tool results as user messages
|
|
2230
|
+
messages = [
|
|
2231
|
+
...messages,
|
|
2232
|
+
{
|
|
2233
|
+
role: 'user',
|
|
2234
|
+
content: formatToolResult(toolCall.function.name, result.output, result.success),
|
|
2235
|
+
},
|
|
2236
|
+
];
|
|
2237
|
+
}
|
|
2238
|
+
// Vision pipeline — downsample before embedding to cap per-image
|
|
2239
|
+
// token cost. Full-res screenshots burn 20-50K tokens each and
|
|
2240
|
+
// get re-sent on every subsequent turn.
|
|
2241
|
+
if (result.metadata?.base64_image) {
|
|
2242
|
+
const rawBase64 = result.metadata.base64_image;
|
|
2243
|
+
const resizedBase64 = downsampleScreenshotBase64(rawBase64);
|
|
2244
|
+
messages = [
|
|
2245
|
+
...messages,
|
|
2246
|
+
{
|
|
2247
|
+
role: 'user',
|
|
2248
|
+
content: [
|
|
2249
|
+
{ type: 'text', text: `[Image captured by ${toolCall.function.name}]` },
|
|
2250
|
+
{ type: 'image_url', image_url: {
|
|
2251
|
+
url: `data:${result.metadata.mime_type || 'image/png'};base64,${resizedBase64}`,
|
|
2252
|
+
} },
|
|
2253
|
+
],
|
|
2254
|
+
},
|
|
2255
|
+
];
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
// ─── Dynamic design context re-injection ─────────────────────────
|
|
2259
|
+
// If any tool call in this batch wrote or edited a UI file, refresh
|
|
2260
|
+
// the Decisions/design context into the message history so it's in
|
|
2261
|
+
// attention for the NEXT turn — not buried behind whatever error
|
|
2262
|
+
// recovery or other noise has accumulated. One injection per batch,
|
|
2263
|
+
// even if multiple UI files were touched. Throttled by turn count
|
|
2264
|
+
// and file mtime cache so we don't re-read the same files 20 times.
|
|
2265
|
+
const uiBatchPath = this.findUIFilePathInBatch(autoCalls);
|
|
2266
|
+
if (uiBatchPath) {
|
|
2267
|
+
this.designReinjectionTurn++;
|
|
2268
|
+
const reinject = await maybeBuildDesignReinjection(runContext.cwd, uiBatchPath, {
|
|
2269
|
+
currentTurn: this.designReinjectionTurn,
|
|
2270
|
+
lastInjectedTurn: this.designReinjectionLastTurn,
|
|
2271
|
+
lastMtimes: this.designReinjectionLastMtimes,
|
|
2272
|
+
});
|
|
2273
|
+
if (reinject) {
|
|
2274
|
+
messages = [
|
|
2275
|
+
...messages,
|
|
2276
|
+
{ role: 'user', content: reinject.content },
|
|
2277
|
+
];
|
|
2278
|
+
this.designReinjectionLastTurn = this.designReinjectionTurn;
|
|
2279
|
+
this.designReinjectionLastMtimes = reinject.updatedMtimes;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
// ─── Exploration budget nudge ──────────────────────────────────
|
|
2283
|
+
// Count read-only tool calls in this batch. If the agent has done
|
|
2284
|
+
// too much exploration without committing to a write, inject a
|
|
2285
|
+
// soft nudge telling her to commit or justify. Never hard-stops.
|
|
2286
|
+
const nudge = this.maybeExplorationBudgetNudge(autoCalls);
|
|
2287
|
+
if (nudge) {
|
|
2288
|
+
messages = [
|
|
2289
|
+
...messages,
|
|
2290
|
+
{ role: 'user', content: nudge },
|
|
2291
|
+
];
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const iterError = new Error(t('error.msg.iteration_limit', { limit: String(MAX_TOOL_CALL_ITERATIONS) }));
|
|
2296
|
+
iterError.code = 'iterations_exceeded';
|
|
2297
|
+
onEvent({ type: 'error', error: iterError });
|
|
2298
|
+
// Always emit done so the UI clears isStreaming
|
|
2299
|
+
onEvent({ type: 'done', finalMessage: { role: 'assistant', content: 'Stopped: tool call iteration limit reached.' } });
|
|
2300
|
+
// Extract memories on iteration limit — bounded extraction only.
|
|
2301
|
+
// Dataset capture, procedural observer, graph state save: disabled
|
|
2302
|
+
// from the hot path (see the clean-exit branch above for the full
|
|
2303
|
+
// rationale).
|
|
2304
|
+
this.extractMemoriesFromRun(messages, runContext);
|
|
2305
|
+
return finalHistory();
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Extract and save memories from a completed run.
|
|
2309
|
+
* Fire-and-forget — never blocks the response.
|
|
2310
|
+
* Errors are logged at debug level so they don't spam the UI but are visible for debugging.
|
|
2311
|
+
*/
|
|
2312
|
+
extractMemoriesFromRun(messages, runContext) {
|
|
2313
|
+
const ma = runContext.sharedState?.memoryAgent;
|
|
2314
|
+
const mm = runContext.sharedState?.memoryManager;
|
|
2315
|
+
if (ma) {
|
|
2316
|
+
// Memory Agent: single extraction call (regex + LLM reflection)
|
|
2317
|
+
logger.debug('[memory] Running Memory Agent extraction');
|
|
2318
|
+
ma.extractAndSave(messages)
|
|
2319
|
+
.then(saved => {
|
|
2320
|
+
if (saved > 0)
|
|
2321
|
+
logger.info(`[memory] Memory Agent saved ${saved} ${saved === 1 ? 'memory' : 'memories'}`);
|
|
2322
|
+
else
|
|
2323
|
+
logger.debug('[memory] Memory Agent: 0 memories extracted from this turn');
|
|
2324
|
+
})
|
|
2325
|
+
.catch(err => logger.warn(`[memory] Memory Agent extraction failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
2326
|
+
}
|
|
2327
|
+
else if (mm) {
|
|
2328
|
+
// Legacy fallback (Memory Agent unavailable): regex extraction only.
|
|
2329
|
+
// Previously this ran three additional passes — reflectAndSave
|
|
2330
|
+
// (LLM reflection), trackAndLearn (pattern tracking), and every
|
|
2331
|
+
// 6 user turns analyseAndSave (insights consolidation). Each was a
|
|
2332
|
+
// concurrent writer reading the conversation and deriving memory
|
|
2333
|
+
// from it. Together they (a) quadrupled the per-turn LLM spend on
|
|
2334
|
+
// ambient memory work and (b) produced conversation-shaped memory
|
|
2335
|
+
// entries that fed back into later turns.
|
|
2336
|
+
//
|
|
2337
|
+
// Keeping only autoExtractAndSave here: it's regex-based, bounded,
|
|
2338
|
+
// and the narrowest path. If its heuristics still save conversation
|
|
2339
|
+
// snippets rather than durable facts, that's a follow-up tightening.
|
|
2340
|
+
logger.debug('[memory] Running legacy memory extraction (regex only)');
|
|
2341
|
+
autoExtractAndSave(messages, mm)
|
|
2342
|
+
.then(saved => {
|
|
2343
|
+
if (saved > 0)
|
|
2344
|
+
logger.info(`[memory] Auto-extract saved ${saved} ${saved === 1 ? 'memory' : 'memories'}`);
|
|
2345
|
+
else
|
|
2346
|
+
logger.debug('[memory] Auto-extract: 0 memories from regex patterns');
|
|
2347
|
+
})
|
|
2348
|
+
.catch(err => logger.warn(`[memory] Auto-extract failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
logger.debug('[memory] No memoryManager in sharedState — skipping extraction. Is memory wired correctly?');
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async streamResponse(request, onEvent, signal) {
|
|
2355
|
+
onEvent({ type: 'stream_start' });
|
|
2356
|
+
let content = '';
|
|
2357
|
+
let reasoningContent = '';
|
|
2358
|
+
let usage;
|
|
2359
|
+
// Holds back a written tool call while it streams. Built from the tools
|
|
2360
|
+
// actually offered this turn, so it can only ever hide something that
|
|
2361
|
+
// recoverWrittenToolCalls would go on to lift out of the finished reply.
|
|
2362
|
+
const writtenCallFilter = new WrittenCallStreamFilter(new Set((request.tools ?? []).map((t) => t.function.name)));
|
|
2363
|
+
const toolCallsAccumulator = new Map();
|
|
2364
|
+
// Local controller linked to the parent signal. Lets us abort the
|
|
2365
|
+
// streaming request from inside the loop (on mid-stream user
|
|
2366
|
+
// injection) without touching the outer agent signal — the outer
|
|
2367
|
+
// run isn't cancelled, just this single streamResponse call.
|
|
2368
|
+
const localController = new AbortController();
|
|
2369
|
+
const forwardAbort = () => localController.abort();
|
|
2370
|
+
if (signal?.aborted) {
|
|
2371
|
+
localController.abort();
|
|
2372
|
+
}
|
|
2373
|
+
else {
|
|
2374
|
+
signal?.addEventListener('abort', forwardAbort);
|
|
2375
|
+
}
|
|
2376
|
+
// Flag set when we abort due to a mid-stream injection so the
|
|
2377
|
+
// caller knows to loop without attempting tool execution on a
|
|
2378
|
+
// partial response.
|
|
2379
|
+
let interruptedByInjection = false;
|
|
2380
|
+
try {
|
|
2381
|
+
for await (const chunk of this.provider.createStreamingCompletion(request, localController.signal)) {
|
|
2382
|
+
if (chunk.usage) {
|
|
2383
|
+
usage = chunk.usage;
|
|
2384
|
+
}
|
|
2385
|
+
const delta = chunk.choices[0]?.delta;
|
|
2386
|
+
if (!delta)
|
|
2387
|
+
continue;
|
|
2388
|
+
// Thinking/reasoning content (DeepSeek R1, GLM, Kimi, Mistral Magistral)
|
|
2389
|
+
const thinking = delta.reasoning_content ?? delta.reasoning;
|
|
2390
|
+
if (thinking) {
|
|
2391
|
+
// Some models leak literal <think>/</think> markers into the reasoning
|
|
2392
|
+
// field — strip them so they never render as raw text in the thought bubble.
|
|
2393
|
+
const cleaned = thinking.replace(/<\/?think>/g, '');
|
|
2394
|
+
if (cleaned) {
|
|
2395
|
+
reasoningContent += cleaned;
|
|
2396
|
+
onEvent({ type: 'thinking_delta', content: cleaned });
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (delta.content) {
|
|
2400
|
+
// COERCE TO TEXT FIRST. delta.content was assumed to be a string and
|
|
2401
|
+
// concatenated straight onto the reply — so a provider that streams
|
|
2402
|
+
// it as a content-part object (or an array of them) produced one
|
|
2403
|
+
// "[object Object]" per chunk, and the user got a wall of them where
|
|
2404
|
+
// the answer should be. Seen live 2026-08-18 on a self_inspect turn.
|
|
2405
|
+
//
|
|
2406
|
+
// Nothing downstream can recover from it either: by the time it is
|
|
2407
|
+
// in `content` the real text is gone, so the transcript, the history
|
|
2408
|
+
// file and the next request all carry the same rubbish.
|
|
2409
|
+
//
|
|
2410
|
+
// getTextContent handles both string and ContentPart[]; the object
|
|
2411
|
+
// case is a single part, so it is wrapped before extraction.
|
|
2412
|
+
const rawDelta = delta.content;
|
|
2413
|
+
let visibleContent = typeof rawDelta === 'string'
|
|
2414
|
+
? rawDelta
|
|
2415
|
+
: getTextContent((Array.isArray(rawDelta) ? rawDelta : [rawDelta]));
|
|
2416
|
+
if (!visibleContent)
|
|
2417
|
+
continue;
|
|
2418
|
+
if (visibleContent.includes('<think>') || visibleContent.includes('</think>') || this._inThinkTag) {
|
|
2419
|
+
// Track if we're inside a think tag across chunks
|
|
2420
|
+
const parts = visibleContent.split(/(<\/?think>)/);
|
|
2421
|
+
let visible = '';
|
|
2422
|
+
for (const part of parts) {
|
|
2423
|
+
if (part === '<think>') {
|
|
2424
|
+
this._inThinkTag = true;
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
if (part === '</think>') {
|
|
2428
|
+
this._inThinkTag = false;
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
if (this._inThinkTag) {
|
|
2432
|
+
reasoningContent += part;
|
|
2433
|
+
onEvent({ type: 'thinking_delta', content: part });
|
|
2434
|
+
}
|
|
2435
|
+
else {
|
|
2436
|
+
visible += part;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
visibleContent = visible;
|
|
2440
|
+
}
|
|
2441
|
+
if (visibleContent) {
|
|
2442
|
+
// Raw content accumulates in full — recoverWrittenToolCalls reads
|
|
2443
|
+
// it after the stream. Only the VIEW is filtered, so a written call
|
|
2444
|
+
// never reaches the screen on its way to becoming a real one.
|
|
2445
|
+
content += visibleContent;
|
|
2446
|
+
const showable = writtenCallFilter.push(visibleContent);
|
|
2447
|
+
if (showable)
|
|
2448
|
+
onEvent({ type: 'stream_delta', content: showable });
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (delta.tool_calls) {
|
|
2452
|
+
if (toolCallsAccumulator.size === 0) {
|
|
2453
|
+
logger.debug('[agent] First tool_call delta received in stream');
|
|
2454
|
+
}
|
|
2455
|
+
for (const tcDelta of delta.tool_calls) {
|
|
2456
|
+
if (!toolCallsAccumulator.has(tcDelta.index)) {
|
|
2457
|
+
toolCallsAccumulator.set(tcDelta.index, {
|
|
2458
|
+
id: tcDelta.id ?? '',
|
|
2459
|
+
type: 'function',
|
|
2460
|
+
function: { name: '', arguments: '' },
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
const acc = toolCallsAccumulator.get(tcDelta.index);
|
|
2464
|
+
if (tcDelta.id)
|
|
2465
|
+
acc.id = tcDelta.id;
|
|
2466
|
+
if (tcDelta.function?.name)
|
|
2467
|
+
acc.function.name += tcDelta.function.name;
|
|
2468
|
+
if (tcDelta.function?.arguments)
|
|
2469
|
+
acc.function.arguments += tcDelta.function.arguments;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
// Mid-stream injection check. If the user sent a message while the
|
|
2473
|
+
// model was streaming, abort this stream and let the outer loop
|
|
2474
|
+
// pick up the interjection on its next iteration (where
|
|
2475
|
+
// pendingInterjections gets drained at the top of the loop).
|
|
2476
|
+
//
|
|
2477
|
+
// Interrupting mid-tool-call would leave dangling tool_calls in
|
|
2478
|
+
// history with no matching tool results — most providers 400 on
|
|
2479
|
+
// that. Only interrupt while we're in the text/thinking phase,
|
|
2480
|
+
// before any tool_calls have started accumulating.
|
|
2481
|
+
if (this.pendingInterjections.length > 0 && toolCallsAccumulator.size === 0) {
|
|
2482
|
+
logger.debug('[agent] Mid-stream injection arrived — aborting stream, interjection will fire next iteration');
|
|
2483
|
+
interruptedByInjection = true;
|
|
2484
|
+
localController.abort();
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
catch (error) {
|
|
2490
|
+
// If we aborted locally because of an injection, fall through to the
|
|
2491
|
+
// normal return path with interruptedByInjection=true. The outer
|
|
2492
|
+
// loop handles the partial message without running tools.
|
|
2493
|
+
if (interruptedByInjection) {
|
|
2494
|
+
// intentional: swallow the abort error, continue to the return below
|
|
2495
|
+
}
|
|
2496
|
+
else if (content || reasoningContent || toolCallsAccumulator.size > 0) {
|
|
2497
|
+
// Preserve partial content AND accumulated tool calls if we collected any before the error
|
|
2498
|
+
const partialToolCalls = toolCallsAccumulator.size > 0
|
|
2499
|
+
? Array.from(toolCallsAccumulator.values()).filter(tc => tc.id && tc.function.name)
|
|
2500
|
+
: [];
|
|
2501
|
+
const partialMessage = {
|
|
2502
|
+
role: 'assistant',
|
|
2503
|
+
content: content || null,
|
|
2504
|
+
...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
|
|
2505
|
+
...(partialToolCalls.length > 0 ? { tool_calls: partialToolCalls } : {}),
|
|
2506
|
+
};
|
|
2507
|
+
// The turn is failing, so nothing downstream will lift a written call
|
|
2508
|
+
// out of it. Release whatever the filter was holding rather than lose
|
|
2509
|
+
// it — a half-typed tag on screen beats a silently truncated reply.
|
|
2510
|
+
const pending = writtenCallFilter.flush();
|
|
2511
|
+
if (pending)
|
|
2512
|
+
onEvent({ type: 'stream_delta', content: pending });
|
|
2513
|
+
onEvent({ type: 'stream_end', message: partialMessage });
|
|
2514
|
+
throw error;
|
|
2515
|
+
}
|
|
2516
|
+
else {
|
|
2517
|
+
throw error;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
finally {
|
|
2521
|
+
signal?.removeEventListener('abort', forwardAbort);
|
|
2522
|
+
}
|
|
2523
|
+
// Anything still held that never became a call is ordinary text and is
|
|
2524
|
+
// owed to the reader. A confirmed call is dropped here on purpose: the
|
|
2525
|
+
// recovery lifts it from `content`, so re-emitting it would put back the
|
|
2526
|
+
// exact block this filter exists to withhold.
|
|
2527
|
+
const heldText = writtenCallFilter.flush();
|
|
2528
|
+
if (heldText)
|
|
2529
|
+
onEvent({ type: 'stream_delta', content: heldText });
|
|
2530
|
+
const toolCalls = toolCallsAccumulator.size > 0 ? Array.from(toolCallsAccumulator.values()) : [];
|
|
2531
|
+
// DeepSeek Reasoner rule: "If reasoning_content is set, content must not be empty."
|
|
2532
|
+
// When the model returns reasoning + tool_calls but no text, content would be null —
|
|
2533
|
+
// which causes a 400 on the next request if reasoning_content is also present.
|
|
2534
|
+
let finalContent = (!content && reasoningContent) ? '' : (content || null);
|
|
2535
|
+
// Honesty gate (soft): on the user-facing answer turn (no tool calls),
|
|
2536
|
+
// flag a state-claim ("done" / "it works" / "it's live") that ran no
|
|
2537
|
+
// verifying tool this run, and append a visible caveat so the unverified
|
|
2538
|
+
// claim doesn't stand as fact. Soft by design — annotates, never blocks.
|
|
2539
|
+
if (toolCalls.length === 0 && typeof finalContent === 'string' && finalContent.trim()) {
|
|
2540
|
+
const audit = auditClaims({ text: finalContent, toolsUsed: this.runToolEvidence });
|
|
2541
|
+
this.lastAudit = audit;
|
|
2542
|
+
if (audit.flagged) {
|
|
2543
|
+
// Record for the verification_evidence dataset event (shape-only:
|
|
2544
|
+
// a boolean, never the claim text). Captured even when there's no
|
|
2545
|
+
// caveat string, so the signal reflects every flagged claim.
|
|
2546
|
+
this.runClaimFlagged = true;
|
|
2547
|
+
}
|
|
2548
|
+
// Active honesty gate: a high/critical claim with no verifying tool is
|
|
2549
|
+
// about to get one verify-or-restate re-prompt in runInner — so DON'T
|
|
2550
|
+
// append the caveat yet in that case. Append it now for soft claims,
|
|
2551
|
+
// once the re-prompt is already spent, or when loop prevention is off:
|
|
2552
|
+
// that's the deterministic floor.
|
|
2553
|
+
const willReRun = this.loopPreventionEnabled &&
|
|
2554
|
+
!this.honestyVerifyAttempted &&
|
|
2555
|
+
(audit.tier === 'high' || audit.tier === 'critical');
|
|
2556
|
+
if (audit.flagged && audit.caveat && !willReRun) {
|
|
2557
|
+
const caveatText = `\n\n${audit.caveat}`;
|
|
2558
|
+
onEvent({ type: 'stream_delta', content: caveatText });
|
|
2559
|
+
finalContent = finalContent + caveatText;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const message = {
|
|
2563
|
+
role: 'assistant',
|
|
2564
|
+
content: finalContent,
|
|
2565
|
+
...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
|
|
2566
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
2567
|
+
};
|
|
2568
|
+
onEvent({ type: 'stream_end', message });
|
|
2569
|
+
if (usage) {
|
|
2570
|
+
let cost;
|
|
2571
|
+
if (this.model.pricing) {
|
|
2572
|
+
cost =
|
|
2573
|
+
(usage.prompt_tokens / 1_000_000) * this.model.pricing.inputPerMillion +
|
|
2574
|
+
(usage.completion_tokens / 1_000_000) * this.model.pricing.outputPerMillion;
|
|
2575
|
+
}
|
|
2576
|
+
onEvent({ type: 'usage', usage, cost });
|
|
2577
|
+
}
|
|
2578
|
+
// Meter the call — one chat_turn charge per agent iteration. A multi-
|
|
2579
|
+
// iteration agent (tool loop) emits one per loop, which is the intended
|
|
2580
|
+
// granularity. Cache-hit detection uses cached_tokens ratio: if >50% of
|
|
2581
|
+
// prompt was cached, treat as a cache hit for discount purposes.
|
|
2582
|
+
// No cast needed since TokenUsage declares the cache fields it always
|
|
2583
|
+
// carried at runtime. The `as unknown as` this replaced was the tell:
|
|
2584
|
+
// the shape was known to be wider than the type all along.
|
|
2585
|
+
const rawUsage = extractUsage(usage);
|
|
2586
|
+
const cacheHit = rawUsage?.cached != null && rawUsage.input > 0 && rawUsage.cached / rawUsage.input > 0.5;
|
|
2587
|
+
chargeCredits('chat_turn', {
|
|
2588
|
+
model: this.model.id,
|
|
2589
|
+
rawTokens: rawUsage,
|
|
2590
|
+
cacheHit,
|
|
2591
|
+
});
|
|
2592
|
+
return {
|
|
2593
|
+
message,
|
|
2594
|
+
promptTokens: usage?.prompt_tokens ?? 0,
|
|
2595
|
+
...(interruptedByInjection ? { interrupted: true } : {}),
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
// ── Single tool call execution (used by sequential confirmation phase) ──
|
|
2599
|
+
/**
|
|
2600
|
+
* Run a tool through the registry while emitting `tool_choice` /
|
|
2601
|
+
* `tool_result` dataset events around the call. Returns the same
|
|
2602
|
+
* shape `toolRegistry.execute` does — never throws (registry
|
|
2603
|
+
* exceptions are converted to a failure result, mirroring the
|
|
2604
|
+
* existing executeToolCall behaviour). Both the sequential and
|
|
2605
|
+
* parallel tool paths in `runInner` go through this helper so the
|
|
2606
|
+
* dataset trajectory captures every tool invocation in order.
|
|
2607
|
+
*/
|
|
2608
|
+
async executeToolWithCapture(toolName, args, ctx) {
|
|
2609
|
+
const traj = getTrajectory();
|
|
2610
|
+
const prevTools = traj?.toolsSoFar ? [...traj.toolsSoFar] : [];
|
|
2611
|
+
// ── Recovery-action emit ────────────────────────────────────────────
|
|
2612
|
+
// If the previous tool in this trajectory failed, the upcoming choice
|
|
2613
|
+
// is implicitly Ava's recovery move. Emit recovery_action linking
|
|
2614
|
+
// back to the specific tool_error before the new choice fires so the
|
|
2615
|
+
// ordering in the dataset reflects cause → response.
|
|
2616
|
+
const recoveringFromErrorId = traj?.pendingErrorEventId;
|
|
2617
|
+
if (recoveringFromErrorId) {
|
|
2618
|
+
const lastTool = prevTools[prevTools.length - 1];
|
|
2619
|
+
const recoveryKind = lastTool === toolName ? 'retry_same' : 'switch_tool';
|
|
2620
|
+
avaEvents.emit('recovery_action', {
|
|
2621
|
+
tool_error_event_id: recoveringFromErrorId,
|
|
2622
|
+
recovery_kind: recoveryKind,
|
|
2623
|
+
next_tool: toolName,
|
|
2624
|
+
});
|
|
2625
|
+
if (traj)
|
|
2626
|
+
traj.pendingErrorEventId = undefined;
|
|
2627
|
+
}
|
|
2628
|
+
const choiceEventId = avaEvents.emit('tool_choice', {
|
|
2629
|
+
tool_name: toolName,
|
|
2630
|
+
args_summary: summarizeToolArgs(toolName, args),
|
|
2631
|
+
// Process category for WHY this tool was reached for — deterministic
|
|
2632
|
+
// from the tool + whether we're recovering from a prior failure.
|
|
2633
|
+
// A label, never the model's raw chain-of-thought.
|
|
2634
|
+
reasoning_summary: categorizeToolPurpose(toolName, { recovering: !!recoveringFromErrorId }),
|
|
2635
|
+
prev_tools_in_trajectory: prevTools,
|
|
2636
|
+
});
|
|
2637
|
+
if (traj?.toolsSoFar)
|
|
2638
|
+
traj.toolsSoFar.push(toolName);
|
|
2639
|
+
const start = Date.now();
|
|
2640
|
+
let result;
|
|
2641
|
+
try {
|
|
2642
|
+
result = await this.toolRegistry.execute(toolName, args, ctx);
|
|
2643
|
+
}
|
|
2644
|
+
catch (err) {
|
|
2645
|
+
result = {
|
|
2646
|
+
output: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
2647
|
+
success: false,
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
const resultEventId = avaEvents.emit('tool_result', {
|
|
2651
|
+
tool_name: toolName,
|
|
2652
|
+
tool_choice_event_id: choiceEventId,
|
|
2653
|
+
success: result.success,
|
|
2654
|
+
result_summary: summarizeToolResult(result.output, result.success),
|
|
2655
|
+
duration_ms: Date.now() - start,
|
|
2656
|
+
error_summary: result.success ? undefined : result.output.slice(0, 200),
|
|
2657
|
+
});
|
|
2658
|
+
// Record for the soft honesty gate: did a verifying tool succeed this run?
|
|
2659
|
+
this.runToolEvidence.push({ name: toolName, ok: result.success });
|
|
2660
|
+
// ── Tool-error + guidance emits ─────────────────────────────────────
|
|
2661
|
+
// On failure, emit tool_error with the matched pattern key (if any),
|
|
2662
|
+
// followed by error_guidance_applied if the pattern library produced
|
|
2663
|
+
// user-facing fix advice. Stash the tool_error event_id on the
|
|
2664
|
+
// trajectory so the next tool_choice can attach a recovery_action.
|
|
2665
|
+
if (!result.success) {
|
|
2666
|
+
const matched = matchToolError(result.output);
|
|
2667
|
+
const errorEventId = avaEvents.emit('tool_error', {
|
|
2668
|
+
tool_name: toolName,
|
|
2669
|
+
tool_result_event_id: resultEventId,
|
|
2670
|
+
error_pattern_match: matched?.pattern_key,
|
|
2671
|
+
error_summary: result.output.slice(0, 200),
|
|
2672
|
+
});
|
|
2673
|
+
if (matched) {
|
|
2674
|
+
avaEvents.emit('error_guidance_applied', {
|
|
2675
|
+
tool_error_event_id: errorEventId,
|
|
2676
|
+
pattern: matched.pattern_key,
|
|
2677
|
+
// Guidance text is bounded and contains no user data — it's
|
|
2678
|
+
// canonical advice strings from the pattern library — so
|
|
2679
|
+
// capturing a short summary is safe.
|
|
2680
|
+
guidance_summary: matched.guidance.slice(0, 200),
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
if (traj)
|
|
2684
|
+
traj.pendingErrorEventId = errorEventId;
|
|
2685
|
+
}
|
|
2686
|
+
// ── Post-edit verify tracking ─────────────────────────────────────
|
|
2687
|
+
// Universal hook: every successful file_write / file_edit registers
|
|
2688
|
+
// its target on the trajectory's pendingVerifyFiles. Read by the
|
|
2689
|
+
// closure-time verify guard in runInner so unverified edits can't
|
|
2690
|
+
// sneak past a turn-end. Lives here (not in AutoCoordinator) so
|
|
2691
|
+
// every routing mode — Maestro / Supernova / Aurora / direct BYOK —
|
|
2692
|
+
// gets the same enforcement.
|
|
2693
|
+
if (traj) {
|
|
2694
|
+
recordEditFromTool(traj, toolName, args, result.success);
|
|
2695
|
+
}
|
|
2696
|
+
return result;
|
|
2697
|
+
}
|
|
2698
|
+
async executeToolCall(toolCall, runContext, onEvent, messages, useNativeTools = true) {
|
|
2699
|
+
onEvent({ type: 'tool_call_start', toolCall });
|
|
2700
|
+
let parsedArgs;
|
|
2701
|
+
try {
|
|
2702
|
+
parsedArgs = JSON.parse(toolCall.function.arguments);
|
|
2703
|
+
}
|
|
2704
|
+
catch {
|
|
2705
|
+
parsedArgs = {};
|
|
2706
|
+
}
|
|
2707
|
+
const toolRunContext = {
|
|
2708
|
+
...runContext,
|
|
2709
|
+
// Thread the model's tool_call ID through so the confirmation handler
|
|
2710
|
+
// can forward it to the UI for exact-match card attachment.
|
|
2711
|
+
toolCallId: toolCall.id,
|
|
2712
|
+
onOutput: (data) => {
|
|
2713
|
+
onEvent({ type: 'tool_call_partial', toolCallId: toolCall.id, data });
|
|
2714
|
+
},
|
|
2715
|
+
};
|
|
2716
|
+
const result = await this.executeToolWithCapture(toolCall.function.name, parsedArgs, toolRunContext);
|
|
2717
|
+
onEvent({
|
|
2718
|
+
type: 'tool_call_end',
|
|
2719
|
+
toolCall,
|
|
2720
|
+
result: result.output,
|
|
2721
|
+
success: result.success,
|
|
2722
|
+
metadata: result.metadata,
|
|
2723
|
+
});
|
|
2724
|
+
// ── Same call, same failure, again ──────────────────────────────────
|
|
2725
|
+
//
|
|
2726
|
+
// Two identical failures is evidence. The third attempt is told to stop and
|
|
2727
|
+
// ask, because nothing about the call has changed and nothing about the
|
|
2728
|
+
// result will. The message replaces the tool output rather than joining it:
|
|
2729
|
+
// repeating the same error text a third time is what convinced the model to
|
|
2730
|
+
// try a fourth.
|
|
2731
|
+
const failKey = `${toolCall.function.name}:${toolCall.function.arguments}`;
|
|
2732
|
+
let output = result.output;
|
|
2733
|
+
if (!result.success) {
|
|
2734
|
+
const n = (this.repeatedToolFailures.get(failKey) ?? 0) + 1;
|
|
2735
|
+
this.repeatedToolFailures.set(failKey, n);
|
|
2736
|
+
if (n >= 3) {
|
|
2737
|
+
output = `${result.output}
|
|
2738
|
+
|
|
2739
|
+
[This is attempt ${n} of \`${toolCall.function.name}\` with identical arguments, and it has failed the same way every time. Do NOT call it again with these arguments — nothing has changed, so nothing will. Tell the user plainly what failed and what the error said, and either change the approach or ask them how they want to proceed.]`;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
else {
|
|
2743
|
+
this.repeatedToolFailures.delete(failKey);
|
|
2744
|
+
}
|
|
2745
|
+
if (useNativeTools) {
|
|
2746
|
+
messages = [
|
|
2747
|
+
...messages,
|
|
2748
|
+
{
|
|
2749
|
+
role: 'tool',
|
|
2750
|
+
tool_call_id: toolCall.id,
|
|
2751
|
+
content: output,
|
|
2752
|
+
},
|
|
2753
|
+
];
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
// Text-based mode: send tool results as user messages
|
|
2757
|
+
messages = [
|
|
2758
|
+
...messages,
|
|
2759
|
+
{
|
|
2760
|
+
role: 'user',
|
|
2761
|
+
content: formatToolResult(toolCall.function.name, output, result.success),
|
|
2762
|
+
},
|
|
2763
|
+
];
|
|
2764
|
+
}
|
|
2765
|
+
// Vision pipeline
|
|
2766
|
+
if (result.metadata?.base64_image) {
|
|
2767
|
+
messages = [
|
|
2768
|
+
...messages,
|
|
2769
|
+
{
|
|
2770
|
+
role: 'user',
|
|
2771
|
+
content: [
|
|
2772
|
+
{ type: 'text', text: `[Image captured by ${toolCall.function.name}]` },
|
|
2773
|
+
{ type: 'image_url', image_url: {
|
|
2774
|
+
url: `data:${result.metadata.mime_type || 'image/png'};base64,${result.metadata.base64_image}`,
|
|
2775
|
+
} },
|
|
2776
|
+
],
|
|
2777
|
+
},
|
|
2778
|
+
];
|
|
2779
|
+
}
|
|
2780
|
+
// Dynamic design context re-injection — same treatment as the parallel
|
|
2781
|
+
// batch path. Throttled by turn count and mtime cache.
|
|
2782
|
+
const uiPath = this.findUIFilePathInBatch([toolCall]);
|
|
2783
|
+
if (uiPath) {
|
|
2784
|
+
this.designReinjectionTurn++;
|
|
2785
|
+
const reinject = await maybeBuildDesignReinjection(runContext.cwd, uiPath, {
|
|
2786
|
+
currentTurn: this.designReinjectionTurn,
|
|
2787
|
+
lastInjectedTurn: this.designReinjectionLastTurn,
|
|
2788
|
+
lastMtimes: this.designReinjectionLastMtimes,
|
|
2789
|
+
});
|
|
2790
|
+
if (reinject) {
|
|
2791
|
+
messages = [
|
|
2792
|
+
...messages,
|
|
2793
|
+
{ role: 'user', content: reinject.content },
|
|
2794
|
+
];
|
|
2795
|
+
this.designReinjectionLastTurn = this.designReinjectionTurn;
|
|
2796
|
+
this.designReinjectionLastMtimes = reinject.updatedMtimes;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
// Exploration budget nudge — same as parallel path
|
|
2800
|
+
const seqNudge = this.maybeExplorationBudgetNudge([toolCall]);
|
|
2801
|
+
if (seqNudge) {
|
|
2802
|
+
messages = [
|
|
2803
|
+
...messages,
|
|
2804
|
+
{ role: 'user', content: seqNudge },
|
|
2805
|
+
];
|
|
2806
|
+
}
|
|
2807
|
+
return messages;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Scan a batch of tool calls for a UI file write/edit and return the first
|
|
2811
|
+
* matching file path. Returns undefined if no UI file was touched.
|
|
2812
|
+
* Used by the design context re-injection hook to decide whether to refresh
|
|
2813
|
+
* the Decisions/design/* content into the next LLM turn.
|
|
2814
|
+
*/
|
|
2815
|
+
findUIFilePathInBatch(toolCalls) {
|
|
2816
|
+
for (const tc of toolCalls) {
|
|
2817
|
+
if (tc.function.name !== 'file_write' && tc.function.name !== 'file_edit')
|
|
2818
|
+
continue;
|
|
2819
|
+
try {
|
|
2820
|
+
const args = JSON.parse(tc.function.arguments);
|
|
2821
|
+
const filePath = (args.file_path ?? args.path);
|
|
2822
|
+
if (filePath && isUIFilePathLocal(filePath))
|
|
2823
|
+
return filePath;
|
|
2824
|
+
}
|
|
2825
|
+
catch { /* malformed args — skip */ }
|
|
2826
|
+
}
|
|
2827
|
+
return undefined;
|
|
2828
|
+
}
|
|
2829
|
+
// ─── Task classification + exploration budget helpers ──────────────────
|
|
2830
|
+
/**
|
|
2831
|
+
* Walk the message array backwards to find the most recent user-role
|
|
2832
|
+
* message that represents a real user request (not a meta injection like
|
|
2833
|
+
* a memory brief or compression summary). Returns the text content, or
|
|
2834
|
+
* null if nothing qualifies.
|
|
2835
|
+
*/
|
|
2836
|
+
findLatestNonMetaUserMessage(messages) {
|
|
2837
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2838
|
+
const m = messages[i];
|
|
2839
|
+
if (m.role !== 'user')
|
|
2840
|
+
continue;
|
|
2841
|
+
const text = getTextContent(m.content);
|
|
2842
|
+
if (!text.trim())
|
|
2843
|
+
continue;
|
|
2844
|
+
if (isMetaPrefix(text))
|
|
2845
|
+
continue;
|
|
2846
|
+
return text;
|
|
2847
|
+
}
|
|
2848
|
+
return null;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Append text to the first system-role message's content, or prepend a
|
|
2852
|
+
* new system message if none exists. Used to merge the directness hint
|
|
2853
|
+
* into the session prompt without creating a separate system message
|
|
2854
|
+
* (which would break Qwen's "system must be at beginning" rule).
|
|
2855
|
+
*/
|
|
2856
|
+
appendToSystemMessage(messages, text) {
|
|
2857
|
+
if (messages.length > 0 && messages[0].role === 'system') {
|
|
2858
|
+
const existing = typeof messages[0].content === 'string' ? messages[0].content : '';
|
|
2859
|
+
return [
|
|
2860
|
+
{ ...messages[0], content: existing + text },
|
|
2861
|
+
...messages.slice(1),
|
|
2862
|
+
];
|
|
2863
|
+
}
|
|
2864
|
+
// No system message — prepend one
|
|
2865
|
+
return [
|
|
2866
|
+
{ role: 'system', content: text.trimStart() },
|
|
2867
|
+
...messages,
|
|
2868
|
+
];
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Classify a tool call as a read-only exploration call (file_read, glob,
|
|
2872
|
+
* grep, list_directory, find_symbol, project_index) vs a write/action
|
|
2873
|
+
* call (file_write, file_edit, bash, git_*, etc). Used by the exploration
|
|
2874
|
+
* budget tracker to count "reads before first write" for each run.
|
|
2875
|
+
*/
|
|
2876
|
+
isReadOnlyToolCall(name) {
|
|
2877
|
+
return (name === 'file_read' ||
|
|
2878
|
+
name === 'glob' ||
|
|
2879
|
+
name === 'grep' ||
|
|
2880
|
+
name === 'list_directory' ||
|
|
2881
|
+
name === 'find_symbol' ||
|
|
2882
|
+
name === 'project_index' ||
|
|
2883
|
+
name === 'git_status' ||
|
|
2884
|
+
name === 'git_diff' ||
|
|
2885
|
+
name === 'docs_lookup');
|
|
2886
|
+
}
|
|
2887
|
+
isWriteCapableToolCall(name) {
|
|
2888
|
+
return (name === 'file_write' ||
|
|
2889
|
+
name === 'file_edit' ||
|
|
2890
|
+
name === 'bash' ||
|
|
2891
|
+
name === 'git_commit' ||
|
|
2892
|
+
name === 'git_create_pr');
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* After each batch of tool calls, update the exploration budget state
|
|
2896
|
+
* and return a nudge message if the budget has been exceeded. The nudge
|
|
2897
|
+
* is a soft signal — it's injected into the next LLM call's context
|
|
2898
|
+
* telling the agent "you're stalling, commit to a direction." It never
|
|
2899
|
+
* hard-stops the run; graceful escalation is the design intent.
|
|
2900
|
+
*
|
|
2901
|
+
* Returns null if no nudge is needed, or the nudge message body if the
|
|
2902
|
+
* caller should inject it before the next turn.
|
|
2903
|
+
*/
|
|
2904
|
+
maybeExplorationBudgetNudge(toolCalls) {
|
|
2905
|
+
// Count this batch's reads + detect any writes
|
|
2906
|
+
let batchReads = 0;
|
|
2907
|
+
let batchHadWrite = false;
|
|
2908
|
+
for (const tc of toolCalls) {
|
|
2909
|
+
if (this.isReadOnlyToolCall(tc.function.name))
|
|
2910
|
+
batchReads++;
|
|
2911
|
+
if (this.isWriteCapableToolCall(tc.function.name))
|
|
2912
|
+
batchHadWrite = true;
|
|
2913
|
+
}
|
|
2914
|
+
// If she wrote at all, mark the run as "past the exploration phase"
|
|
2915
|
+
// and stop counting. The budget is specifically about read-before-write.
|
|
2916
|
+
if (batchHadWrite || this.hasWrittenInThisRun) {
|
|
2917
|
+
this.hasWrittenInThisRun = true;
|
|
2918
|
+
return null;
|
|
2919
|
+
}
|
|
2920
|
+
this.readCountBeforeFirstWrite += batchReads;
|
|
2921
|
+
// Don't re-fire the nudge once it's fired — one soft signal per run
|
|
2922
|
+
if (this.explorationNudgeFired)
|
|
2923
|
+
return null;
|
|
2924
|
+
const budget = COMPLEXITY_BUDGETS[this.currentTaskComplexity];
|
|
2925
|
+
if (this.readCountBeforeFirstWrite < budget.readCapBeforeFirstWrite)
|
|
2926
|
+
return null;
|
|
2927
|
+
this.explorationNudgeFired = true;
|
|
2928
|
+
logger.debug(`[agent] Exploration budget nudge: ${this.readCountBeforeFirstWrite} reads before first write (cap ${budget.readCapBeforeFirstWrite}) for ${this.currentTaskComplexity} task`);
|
|
2929
|
+
return [
|
|
2930
|
+
`[Exploration budget check — ${this.readCountBeforeFirstWrite} read-only tool calls and zero writes so far on a ${this.currentTaskComplexity} task.]`,
|
|
2931
|
+
'',
|
|
2932
|
+
`You're past the comfortable exploration window for this task size. Two honest options:`,
|
|
2933
|
+
` 1. You have enough context now — commit to a direction and make the change. Pick the most likely correct path and execute. You can always iterate.`,
|
|
2934
|
+
` 2. The task is actually bigger than it looked at first — say so clearly in your next response ("this looked focused but it needs broader changes because..."), then continue exploring with justification.`,
|
|
2935
|
+
'',
|
|
2936
|
+
`What you MUST NOT do: keep reading files silently. Either commit, or explain why you need more context. Stalling is the one unacceptable outcome.`,
|
|
2937
|
+
].join('\n');
|
|
2938
|
+
}
|
|
2939
|
+
// v3 graph integration (feedProceduralObserver / saveGraphState) was
|
|
2940
|
+
// removed from the hot path in the memory-cleanup sweep. Those features
|
|
2941
|
+
// read the live conversation and wrote derived state on every turn —
|
|
2942
|
+
// exactly the conversation→memory leak we are cutting. If procedural
|
|
2943
|
+
// learning and graph persistence come back, they belong in a scheduled
|
|
2944
|
+
// background job or an end-of-session hook, reading already-distilled
|
|
2945
|
+
// memory entries, not the live conversation.
|
|
2946
|
+
// ── Context usage ────────────────────────────────────────────────────────
|
|
2947
|
+
/** Get current context usage for a set of messages. */
|
|
2948
|
+
getContextUsage(messages) {
|
|
2949
|
+
const used = this.estimateTokenCount(messages);
|
|
2950
|
+
const limit = this.model.contextWindow;
|
|
2951
|
+
return { used, limit, percent: Math.round((used / limit) * 100) };
|
|
2952
|
+
}
|
|
2953
|
+
/** Manually compress context — triggered by user clicking the context bar. */
|
|
2954
|
+
async manualCompress(messages, onEvent, signal) {
|
|
2955
|
+
return this.compressContext(messages, onEvent, signal);
|
|
2956
|
+
}
|
|
2957
|
+
// ── Context compression ──────────────────────────────────────────────────
|
|
2958
|
+
/**
|
|
2959
|
+
* Compress conversation context by summarizing older messages.
|
|
2960
|
+
* Keeps the system prompt and last 8 messages (4 user-assistant exchanges)
|
|
2961
|
+
* verbatim, summarizes everything in between using the model.
|
|
2962
|
+
* Falls back silently if the compression API call fails.
|
|
2963
|
+
*/
|
|
2964
|
+
async compressContext(messages, onEvent, signal) {
|
|
2965
|
+
onEvent({ type: 'context_compression_start' });
|
|
2966
|
+
const systemMsg = messages[0]?.role === 'system' ? messages[0] : null;
|
|
2967
|
+
const rest = systemMsg ? messages.slice(1) : [...messages];
|
|
2968
|
+
// Keep last 8 messages verbatim (4 exchange pairs) for better continuity
|
|
2969
|
+
const KEEP_RECENT = 8;
|
|
2970
|
+
if (rest.length <= KEEP_RECENT) {
|
|
2971
|
+
onEvent({ type: 'context_compression_end', originalTokens: 0, compressedTokens: 0 });
|
|
2972
|
+
return messages;
|
|
2973
|
+
}
|
|
2974
|
+
// ── Preserve the pinned original user task ─────────────────────────
|
|
2975
|
+
// The first real user message (not a meta injection like a memory brief
|
|
2976
|
+
// or compression summary) is the root intent of the whole session. It
|
|
2977
|
+
// must survive every compression pass or the post-compression agent
|
|
2978
|
+
// loses its sense of "what am I doing here" and fresh-greets the user.
|
|
2979
|
+
//
|
|
2980
|
+
// We find it by walking the pre-slice messages, and if it falls in the
|
|
2981
|
+
// compress zone (not already in the recent window), we pin it to be
|
|
2982
|
+
// re-added after the summary.
|
|
2983
|
+
const pinnedIdxInMessages = findOriginalUserTaskIndex(messages);
|
|
2984
|
+
const pinnedMessage = pinnedIdxInMessages !== -1 ? messages[pinnedIdxInMessages] : null;
|
|
2985
|
+
const pinnedIsInRecentWindow = pinnedIdxInMessages !== -1
|
|
2986
|
+
&& pinnedIdxInMessages >= messages.length - KEEP_RECENT;
|
|
2987
|
+
const toCompress = rest.slice(0, -KEEP_RECENT);
|
|
2988
|
+
const toKeep = rest.slice(-KEEP_RECENT);
|
|
2989
|
+
// Build the text to summarize. Tool-role bodies are already trimmed to
|
|
2990
|
+
// ~200 chars by trimOldToolResults before we get here (token economy), so
|
|
2991
|
+
// the summariser sees each tool result's head, not raw JSON. Full tool
|
|
2992
|
+
// outputs remain in the persisted transcript and are retrievable via
|
|
2993
|
+
// conversation_recall — the backstop, not the summary, is the place for
|
|
2994
|
+
// exact tool detail.
|
|
2995
|
+
const transcript = toCompress
|
|
2996
|
+
.map((m) => {
|
|
2997
|
+
const text = getTextContent(m.content);
|
|
2998
|
+
return `[${m.role}]: ${text || '(no text)'}`;
|
|
2999
|
+
})
|
|
3000
|
+
.join('\n');
|
|
3001
|
+
const compressionPrompt = `You are a conversation summarizer preparing a handoff for an AI agent that will continue the work. The agent will have zero memory of this transcript except for what you produce, so your summary must be structured and decision-focused, not narrative.
|
|
3002
|
+
|
|
3003
|
+
Produce your output in EXACTLY this format:
|
|
3004
|
+
|
|
3005
|
+
CURRENT_TASK: <one sentence describing what the agent was actively working on at the end of the transcript. This is the single most important field — the agent uses it to decide what to do next. If multiple tasks were interleaved, pick the one that was most recently in flight.>
|
|
3006
|
+
|
|
3007
|
+
LAST_STEP: <one sentence describing the most recent concrete action the agent completed. Example: "Wrote src/components/HabitTracker.tsx with Tauri invoke calls for get_habit_logs."</ >
|
|
3008
|
+
|
|
3009
|
+
NEXT_STEP: <one sentence describing what the agent should do next to continue the task. Example: "Fix the missing habitId argument being passed to get_habit_logs in App.tsx." >
|
|
3010
|
+
|
|
3011
|
+
BLOCKERS: <any active blockers the agent needs to know about. Write "none" if there are none.>
|
|
3012
|
+
|
|
3013
|
+
SUMMARY:
|
|
3014
|
+
<Free-form bullet-point summary of everything else worth preserving: key decisions, file paths, function names, tool results, errors and how they were resolved, technical context. Be thorough but concise. Do NOT repeat what you put in the structured fields above.>
|
|
3015
|
+
|
|
3016
|
+
Rules:
|
|
3017
|
+
- Every field above is MANDATORY. If you can't extract a value for one, write "unclear" but never omit the field.
|
|
3018
|
+
- No pleasantries, no meta-commentary, no "Here's the summary" preamble.
|
|
3019
|
+
- Use plain text in the structured fields — no markdown, no bullet points, no multi-line values.
|
|
3020
|
+
- Keep the CURRENT_TASK, LAST_STEP, NEXT_STEP fields to a single sentence each.
|
|
3021
|
+
|
|
3022
|
+
TRANSCRIPT:
|
|
3023
|
+
${transcript}`;
|
|
3024
|
+
// Scale the summary budget to how much is being compressed. A flat cap
|
|
3025
|
+
// under-summarises a large zone — 1500 tokens for a 300K-token compress
|
|
3026
|
+
// zone loses real fidelity. Proportional (~8%) keeps detail where there's
|
|
3027
|
+
// a lot to keep, with a 1500 floor (the old default, fine for small zones)
|
|
3028
|
+
// and a 4000 ceiling so the summary can't itself bloat the context it's
|
|
3029
|
+
// meant to shrink.
|
|
3030
|
+
const compressedTokens = this.estimateTokenCount(toCompress);
|
|
3031
|
+
const summaryBudget = Math.min(4000, Math.max(1500, Math.floor(compressedTokens / 12)));
|
|
3032
|
+
try {
|
|
3033
|
+
const response = await this.provider.createCompletion({
|
|
3034
|
+
model: this.model.id,
|
|
3035
|
+
messages: [
|
|
3036
|
+
{ role: 'system', content: 'You are a precise conversation summarizer.' },
|
|
3037
|
+
{ role: 'user', content: compressionPrompt },
|
|
3038
|
+
],
|
|
3039
|
+
// Compression is a real cost the turn incurred, so it belongs to the
|
|
3040
|
+
// turn. Leaving it untagged would quietly understate what a long
|
|
3041
|
+
// conversation actually costs to answer.
|
|
3042
|
+
turnId: this.runTurnId,
|
|
3043
|
+
max_tokens: summaryBudget,
|
|
3044
|
+
temperature: 0.2,
|
|
3045
|
+
}, signal);
|
|
3046
|
+
// Meter the compression call. It's a heavy-model completion the same
|
|
3047
|
+
// shape as a chat turn, just summarising rather than answering a user.
|
|
3048
|
+
chargeCredits('chat_turn', {
|
|
3049
|
+
model: this.model.id,
|
|
3050
|
+
rawTokens: extractUsage(response.usage),
|
|
3051
|
+
});
|
|
3052
|
+
const summary = response.choices?.[0]?.message?.content || '';
|
|
3053
|
+
if (!summary)
|
|
3054
|
+
throw new Error('Empty compression response');
|
|
3055
|
+
// Memory policy: see notes at the earlier compression site. The
|
|
3056
|
+
// compression summary lives in the conversation history (persisted
|
|
3057
|
+
// per-conversation) — it does NOT get pushed into user or project
|
|
3058
|
+
// memory. Previously this block ran reflectAndSave on the compressed
|
|
3059
|
+
// messages and dumped the raw summary as a project memory entry.
|
|
3060
|
+
// Both paths created a conversation→memory feedback loop where
|
|
3061
|
+
// earlier turns' text got re-injected via memory on later turns,
|
|
3062
|
+
// shaping new responses, which then got saved again. Memory should
|
|
3063
|
+
// be durable user/project facts the model (or user) explicitly
|
|
3064
|
+
// chose to persist — not a rolling transcript of the conversation.
|
|
3065
|
+
// ── Build the continuation-first summary message ────────────────
|
|
3066
|
+
// Extract structured CURRENT_TASK / LAST_STEP / NEXT_STEP / BLOCKERS
|
|
3067
|
+
// fields from the summariser's output. The summariser prompt asks
|
|
3068
|
+
// for these explicitly but LLMs paraphrase — the parser is lenient.
|
|
3069
|
+
const structured = extractStructuredFields(summary);
|
|
3070
|
+
const continuationHeader = buildCompressionContinuationHeader(summary, structured);
|
|
3071
|
+
const summaryMessage = {
|
|
3072
|
+
role: 'user',
|
|
3073
|
+
content: continuationHeader,
|
|
3074
|
+
};
|
|
3075
|
+
// ── Build the session-tasks re-injection block ──────────────────
|
|
3076
|
+
// If the TaskManager has active session tasks, format them as a
|
|
3077
|
+
// continuation-focused block for direct injection into the
|
|
3078
|
+
// post-compression context. This is the single biggest signal that
|
|
3079
|
+
// stops the agent from treating compression as a fresh chat.
|
|
3080
|
+
let sessionTasksMessage = null;
|
|
3081
|
+
try {
|
|
3082
|
+
const tm = this.toolContext.sharedState?.taskManager;
|
|
3083
|
+
if (tm && typeof tm.getSessionTasks === 'function') {
|
|
3084
|
+
const tasks = tm.getSessionTasks();
|
|
3085
|
+
const block = formatSessionTasksBlock(tasks);
|
|
3086
|
+
if (block) {
|
|
3087
|
+
sessionTasksMessage = { role: 'user', content: block };
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
catch {
|
|
3092
|
+
/* non-critical — proceed without the task block */
|
|
3093
|
+
}
|
|
3094
|
+
// ── Fold original task into the system prompt instead of replaying ──
|
|
3095
|
+
// Previously this prepended the original user message verbatim as
|
|
3096
|
+
// a user-role message — which the model then treated as a freshly
|
|
3097
|
+
// sent user turn ("acts on the initial message again" bug). Fix:
|
|
3098
|
+
// merge the original task text into the system message as a
|
|
3099
|
+
// reference note. The model sees what the task was without
|
|
3100
|
+
// interpreting its replay as a new request.
|
|
3101
|
+
let pinnedNote = '';
|
|
3102
|
+
if (pinnedMessage && !pinnedIsInRecentWindow) {
|
|
3103
|
+
const pinnedText = getTextContent(pinnedMessage.content);
|
|
3104
|
+
if (pinnedText) {
|
|
3105
|
+
pinnedNote = `\n\n[Original request at session start] "${pinnedText.slice(0, 800)}" — context for what the user initially asked. You were already in the middle of working on this; continue from where you left off. Do NOT treat this as a new request.`;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
const fixedTail = this.fixToolPairing(toKeep);
|
|
3109
|
+
// Assembly order — simplified to remove the replayed original user
|
|
3110
|
+
// message. The continuation header (summaryMessage) now carries
|
|
3111
|
+
// CURRENT_TASK / LAST_STEP / NEXT_STEP, and the system prompt
|
|
3112
|
+
// carries the original-request note. The model reads top to bottom
|
|
3113
|
+
// and gets the full context without seeing what looks like a new
|
|
3114
|
+
// user turn.
|
|
3115
|
+
const middle = [summaryMessage];
|
|
3116
|
+
// Keep the user's own turns from the compress zone verbatim — the
|
|
3117
|
+
// summariser paraphrases them, and their exact words are the truest
|
|
3118
|
+
// record of intent. Framed as historical reference, not new requests.
|
|
3119
|
+
const verbatimUserTurns = buildVerbatimUserTurnsBlock(toCompress);
|
|
3120
|
+
if (verbatimUserTurns)
|
|
3121
|
+
middle.push(verbatimUserTurns);
|
|
3122
|
+
if (sessionTasksMessage)
|
|
3123
|
+
middle.push(sessionTasksMessage);
|
|
3124
|
+
const enrichedSystem = systemMsg
|
|
3125
|
+
? { ...systemMsg, content: (typeof systemMsg.content === 'string' ? systemMsg.content : '') + pinnedNote }
|
|
3126
|
+
: (pinnedNote ? { role: 'system', content: pinnedNote.trimStart() } : null);
|
|
3127
|
+
const result = enrichedSystem
|
|
3128
|
+
? [enrichedSystem, ...middle, ...fixedTail]
|
|
3129
|
+
: [...middle, ...fixedTail];
|
|
3130
|
+
const originalTokens = this.estimateTokenCount(messages);
|
|
3131
|
+
const compressedTokens = this.estimateTokenCount(result);
|
|
3132
|
+
onEvent({ type: 'context_compression_end', originalTokens, compressedTokens });
|
|
3133
|
+
// Emit updated context usage so UI bars refresh after compression
|
|
3134
|
+
const newPercent = Math.round((compressedTokens / this.model.contextWindow) * 100);
|
|
3135
|
+
onEvent({
|
|
3136
|
+
type: 'context_usage',
|
|
3137
|
+
context: { used: compressedTokens, limit: this.model.contextWindow, percent: newPercent },
|
|
3138
|
+
});
|
|
3139
|
+
return result;
|
|
3140
|
+
}
|
|
3141
|
+
catch {
|
|
3142
|
+
// Compression failed — fall back silently (caller will truncate if needed)
|
|
3143
|
+
onEvent({ type: 'context_compression_end', originalTokens: 0, compressedTokens: 0 });
|
|
3144
|
+
return messages;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
// ── Token estimation ──────────────────────────────────────────────────────
|
|
3148
|
+
static estimateTextTokens(text) {
|
|
3149
|
+
// Conservative: uses length/3 (not length/4) because code, JSON, and
|
|
3150
|
+
// tool results tokenize at ~2.5-3 chars per token.
|
|
3151
|
+
return Math.ceil(text.length / 3);
|
|
3152
|
+
}
|
|
3153
|
+
estimateMessageTokens(msg) {
|
|
3154
|
+
let tokens = 4; // message overhead (role, separators)
|
|
3155
|
+
const { content } = msg;
|
|
3156
|
+
if (content === null) {
|
|
3157
|
+
// no content
|
|
3158
|
+
}
|
|
3159
|
+
else if (typeof content === 'string') {
|
|
3160
|
+
tokens += Agent.estimateTextTokens(content);
|
|
3161
|
+
}
|
|
3162
|
+
else {
|
|
3163
|
+
for (const part of content) {
|
|
3164
|
+
if (part.type === 'text')
|
|
3165
|
+
tokens += Agent.estimateTextTokens(part.text);
|
|
3166
|
+
else if (part.type === 'image_url')
|
|
3167
|
+
tokens += 85;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
// Count tool calls in assistant messages (function name + JSON arguments)
|
|
3171
|
+
const toolCalls = msg.tool_calls;
|
|
3172
|
+
if (toolCalls) {
|
|
3173
|
+
for (const tc of toolCalls) {
|
|
3174
|
+
tokens += Agent.estimateTextTokens(tc.function.name) + Agent.estimateTextTokens(tc.function.arguments) + 8;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
return tokens;
|
|
3178
|
+
}
|
|
3179
|
+
/** Estimate total token count across an array of messages. */
|
|
3180
|
+
estimateTokenCount(messages) {
|
|
3181
|
+
return messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
|
|
3182
|
+
}
|
|
3183
|
+
// ── Tool result trimming ────────────────────────────────────────────────
|
|
3184
|
+
/**
|
|
3185
|
+
* Collapse old tool results to save tokens. Tool outputs older than
|
|
3186
|
+
* KEEP_RECENT messages get trimmed to 200 chars + a note.
|
|
3187
|
+
* This prevents token bleed from accumulated file reads, grep results, etc.
|
|
3188
|
+
*/
|
|
3189
|
+
/**
|
|
3190
|
+
* Trim older messages for token-cost control, preserving everything that
|
|
3191
|
+
* matters for continuity:
|
|
3192
|
+
* - The system prompt is never touched.
|
|
3193
|
+
* - The pinned original user task is preserved verbatim (it's the root
|
|
3194
|
+
* intent of the whole session and must survive every trim pass).
|
|
3195
|
+
* - The last 8 messages are kept verbatim for recent context.
|
|
3196
|
+
* - `tool`-role messages older than the recent window are trimmed to
|
|
3197
|
+
* MAX_OLD_TOOL_CHARS (very aggressive — 200 chars — because tool
|
|
3198
|
+
* outputs rarely matter in full once the next turn has consumed them).
|
|
3199
|
+
* - `user` and `assistant` message bodies older than the recent window
|
|
3200
|
+
* get trimmed if they exceed OLD_MESSAGE_BODY_MAX_CHARS. The structural
|
|
3201
|
+
* "who said what" stays intact but verbose inlined content gets cut.
|
|
3202
|
+
* - `reasoning_content` on old plain-text assistant messages is stripped
|
|
3203
|
+
* entirely. Reasoning is working memory for the turn that produced it
|
|
3204
|
+
* and has zero value once the next turn has landed — but it can be
|
|
3205
|
+
* 10x larger than the actual response and was previously kept forever.
|
|
3206
|
+
* Tool-calling assistant turns are an exception: DeepSeek V4 thinking
|
|
3207
|
+
* mode requires reasoning_content to be re-sent on every subsequent
|
|
3208
|
+
* request that follows a tool call, so it stays put on those.
|
|
3209
|
+
*
|
|
3210
|
+
* This is the primary lever for keeping per-turn token cost in check on
|
|
3211
|
+
* long sessions. Combined with the earlier compression trigger (40%
|
|
3212
|
+
* instead of 70%), it dramatically reduces the cost of running an agent
|
|
3213
|
+
* for 60+ minutes on a single conversation.
|
|
3214
|
+
*/
|
|
3215
|
+
trimOldToolResults(messages) {
|
|
3216
|
+
const KEEP_RECENT = 8; // Keep last 8 messages at full size
|
|
3217
|
+
const MAX_OLD_TOOL_CHARS = 200;
|
|
3218
|
+
if (messages.length <= KEEP_RECENT + 1)
|
|
3219
|
+
return messages; // +1 for system
|
|
3220
|
+
const cutoff = messages.length - KEEP_RECENT;
|
|
3221
|
+
const pinnedIdx = findOriginalUserTaskIndex(messages);
|
|
3222
|
+
return messages.map((m, i) => {
|
|
3223
|
+
// Never touch the system prompt or messages in the recent window
|
|
3224
|
+
if (i === 0 || i >= cutoff)
|
|
3225
|
+
return m;
|
|
3226
|
+
// Never touch the pinned original user task — it's the root of the
|
|
3227
|
+
// whole session and must survive every trim pass
|
|
3228
|
+
if (i === pinnedIdx)
|
|
3229
|
+
return m;
|
|
3230
|
+
// ── Tool-role trimming (most aggressive) ─────────────────────────
|
|
3231
|
+
if (m.role === 'tool' && typeof m.content === 'string') {
|
|
3232
|
+
if (m.content.length <= MAX_OLD_TOOL_CHARS)
|
|
3233
|
+
return m;
|
|
3234
|
+
return {
|
|
3235
|
+
...m,
|
|
3236
|
+
content: m.content.slice(0, MAX_OLD_TOOL_CHARS) + `\n\n[Trimmed — original ${m.content.length} chars]`,
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
// ── Assistant-role: strip reasoning_content + trim body ───────────
|
|
3240
|
+
if (m.role === 'assistant') {
|
|
3241
|
+
const assistantMsg = m;
|
|
3242
|
+
const hasReasoning = assistantMsg.reasoning_content !== undefined && assistantMsg.reasoning_content !== null;
|
|
3243
|
+
const textContent = typeof assistantMsg.content === 'string' ? assistantMsg.content : null;
|
|
3244
|
+
const needsBodyTrim = textContent !== null && textContent.length > OLD_MESSAGE_BODY_MAX_CHARS;
|
|
3245
|
+
// DeepSeek V4 thinking-mode rule: assistant turns that produced
|
|
3246
|
+
// tool_calls MUST keep their reasoning_content in every subsequent
|
|
3247
|
+
// request, or the API rejects with 400 "reasoning_content in the
|
|
3248
|
+
// thinking mode must be passed back". Plain-text assistant turns
|
|
3249
|
+
// can still drop it (the field is ignored on those by DeepSeek and
|
|
3250
|
+
// by every other provider).
|
|
3251
|
+
const hasToolCalls = Array.isArray(assistantMsg.tool_calls) && assistantMsg.tool_calls.length > 0;
|
|
3252
|
+
const stripReasoning = hasReasoning && !hasToolCalls;
|
|
3253
|
+
if (!stripReasoning && !needsBodyTrim)
|
|
3254
|
+
return m;
|
|
3255
|
+
const trimmed = {
|
|
3256
|
+
...assistantMsg,
|
|
3257
|
+
// Reasoning is stripped from old plain-text turns (zero value once
|
|
3258
|
+
// the next turn is live, and often the biggest single allocation
|
|
3259
|
+
// in a long conversation's token budget) but PRESERVED on
|
|
3260
|
+
// tool-calling turns for DeepSeek V4 multi-turn correctness.
|
|
3261
|
+
...(stripReasoning ? { reasoning_content: null } : {}),
|
|
3262
|
+
// Body is trimmed only if it's over threshold
|
|
3263
|
+
content: needsBodyTrim && textContent !== null
|
|
3264
|
+
? trimMessageBody(textContent)
|
|
3265
|
+
: assistantMsg.content,
|
|
3266
|
+
};
|
|
3267
|
+
return trimmed;
|
|
3268
|
+
}
|
|
3269
|
+
// ── User-role: trim long bodies (skip meta-prefixed messages) ─────
|
|
3270
|
+
if (m.role === 'user' && typeof m.content === 'string') {
|
|
3271
|
+
// Don't trim meta-prefixed messages (compression summaries, memory
|
|
3272
|
+
// briefs, system notices, task blocks) — their headers matter and
|
|
3273
|
+
// they're usually already short enough anyway.
|
|
3274
|
+
if (isMetaPrefix(m.content))
|
|
3275
|
+
return m;
|
|
3276
|
+
if (m.content.length <= OLD_MESSAGE_BODY_MAX_CHARS)
|
|
3277
|
+
return m;
|
|
3278
|
+
return { ...m, content: trimMessageBody(m.content) };
|
|
3279
|
+
}
|
|
3280
|
+
return m;
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
// ── Truncation ──────────────────────────────────────────────────────────
|
|
3284
|
+
truncateMessages(messages, maxTokens) {
|
|
3285
|
+
const total = messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
|
|
3286
|
+
if (total <= maxTokens)
|
|
3287
|
+
return messages;
|
|
3288
|
+
// Keep system prompt (first message) and trim from the beginning of the rest
|
|
3289
|
+
const systemMsg = messages[0]?.role === 'system' ? messages[0] : null;
|
|
3290
|
+
const rest = systemMsg ? messages.slice(1) : [...messages];
|
|
3291
|
+
const systemTokens = systemMsg ? this.estimateMessageTokens(systemMsg) : 0;
|
|
3292
|
+
// ── Preserve the pinned original user task ──────────────────────
|
|
3293
|
+
// Same reasoning as compression paths: the root intent of the session
|
|
3294
|
+
// must survive even emergency truncation. We reserve tokens for it
|
|
3295
|
+
// upfront and then fill the rest of the budget from the most recent
|
|
3296
|
+
// messages backwards.
|
|
3297
|
+
const pinnedIdx = findOriginalUserTaskIndex(messages);
|
|
3298
|
+
const pinnedMsg = pinnedIdx !== -1 ? messages[pinnedIdx] : null;
|
|
3299
|
+
const pinnedTokens = pinnedMsg ? this.estimateMessageTokens(pinnedMsg) : 0;
|
|
3300
|
+
const budget = maxTokens - systemTokens - pinnedTokens;
|
|
3301
|
+
const kept = [];
|
|
3302
|
+
let used = 0;
|
|
3303
|
+
for (let i = rest.length - 1; i >= 0; i--) {
|
|
3304
|
+
// Skip the pinned message during the backward walk — it will be
|
|
3305
|
+
// re-inserted at the pinned slot at the end. Including it twice
|
|
3306
|
+
// would double-charge its tokens and confuse the final order.
|
|
3307
|
+
if (pinnedMsg && rest[i] === pinnedMsg)
|
|
3308
|
+
continue;
|
|
3309
|
+
const msgTokens = this.estimateMessageTokens(rest[i]);
|
|
3310
|
+
if (used + msgTokens > budget)
|
|
3311
|
+
break;
|
|
3312
|
+
kept.unshift(rest[i]);
|
|
3313
|
+
used += msgTokens;
|
|
3314
|
+
}
|
|
3315
|
+
// Fix orphaned tool messages — if truncation cut in the middle of a
|
|
3316
|
+
// tool call/result sequence, the kept list may start with `tool` messages
|
|
3317
|
+
// that reference a dropped assistant message. The API rejects these.
|
|
3318
|
+
// Also drop any assistant messages whose tool_calls lost their results.
|
|
3319
|
+
const fixed = this.fixToolPairing(kept);
|
|
3320
|
+
// Fold the original task into the system prompt as a reference note
|
|
3321
|
+
// instead of re-injecting it as a user message. Re-injection made the
|
|
3322
|
+
// model treat the replay as a fresh user turn ("acts on initial
|
|
3323
|
+
// message again"). A system-prompt note preserves the task context
|
|
3324
|
+
// without the new-input signal.
|
|
3325
|
+
let enrichedSystem = systemMsg;
|
|
3326
|
+
if (pinnedMsg && systemMsg) {
|
|
3327
|
+
const pinnedText = getTextContent(pinnedMsg.content);
|
|
3328
|
+
if (pinnedText) {
|
|
3329
|
+
const pinnedNote = `\n\n[Original request at session start] "${pinnedText.slice(0, 800)}" — context for what the user initially asked. You were already in the middle of working on this; continue from where you left off. Do NOT treat this as a new request.`;
|
|
3330
|
+
enrichedSystem = { ...systemMsg, content: (typeof systemMsg.content === 'string' ? systemMsg.content : '') + pinnedNote };
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return enrichedSystem ? [enrichedSystem, ...fixed] : fixed;
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* Detect the "first turn after Stop" pattern and strip prior conversation.
|
|
3337
|
+
*
|
|
3338
|
+
* When cancelRun() fires in the extension, a marker is pushed into the
|
|
3339
|
+
* conversation: `[User pressed Stop — previous task terminated...]`.
|
|
3340
|
+
* After the user's next real message, the conversation looks like:
|
|
3341
|
+
*
|
|
3342
|
+
* [... prior task, maybe 150K tokens ...]
|
|
3343
|
+
* [User pressed Stop — ...] (meta user message)
|
|
3344
|
+
* Actual new user message
|
|
3345
|
+
*
|
|
3346
|
+
* The prior 150K tokens are dead weight. Sending them back means the
|
|
3347
|
+
* model re-draws on the task the user told us to abandon. The fix is
|
|
3348
|
+
* to detect this pattern and return a restricted message array:
|
|
3349
|
+
*
|
|
3350
|
+
* [system prompt + stop directive]
|
|
3351
|
+
* [new user message]
|
|
3352
|
+
*
|
|
3353
|
+
* Detection: find the most recent stop marker. If it exists AND there
|
|
3354
|
+
* is exactly one non-meta user message after it (and the assistant
|
|
3355
|
+
* hasn't yet responded to that message), we're in the first post-stop
|
|
3356
|
+
* turn — restrict.
|
|
3357
|
+
*
|
|
3358
|
+
* If more messages exist after that point (assistant replies, tool
|
|
3359
|
+
* results), we've already handled the first post-stop turn normally;
|
|
3360
|
+
* further turns see full context and operate as normal.
|
|
3361
|
+
*/
|
|
3362
|
+
maybeRestrictPostStopContext(messages) {
|
|
3363
|
+
// Find the most recent stop marker (a user-role message starting with the marker prefix).
|
|
3364
|
+
let markerIdx = -1;
|
|
3365
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3366
|
+
const m = messages[i];
|
|
3367
|
+
if (m.role !== 'user')
|
|
3368
|
+
continue;
|
|
3369
|
+
const text = getTextContent(m.content);
|
|
3370
|
+
if (text.trimStart().startsWith('[User pressed Stop')) {
|
|
3371
|
+
markerIdx = i;
|
|
3372
|
+
break;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
if (markerIdx < 0)
|
|
3376
|
+
return messages;
|
|
3377
|
+
// Everything after the marker must be: exactly one non-meta user
|
|
3378
|
+
// message, nothing else. If there's a REAL assistant reply after the
|
|
3379
|
+
// marker, we're past the first post-stop turn — don't restrict.
|
|
3380
|
+
//
|
|
3381
|
+
// Critical edge case: if the abort fired mid-stream, an empty or
|
|
3382
|
+
// partial assistant message can sit in the transcript (assistant
|
|
3383
|
+
// started speaking, stop fired, no content). That half-message must
|
|
3384
|
+
// NOT count as a "real response" or the restriction bails and all
|
|
3385
|
+
// pre-stop context leaks into the next turn. An empty string, an
|
|
3386
|
+
// empty content parts array, or pure whitespace all count as "no
|
|
3387
|
+
// real response" for this purpose.
|
|
3388
|
+
const afterMarker = messages.slice(markerIdx + 1);
|
|
3389
|
+
const nonMetaUsers = afterMarker.filter(m => m.role === 'user' && !isMetaPrefix(getTextContent(m.content)));
|
|
3390
|
+
const hasRealAssistantResponse = afterMarker.some(m => {
|
|
3391
|
+
if (m.role !== 'assistant')
|
|
3392
|
+
return false;
|
|
3393
|
+
const text = getTextContent(m.content);
|
|
3394
|
+
if (text.trim().length > 0)
|
|
3395
|
+
return true;
|
|
3396
|
+
const hasToolCalls = Array.isArray(m.tool_calls)
|
|
3397
|
+
&& m.tool_calls.length > 0;
|
|
3398
|
+
return hasToolCalls;
|
|
3399
|
+
});
|
|
3400
|
+
if (nonMetaUsers.length !== 1 || hasRealAssistantResponse) {
|
|
3401
|
+
return messages;
|
|
3402
|
+
}
|
|
3403
|
+
const newUserMessage = nonMetaUsers[0];
|
|
3404
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
3405
|
+
// Merge the stop directive into the system prompt so the model sees
|
|
3406
|
+
// it at the highest-authority layer, not as a floating user message
|
|
3407
|
+
// (which some providers reject when followed by another user msg).
|
|
3408
|
+
const stopDirective = `\n\n[Post-stop context] The previous task was terminated by the user. Do not resume it. Treat the user's message below as a fresh request on its own terms. If they reference prior work ambiguously (e.g. "fix that", "continue"), ask them to be specific — you do not have the prior context and should not assume.`;
|
|
3409
|
+
const restrictedSystem = systemMsg
|
|
3410
|
+
? { ...systemMsg, content: (typeof systemMsg.content === 'string' ? systemMsg.content : '') + stopDirective }
|
|
3411
|
+
: { role: 'system', content: stopDirective.trimStart() };
|
|
3412
|
+
logger.debug(`[agent] Post-stop context restriction: dropped ${messages.length - 2} prior messages, keeping system + new user message`);
|
|
3413
|
+
return restrictedSystem ? [restrictedSystem, newUserMessage] : [newUserMessage];
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Age out old images and old tool results to cut per-turn token cost.
|
|
3417
|
+
*
|
|
3418
|
+
* Keeps the N most recent image-bearing user messages and the M most
|
|
3419
|
+
* recent tool results in full fidelity. Older ones become text-only
|
|
3420
|
+
* placeholders. The assistant's prior reasoning about the content
|
|
3421
|
+
* remains intact in the assistant messages — we're just dropping raw
|
|
3422
|
+
* payloads that the model no longer needs pixel-for-pixel.
|
|
3423
|
+
*
|
|
3424
|
+
* Why this exists: a single 25K-token screenshot re-sent across 10
|
|
3425
|
+
* turns burns 250K tokens for no informational gain after turn 2 or 3.
|
|
3426
|
+
* Same applies to verbose bash/file_read output: after the assistant
|
|
3427
|
+
* has reasoned about it, we don't need the full dump in context
|
|
3428
|
+
* anymore. This is the single biggest lever on token consumption in
|
|
3429
|
+
* the agent loop. Conservative keep counts (2 images, 5 tool results)
|
|
3430
|
+
* preserve enough active context for normal multi-step work while
|
|
3431
|
+
* eliminating the long tail of stale payloads.
|
|
3432
|
+
*/
|
|
3433
|
+
ageHistoryContent(messages) {
|
|
3434
|
+
const KEEP_RECENT_IMAGES = 2;
|
|
3435
|
+
// DIAGNOSTIC: tool-result trimming disabled while we confirm it's
|
|
3436
|
+
// corrupting Qwen's function-calling expectations. Leaving trimmed
|
|
3437
|
+
// summaries in role:'tool' messages appears to make the model emit
|
|
3438
|
+
// malformed tool_calls (empty function.name) and fall back to
|
|
3439
|
+
// text-format tool calls. Image trimming stays on (biggest savings,
|
|
3440
|
+
// doesn't touch tool-pair structure). If disabling this resolves
|
|
3441
|
+
// the empty-name tool_call regression, the real fix is to collapse
|
|
3442
|
+
// old tool_call + tool_result pairs into a single assistant text
|
|
3443
|
+
// summary rather than leaving orphan-style summaries in tool slots.
|
|
3444
|
+
const KEEP_RECENT_TOOL_RESULTS = Number.POSITIVE_INFINITY;
|
|
3445
|
+
const TOOL_RESULT_TRIM_THRESHOLD = 300;
|
|
3446
|
+
// Walk backward to find indices of the N most recent image-bearing
|
|
3447
|
+
// user messages and tool results.
|
|
3448
|
+
const imageIndicesSeen = [];
|
|
3449
|
+
const toolIndicesSeen = [];
|
|
3450
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3451
|
+
const m = messages[i];
|
|
3452
|
+
if (m.role === 'user' && Array.isArray(m.content)) {
|
|
3453
|
+
const hasImage = m.content.some(p => p.type === 'image_url');
|
|
3454
|
+
if (hasImage)
|
|
3455
|
+
imageIndicesSeen.push(i);
|
|
3456
|
+
}
|
|
3457
|
+
if (m.role === 'tool')
|
|
3458
|
+
toolIndicesSeen.push(i);
|
|
3459
|
+
}
|
|
3460
|
+
const imageIndicesToStrip = new Set(imageIndicesSeen.slice(KEEP_RECENT_IMAGES));
|
|
3461
|
+
const toolIndicesToTrim = new Set(toolIndicesSeen.slice(KEEP_RECENT_TOOL_RESULTS));
|
|
3462
|
+
if (imageIndicesToStrip.size === 0 && toolIndicesToTrim.size === 0) {
|
|
3463
|
+
return messages;
|
|
3464
|
+
}
|
|
3465
|
+
return messages.map((m, i) => {
|
|
3466
|
+
// Strip image payload from old image-bearing user messages.
|
|
3467
|
+
if (imageIndicesToStrip.has(i) && Array.isArray(m.content)) {
|
|
3468
|
+
const textParts = m.content
|
|
3469
|
+
.filter(p => p.type === 'text')
|
|
3470
|
+
.map(p => ('text' in p ? p.text : ''))
|
|
3471
|
+
.filter(Boolean)
|
|
3472
|
+
.join(' ')
|
|
3473
|
+
.trim();
|
|
3474
|
+
const placeholder = textParts
|
|
3475
|
+
? `${textParts} — image discarded from history to save context. Re-capture if you need to see it again.`
|
|
3476
|
+
: '[Image previously captured — discarded from history to save context. Re-capture if needed.]';
|
|
3477
|
+
return { ...m, content: placeholder };
|
|
3478
|
+
}
|
|
3479
|
+
// Trim old tool results to a short summary.
|
|
3480
|
+
if (toolIndicesToTrim.has(i) && typeof m.content === 'string' && m.content.length > TOOL_RESULT_TRIM_THRESHOLD) {
|
|
3481
|
+
const preview = m.content.slice(0, 160).replace(/\s+/g, ' ').trim();
|
|
3482
|
+
const toolName = m.name || 'tool';
|
|
3483
|
+
return {
|
|
3484
|
+
...m,
|
|
3485
|
+
content: `[${toolName} result from earlier turn — ${m.content.length} chars trimmed to save context. Preview: ${preview}...]`,
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
return m;
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Public wrapper around fixToolPairing for post-error recovery flows.
|
|
3493
|
+
* When a provider returns 400, the message history may have landed in
|
|
3494
|
+
* an invalid shape (orphan tool_calls, unmatched tool results). The
|
|
3495
|
+
* extension's Retry handler calls this on the live conversation before
|
|
3496
|
+
* issuing another request so the user isn't stuck in a loop of the
|
|
3497
|
+
* same broken payload bouncing off the provider.
|
|
3498
|
+
*/
|
|
3499
|
+
repairMessages(messages) {
|
|
3500
|
+
return this.fixToolPairing(messages);
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Ensure every `tool` message has a preceding `assistant` with a matching
|
|
3504
|
+
* `tool_calls` entry, and every `assistant` with `tool_calls` has all its
|
|
3505
|
+
* `tool` results following it. Drops orphans from the front.
|
|
3506
|
+
*/
|
|
3507
|
+
fixToolPairing(messages) {
|
|
3508
|
+
// 1. Drop leading orphaned tool messages (their assistant parent was truncated)
|
|
3509
|
+
let start = 0;
|
|
3510
|
+
while (start < messages.length && messages[start].role === 'tool') {
|
|
3511
|
+
start++;
|
|
3512
|
+
}
|
|
3513
|
+
if (start === messages.length)
|
|
3514
|
+
return [];
|
|
3515
|
+
const trimmed = start > 0 ? messages.slice(start) : messages;
|
|
3516
|
+
// 2. Scan ALL messages — remove any tool message whose parent assistant
|
|
3517
|
+
// (with matching tool_call_id) is not in the conversation
|
|
3518
|
+
const assistantToolCallIds = new Set();
|
|
3519
|
+
for (const m of trimmed) {
|
|
3520
|
+
if (m.role === 'assistant') {
|
|
3521
|
+
const toolCalls = m.tool_calls;
|
|
3522
|
+
if (toolCalls) {
|
|
3523
|
+
for (const tc of toolCalls) {
|
|
3524
|
+
assistantToolCallIds.add(tc.id);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
const fixed = trimmed.filter(m => {
|
|
3530
|
+
if (m.role === 'tool') {
|
|
3531
|
+
const toolMsg = m;
|
|
3532
|
+
return assistantToolCallIds.has(toolMsg.tool_call_id ?? '');
|
|
3533
|
+
}
|
|
3534
|
+
return true;
|
|
3535
|
+
});
|
|
3536
|
+
// 3. Check for assistant messages with tool_calls but missing ALL tool results
|
|
3537
|
+
// (incomplete pair) — remove them too
|
|
3538
|
+
const toolResultIds = new Set();
|
|
3539
|
+
for (const m of fixed) {
|
|
3540
|
+
if (m.role === 'tool') {
|
|
3541
|
+
const toolMsg = m;
|
|
3542
|
+
if (toolMsg.tool_call_id)
|
|
3543
|
+
toolResultIds.add(toolMsg.tool_call_id);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return fixed.filter(m => {
|
|
3547
|
+
if (m.role === 'assistant') {
|
|
3548
|
+
const toolCalls = m.tool_calls;
|
|
3549
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
3550
|
+
// Keep only if at least one tool result exists
|
|
3551
|
+
return toolCalls.some(tc => toolResultIds.has(tc.id));
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return true;
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
//# sourceMappingURL=agent.js.map
|