@clawpump/claw-agent 0.1.4 → 0.1.6
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/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2294 -3146
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/clawpump_cli.py +3 -3
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +216 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/distribution.py +227 -0
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6188 -975
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +15 -5
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +14 -4
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +53 -5
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +308 -696
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
|
@@ -31,6 +31,8 @@ from agent.codex_responses_adapter import _summarize_user_message_for_log
|
|
|
31
31
|
from agent.display import KawaiiSpinner
|
|
32
32
|
from agent.error_classifier import FailoverReason, classify_api_error
|
|
33
33
|
from agent.iteration_budget import IterationBudget
|
|
34
|
+
from agent.turn_context import build_turn_context
|
|
35
|
+
from agent.turn_retry_state import TurnRetryState
|
|
34
36
|
from agent.memory_manager import build_memory_context_block
|
|
35
37
|
from agent.message_sanitization import (
|
|
36
38
|
_repair_tool_call_arguments,
|
|
@@ -63,6 +65,40 @@ from utils import base_url_host_matches, env_var_enabled
|
|
|
63
65
|
|
|
64
66
|
logger = logging.getLogger(__name__)
|
|
65
67
|
|
|
68
|
+
# Stable prefix of the local interrupt status string emitted when a turn is
|
|
69
|
+
# cancelled while waiting on the provider. Surfaces (ACP, TUI) match on this
|
|
70
|
+
# to treat it as cancellation metadata rather than assistant prose.
|
|
71
|
+
INTERRUPT_WAITING_FOR_MODEL_PREFIX = "Operation interrupted: waiting for model response ("
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _image_error_max_dimension(error: Exception) -> Optional[int]:
|
|
75
|
+
"""Extract a provider-reported image dimension ceiling, if present."""
|
|
76
|
+
parts = []
|
|
77
|
+
for value in (
|
|
78
|
+
error,
|
|
79
|
+
getattr(error, "message", None),
|
|
80
|
+
getattr(error, "body", None),
|
|
81
|
+
):
|
|
82
|
+
if value:
|
|
83
|
+
try:
|
|
84
|
+
parts.append(str(value))
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
text = " ".join(parts).lower()
|
|
88
|
+
if "image" not in text or "dimension" not in text or "max allowed size" not in text:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
match = re.search(r"max allowed size(?:\s+for [^:]+)?:\s*(\d{3,5})\s*pixels?", text)
|
|
92
|
+
if not match:
|
|
93
|
+
return None
|
|
94
|
+
try:
|
|
95
|
+
max_dimension = int(match.group(1))
|
|
96
|
+
except ValueError:
|
|
97
|
+
return None
|
|
98
|
+
if 512 <= max_dimension <= 8000:
|
|
99
|
+
return max_dimension
|
|
100
|
+
return None
|
|
101
|
+
|
|
66
102
|
|
|
67
103
|
def _ollama_context_limit_error(agent: Any, request_tokens: int) -> Optional[str]:
|
|
68
104
|
"""Return a user-facing error when Ollama is loaded with too little context."""
|
|
@@ -264,11 +300,20 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
|
|
|
264
300
|
agent.session_id, exc,
|
|
265
301
|
)
|
|
266
302
|
|
|
267
|
-
if stored_prompt:
|
|
303
|
+
if stored_prompt and _stored_prompt_matches_runtime(agent, stored_prompt):
|
|
268
304
|
# Continuing session — reuse the exact system prompt from the
|
|
269
305
|
# previous turn so the Anthropic cache prefix matches.
|
|
270
306
|
agent._cached_system_prompt = stored_prompt
|
|
271
307
|
return
|
|
308
|
+
if stored_prompt:
|
|
309
|
+
stored_state = "stale_runtime"
|
|
310
|
+
logger.info(
|
|
311
|
+
"Stored system prompt for session %s has stale runtime identity; "
|
|
312
|
+
"rebuilding for model=%s provider=%s.",
|
|
313
|
+
agent.session_id,
|
|
314
|
+
getattr(agent, "model", "") or "",
|
|
315
|
+
getattr(agent, "provider", "") or "",
|
|
316
|
+
)
|
|
272
317
|
|
|
273
318
|
if conversation_history and stored_state in ("null", "empty"):
|
|
274
319
|
# Continuing session whose stored prompt is unusable. The
|
|
@@ -301,6 +346,19 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
|
|
|
301
346
|
except Exception as exc:
|
|
302
347
|
logger.warning("on_session_start hook failed: %s", exc)
|
|
303
348
|
|
|
349
|
+
# Cold-start credits seed (L3) — fallback for the first-turn path. The TUI/
|
|
350
|
+
# desktop build seeds at session OPEN (see seed_credits_at_session_start in
|
|
351
|
+
# tui_gateway), so this call is usually a no-op there (idempotent: skips when
|
|
352
|
+
# _credits_state already exists). For the plain CLI / any path that didn't seed
|
|
353
|
+
# at build, it primes credits state from /api/oauth/account (or a fixture) on the
|
|
354
|
+
# first turn so depletion / usage-band warnings fire. Fail-open inside the helper.
|
|
355
|
+
try:
|
|
356
|
+
from agent.credits_tracker import seed_credits_at_session_start
|
|
357
|
+
|
|
358
|
+
seed_credits_at_session_start(agent)
|
|
359
|
+
except Exception:
|
|
360
|
+
logger.debug("cold-start credits seed failed (fail-open)", exc_info=True)
|
|
361
|
+
|
|
304
362
|
# Persist the system prompt snapshot in SQLite. Failure here used
|
|
305
363
|
# to log at DEBUG, which silently broke prefix-cache reuse on the
|
|
306
364
|
# gateway path (fresh AIAgent per turn → reads from this row every
|
|
@@ -317,6 +375,30 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
|
|
|
317
375
|
)
|
|
318
376
|
|
|
319
377
|
|
|
378
|
+
def _stored_prompt_matches_runtime(agent, prompt: str) -> bool:
|
|
379
|
+
"""Return False when the persisted Model/Provider lines are stale."""
|
|
380
|
+
|
|
381
|
+
def line_value(label: str) -> str:
|
|
382
|
+
prefix = f"{label}:"
|
|
383
|
+
value = ""
|
|
384
|
+
for line in prompt.splitlines():
|
|
385
|
+
if line.startswith(prefix):
|
|
386
|
+
value = line[len(prefix):].strip()
|
|
387
|
+
return value
|
|
388
|
+
|
|
389
|
+
stored_model = line_value("Model")
|
|
390
|
+
current_model = str(getattr(agent, "model", "") or "").strip()
|
|
391
|
+
if stored_model and current_model and stored_model != current_model:
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
stored_provider = line_value("Provider")
|
|
395
|
+
current_provider = str(getattr(agent, "provider", "") or "").strip()
|
|
396
|
+
if stored_provider and current_provider and stored_provider != current_provider:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
|
|
320
402
|
def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List[str]] = None) -> str:
|
|
321
403
|
if is_partial_stub and dropped_tools:
|
|
322
404
|
tool_list = ", ".join(dropped_tools[:3])
|
|
@@ -348,6 +430,42 @@ def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List
|
|
|
348
430
|
)
|
|
349
431
|
|
|
350
432
|
|
|
433
|
+
# Shared recovery hint appended to every content-policy refusal message. Both
|
|
434
|
+
# the HTTP-200 refusal path (``finish_reason=content_filter``) and the
|
|
435
|
+
# exception path (a provider moderation error classified as
|
|
436
|
+
# ``content_policy_blocked``) end with the same actionable next steps, so they
|
|
437
|
+
# share one trailer to keep the guidance from drifting between the two sites.
|
|
438
|
+
_CONTENT_POLICY_RECOVERY_HINT = (
|
|
439
|
+
"Try rephrasing the request, narrowing the context, or "
|
|
440
|
+
"adding a fallback provider with `hermes fallback add`."
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _content_policy_blocked_result(
|
|
445
|
+
messages: List[Dict],
|
|
446
|
+
api_call_count: int,
|
|
447
|
+
*,
|
|
448
|
+
final_response: str,
|
|
449
|
+
error_detail: str,
|
|
450
|
+
) -> Dict[str, Any]:
|
|
451
|
+
"""Build the terminal turn result for a content-policy block.
|
|
452
|
+
|
|
453
|
+
A content-policy refusal is deterministic for the unchanged prompt, so the
|
|
454
|
+
turn ends here (no retry). Both the HTTP-200 refusal handler and the
|
|
455
|
+
exception-path handler return the identical shape — a failed, non-completed
|
|
456
|
+
turn carrying the user-facing message and a ``content_policy_blocked:``
|
|
457
|
+
prefixed error — so they funnel through this one builder.
|
|
458
|
+
"""
|
|
459
|
+
return {
|
|
460
|
+
"final_response": final_response,
|
|
461
|
+
"messages": messages,
|
|
462
|
+
"api_calls": api_call_count,
|
|
463
|
+
"completed": False,
|
|
464
|
+
"failed": True,
|
|
465
|
+
"error": f"content_policy_blocked: {error_detail}",
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
351
469
|
def run_conversation(
|
|
352
470
|
agent,
|
|
353
471
|
user_message: str,
|
|
@@ -356,6 +474,7 @@ def run_conversation(
|
|
|
356
474
|
task_id: str = None,
|
|
357
475
|
stream_callback: Optional[callable] = None,
|
|
358
476
|
persist_user_message: Optional[str] = None,
|
|
477
|
+
persist_user_timestamp: Optional[float] = None,
|
|
359
478
|
) -> Dict[str, Any]:
|
|
360
479
|
"""
|
|
361
480
|
Run a complete conversation with tool calling until completion.
|
|
@@ -371,356 +490,51 @@ def run_conversation(
|
|
|
371
490
|
persist_user_message: Optional clean user message to store in
|
|
372
491
|
transcripts/history when user_message contains API-only
|
|
373
492
|
synthetic prefixes.
|
|
493
|
+
persist_user_timestamp: Optional platform event timestamp to store
|
|
494
|
+
as metadata on that persisted user message.
|
|
374
495
|
or queuing follow-up prefetch work.
|
|
375
496
|
|
|
376
497
|
Returns:
|
|
377
498
|
Dict: Complete conversation result with final response and message history
|
|
378
499
|
"""
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
# ``hermes logs --session <id>`` can filter a single conversation.
|
|
404
|
-
set_session_context(agent.session_id)
|
|
405
|
-
|
|
406
|
-
# Bind the skill write-origin ContextVar for this thread so tool
|
|
407
|
-
# handlers (e.g. skill_manage create) can tell whether they are
|
|
408
|
-
# running inside the background agent-improvement review fork vs.
|
|
409
|
-
# a foreground user-directed turn. Set at the top of each call;
|
|
410
|
-
# the review fork runs on its own thread with a fresh context,
|
|
411
|
-
# so the foreground value here does not leak into it.
|
|
412
|
-
set_current_write_origin(getattr(agent, "_memory_write_origin", "assistant_tool"))
|
|
413
|
-
|
|
414
|
-
# If the previous turn activated fallback, restore the primary
|
|
415
|
-
# runtime so this turn gets a fresh attempt with the preferred model.
|
|
416
|
-
# No-op when _fallback_activated is False (gateway, first turn, etc.).
|
|
417
|
-
agent._restore_primary_runtime()
|
|
418
|
-
|
|
419
|
-
# Sanitize surrogate characters from user input. Clipboard paste from
|
|
420
|
-
# rich-text editors (Google Docs, Word, etc.) can inject lone surrogates
|
|
421
|
-
# that are invalid UTF-8 and crash JSON serialization in the OpenAI SDK.
|
|
422
|
-
if isinstance(user_message, str):
|
|
423
|
-
user_message = _sanitize_surrogates(user_message)
|
|
424
|
-
if isinstance(persist_user_message, str):
|
|
425
|
-
persist_user_message = _sanitize_surrogates(persist_user_message)
|
|
426
|
-
|
|
427
|
-
# Store stream callback for _interruptible_api_call to pick up
|
|
428
|
-
agent._stream_callback = stream_callback
|
|
429
|
-
agent._persist_user_message_idx = None
|
|
430
|
-
agent._persist_user_message_override = persist_user_message
|
|
431
|
-
# Generate unique task_id if not provided to isolate VMs between concurrent tasks
|
|
432
|
-
effective_task_id = task_id or str(uuid.uuid4())
|
|
433
|
-
# Expose the active task_id so tools running mid-turn (e.g. delegate_task
|
|
434
|
-
# in delegate_tool.py) can identify this agent for the cross-agent file
|
|
435
|
-
# state registry. Set BEFORE any tool dispatch so snapshots taken at
|
|
436
|
-
# child-launch time see the parent's real id, not None.
|
|
437
|
-
agent._current_task_id = effective_task_id
|
|
438
|
-
|
|
439
|
-
# Reset retry counters and iteration budget at the start of each turn
|
|
440
|
-
# so subagent usage from a previous turn doesn't eat into the next one.
|
|
441
|
-
agent._invalid_tool_retries = 0
|
|
442
|
-
agent._invalid_json_retries = 0
|
|
443
|
-
agent._empty_content_retries = 0
|
|
444
|
-
agent._incomplete_scratchpad_retries = 0
|
|
445
|
-
agent._codex_incomplete_retries = 0
|
|
446
|
-
agent._thinking_prefill_retries = 0
|
|
447
|
-
agent._post_tool_empty_retried = False
|
|
448
|
-
agent._last_content_with_tools = None
|
|
449
|
-
agent._last_content_tools_all_housekeeping = False
|
|
450
|
-
agent._mute_post_response = False
|
|
451
|
-
agent._unicode_sanitization_passes = 0
|
|
452
|
-
agent._tool_guardrails.reset_for_turn()
|
|
453
|
-
agent._tool_guardrail_halt_decision = None
|
|
454
|
-
# True until the server rejects an image_url content part with an error
|
|
455
|
-
# like "Only 'text' content type is supported." Set to False on first
|
|
456
|
-
# rejection and kept False for the rest of the session so we never re-send
|
|
457
|
-
# images to a text-only endpoint. Scoped per `_run()` call, not per instance.
|
|
458
|
-
agent._vision_supported = True
|
|
459
|
-
|
|
460
|
-
# Pre-turn connection health check: detect and clean up dead TCP
|
|
461
|
-
# connections left over from provider outages or dropped streams.
|
|
462
|
-
# This prevents the next API call from hanging on a zombie socket.
|
|
463
|
-
if agent.api_mode != "anthropic_messages":
|
|
464
|
-
try:
|
|
465
|
-
if agent._cleanup_dead_connections():
|
|
466
|
-
agent._emit_status(
|
|
467
|
-
"🔌 Detected stale connections from a previous provider "
|
|
468
|
-
"issue — cleaned up automatically. Proceeding with fresh "
|
|
469
|
-
"connection."
|
|
470
|
-
)
|
|
471
|
-
except Exception:
|
|
472
|
-
pass
|
|
473
|
-
# Replay compression warning through status_callback for gateway
|
|
474
|
-
# platforms (the callback was not wired during __init__).
|
|
475
|
-
if agent._compression_warning:
|
|
476
|
-
agent._replay_compression_warning()
|
|
477
|
-
agent._compression_warning = None # send once
|
|
478
|
-
|
|
479
|
-
# NOTE: _turns_since_memory and _iters_since_skill are NOT reset here.
|
|
480
|
-
# They are initialized in __init__ and must persist across run_conversation
|
|
481
|
-
# calls so that nudge logic accumulates correctly in CLI mode.
|
|
482
|
-
agent.iteration_budget = IterationBudget(agent.max_iterations)
|
|
483
|
-
|
|
484
|
-
# Log conversation turn start for debugging/observability
|
|
485
|
-
_preview_text = _summarize_user_message_for_log(user_message)
|
|
486
|
-
_msg_preview = (_preview_text[:80] + "...") if len(_preview_text) > 80 else _preview_text
|
|
487
|
-
_msg_preview = _msg_preview.replace("\n", " ")
|
|
488
|
-
logger.info(
|
|
489
|
-
"conversation turn: session=%s model=%s provider=%s platform=%s history=%d msg=%r",
|
|
490
|
-
agent.session_id or "none", agent.model, agent.provider or "unknown",
|
|
491
|
-
agent.platform or "unknown", len(conversation_history or []),
|
|
492
|
-
_msg_preview,
|
|
500
|
+
# ── Per-turn setup (the prologue) ──
|
|
501
|
+
# All once-per-turn setup — stdio guarding, retry-counter resets, user
|
|
502
|
+
# message sanitization, todo/nudge hydration, system-prompt restore-or-
|
|
503
|
+
# build, crash-resilience persistence, preflight compression, the
|
|
504
|
+
# ``pre_llm_call`` plugin hook, and external-memory prefetch — lives in
|
|
505
|
+
# ``build_turn_context``. It mutates ``agent`` exactly as the inline code
|
|
506
|
+
# did and returns the locals the loop below reads back. See
|
|
507
|
+
# ``agent/turn_context.py``.
|
|
508
|
+
_ctx = build_turn_context(
|
|
509
|
+
agent,
|
|
510
|
+
user_message,
|
|
511
|
+
system_message,
|
|
512
|
+
conversation_history,
|
|
513
|
+
task_id,
|
|
514
|
+
stream_callback,
|
|
515
|
+
persist_user_message,
|
|
516
|
+
persist_user_timestamp,
|
|
517
|
+
restore_or_build_system_prompt=_restore_or_build_system_prompt,
|
|
518
|
+
install_safe_stdio=_install_safe_stdio,
|
|
519
|
+
sanitize_surrogates=_sanitize_surrogates,
|
|
520
|
+
summarize_user_message_for_log=_summarize_user_message_for_log,
|
|
521
|
+
set_session_context=set_session_context,
|
|
522
|
+
set_current_write_origin=set_current_write_origin,
|
|
523
|
+
ra=_ra,
|
|
493
524
|
)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
messages =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
#
|
|
507
|
-
# _turns_since_memory and _user_turn_count start at 0 every turn and
|
|
508
|
-
# the memory.nudge_interval trigger may never be reached. Reconstruct
|
|
509
|
-
# an effective count from prior user turns in conversation_history.
|
|
510
|
-
# Idempotent: a cached agent that already accumulated counters keeps
|
|
511
|
-
# them; only a freshly-built agent with empty in-memory state hydrates.
|
|
512
|
-
# See issue #22357.
|
|
513
|
-
if conversation_history and agent._user_turn_count == 0:
|
|
514
|
-
prior_user_turns = sum(
|
|
515
|
-
1 for m in conversation_history if m.get("role") == "user"
|
|
516
|
-
)
|
|
517
|
-
if prior_user_turns > 0:
|
|
518
|
-
agent._user_turn_count = prior_user_turns
|
|
519
|
-
if agent._memory_nudge_interval > 0 and agent._turns_since_memory == 0:
|
|
520
|
-
# % preserves original 1-in-N cadence rather than firing a
|
|
521
|
-
# review immediately on resume (which would surprise users
|
|
522
|
-
# whose session happened to land just past a multiple of N).
|
|
523
|
-
agent._turns_since_memory = prior_user_turns % agent._memory_nudge_interval
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# Prefill messages (few-shot priming) are injected at API-call time only,
|
|
527
|
-
# never stored in the messages list. This keeps them ephemeral: they won't
|
|
528
|
-
# be saved to session DB, session logs, or batch trajectories, but they're
|
|
529
|
-
# automatically re-applied on every API call (including session continuations).
|
|
530
|
-
|
|
531
|
-
# Track user turns for memory flush and periodic nudge logic
|
|
532
|
-
agent._user_turn_count += 1
|
|
533
|
-
|
|
534
|
-
# Reset the streaming context scrubber at the top of each turn so a
|
|
535
|
-
# hung span from a prior interrupted stream can't taint this turn's
|
|
536
|
-
# output.
|
|
537
|
-
scrubber = getattr(agent, "_stream_context_scrubber", None)
|
|
538
|
-
if scrubber is not None:
|
|
539
|
-
scrubber.reset()
|
|
540
|
-
# Reset the think scrubber for the same reason — an interrupted
|
|
541
|
-
# prior stream may have left us inside an unterminated block.
|
|
542
|
-
think_scrubber = getattr(agent, "_stream_think_scrubber", None)
|
|
543
|
-
if think_scrubber is not None:
|
|
544
|
-
think_scrubber.reset()
|
|
545
|
-
|
|
546
|
-
# Preserve the original user message (no nudge injection).
|
|
547
|
-
original_user_message = persist_user_message if persist_user_message is not None else user_message
|
|
548
|
-
|
|
549
|
-
# Track memory nudge trigger (turn-based, checked here).
|
|
550
|
-
# Skill trigger is checked AFTER the agent loop completes, based on
|
|
551
|
-
# how many tool iterations THIS turn used.
|
|
552
|
-
_should_review_memory = False
|
|
553
|
-
if (agent._memory_nudge_interval > 0
|
|
554
|
-
and "memory" in agent.valid_tool_names
|
|
555
|
-
and agent._memory_store):
|
|
556
|
-
agent._turns_since_memory += 1
|
|
557
|
-
if agent._turns_since_memory >= agent._memory_nudge_interval:
|
|
558
|
-
_should_review_memory = True
|
|
559
|
-
agent._turns_since_memory = 0
|
|
560
|
-
|
|
561
|
-
# Add user message
|
|
562
|
-
user_msg = {"role": "user", "content": user_message}
|
|
563
|
-
messages.append(user_msg)
|
|
564
|
-
current_turn_user_idx = len(messages) - 1
|
|
565
|
-
agent._persist_user_message_idx = current_turn_user_idx
|
|
566
|
-
|
|
567
|
-
if not agent.quiet_mode:
|
|
568
|
-
_print_preview = _summarize_user_message_for_log(user_message)
|
|
569
|
-
agent._safe_print(f"💬 Starting conversation: '{_print_preview[:60]}{'...' if len(_print_preview) > 60 else ''}'")
|
|
570
|
-
|
|
571
|
-
# ── System prompt (cached per session for prefix caching) ──
|
|
572
|
-
# Built once on first call, reused for all subsequent calls.
|
|
573
|
-
# Only rebuilt after context compression events (which invalidate
|
|
574
|
-
# the cache and reload memory from disk).
|
|
575
|
-
#
|
|
576
|
-
# For continuing sessions (gateway creates a fresh AIAgent per
|
|
577
|
-
# message), we load the stored system prompt from the session DB
|
|
578
|
-
# instead of rebuilding. Rebuilding would pick up memory changes
|
|
579
|
-
# from disk that the model already knows about (it wrote them!),
|
|
580
|
-
# producing a different system prompt and breaking the Anthropic
|
|
581
|
-
# prefix cache.
|
|
582
|
-
if agent._cached_system_prompt is None:
|
|
583
|
-
_restore_or_build_system_prompt(agent, system_message, conversation_history)
|
|
584
|
-
|
|
585
|
-
active_system_prompt = agent._cached_system_prompt
|
|
586
|
-
|
|
587
|
-
# ── Preflight context compression ──
|
|
588
|
-
# Before entering the main loop, check if the loaded conversation
|
|
589
|
-
# history already exceeds the model's context threshold. This handles
|
|
590
|
-
# cases where a user switches to a model with a smaller context window
|
|
591
|
-
# while having a large existing session — compress proactively rather
|
|
592
|
-
# than waiting for an API error (which might be caught as a non-retryable
|
|
593
|
-
# 4xx and abort the request entirely).
|
|
594
|
-
if (
|
|
595
|
-
agent.compression_enabled
|
|
596
|
-
and len(messages) > agent.context_compressor.protect_first_n
|
|
597
|
-
+ agent.context_compressor.protect_last_n + 1
|
|
598
|
-
):
|
|
599
|
-
# Include tool schema tokens — with many tools these can add
|
|
600
|
-
# 20-30K+ tokens that the old sys+msg estimate missed entirely.
|
|
601
|
-
_preflight_tokens = estimate_request_tokens_rough(
|
|
602
|
-
messages,
|
|
603
|
-
system_prompt=active_system_prompt or "",
|
|
604
|
-
tools=agent.tools or None,
|
|
605
|
-
)
|
|
606
|
-
_compressor = agent.context_compressor
|
|
607
|
-
_defer_preflight = getattr(
|
|
608
|
-
_compressor,
|
|
609
|
-
"should_defer_preflight_to_real_usage",
|
|
610
|
-
lambda _tokens: False,
|
|
611
|
-
)
|
|
612
|
-
_preflight_deferred = _defer_preflight(_preflight_tokens)
|
|
613
|
-
|
|
614
|
-
if not _preflight_deferred:
|
|
615
|
-
# Keep the CLI/ACP context display in sync with what preflight
|
|
616
|
-
# actually measured. The status bar reads
|
|
617
|
-
# ``compressor.last_prompt_tokens``, which otherwise only updates
|
|
618
|
-
# from a *successful* API response. When the conversation has grown
|
|
619
|
-
# since the last successful call — or when compression then fails
|
|
620
|
-
# (e.g. the auxiliary summary model times out) and no fresh usage
|
|
621
|
-
# arrives — the bar stays stuck at the old, smaller value while
|
|
622
|
-
# preflight reports a much larger number, looking out of sync.
|
|
623
|
-
# Seed it with the fresh estimate (only ever revising upward; a real
|
|
624
|
-
# ``update_from_response`` will correct it after the next API call).
|
|
625
|
-
# Skipped when deferring — a deferred estimate is known to over-count
|
|
626
|
-
# vs the last real provider prompt, so trusting it for the display
|
|
627
|
-
# would re-introduce the very desync we're avoiding.
|
|
628
|
-
if _preflight_tokens > (_compressor.last_prompt_tokens or 0):
|
|
629
|
-
_compressor.last_prompt_tokens = _preflight_tokens
|
|
630
|
-
|
|
631
|
-
if _preflight_deferred:
|
|
632
|
-
logger.info(
|
|
633
|
-
"Skipping preflight compression: rough estimate ~%s >= %s, "
|
|
634
|
-
"but last real provider prompt was %s after compression",
|
|
635
|
-
f"{_preflight_tokens:,}",
|
|
636
|
-
f"{_compressor.threshold_tokens:,}",
|
|
637
|
-
f"{_compressor.last_real_prompt_tokens:,}",
|
|
638
|
-
)
|
|
639
|
-
elif _compressor.should_compress(_preflight_tokens):
|
|
640
|
-
logger.info(
|
|
641
|
-
"Preflight compression: ~%s tokens >= %s threshold (model %s, ctx %s)",
|
|
642
|
-
f"{_preflight_tokens:,}",
|
|
643
|
-
f"{_compressor.threshold_tokens:,}",
|
|
644
|
-
agent.model,
|
|
645
|
-
f"{_compressor.context_length:,}",
|
|
646
|
-
)
|
|
647
|
-
agent._emit_status(
|
|
648
|
-
f"📦 Preflight compression: ~{_preflight_tokens:,} tokens "
|
|
649
|
-
f">= {_compressor.threshold_tokens:,} threshold. "
|
|
650
|
-
"This may take a moment."
|
|
651
|
-
)
|
|
652
|
-
# May need multiple passes for very large sessions with small
|
|
653
|
-
# context windows (each pass summarises the middle N turns).
|
|
654
|
-
for _pass in range(3):
|
|
655
|
-
_orig_len = len(messages)
|
|
656
|
-
messages, active_system_prompt = agent._compress_context(
|
|
657
|
-
messages, system_message, approx_tokens=_preflight_tokens,
|
|
658
|
-
task_id=effective_task_id,
|
|
659
|
-
)
|
|
660
|
-
if len(messages) >= _orig_len:
|
|
661
|
-
break # Cannot compress further
|
|
662
|
-
# Compression created a new session — clear the history
|
|
663
|
-
# reference so _flush_messages_to_session_db writes ALL
|
|
664
|
-
# compressed messages to the new session's SQLite, not
|
|
665
|
-
# skipping them because conversation_history is still the
|
|
666
|
-
# pre-compression length.
|
|
667
|
-
conversation_history = None
|
|
668
|
-
# Fix: reset retry counters after compression so the model
|
|
669
|
-
# gets a fresh budget on the compressed context. Without
|
|
670
|
-
# this, pre-compression retries carry over and the model
|
|
671
|
-
# hits "(empty)" immediately after compression-induced
|
|
672
|
-
# context loss.
|
|
673
|
-
agent._empty_content_retries = 0
|
|
674
|
-
agent._thinking_prefill_retries = 0
|
|
675
|
-
agent._last_content_with_tools = None
|
|
676
|
-
agent._last_content_tools_all_housekeeping = False
|
|
677
|
-
agent._mute_post_response = False
|
|
678
|
-
# Re-estimate after compression
|
|
679
|
-
_preflight_tokens = estimate_request_tokens_rough(
|
|
680
|
-
messages,
|
|
681
|
-
system_prompt=active_system_prompt or "",
|
|
682
|
-
tools=agent.tools or None,
|
|
683
|
-
)
|
|
684
|
-
if not _compressor.should_compress(_preflight_tokens):
|
|
685
|
-
break # Under threshold or anti-thrash guard stopped it
|
|
686
|
-
|
|
687
|
-
# Plugin hook: pre_llm_call
|
|
688
|
-
# Fired once per turn before the tool-calling loop. Plugins can
|
|
689
|
-
# return a dict with a ``context`` key (or a plain string) whose
|
|
690
|
-
# value is appended to the current turn's user message.
|
|
691
|
-
#
|
|
692
|
-
# Context is ALWAYS injected into the user message, never the
|
|
693
|
-
# system prompt. This preserves the prompt cache prefix — the
|
|
694
|
-
# system prompt stays identical across turns so cached tokens
|
|
695
|
-
# are reused. The system prompt is Hermes's territory; plugins
|
|
696
|
-
# contribute context alongside the user's input.
|
|
697
|
-
#
|
|
698
|
-
# All injected context is ephemeral (not persisted to session DB).
|
|
699
|
-
_plugin_user_context = ""
|
|
700
|
-
try:
|
|
701
|
-
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
|
702
|
-
_pre_results = _invoke_hook(
|
|
703
|
-
"pre_llm_call",
|
|
704
|
-
session_id=agent.session_id,
|
|
705
|
-
user_message=original_user_message,
|
|
706
|
-
conversation_history=list(messages),
|
|
707
|
-
is_first_turn=(not bool(conversation_history)),
|
|
708
|
-
model=agent.model,
|
|
709
|
-
platform=getattr(agent, "platform", None) or "",
|
|
710
|
-
sender_id=getattr(agent, "_user_id", None) or "",
|
|
711
|
-
)
|
|
712
|
-
_ctx_parts: list[str] = []
|
|
713
|
-
for r in _pre_results:
|
|
714
|
-
if isinstance(r, dict) and r.get("context"):
|
|
715
|
-
_ctx_parts.append(str(r["context"]))
|
|
716
|
-
elif isinstance(r, str) and r.strip():
|
|
717
|
-
_ctx_parts.append(r)
|
|
718
|
-
if _ctx_parts:
|
|
719
|
-
_plugin_user_context = "\n\n".join(_ctx_parts)
|
|
720
|
-
except Exception as exc:
|
|
721
|
-
logger.warning("pre_llm_call hook failed: %s", exc)
|
|
722
|
-
|
|
723
|
-
# Main conversation loop
|
|
525
|
+
user_message = _ctx.user_message
|
|
526
|
+
original_user_message = _ctx.original_user_message
|
|
527
|
+
messages = _ctx.messages
|
|
528
|
+
conversation_history = _ctx.conversation_history
|
|
529
|
+
active_system_prompt = _ctx.active_system_prompt
|
|
530
|
+
effective_task_id = _ctx.effective_task_id
|
|
531
|
+
turn_id = _ctx.turn_id
|
|
532
|
+
current_turn_user_idx = _ctx.current_turn_user_idx
|
|
533
|
+
_should_review_memory = _ctx.should_review_memory
|
|
534
|
+
_plugin_user_context = _ctx.plugin_user_context
|
|
535
|
+
_ext_prefetch_cache = _ctx.ext_prefetch_cache
|
|
536
|
+
|
|
537
|
+
# Main conversation loop counters (pure locals consumed by the loop below).
|
|
724
538
|
api_call_count = 0
|
|
725
539
|
final_response = None
|
|
726
540
|
interrupted = False
|
|
@@ -732,53 +546,6 @@ def run_conversation(
|
|
|
732
546
|
compression_attempts = 0
|
|
733
547
|
_turn_exit_reason = "unknown" # Diagnostic: why the loop ended
|
|
734
548
|
|
|
735
|
-
# Per-turn file-mutation verifier state. Keyed by resolved path;
|
|
736
|
-
# each failed ``write_file`` / ``patch`` call records the error
|
|
737
|
-
# preview. Later successful writes to the same path remove the
|
|
738
|
-
# entry (the model recovered). At end-of-turn, any entries still
|
|
739
|
-
# present are surfaced in an advisory footer so the model cannot
|
|
740
|
-
# over-claim success while the file is actually unchanged on disk.
|
|
741
|
-
agent._turn_failed_file_mutations: Dict[str, Dict[str, Any]] = {}
|
|
742
|
-
|
|
743
|
-
# Record the execution thread so interrupt()/clear_interrupt() can
|
|
744
|
-
# scope the tool-level interrupt signal to THIS agent's thread only.
|
|
745
|
-
# Must be set before any thread-scoped interrupt syncing.
|
|
746
|
-
agent._execution_thread_id = threading.current_thread().ident
|
|
747
|
-
|
|
748
|
-
# Always clear stale per-thread state from a previous turn. If an
|
|
749
|
-
# interrupt arrived before startup finished, preserve it and bind it
|
|
750
|
-
# to this execution thread now instead of dropping it on the floor.
|
|
751
|
-
_ra()._set_interrupt(False, agent._execution_thread_id)
|
|
752
|
-
if agent._interrupt_requested:
|
|
753
|
-
_ra()._set_interrupt(True, agent._execution_thread_id)
|
|
754
|
-
agent._interrupt_thread_signal_pending = False
|
|
755
|
-
else:
|
|
756
|
-
agent._interrupt_message = None
|
|
757
|
-
agent._interrupt_thread_signal_pending = False
|
|
758
|
-
|
|
759
|
-
# Notify memory providers of the new turn so cadence tracking works.
|
|
760
|
-
# Must happen BEFORE prefetch_all() so providers know which turn it is
|
|
761
|
-
# and can gate context/dialectic refresh via contextCadence/dialecticCadence.
|
|
762
|
-
if agent._memory_manager:
|
|
763
|
-
try:
|
|
764
|
-
_turn_msg = original_user_message if isinstance(original_user_message, str) else ""
|
|
765
|
-
agent._memory_manager.on_turn_start(agent._user_turn_count, _turn_msg)
|
|
766
|
-
except Exception:
|
|
767
|
-
pass
|
|
768
|
-
|
|
769
|
-
# External memory provider: prefetch once before the tool loop.
|
|
770
|
-
# Reuse the cached result on every iteration to avoid re-calling
|
|
771
|
-
# prefetch_all() on each tool call (10 tool calls = 10x latency + cost).
|
|
772
|
-
# Use original_user_message (clean input) — user_message may contain
|
|
773
|
-
# injected skill content that bloats / breaks provider queries.
|
|
774
|
-
_ext_prefetch_cache = ""
|
|
775
|
-
if agent._memory_manager:
|
|
776
|
-
try:
|
|
777
|
-
_query = original_user_message if isinstance(original_user_message, str) else ""
|
|
778
|
-
_ext_prefetch_cache = agent._memory_manager.prefetch_all(_query) or ""
|
|
779
|
-
except Exception:
|
|
780
|
-
pass
|
|
781
|
-
|
|
782
549
|
# Optional opt-in runtime: if api_mode == codex_app_server, hand the
|
|
783
550
|
# turn to the codex app-server subprocess (terminal/file ops/patching
|
|
784
551
|
# all run inside Codex). Default Hermes path is bypassed entirely.
|
|
@@ -872,7 +639,8 @@ def run_conversation(
|
|
|
872
639
|
for _si in range(len(messages) - 1, -1, -1):
|
|
873
640
|
_sm = messages[_si]
|
|
874
641
|
if isinstance(_sm, dict) and _sm.get("role") == "tool":
|
|
875
|
-
|
|
642
|
+
from agent.prompt_builder import format_steer_marker
|
|
643
|
+
marker = format_steer_marker(_pre_api_steer)
|
|
876
644
|
existing = _sm.get("content", "")
|
|
877
645
|
if isinstance(existing, str):
|
|
878
646
|
_sm["content"] = existing + marker
|
|
@@ -929,7 +697,11 @@ def run_conversation(
|
|
|
929
697
|
# landed after an orphan tool result). Most providers return
|
|
930
698
|
# empty content on malformed sequences, which would otherwise
|
|
931
699
|
# retrigger the empty-retry loop indefinitely.
|
|
932
|
-
|
|
700
|
+
# repair_message_sequence_with_cursor also recomputes the SessionDB
|
|
701
|
+
# flush cursor (_last_flushed_db_idx) when repair compacts the list,
|
|
702
|
+
# so the turn-end flush doesn't skip the assistant/tool chain (#44837).
|
|
703
|
+
from agent.agent_runtime_helpers import repair_message_sequence_with_cursor
|
|
704
|
+
repaired_seq = repair_message_sequence_with_cursor(agent, messages)
|
|
933
705
|
if repaired_seq > 0:
|
|
934
706
|
request_logger.info(
|
|
935
707
|
"Repaired %s message-alternation violations before request (session=%s)",
|
|
@@ -977,7 +749,7 @@ def run_conversation(
|
|
|
977
749
|
# Uses new dicts so the internal messages list retains the fields
|
|
978
750
|
# for Codex Responses compatibility.
|
|
979
751
|
if agent._should_sanitize_tool_calls():
|
|
980
|
-
agent._sanitize_tool_calls_for_strict_api(api_msg)
|
|
752
|
+
agent._sanitize_tool_calls_for_strict_api(api_msg, model=agent.model)
|
|
981
753
|
# Keep 'reasoning_details' - OpenRouter uses this for multi-turn reasoning context
|
|
982
754
|
# The signature field helps maintain reasoning continuity
|
|
983
755
|
api_messages.append(api_msg)
|
|
@@ -1037,7 +809,10 @@ def run_conversation(
|
|
|
1037
809
|
# a thinking-only turn. Runs on the per-call copy only — the
|
|
1038
810
|
# stored conversation history keeps the reasoning block for the
|
|
1039
811
|
# UI transcript and session persistence.
|
|
1040
|
-
api_messages = agent._drop_thinking_only_and_merge_users(
|
|
812
|
+
api_messages = agent._drop_thinking_only_and_merge_users(
|
|
813
|
+
api_messages,
|
|
814
|
+
drop_codex_reasoning_items=agent.api_mode != "codex_responses",
|
|
815
|
+
)
|
|
1041
816
|
|
|
1042
817
|
# Normalize message whitespace and tool-call JSON for consistent
|
|
1043
818
|
# prefix matching. Ensures bit-perfect prefixes across turns,
|
|
@@ -1133,26 +908,14 @@ def run_conversation(
|
|
|
1133
908
|
api_start_time = time.time()
|
|
1134
909
|
retry_count = 0
|
|
1135
910
|
max_retries = agent._api_max_retries
|
|
1136
|
-
|
|
911
|
+
_retry = TurnRetryState()
|
|
1137
912
|
max_compression_attempts = 3
|
|
1138
|
-
codex_auth_retry_attempted=False
|
|
1139
|
-
anthropic_auth_retry_attempted=False
|
|
1140
|
-
nous_auth_retry_attempted=False
|
|
1141
|
-
nous_paid_entitlement_refresh_attempted=False
|
|
1142
|
-
copilot_auth_retry_attempted=False
|
|
1143
|
-
thinking_sig_retry_attempted = False
|
|
1144
|
-
invalid_encrypted_content_retry_attempted = False
|
|
1145
|
-
image_shrink_retry_attempted = False
|
|
1146
|
-
multimodal_tool_content_retry_attempted = False
|
|
1147
|
-
oauth_1m_beta_retry_attempted = False
|
|
1148
|
-
llama_cpp_grammar_retry_attempted = False
|
|
1149
|
-
has_retried_429 = False
|
|
1150
|
-
restart_with_compressed_messages = False
|
|
1151
|
-
restart_with_length_continuation = False
|
|
1152
913
|
|
|
1153
914
|
finish_reason = "stop"
|
|
1154
915
|
response = None # Guard against UnboundLocalError if all retries fail
|
|
1155
916
|
api_kwargs = None # Guard against UnboundLocalError in except handler
|
|
917
|
+
api_request_id = f"{turn_id}:api:{api_call_count}"
|
|
918
|
+
agent._current_api_request_id = api_request_id
|
|
1156
919
|
|
|
1157
920
|
while retry_count < max_retries:
|
|
1158
921
|
# ── Nous Portal rate limit guard ──────────────────────
|
|
@@ -1179,7 +942,7 @@ def run_conversation(
|
|
|
1179
942
|
if agent._try_activate_fallback():
|
|
1180
943
|
retry_count = 0
|
|
1181
944
|
compression_attempts = 0
|
|
1182
|
-
primary_recovery_attempted = False
|
|
945
|
+
_retry.primary_recovery_attempted = False
|
|
1183
946
|
continue
|
|
1184
947
|
# No fallback available — surface buffered context
|
|
1185
948
|
# so user sees the rate-limit message that led here.
|
|
@@ -1218,39 +981,83 @@ def run_conversation(
|
|
|
1218
981
|
_sanitize_structure_non_ascii(api_kwargs)
|
|
1219
982
|
if agent.api_mode == "codex_responses":
|
|
1220
983
|
api_kwargs = agent._get_transport().preflight_kwargs(api_kwargs, allow_stream=False)
|
|
1221
|
-
|
|
1222
984
|
try:
|
|
1223
|
-
from hermes_cli.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if not isinstance(request_messages, list):
|
|
1228
|
-
request_messages = api_messages
|
|
1229
|
-
# Shallow-copy the outer list so plugins that retain the
|
|
1230
|
-
# reference for async snapshotting don't observe later
|
|
1231
|
-
# mutations of api_messages. The inner dicts are not
|
|
1232
|
-
# mutated by the agent loop, so a shallow copy is
|
|
1233
|
-
# sufficient; a deepcopy would walk every tool result
|
|
1234
|
-
# and base64 image on every API call.
|
|
1235
|
-
_invoke_hook(
|
|
1236
|
-
"pre_api_request",
|
|
985
|
+
from hermes_cli.middleware import apply_llm_request_middleware
|
|
986
|
+
|
|
987
|
+
_llm_request_mw = apply_llm_request_middleware(
|
|
988
|
+
api_kwargs,
|
|
1237
989
|
task_id=effective_task_id,
|
|
990
|
+
turn_id=turn_id,
|
|
991
|
+
api_request_id=api_request_id,
|
|
1238
992
|
session_id=agent.session_id or "",
|
|
1239
|
-
user_message=original_user_message,
|
|
1240
|
-
conversation_history=list(messages),
|
|
1241
993
|
platform=agent.platform or "",
|
|
1242
994
|
model=agent.model,
|
|
1243
995
|
provider=agent.provider,
|
|
1244
996
|
base_url=agent.base_url,
|
|
1245
997
|
api_mode=agent.api_mode,
|
|
1246
998
|
api_call_count=api_call_count,
|
|
1247
|
-
request_messages=list(request_messages) if isinstance(request_messages, list) else [],
|
|
1248
|
-
message_count=len(api_messages),
|
|
1249
|
-
tool_count=len(agent.tools or []),
|
|
1250
|
-
approx_input_tokens=approx_tokens,
|
|
1251
|
-
request_char_count=total_chars,
|
|
1252
|
-
max_tokens=agent.max_tokens,
|
|
1253
999
|
)
|
|
1000
|
+
api_kwargs = _llm_request_mw.payload
|
|
1001
|
+
_original_api_kwargs = _llm_request_mw.original_payload
|
|
1002
|
+
_llm_middleware_trace = _llm_request_mw.trace
|
|
1003
|
+
except Exception:
|
|
1004
|
+
_original_api_kwargs = dict(api_kwargs)
|
|
1005
|
+
_llm_middleware_trace = []
|
|
1006
|
+
|
|
1007
|
+
try:
|
|
1008
|
+
from hermes_cli.plugins import (
|
|
1009
|
+
has_hook,
|
|
1010
|
+
invoke_hook as _invoke_hook,
|
|
1011
|
+
)
|
|
1012
|
+
if has_hook("pre_api_request"):
|
|
1013
|
+
request_messages = api_kwargs.get("messages")
|
|
1014
|
+
if not isinstance(request_messages, list):
|
|
1015
|
+
request_messages = api_kwargs.get("input")
|
|
1016
|
+
if not isinstance(request_messages, list):
|
|
1017
|
+
request_messages = api_messages
|
|
1018
|
+
# Shallow-copy the outer list so plugins that retain the
|
|
1019
|
+
# reference for async snapshotting don't observe later
|
|
1020
|
+
# mutations of api_messages. The inner dicts are not
|
|
1021
|
+
# mutated by the agent loop, so a shallow copy is
|
|
1022
|
+
# sufficient; a deepcopy would walk every tool result
|
|
1023
|
+
# and base64 image on every API call.
|
|
1024
|
+
#
|
|
1025
|
+
# The ``request_messages`` and ``conversation_history``
|
|
1026
|
+
# kwargs below are pre-existing raw passthroughs
|
|
1027
|
+
# consumed by the bundled langfuse plugin
|
|
1028
|
+
# (``plugins/observability/langfuse/__init__.py:_coerce_request_messages``).
|
|
1029
|
+
# They predate ``request`` and are intentionally NOT
|
|
1030
|
+
# sanitised — secrets are not expected here because
|
|
1031
|
+
# ``api_kwargs`` is the same object passed to the
|
|
1032
|
+
# provider client. New consumers should read the
|
|
1033
|
+
# sanitised view from ``request["body"]["messages"]``.
|
|
1034
|
+
_request_payload = agent._api_request_payload_for_hook(api_kwargs)
|
|
1035
|
+
_invoke_hook(
|
|
1036
|
+
"pre_api_request",
|
|
1037
|
+
task_id=effective_task_id,
|
|
1038
|
+
turn_id=turn_id,
|
|
1039
|
+
api_request_id=api_request_id,
|
|
1040
|
+
session_id=agent.session_id or "",
|
|
1041
|
+
user_message=original_user_message,
|
|
1042
|
+
conversation_history=list(messages),
|
|
1043
|
+
platform=agent.platform or "",
|
|
1044
|
+
model=agent.model,
|
|
1045
|
+
provider=agent.provider,
|
|
1046
|
+
base_url=agent.base_url,
|
|
1047
|
+
api_mode=agent.api_mode,
|
|
1048
|
+
api_call_count=api_call_count,
|
|
1049
|
+
request_messages=list(request_messages)
|
|
1050
|
+
if isinstance(request_messages, list)
|
|
1051
|
+
else [],
|
|
1052
|
+
message_count=len(api_messages),
|
|
1053
|
+
tool_count=len(agent.tools or []),
|
|
1054
|
+
approx_input_tokens=approx_tokens,
|
|
1055
|
+
request_char_count=total_chars,
|
|
1056
|
+
max_tokens=agent.max_tokens,
|
|
1057
|
+
started_at=api_start_time,
|
|
1058
|
+
middleware_trace=list(_llm_middleware_trace),
|
|
1059
|
+
request=_request_payload,
|
|
1060
|
+
)
|
|
1254
1061
|
except Exception:
|
|
1255
1062
|
pass
|
|
1256
1063
|
|
|
@@ -1300,12 +1107,31 @@ def run_conversation(
|
|
|
1300
1107
|
if isinstance(getattr(agent, "client", None), Mock):
|
|
1301
1108
|
_use_streaming = False
|
|
1302
1109
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1110
|
+
def _perform_api_call(next_api_kwargs):
|
|
1111
|
+
if _use_streaming:
|
|
1112
|
+
return agent._interruptible_streaming_api_call(
|
|
1113
|
+
next_api_kwargs, on_first_delta=_stop_spinner
|
|
1114
|
+
)
|
|
1115
|
+
return agent._interruptible_api_call(next_api_kwargs)
|
|
1116
|
+
|
|
1117
|
+
from hermes_cli.middleware import run_llm_execution_middleware
|
|
1118
|
+
|
|
1119
|
+
response = run_llm_execution_middleware(
|
|
1120
|
+
api_kwargs,
|
|
1121
|
+
_perform_api_call,
|
|
1122
|
+
original_request=_original_api_kwargs,
|
|
1123
|
+
task_id=effective_task_id,
|
|
1124
|
+
turn_id=turn_id,
|
|
1125
|
+
api_request_id=api_request_id,
|
|
1126
|
+
session_id=agent.session_id or "",
|
|
1127
|
+
platform=agent.platform or "",
|
|
1128
|
+
model=agent.model,
|
|
1129
|
+
provider=agent.provider,
|
|
1130
|
+
base_url=agent.base_url,
|
|
1131
|
+
api_mode=agent.api_mode,
|
|
1132
|
+
api_call_count=api_call_count,
|
|
1133
|
+
middleware_trace=list(_llm_middleware_trace),
|
|
1134
|
+
)
|
|
1309
1135
|
|
|
1310
1136
|
api_duration = time.time() - api_start_time
|
|
1311
1137
|
|
|
@@ -1406,6 +1232,21 @@ def run_conversation(
|
|
|
1406
1232
|
error_details.append("response.choices is empty")
|
|
1407
1233
|
|
|
1408
1234
|
if response_invalid:
|
|
1235
|
+
agent._invoke_api_request_error_hook(
|
|
1236
|
+
task_id=effective_task_id,
|
|
1237
|
+
turn_id=turn_id,
|
|
1238
|
+
api_request_id=api_request_id,
|
|
1239
|
+
api_call_count=api_call_count,
|
|
1240
|
+
api_start_time=api_start_time,
|
|
1241
|
+
api_kwargs=api_kwargs,
|
|
1242
|
+
error_type="InvalidAPIResponse",
|
|
1243
|
+
error_message=", ".join(error_details) or "Invalid API response",
|
|
1244
|
+
status_code=getattr(getattr(response, "error", None), "code", None),
|
|
1245
|
+
retry_count=retry_count,
|
|
1246
|
+
max_retries=max_retries,
|
|
1247
|
+
retryable=True,
|
|
1248
|
+
reason="invalid_response",
|
|
1249
|
+
)
|
|
1409
1250
|
# Stop spinner silently — retry status is now buffered
|
|
1410
1251
|
# and only surfaced if every retry+fallback exhausts.
|
|
1411
1252
|
if thinking_spinner:
|
|
@@ -1426,7 +1267,7 @@ def run_conversation(
|
|
|
1426
1267
|
if agent._try_activate_fallback():
|
|
1427
1268
|
retry_count = 0
|
|
1428
1269
|
compression_attempts = 0
|
|
1429
|
-
primary_recovery_attempted = False
|
|
1270
|
+
_retry.primary_recovery_attempted = False
|
|
1430
1271
|
continue
|
|
1431
1272
|
|
|
1432
1273
|
# Check for error field in response (some providers include this)
|
|
@@ -1497,7 +1338,7 @@ def run_conversation(
|
|
|
1497
1338
|
if agent._try_activate_fallback():
|
|
1498
1339
|
retry_count = 0
|
|
1499
1340
|
compression_attempts = 0
|
|
1500
|
-
primary_recovery_attempted = False
|
|
1341
|
+
_retry.primary_recovery_attempted = False
|
|
1501
1342
|
continue
|
|
1502
1343
|
# Terminal — flush buffered retry trace so user sees what happened.
|
|
1503
1344
|
agent._flush_status_buffer()
|
|
@@ -1580,6 +1421,106 @@ def run_conversation(
|
|
|
1580
1421
|
)
|
|
1581
1422
|
finish_reason = "length"
|
|
1582
1423
|
|
|
1424
|
+
# ── Content-policy refusal (HTTP 200) ──────────────────
|
|
1425
|
+
# The model — or the provider's safety system — returned a
|
|
1426
|
+
# *successful* response whose stop/finish reason is a refusal:
|
|
1427
|
+
# Anthropic ``stop_reason="refusal"`` → ``content_filter``;
|
|
1428
|
+
# OpenAI / portal ``finish_reason="content_filter"`` or a
|
|
1429
|
+
# populated ``message.refusal`` (mapped in the chat_completions
|
|
1430
|
+
# transport); Bedrock ``guardrail_intervened``. The content is
|
|
1431
|
+
# typically empty, so without this branch the response falls
|
|
1432
|
+
# through to the empty-response / invalid-response retry loops
|
|
1433
|
+
# and is mis-surfaced as "rate limited" / "no content after
|
|
1434
|
+
# retries" — burning paid attempts reproducing a deterministic
|
|
1435
|
+
# refusal. Surface it clearly and stop. Mirrors the
|
|
1436
|
+
# exception-based ``content_policy_blocked`` recovery: try a
|
|
1437
|
+
# configured fallback once, otherwise return the refusal.
|
|
1438
|
+
if finish_reason == "content_filter":
|
|
1439
|
+
_refusal_transport = agent._get_transport()
|
|
1440
|
+
if agent.api_mode == "anthropic_messages":
|
|
1441
|
+
_refusal_result = _refusal_transport.normalize_response(
|
|
1442
|
+
response, strip_tool_prefix=agent._is_anthropic_oauth
|
|
1443
|
+
)
|
|
1444
|
+
else:
|
|
1445
|
+
_refusal_result = _refusal_transport.normalize_response(response)
|
|
1446
|
+
_refusal_text = (getattr(_refusal_result, "content", None) or "").strip()
|
|
1447
|
+
# Some refusals carry the explanation only in the reasoning
|
|
1448
|
+
# channel; fall back to it so the user sees *something*.
|
|
1449
|
+
if not _refusal_text:
|
|
1450
|
+
_refusal_text = (agent._extract_reasoning(_refusal_result) or "").strip()
|
|
1451
|
+
|
|
1452
|
+
agent._invoke_api_request_error_hook(
|
|
1453
|
+
task_id=effective_task_id,
|
|
1454
|
+
turn_id=turn_id,
|
|
1455
|
+
api_request_id=api_request_id,
|
|
1456
|
+
api_call_count=api_call_count,
|
|
1457
|
+
api_start_time=api_start_time,
|
|
1458
|
+
api_kwargs=api_kwargs,
|
|
1459
|
+
error_type="ContentPolicyBlocked",
|
|
1460
|
+
error_message=_refusal_text or "model declined to respond (content_filter)",
|
|
1461
|
+
status_code=None,
|
|
1462
|
+
retry_count=retry_count,
|
|
1463
|
+
max_retries=max_retries,
|
|
1464
|
+
retryable=False,
|
|
1465
|
+
reason=FailoverReason.content_policy_blocked.value,
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
if thinking_spinner:
|
|
1469
|
+
thinking_spinner.stop("")
|
|
1470
|
+
thinking_spinner = None
|
|
1471
|
+
if agent.thinking_callback:
|
|
1472
|
+
agent.thinking_callback("")
|
|
1473
|
+
|
|
1474
|
+
# Deterministic for the unchanged prompt — never retry.
|
|
1475
|
+
# Try a configured fallback once (a different model may not
|
|
1476
|
+
# refuse); otherwise surface the refusal terminally.
|
|
1477
|
+
if agent._has_pending_fallback():
|
|
1478
|
+
agent._buffer_status(
|
|
1479
|
+
"⚠️ Model declined to respond (safety refusal) — trying fallback..."
|
|
1480
|
+
)
|
|
1481
|
+
if agent._try_activate_fallback():
|
|
1482
|
+
retry_count = 0
|
|
1483
|
+
compression_attempts = 0
|
|
1484
|
+
_retry.primary_recovery_attempted = False
|
|
1485
|
+
continue
|
|
1486
|
+
|
|
1487
|
+
agent._flush_status_buffer()
|
|
1488
|
+
_refusal_log = (
|
|
1489
|
+
_refusal_text[:500] + "..."
|
|
1490
|
+
if len(_refusal_text) > 500
|
|
1491
|
+
else _refusal_text
|
|
1492
|
+
)
|
|
1493
|
+
logger.warning(
|
|
1494
|
+
"%sModel declined to respond (finish_reason=content_filter). "
|
|
1495
|
+
"model=%s provider=%s refusal=%s",
|
|
1496
|
+
agent.log_prefix, agent.model, agent.provider,
|
|
1497
|
+
_refusal_log or "(no text)",
|
|
1498
|
+
)
|
|
1499
|
+
agent._emit_status(
|
|
1500
|
+
"⚠️ The model declined to respond to this request (safety refusal)."
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
_refusal_detail = (
|
|
1504
|
+
f"Model's explanation: {_refusal_text}"
|
|
1505
|
+
if _refusal_text
|
|
1506
|
+
else "The model returned no explanation."
|
|
1507
|
+
)
|
|
1508
|
+
_refusal_response = (
|
|
1509
|
+
"⚠️ The model declined to respond to this request "
|
|
1510
|
+
"(safety refusal — not a Hermes/gateway failure).\n\n"
|
|
1511
|
+
f"{_refusal_detail}\n\n"
|
|
1512
|
+
f"{_CONTENT_POLICY_RECOVERY_HINT}"
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
agent._cleanup_task_resources(effective_task_id)
|
|
1516
|
+
agent._persist_session(messages, conversation_history)
|
|
1517
|
+
return _content_policy_blocked_result(
|
|
1518
|
+
messages,
|
|
1519
|
+
api_call_count,
|
|
1520
|
+
final_response=_refusal_response,
|
|
1521
|
+
error_detail=_refusal_text or "model declined (content_filter)",
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1583
1524
|
if finish_reason == "length":
|
|
1584
1525
|
if getattr(response, "id", "") == PARTIAL_STREAM_STUB_ID:
|
|
1585
1526
|
agent._vprint(
|
|
@@ -1721,7 +1662,7 @@ def run_conversation(
|
|
|
1721
1662
|
}
|
|
1722
1663
|
messages.append(continue_msg)
|
|
1723
1664
|
agent._session_messages = messages
|
|
1724
|
-
restart_with_length_continuation = True
|
|
1665
|
+
_retry.restart_with_length_continuation = True
|
|
1725
1666
|
break
|
|
1726
1667
|
|
|
1727
1668
|
partial_response = agent._strip_think_blocks("".join(truncated_response_parts)).strip()
|
|
@@ -1970,7 +1911,7 @@ def run_conversation(
|
|
|
1970
1911
|
f"({hit_pct:.0f}% hit, {written:,} written)"
|
|
1971
1912
|
)
|
|
1972
1913
|
|
|
1973
|
-
has_retried_429 = False # Reset on success
|
|
1914
|
+
_retry.has_retried_429 = False # Reset on success
|
|
1974
1915
|
# Note: don't clear the retry buffer here — an "API call
|
|
1975
1916
|
# success" only means we got bytes back, not that we got
|
|
1976
1917
|
# usable content. Empty responses still loop through the
|
|
@@ -1998,7 +1939,7 @@ def run_conversation(
|
|
|
1998
1939
|
agent._vprint(f"{agent.log_prefix}⚡ Interrupted during API call.", force=True)
|
|
1999
1940
|
agent._persist_session(messages, conversation_history)
|
|
2000
1941
|
interrupted = True
|
|
2001
|
-
final_response = f"
|
|
1942
|
+
final_response = f"{INTERRUPT_WAITING_FOR_MODEL_PREFIX}{api_elapsed:.1f}s elapsed)."
|
|
2002
1943
|
break
|
|
2003
1944
|
|
|
2004
1945
|
except Exception as api_error:
|
|
@@ -2278,6 +2219,21 @@ def run_conversation(
|
|
|
2278
2219
|
classified.retryable, classified.should_compress,
|
|
2279
2220
|
classified.should_rotate_credential, classified.should_fallback,
|
|
2280
2221
|
)
|
|
2222
|
+
agent._invoke_api_request_error_hook(
|
|
2223
|
+
task_id=effective_task_id,
|
|
2224
|
+
turn_id=turn_id,
|
|
2225
|
+
api_request_id=api_request_id,
|
|
2226
|
+
api_call_count=api_call_count,
|
|
2227
|
+
api_start_time=api_start_time,
|
|
2228
|
+
api_kwargs=api_kwargs,
|
|
2229
|
+
error_type=type(api_error).__name__,
|
|
2230
|
+
error_message=str(api_error),
|
|
2231
|
+
status_code=status_code,
|
|
2232
|
+
retry_count=retry_count,
|
|
2233
|
+
max_retries=max_retries,
|
|
2234
|
+
retryable=classified.retryable,
|
|
2235
|
+
reason=classified.reason.value,
|
|
2236
|
+
)
|
|
2281
2237
|
|
|
2282
2238
|
if (
|
|
2283
2239
|
classified.reason == FailoverReason.billing
|
|
@@ -2285,9 +2241,9 @@ def run_conversation(
|
|
|
2285
2241
|
getattr(agent, "provider", "") or "",
|
|
2286
2242
|
getattr(agent, "base_url", "") or "",
|
|
2287
2243
|
)
|
|
2288
|
-
and not nous_paid_entitlement_refresh_attempted
|
|
2244
|
+
and not _retry.nous_paid_entitlement_refresh_attempted
|
|
2289
2245
|
):
|
|
2290
|
-
nous_paid_entitlement_refresh_attempted = True
|
|
2246
|
+
_retry.nous_paid_entitlement_refresh_attempted = True
|
|
2291
2247
|
if _try_refresh_nous_paid_entitlement_credentials(agent):
|
|
2292
2248
|
agent._vprint(
|
|
2293
2249
|
f"{agent.log_prefix}🔐 Nous paid access verified — "
|
|
@@ -2296,9 +2252,9 @@ def run_conversation(
|
|
|
2296
2252
|
)
|
|
2297
2253
|
continue
|
|
2298
2254
|
|
|
2299
|
-
recovered_with_pool, has_retried_429 = agent._recover_with_credential_pool(
|
|
2255
|
+
recovered_with_pool, _retry.has_retried_429 = agent._recover_with_credential_pool(
|
|
2300
2256
|
status_code=status_code,
|
|
2301
|
-
has_retried_429=has_retried_429,
|
|
2257
|
+
has_retried_429=_retry.has_retried_429,
|
|
2302
2258
|
classified_reason=classified.reason,
|
|
2303
2259
|
error_context=error_context,
|
|
2304
2260
|
)
|
|
@@ -2313,10 +2269,14 @@ def run_conversation(
|
|
|
2313
2269
|
# fails, fall through to normal error handling.
|
|
2314
2270
|
if (
|
|
2315
2271
|
classified.reason == FailoverReason.image_too_large
|
|
2316
|
-
and not image_shrink_retry_attempted
|
|
2272
|
+
and not _retry.image_shrink_retry_attempted
|
|
2317
2273
|
):
|
|
2318
|
-
image_shrink_retry_attempted = True
|
|
2319
|
-
|
|
2274
|
+
_retry.image_shrink_retry_attempted = True
|
|
2275
|
+
image_max_dimension = _image_error_max_dimension(api_error) or 8000
|
|
2276
|
+
if agent._try_shrink_image_parts_in_messages(
|
|
2277
|
+
api_messages,
|
|
2278
|
+
max_dimension=image_max_dimension,
|
|
2279
|
+
):
|
|
2320
2280
|
agent._vprint(
|
|
2321
2281
|
f"{agent.log_prefix}📐 Image(s) exceeded provider size limit — "
|
|
2322
2282
|
f"shrank and retrying...",
|
|
@@ -2338,9 +2298,9 @@ def run_conversation(
|
|
|
2338
2298
|
# downgrade, and retry once. See issue #27344.
|
|
2339
2299
|
if (
|
|
2340
2300
|
classified.reason == FailoverReason.multimodal_tool_content_unsupported
|
|
2341
|
-
and not multimodal_tool_content_retry_attempted
|
|
2301
|
+
and not _retry.multimodal_tool_content_retry_attempted
|
|
2342
2302
|
):
|
|
2343
|
-
multimodal_tool_content_retry_attempted = True
|
|
2303
|
+
_retry.multimodal_tool_content_retry_attempted = True
|
|
2344
2304
|
if agent._try_strip_image_parts_from_tool_messages(api_messages):
|
|
2345
2305
|
agent._vprint(
|
|
2346
2306
|
f"{agent.log_prefix}📐 Provider rejected list-type tool content — "
|
|
@@ -2367,9 +2327,9 @@ def run_conversation(
|
|
|
2367
2327
|
classified.reason == FailoverReason.oauth_long_context_beta_forbidden
|
|
2368
2328
|
and agent.api_mode == "anthropic_messages"
|
|
2369
2329
|
and agent._is_anthropic_oauth
|
|
2370
|
-
and not oauth_1m_beta_retry_attempted
|
|
2330
|
+
and not _retry.oauth_1m_beta_retry_attempted
|
|
2371
2331
|
):
|
|
2372
|
-
oauth_1m_beta_retry_attempted = True
|
|
2332
|
+
_retry.oauth_1m_beta_retry_attempted = True
|
|
2373
2333
|
if not getattr(agent, "_oauth_1m_beta_disabled", False):
|
|
2374
2334
|
agent._oauth_1m_beta_disabled = True
|
|
2375
2335
|
try:
|
|
@@ -2388,9 +2348,9 @@ def run_conversation(
|
|
|
2388
2348
|
agent.api_mode == "codex_responses"
|
|
2389
2349
|
and agent.provider in {"openai-codex", "xai-oauth"}
|
|
2390
2350
|
and status_code == 401
|
|
2391
|
-
and not codex_auth_retry_attempted
|
|
2351
|
+
and not _retry.codex_auth_retry_attempted
|
|
2392
2352
|
):
|
|
2393
|
-
codex_auth_retry_attempted = True
|
|
2353
|
+
_retry.codex_auth_retry_attempted = True
|
|
2394
2354
|
if agent._try_refresh_codex_client_credentials(force=True):
|
|
2395
2355
|
_label = "xAI OAuth" if agent.provider == "xai-oauth" else "Codex"
|
|
2396
2356
|
agent._buffer_vprint(f"🔐 {_label} auth refreshed after 401. Retrying request...")
|
|
@@ -2399,9 +2359,9 @@ def run_conversation(
|
|
|
2399
2359
|
agent.api_mode == "chat_completions"
|
|
2400
2360
|
and agent.provider == "nous"
|
|
2401
2361
|
and status_code == 401
|
|
2402
|
-
and not nous_auth_retry_attempted
|
|
2362
|
+
and not _retry.nous_auth_retry_attempted
|
|
2403
2363
|
):
|
|
2404
|
-
nous_auth_retry_attempted = True
|
|
2364
|
+
_retry.nous_auth_retry_attempted = True
|
|
2405
2365
|
if agent._try_refresh_nous_client_credentials(force=True):
|
|
2406
2366
|
print(f"{agent.log_prefix}🔐 Nous agent key refreshed after 401. Retrying request...")
|
|
2407
2367
|
continue
|
|
@@ -2430,9 +2390,9 @@ def run_conversation(
|
|
|
2430
2390
|
if (
|
|
2431
2391
|
agent.provider == "copilot"
|
|
2432
2392
|
and status_code == 401
|
|
2433
|
-
and not copilot_auth_retry_attempted
|
|
2393
|
+
and not _retry.copilot_auth_retry_attempted
|
|
2434
2394
|
):
|
|
2435
|
-
copilot_auth_retry_attempted = True
|
|
2395
|
+
_retry.copilot_auth_retry_attempted = True
|
|
2436
2396
|
if agent._try_refresh_copilot_client_credentials():
|
|
2437
2397
|
agent._buffer_vprint(f"🔐 Copilot credentials refreshed after 401. Retrying request...")
|
|
2438
2398
|
continue
|
|
@@ -2440,9 +2400,9 @@ def run_conversation(
|
|
|
2440
2400
|
agent.api_mode == "anthropic_messages"
|
|
2441
2401
|
and status_code == 401
|
|
2442
2402
|
and hasattr(agent, '_anthropic_api_key')
|
|
2443
|
-
and not anthropic_auth_retry_attempted
|
|
2403
|
+
and not _retry.anthropic_auth_retry_attempted
|
|
2444
2404
|
):
|
|
2445
|
-
anthropic_auth_retry_attempted = True
|
|
2405
|
+
_retry.anthropic_auth_retry_attempted = True
|
|
2446
2406
|
from agent.anthropic_adapter import _is_oauth_token
|
|
2447
2407
|
from agent.azure_identity_adapter import is_token_provider
|
|
2448
2408
|
if agent._try_refresh_anthropic_client_credentials():
|
|
@@ -2474,30 +2434,54 @@ def run_conversation(
|
|
|
2474
2434
|
print(f"{agent.log_prefix} • Legacy cleanup: hermes config set ANTHROPIC_TOKEN \"\"")
|
|
2475
2435
|
print(f"{agent.log_prefix} • Clear stale keys: hermes config set ANTHROPIC_API_KEY \"\"")
|
|
2476
2436
|
|
|
2477
|
-
#
|
|
2437
|
+
# Thinking block signature recovery.
|
|
2438
|
+
#
|
|
2478
2439
|
# Anthropic signs thinking blocks against the full turn
|
|
2479
|
-
# content.
|
|
2440
|
+
# content. Any upstream mutation (context compression,
|
|
2480
2441
|
# session truncation, message merging) invalidates the
|
|
2481
|
-
# signature
|
|
2482
|
-
#
|
|
2483
|
-
#
|
|
2442
|
+
# signature and the API replies HTTP 400 ("invalid
|
|
2443
|
+
# signature" or "cannot be modified"). Recovery strips
|
|
2444
|
+
# ``reasoning_details`` so the retry sends no thinking
|
|
2445
|
+
# blocks at all. One-shot per outer loop.
|
|
2446
|
+
#
|
|
2447
|
+
# The strip targets ``api_messages``, which is the
|
|
2448
|
+
# API-call-time list that ``_build_api_kwargs`` consumes
|
|
2449
|
+
# on every retry. ``api_messages`` was populated once at
|
|
2450
|
+
# the start of the turn from shallow copies of
|
|
2451
|
+
# ``messages``, so mutating it does not touch the
|
|
2452
|
+
# canonical store. The previous implementation popped
|
|
2453
|
+
# ``reasoning_details`` from ``messages`` instead, which
|
|
2454
|
+
# had two problems: ``api_messages`` carried its own
|
|
2455
|
+
# reference to the field through the shallow copy, so the
|
|
2456
|
+
# retry's wire payload still included thinking blocks and
|
|
2457
|
+
# the recovery never reached the API; and the mutation
|
|
2458
|
+
# persisted into ``state.db`` through any subsequent
|
|
2459
|
+
# ``_persist_session`` call, permanently corrupting the
|
|
2460
|
+
# conversation. Future turns would replay the stripped
|
|
2461
|
+
# state, hit the same 400, and the agent would terminate
|
|
2462
|
+
# with ``max_retries_exhausted``, often spawning
|
|
2463
|
+
# cascading compaction-ended sessions chained off the
|
|
2464
|
+
# corrupted parent.
|
|
2484
2465
|
if (
|
|
2485
2466
|
classified.reason == FailoverReason.thinking_signature
|
|
2486
|
-
and not thinking_sig_retry_attempted
|
|
2467
|
+
and not _retry.thinking_sig_retry_attempted
|
|
2487
2468
|
):
|
|
2488
|
-
thinking_sig_retry_attempted = True
|
|
2489
|
-
|
|
2490
|
-
|
|
2469
|
+
_retry.thinking_sig_retry_attempted = True
|
|
2470
|
+
_api_stripped = 0
|
|
2471
|
+
for _m in api_messages:
|
|
2472
|
+
if isinstance(_m, dict) and "reasoning_details" in _m:
|
|
2491
2473
|
_m.pop("reasoning_details", None)
|
|
2474
|
+
_api_stripped += 1
|
|
2492
2475
|
agent._vprint(
|
|
2493
|
-
f"{agent.log_prefix}⚠️ Thinking block signature invalid
|
|
2494
|
-
f"stripped
|
|
2476
|
+
f"{agent.log_prefix}⚠️ Thinking block signature invalid, "
|
|
2477
|
+
f"stripped reasoning_details from api_messages for retry...",
|
|
2495
2478
|
force=True,
|
|
2496
2479
|
)
|
|
2497
2480
|
logger.warning(
|
|
2498
2481
|
"%sThinking block signature recovery: stripped "
|
|
2499
|
-
"reasoning_details from %d
|
|
2500
|
-
|
|
2482
|
+
"reasoning_details from %d api_messages "
|
|
2483
|
+
"(canonical messages unchanged)",
|
|
2484
|
+
agent.log_prefix, _api_stripped,
|
|
2501
2485
|
)
|
|
2502
2486
|
continue
|
|
2503
2487
|
|
|
@@ -2517,7 +2501,7 @@ def run_conversation(
|
|
|
2517
2501
|
# handles it (the provider is rejecting something else).
|
|
2518
2502
|
if (
|
|
2519
2503
|
classified.reason == FailoverReason.invalid_encrypted_content
|
|
2520
|
-
and not invalid_encrypted_content_retry_attempted
|
|
2504
|
+
and not _retry.invalid_encrypted_content_retry_attempted
|
|
2521
2505
|
and agent.api_mode == "codex_responses"
|
|
2522
2506
|
and bool(getattr(agent, "_codex_reasoning_replay_enabled", True))
|
|
2523
2507
|
and any(
|
|
@@ -2528,7 +2512,7 @@ def run_conversation(
|
|
|
2528
2512
|
for _m in messages
|
|
2529
2513
|
)
|
|
2530
2514
|
):
|
|
2531
|
-
invalid_encrypted_content_retry_attempted = True
|
|
2515
|
+
_retry.invalid_encrypted_content_retry_attempted = True
|
|
2532
2516
|
replay_stats = agent._disable_codex_reasoning_replay(messages)
|
|
2533
2517
|
agent._vprint(
|
|
2534
2518
|
f"{agent.log_prefix}⚠️ Encrypted reasoning replay was rejected by the provider — "
|
|
@@ -2555,9 +2539,9 @@ def run_conversation(
|
|
|
2555
2539
|
# fires only for users on llama.cpp's OAI server.
|
|
2556
2540
|
if (
|
|
2557
2541
|
classified.reason == FailoverReason.llama_cpp_grammar_pattern
|
|
2558
|
-
and not llama_cpp_grammar_retry_attempted
|
|
2542
|
+
and not _retry.llama_cpp_grammar_retry_attempted
|
|
2559
2543
|
):
|
|
2560
|
-
llama_cpp_grammar_retry_attempted = True
|
|
2544
|
+
_retry.llama_cpp_grammar_retry_attempted = True
|
|
2561
2545
|
try:
|
|
2562
2546
|
from tools.schema_sanitizer import strip_pattern_and_format
|
|
2563
2547
|
_, _stripped = strip_pattern_and_format(agent.tools)
|
|
@@ -2660,6 +2644,61 @@ def run_conversation(
|
|
|
2660
2644
|
# compress history and retry, not abort immediately.
|
|
2661
2645
|
status_code = getattr(api_error, "status_code", None)
|
|
2662
2646
|
|
|
2647
|
+
# ── Respect disabled auto-compaction on overflow ──────
|
|
2648
|
+
# Ported from anomalyco/opencode#30749. When the user has
|
|
2649
|
+
# turned auto-compaction off (``compression.enabled: false``),
|
|
2650
|
+
# NO automatic compaction trigger may fire — including the
|
|
2651
|
+
# provider/request-size overflow recovery paths below
|
|
2652
|
+
# (long-context-tier 429, 413 payload-too-large, and
|
|
2653
|
+
# context-overflow). Without this guard the proactive
|
|
2654
|
+
# threshold path correctly honours the setting (see the
|
|
2655
|
+
# preflight check and the post-response ``should_compress``
|
|
2656
|
+
# gate) but a provider overflow error would still silently
|
|
2657
|
+
# compress + rotate the session, bypassing the user's
|
|
2658
|
+
# explicit choice. Surface a terminal error instead so the
|
|
2659
|
+
# user can compact manually (``/compress``), start fresh
|
|
2660
|
+
# (``/new``), switch to a larger-context model, or reduce
|
|
2661
|
+
# attachments. Forced compaction via ``/compress``
|
|
2662
|
+
# (``force=True``) is unaffected — it never reaches this loop.
|
|
2663
|
+
_overflow_reasons = {
|
|
2664
|
+
FailoverReason.long_context_tier,
|
|
2665
|
+
FailoverReason.payload_too_large,
|
|
2666
|
+
FailoverReason.context_overflow,
|
|
2667
|
+
}
|
|
2668
|
+
if (
|
|
2669
|
+
classified.reason in _overflow_reasons
|
|
2670
|
+
and not getattr(agent, "compression_enabled", True)
|
|
2671
|
+
):
|
|
2672
|
+
agent._flush_status_buffer()
|
|
2673
|
+
agent._vprint(
|
|
2674
|
+
f"{agent.log_prefix}❌ Context overflow, but auto-compaction is disabled "
|
|
2675
|
+
f"(compression.enabled: false).",
|
|
2676
|
+
force=True,
|
|
2677
|
+
)
|
|
2678
|
+
agent._vprint(
|
|
2679
|
+
f"{agent.log_prefix} 💡 Run /compress to compact manually, /new to start fresh, "
|
|
2680
|
+
f"switch to a larger-context model, or reduce attachments.",
|
|
2681
|
+
force=True,
|
|
2682
|
+
)
|
|
2683
|
+
logger.error(
|
|
2684
|
+
f"{agent.log_prefix}Context overflow ({classified.reason.value}) with "
|
|
2685
|
+
f"auto-compaction disabled — not compressing."
|
|
2686
|
+
)
|
|
2687
|
+
agent._persist_session(messages, conversation_history)
|
|
2688
|
+
return {
|
|
2689
|
+
"messages": messages,
|
|
2690
|
+
"completed": False,
|
|
2691
|
+
"api_calls": api_call_count,
|
|
2692
|
+
"error": (
|
|
2693
|
+
"Context overflow and auto-compaction is disabled "
|
|
2694
|
+
"(compression.enabled: false). Run /compress to compact manually, "
|
|
2695
|
+
"/new to start fresh, or switch to a larger-context model."
|
|
2696
|
+
),
|
|
2697
|
+
"partial": True,
|
|
2698
|
+
"failed": True,
|
|
2699
|
+
"compaction_disabled": True,
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2663
2702
|
# ── Anthropic Sonnet long-context tier gate ───────────
|
|
2664
2703
|
# Anthropic returns HTTP 429 "Extra usage is required for
|
|
2665
2704
|
# long context requests" when a Claude Max (or similar)
|
|
@@ -2713,7 +2752,7 @@ def run_conversation(
|
|
|
2713
2752
|
f"(was {old_ctx:,}), retrying..."
|
|
2714
2753
|
)
|
|
2715
2754
|
time.sleep(2)
|
|
2716
|
-
restart_with_compressed_messages = True
|
|
2755
|
+
_retry.restart_with_compressed_messages = True
|
|
2717
2756
|
break
|
|
2718
2757
|
# Fall through to normal error handling if compression
|
|
2719
2758
|
# is exhausted or didn't help.
|
|
@@ -2746,7 +2785,7 @@ def run_conversation(
|
|
|
2746
2785
|
if agent._try_activate_fallback(reason=classified.reason):
|
|
2747
2786
|
retry_count = 0
|
|
2748
2787
|
compression_attempts = 0
|
|
2749
|
-
primary_recovery_attempted = False
|
|
2788
|
+
_retry.primary_recovery_attempted = False
|
|
2750
2789
|
continue
|
|
2751
2790
|
|
|
2752
2791
|
# ── Nous Portal: record rate limit & skip retries ─────
|
|
@@ -2805,10 +2844,13 @@ def run_conversation(
|
|
|
2805
2844
|
except Exception:
|
|
2806
2845
|
pass
|
|
2807
2846
|
if _genuine_nous_rate_limit:
|
|
2808
|
-
#
|
|
2809
|
-
# top-of-loop guard
|
|
2810
|
-
#
|
|
2811
|
-
|
|
2847
|
+
# Re-enter the loop exactly once so the
|
|
2848
|
+
# top-of-loop Nous guard handles fallback or
|
|
2849
|
+
# bails cleanly. (Setting retry_count to
|
|
2850
|
+
# max_retries would make the while condition
|
|
2851
|
+
# false immediately and the guard would never
|
|
2852
|
+
# run -- no fallback, generic exhaustion error.)
|
|
2853
|
+
retry_count = max(0, max_retries - 1)
|
|
2812
2854
|
continue
|
|
2813
2855
|
# Upstream capacity 429: fall through to normal
|
|
2814
2856
|
# retry logic. A different model (or the same
|
|
@@ -2884,7 +2926,7 @@ def run_conversation(
|
|
|
2884
2926
|
if len(messages) < original_len:
|
|
2885
2927
|
agent._buffer_status(f"🗜️ Compressed {original_len} → {len(messages)} messages, retrying...")
|
|
2886
2928
|
time.sleep(2) # Brief pause between compression retries
|
|
2887
|
-
restart_with_compressed_messages = True
|
|
2929
|
+
_retry.restart_with_compressed_messages = True
|
|
2888
2930
|
break
|
|
2889
2931
|
else:
|
|
2890
2932
|
# Terminal — surface buffered context so the user
|
|
@@ -2956,7 +2998,7 @@ def run_conversation(
|
|
|
2956
2998
|
"failed": True,
|
|
2957
2999
|
"compression_exhausted": True,
|
|
2958
3000
|
}
|
|
2959
|
-
restart_with_compressed_messages = True
|
|
3001
|
+
_retry.restart_with_compressed_messages = True
|
|
2960
3002
|
break
|
|
2961
3003
|
|
|
2962
3004
|
# Error is about the INPUT being too large. Only reduce
|
|
@@ -3041,7 +3083,7 @@ def run_conversation(
|
|
|
3041
3083
|
if len(messages) < original_len:
|
|
3042
3084
|
agent._buffer_status(f"🗜️ Compressed {original_len} → {len(messages)} messages, retrying...")
|
|
3043
3085
|
time.sleep(2) # Brief pause between compression retries
|
|
3044
|
-
restart_with_compressed_messages = True
|
|
3086
|
+
_retry.restart_with_compressed_messages = True
|
|
3045
3087
|
break
|
|
3046
3088
|
else:
|
|
3047
3089
|
# Can't compress further and already at minimum tier
|
|
@@ -3146,7 +3188,7 @@ def run_conversation(
|
|
|
3146
3188
|
if agent._try_activate_fallback():
|
|
3147
3189
|
retry_count = 0
|
|
3148
3190
|
compression_attempts = 0
|
|
3149
|
-
primary_recovery_attempted = False
|
|
3191
|
+
_retry.primary_recovery_attempted = False
|
|
3150
3192
|
continue
|
|
3151
3193
|
if api_kwargs is not None:
|
|
3152
3194
|
agent._dump_api_request_debug(
|
|
@@ -3155,15 +3197,22 @@ def run_conversation(
|
|
|
3155
3197
|
# Terminal — flush buffered context so the user sees
|
|
3156
3198
|
# what was tried before the abort.
|
|
3157
3199
|
agent._flush_status_buffer()
|
|
3200
|
+
# Summarize once: Cloudflare/proxy HTML challenge pages and
|
|
3201
|
+
# other raw provider bodies must be collapsed to a short
|
|
3202
|
+
# one-liner here, otherwise the full page leaks into the
|
|
3203
|
+
# returned ``error`` field and downstream consumers deliver
|
|
3204
|
+
# it verbatim (e.g. a cron failure notification dumped a
|
|
3205
|
+
# ~60KB Cloudflare challenge page as 31 Discord messages).
|
|
3206
|
+
_nonretryable_summary = agent._summarize_api_error(api_error)
|
|
3158
3207
|
if classified.reason == FailoverReason.content_policy_blocked:
|
|
3159
3208
|
agent._emit_status(
|
|
3160
3209
|
f"❌ Provider safety filter blocked this request: "
|
|
3161
|
-
f"{
|
|
3210
|
+
f"{_nonretryable_summary}"
|
|
3162
3211
|
)
|
|
3163
3212
|
else:
|
|
3164
3213
|
agent._emit_status(
|
|
3165
3214
|
f"❌ Non-retryable error (HTTP {status_code}): "
|
|
3166
|
-
f"{
|
|
3215
|
+
f"{_nonretryable_summary}"
|
|
3167
3216
|
)
|
|
3168
3217
|
agent._vprint(f"{agent.log_prefix}❌ Non-retryable client error (HTTP {status_code}). Aborting.", force=True)
|
|
3169
3218
|
agent._vprint(f"{agent.log_prefix} 🔌 Provider: {_provider} Model: {_model}", force=True)
|
|
@@ -3195,7 +3244,7 @@ def run_conversation(
|
|
|
3195
3244
|
else: # nous
|
|
3196
3245
|
agent._vprint(f"{agent.log_prefix} 💡 Nous Portal OAuth token was rejected (HTTP 401). Your token may be", force=True)
|
|
3197
3246
|
agent._vprint(f"{agent.log_prefix} expired, revoked, or your account may be out of credits. To fix:", force=True)
|
|
3198
|
-
agent._vprint(f"{agent.log_prefix} 1. Re-authenticate: hermes
|
|
3247
|
+
agent._vprint(f"{agent.log_prefix} 1. Re-authenticate: hermes portal", force=True)
|
|
3199
3248
|
agent._vprint(f"{agent.log_prefix} 2. Check your portal account: https://portal.nousresearch.com", force=True)
|
|
3200
3249
|
# ``:free`` is OpenRouter slug syntax; Nous Portal will reject
|
|
3201
3250
|
# the model name even after a successful re-auth.
|
|
@@ -3248,29 +3297,25 @@ def run_conversation(
|
|
|
3248
3297
|
else:
|
|
3249
3298
|
agent._persist_session(messages, conversation_history)
|
|
3250
3299
|
if classified.reason == FailoverReason.content_policy_blocked:
|
|
3251
|
-
_summary = agent._summarize_api_error(api_error)
|
|
3252
3300
|
_policy_response = (
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
f"Provider message: {
|
|
3256
|
-
f"
|
|
3257
|
-
|
|
3301
|
+
"⚠️ The model provider's safety filter blocked this request "
|
|
3302
|
+
"(not a Hermes/gateway failure).\n\n"
|
|
3303
|
+
f"Provider message: {_nonretryable_summary}\n\n"
|
|
3304
|
+
f"{_CONTENT_POLICY_RECOVERY_HINT}"
|
|
3305
|
+
)
|
|
3306
|
+
return _content_policy_blocked_result(
|
|
3307
|
+
messages,
|
|
3308
|
+
api_call_count,
|
|
3309
|
+
final_response=_policy_response,
|
|
3310
|
+
error_detail=_nonretryable_summary,
|
|
3258
3311
|
)
|
|
3259
|
-
return {
|
|
3260
|
-
"final_response": _policy_response,
|
|
3261
|
-
"messages": messages,
|
|
3262
|
-
"api_calls": api_call_count,
|
|
3263
|
-
"completed": False,
|
|
3264
|
-
"failed": True,
|
|
3265
|
-
"error": f"content_policy_blocked: {_summary}",
|
|
3266
|
-
}
|
|
3267
3312
|
return {
|
|
3268
3313
|
"final_response": None,
|
|
3269
3314
|
"messages": messages,
|
|
3270
3315
|
"api_calls": api_call_count,
|
|
3271
3316
|
"completed": False,
|
|
3272
3317
|
"failed": True,
|
|
3273
|
-
"error":
|
|
3318
|
+
"error": _nonretryable_summary,
|
|
3274
3319
|
}
|
|
3275
3320
|
|
|
3276
3321
|
if retry_count >= max_retries:
|
|
@@ -3278,10 +3323,10 @@ def run_conversation(
|
|
|
3278
3323
|
# client once for transient transport errors (stale
|
|
3279
3324
|
# connection pool, TCP reset). Only attempted once
|
|
3280
3325
|
# per API call block.
|
|
3281
|
-
if not primary_recovery_attempted and agent._try_recover_primary_transport(
|
|
3326
|
+
if not _retry.primary_recovery_attempted and agent._try_recover_primary_transport(
|
|
3282
3327
|
api_error, retry_count=retry_count, max_retries=max_retries,
|
|
3283
3328
|
):
|
|
3284
|
-
primary_recovery_attempted = True
|
|
3329
|
+
_retry.primary_recovery_attempted = True
|
|
3285
3330
|
retry_count = 0
|
|
3286
3331
|
continue
|
|
3287
3332
|
# Try fallback before giving up entirely
|
|
@@ -3290,7 +3335,7 @@ def run_conversation(
|
|
|
3290
3335
|
if agent._try_activate_fallback():
|
|
3291
3336
|
retry_count = 0
|
|
3292
3337
|
compression_attempts = 0
|
|
3293
|
-
primary_recovery_attempted = False
|
|
3338
|
+
_retry.primary_recovery_attempted = False
|
|
3294
3339
|
continue
|
|
3295
3340
|
# Terminal — flush buffered retry/fallback trace.
|
|
3296
3341
|
agent._flush_status_buffer()
|
|
@@ -3378,6 +3423,12 @@ def run_conversation(
|
|
|
3378
3423
|
"completed": False,
|
|
3379
3424
|
"failed": True,
|
|
3380
3425
|
"error": _final_summary,
|
|
3426
|
+
# Surface the classified reason so callers (notably the
|
|
3427
|
+
# kanban worker path in cli.py) can distinguish a
|
|
3428
|
+
# transient throttle from a real failure and choose a
|
|
3429
|
+
# different exit code. ``rate_limit`` / ``billing`` here
|
|
3430
|
+
# mean "quota wall, not a task error".
|
|
3431
|
+
"failure_reason": classified.reason.value,
|
|
3381
3432
|
}
|
|
3382
3433
|
|
|
3383
3434
|
# For rate limits, respect the Retry-After header if present
|
|
@@ -3435,17 +3486,17 @@ def run_conversation(
|
|
|
3435
3486
|
_turn_exit_reason = "interrupted_during_api_call"
|
|
3436
3487
|
break
|
|
3437
3488
|
|
|
3438
|
-
if restart_with_compressed_messages:
|
|
3489
|
+
if _retry.restart_with_compressed_messages:
|
|
3439
3490
|
api_call_count -= 1
|
|
3440
3491
|
agent.iteration_budget.refund()
|
|
3441
3492
|
# Count compression restarts toward the retry limit to prevent
|
|
3442
3493
|
# infinite loops when compression reduces messages but not enough
|
|
3443
3494
|
# to fit the context window.
|
|
3444
3495
|
retry_count += 1
|
|
3445
|
-
restart_with_compressed_messages = False
|
|
3496
|
+
_retry.restart_with_compressed_messages = False
|
|
3446
3497
|
continue
|
|
3447
3498
|
|
|
3448
|
-
if restart_with_length_continuation:
|
|
3499
|
+
if _retry.restart_with_length_continuation:
|
|
3449
3500
|
# Progressively boost the output token budget on each retry.
|
|
3450
3501
|
# Retry 1 → 2× base, retry 2 → 3× base, capped at 32 768.
|
|
3451
3502
|
# Applies to all providers via _ephemeral_max_output_tokens.
|
|
@@ -3501,29 +3552,44 @@ def run_conversation(
|
|
|
3501
3552
|
assistant_message.content = str(raw)
|
|
3502
3553
|
|
|
3503
3554
|
try:
|
|
3504
|
-
from hermes_cli.plugins import
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
_invoke_hook(
|
|
3508
|
-
"post_api_request",
|
|
3509
|
-
task_id=effective_task_id,
|
|
3510
|
-
session_id=agent.session_id or "",
|
|
3511
|
-
platform=agent.platform or "",
|
|
3512
|
-
model=agent.model,
|
|
3513
|
-
provider=agent.provider,
|
|
3514
|
-
base_url=agent.base_url,
|
|
3515
|
-
api_mode=agent.api_mode,
|
|
3516
|
-
api_call_count=api_call_count,
|
|
3517
|
-
api_duration=api_duration,
|
|
3518
|
-
finish_reason=finish_reason,
|
|
3519
|
-
message_count=len(api_messages),
|
|
3520
|
-
response_model=getattr(response, "model", None),
|
|
3521
|
-
response=response,
|
|
3522
|
-
usage=agent._usage_summary_for_api_request_hook(response),
|
|
3523
|
-
assistant_message=assistant_message,
|
|
3524
|
-
assistant_content_chars=len(_assistant_text),
|
|
3525
|
-
assistant_tool_call_count=len(_assistant_tool_calls),
|
|
3555
|
+
from hermes_cli.plugins import (
|
|
3556
|
+
has_hook,
|
|
3557
|
+
invoke_hook as _invoke_hook,
|
|
3526
3558
|
)
|
|
3559
|
+
if has_hook("post_api_request"):
|
|
3560
|
+
_assistant_tool_calls = (
|
|
3561
|
+
getattr(assistant_message, "tool_calls", None) or []
|
|
3562
|
+
)
|
|
3563
|
+
_assistant_text = assistant_message.content or ""
|
|
3564
|
+
_api_ended_at = api_start_time + api_duration
|
|
3565
|
+
_invoke_hook(
|
|
3566
|
+
"post_api_request",
|
|
3567
|
+
task_id=effective_task_id,
|
|
3568
|
+
turn_id=turn_id,
|
|
3569
|
+
api_request_id=api_request_id,
|
|
3570
|
+
session_id=agent.session_id or "",
|
|
3571
|
+
platform=agent.platform or "",
|
|
3572
|
+
model=agent.model,
|
|
3573
|
+
provider=agent.provider,
|
|
3574
|
+
base_url=agent.base_url,
|
|
3575
|
+
api_mode=agent.api_mode,
|
|
3576
|
+
api_call_count=api_call_count,
|
|
3577
|
+
api_duration=api_duration,
|
|
3578
|
+
started_at=api_start_time,
|
|
3579
|
+
ended_at=_api_ended_at,
|
|
3580
|
+
finish_reason=finish_reason,
|
|
3581
|
+
message_count=len(api_messages),
|
|
3582
|
+
response_model=getattr(response, "model", None),
|
|
3583
|
+
response=agent._api_response_payload_for_hook(
|
|
3584
|
+
response,
|
|
3585
|
+
assistant_message,
|
|
3586
|
+
finish_reason=finish_reason,
|
|
3587
|
+
),
|
|
3588
|
+
usage=agent._usage_summary_for_api_request_hook(response),
|
|
3589
|
+
assistant_message=assistant_message,
|
|
3590
|
+
assistant_content_chars=len(_assistant_text),
|
|
3591
|
+
assistant_tool_call_count=len(_assistant_tool_calls),
|
|
3592
|
+
)
|
|
3527
3593
|
except Exception:
|
|
3528
3594
|
pass
|
|
3529
3595
|
|
|
@@ -3696,8 +3762,30 @@ def run_conversation(
|
|
|
3696
3762
|
assistant_msg = agent._build_assistant_message(assistant_message, finish_reason)
|
|
3697
3763
|
messages.append(assistant_msg)
|
|
3698
3764
|
for tc in assistant_message.tool_calls:
|
|
3699
|
-
|
|
3700
|
-
|
|
3765
|
+
_tc_name = tc.function.name
|
|
3766
|
+
if _tc_name not in agent.valid_tool_names:
|
|
3767
|
+
# A blank/whitespace-only name is not a typo the
|
|
3768
|
+
# model can fuzzy-correct toward a real tool — it is
|
|
3769
|
+
# almost always a weak open model echoing tool-call
|
|
3770
|
+
# XML/JSON it saw in file or tool output (#47967:
|
|
3771
|
+
# <tool_call>/<invoke name=...> payloads in a file
|
|
3772
|
+
# prime mimo/nemotron-class models to emit empty
|
|
3773
|
+
# structured calls). Dumping the full tool catalog
|
|
3774
|
+
# in that case feeds the priming loop more names to
|
|
3775
|
+
# mimic and inflates context 3-4x across retries, so
|
|
3776
|
+
# send a terse error that tells the model in-context
|
|
3777
|
+
# tool-call syntax is DATA, not a call to make.
|
|
3778
|
+
if not (_tc_name or "").strip():
|
|
3779
|
+
content = (
|
|
3780
|
+
"Tool call rejected: the tool name was empty. "
|
|
3781
|
+
"If tool-call XML or JSON appeared in file "
|
|
3782
|
+
"contents or tool output, that is data — do "
|
|
3783
|
+
"not re-emit it as a tool call. To call a "
|
|
3784
|
+
"tool, use a valid name from your tool list; "
|
|
3785
|
+
"otherwise reply in plain text."
|
|
3786
|
+
)
|
|
3787
|
+
else:
|
|
3788
|
+
content = f"Tool '{_tc_name}' does not exist. Available tools: {available}"
|
|
3701
3789
|
else:
|
|
3702
3790
|
content = "Skipped: another tool call in this turn used an invalid name. Please retry this tool call."
|
|
3703
3791
|
messages.append({
|
|
@@ -4373,379 +4461,26 @@ def run_conversation(
|
|
|
4373
4461
|
messages.append({"role": "assistant", "content": final_response})
|
|
4374
4462
|
break
|
|
4375
4463
|
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
# user message and makes a single toolless request.
|
|
4383
|
-
_turn_exit_reason = f"max_iterations_reached({api_call_count}/{agent.max_iterations})"
|
|
4384
|
-
agent._emit_status(
|
|
4385
|
-
f"⚠️ Iteration budget exhausted ({api_call_count}/{agent.max_iterations}) "
|
|
4386
|
-
"— asking model to summarise"
|
|
4387
|
-
)
|
|
4388
|
-
if not agent.quiet_mode:
|
|
4389
|
-
agent._safe_print(
|
|
4390
|
-
f"\n⚠️ Iteration budget exhausted ({api_call_count}/{agent.max_iterations}) "
|
|
4391
|
-
"— requesting summary..."
|
|
4392
|
-
)
|
|
4393
|
-
final_response = agent._handle_max_iterations(messages, api_call_count)
|
|
4394
|
-
|
|
4395
|
-
# If running as a kanban worker, signal the dispatcher that the
|
|
4396
|
-
# worker could not complete (rather than treating it as a
|
|
4397
|
-
# protocol violation). The agent loop strips tools before calling
|
|
4398
|
-
# _handle_max_iterations, so the model cannot call kanban_block
|
|
4399
|
-
# itself — we must do it on its behalf.
|
|
4400
|
-
#
|
|
4401
|
-
# We route through ``_record_task_failure(outcome="timed_out")``
|
|
4402
|
-
# rather than ``kanban_block`` so this counts toward the
|
|
4403
|
-
# ``consecutive_failures`` counter and the dispatcher's
|
|
4404
|
-
# ``failure_limit`` circuit breaker (#29747 gap 2). Without this,
|
|
4405
|
-
# a task whose worker keeps exhausting its budget would block
|
|
4406
|
-
# silently each run, get auto-promoted by the operator (or never
|
|
4407
|
-
# surface), and re-block in an endless loop with no signal.
|
|
4408
|
-
_kanban_task = os.environ.get("HERMES_KANBAN_TASK")
|
|
4409
|
-
if _kanban_task:
|
|
4410
|
-
try:
|
|
4411
|
-
from hermes_cli import kanban_db as _kb
|
|
4412
|
-
_conn = _kb.connect()
|
|
4413
|
-
try:
|
|
4414
|
-
_kb._record_task_failure(
|
|
4415
|
-
_conn,
|
|
4416
|
-
_kanban_task,
|
|
4417
|
-
error=(
|
|
4418
|
-
f"Iteration budget exhausted "
|
|
4419
|
-
f"({api_call_count}/{agent.max_iterations}) — "
|
|
4420
|
-
"task could not complete within the allowed "
|
|
4421
|
-
"iterations"
|
|
4422
|
-
),
|
|
4423
|
-
outcome="timed_out",
|
|
4424
|
-
release_claim=True,
|
|
4425
|
-
end_run=True,
|
|
4426
|
-
event_payload_extra={
|
|
4427
|
-
"budget_used": api_call_count,
|
|
4428
|
-
"budget_max": agent.max_iterations,
|
|
4429
|
-
},
|
|
4430
|
-
)
|
|
4431
|
-
logger.info(
|
|
4432
|
-
"recorded budget-exhausted failure for task %s (%d/%d)",
|
|
4433
|
-
_kanban_task, api_call_count, agent.max_iterations,
|
|
4434
|
-
)
|
|
4435
|
-
finally:
|
|
4436
|
-
try:
|
|
4437
|
-
_conn.close()
|
|
4438
|
-
except Exception:
|
|
4439
|
-
pass
|
|
4440
|
-
except Exception:
|
|
4441
|
-
logger.warning(
|
|
4442
|
-
"Failed to record budget-exhausted failure for task %s",
|
|
4443
|
-
_kanban_task,
|
|
4444
|
-
exc_info=True,
|
|
4445
|
-
)
|
|
4446
|
-
|
|
4447
|
-
# Determine if conversation completed successfully
|
|
4448
|
-
completed = (
|
|
4449
|
-
final_response is not None
|
|
4450
|
-
and api_call_count < agent.max_iterations
|
|
4451
|
-
and not failed
|
|
4452
|
-
)
|
|
4453
|
-
|
|
4454
|
-
# Save trajectory if enabled. ``user_message`` may be a multimodal
|
|
4455
|
-
# list of parts; the trajectory format wants a plain string.
|
|
4456
|
-
agent._save_trajectory(messages, _summarize_user_message_for_log(user_message), completed)
|
|
4457
|
-
|
|
4458
|
-
# Clean up VM and browser for this task after conversation completes
|
|
4459
|
-
agent._cleanup_task_resources(effective_task_id)
|
|
4460
|
-
|
|
4461
|
-
# Persist session to both JSON log and SQLite only after private retry
|
|
4462
|
-
# scaffolding has been removed. Otherwise a later user "continue" turn
|
|
4463
|
-
# can replay assistant("(empty)") / recovery nudges and fall into the
|
|
4464
|
-
# same empty-response loop again.
|
|
4465
|
-
agent._drop_trailing_empty_response_scaffolding(messages)
|
|
4466
|
-
agent._persist_session(messages, conversation_history)
|
|
4467
|
-
|
|
4468
|
-
# ── Turn-exit diagnostic log ─────────────────────────────────────
|
|
4469
|
-
# Always logged at INFO so agent.log captures WHY every turn ended.
|
|
4470
|
-
# When the last message is a tool result (agent was mid-work), log
|
|
4471
|
-
# at WARNING — this is the "just stops" scenario users report.
|
|
4472
|
-
_last_msg_role = messages[-1].get("role") if messages else None
|
|
4473
|
-
_last_tool_name = None
|
|
4474
|
-
if _last_msg_role == "tool":
|
|
4475
|
-
# Walk back to find the assistant message with the tool call
|
|
4476
|
-
for _m in reversed(messages):
|
|
4477
|
-
if _m.get("role") == "assistant" and _m.get("tool_calls"):
|
|
4478
|
-
_tcs = _m["tool_calls"]
|
|
4479
|
-
if _tcs and isinstance(_tcs[0], dict):
|
|
4480
|
-
_last_tool_name = _tcs[-1].get("function", {}).get("name")
|
|
4481
|
-
break
|
|
4482
|
-
|
|
4483
|
-
_turn_tool_count = sum(
|
|
4484
|
-
1 for m in messages
|
|
4485
|
-
if isinstance(m, dict) and m.get("role") == "assistant" and m.get("tool_calls")
|
|
4486
|
-
)
|
|
4487
|
-
_resp_len = len(final_response) if final_response else 0
|
|
4488
|
-
_budget_used = agent.iteration_budget.used if agent.iteration_budget else 0
|
|
4489
|
-
_budget_max = agent.iteration_budget.max_total if agent.iteration_budget else 0
|
|
4490
|
-
|
|
4491
|
-
_diag_msg = (
|
|
4492
|
-
"Turn ended: reason=%s model=%s api_calls=%d/%d budget=%d/%d "
|
|
4493
|
-
"tool_turns=%d last_msg_role=%s response_len=%d session=%s"
|
|
4494
|
-
)
|
|
4495
|
-
_diag_args = (
|
|
4496
|
-
_turn_exit_reason, agent.model, api_call_count, agent.max_iterations,
|
|
4497
|
-
_budget_used, _budget_max,
|
|
4498
|
-
_turn_tool_count, _last_msg_role, _resp_len,
|
|
4499
|
-
agent.session_id or "none",
|
|
4500
|
-
)
|
|
4501
|
-
|
|
4502
|
-
if _last_msg_role == "tool" and not interrupted:
|
|
4503
|
-
# Agent was mid-work — this is the "just stops" case.
|
|
4504
|
-
logger.warning(
|
|
4505
|
-
"Turn ended with pending tool result (agent may appear stuck). "
|
|
4506
|
-
+ _diag_msg + " last_tool=%s",
|
|
4507
|
-
*_diag_args, _last_tool_name,
|
|
4508
|
-
)
|
|
4509
|
-
else:
|
|
4510
|
-
logger.info(_diag_msg, *_diag_args)
|
|
4511
|
-
|
|
4512
|
-
# File-mutation verifier footer.
|
|
4513
|
-
# If one or more ``write_file`` / ``patch`` calls failed during this
|
|
4514
|
-
# turn and were never superseded by a successful write to the same
|
|
4515
|
-
# path, append an advisory footer to the assistant response. This
|
|
4516
|
-
# catches the specific case — reported by Ben Eng (#15524-adjacent)
|
|
4517
|
-
# — where a model issues a batch of parallel patches, half of them
|
|
4518
|
-
# fail with "Could not find old_string", and the model summarises
|
|
4519
|
-
# the turn claiming every file was edited. The user then has to
|
|
4520
|
-
# manually run ``git status`` to catch the lie. With this footer
|
|
4521
|
-
# the truth is surfaced on every turn, so over-claiming is
|
|
4522
|
-
# structurally impossible past the model.
|
|
4523
|
-
#
|
|
4524
|
-
# Gate: only applied when a real text response exists for this
|
|
4525
|
-
# turn and the user didn't interrupt. Empty/interrupted turns
|
|
4526
|
-
# already have other surface text that shouldn't be augmented.
|
|
4527
|
-
if final_response and not interrupted:
|
|
4528
|
-
try:
|
|
4529
|
-
_failed = getattr(agent, "_turn_failed_file_mutations", None) or {}
|
|
4530
|
-
if _failed and agent._file_mutation_verifier_enabled():
|
|
4531
|
-
footer = agent._format_file_mutation_failure_footer(_failed)
|
|
4532
|
-
if footer:
|
|
4533
|
-
final_response = final_response.rstrip() + "\n\n" + footer
|
|
4534
|
-
except Exception as _ver_err:
|
|
4535
|
-
logger.debug("file-mutation verifier footer failed: %s", _ver_err)
|
|
4536
|
-
|
|
4537
|
-
# Turn-completion explainer.
|
|
4538
|
-
# When a turn ends abnormally after substantive work — empty content
|
|
4539
|
-
# after retries, a partial/truncated stream, a still-pending tool
|
|
4540
|
-
# result, or an iteration/budget limit — the user otherwise gets a
|
|
4541
|
-
# blank or fragmentary response box with no consolidated reason why
|
|
4542
|
-
# the agent stopped (#34452). Surface a single user-visible
|
|
4543
|
-
# explanation derived from ``_turn_exit_reason``, mirroring the
|
|
4544
|
-
# file-mutation verifier footer pattern above.
|
|
4545
|
-
#
|
|
4546
|
-
# Gate carefully so healthy turns stay quiet:
|
|
4547
|
-
# - ``text_response(...)`` exits never produce an explanation
|
|
4548
|
-
# (handled inside the formatter), so a terse ``Done.`` is silent.
|
|
4549
|
-
# - We only ACT when there is no genuinely usable reply this turn:
|
|
4550
|
-
# an empty response, the "(empty)" terminal sentinel, or a
|
|
4551
|
-
# suspiciously short partial fragment with no terminating
|
|
4552
|
-
# punctuation (e.g. "The"). A real short answer keeps its text.
|
|
4553
|
-
if not interrupted:
|
|
4554
|
-
try:
|
|
4555
|
-
if agent._turn_completion_explainer_enabled():
|
|
4556
|
-
_stripped = (final_response or "").strip()
|
|
4557
|
-
_is_empty_terminal = _stripped == "" or _stripped == "(empty)"
|
|
4558
|
-
# A short fragment that is not a normal text_response exit
|
|
4559
|
-
# and lacks sentence-ending punctuation is treated as a
|
|
4560
|
-
# truncated partial (the "The" case from #34452).
|
|
4561
|
-
_is_partial_fragment = (
|
|
4562
|
-
not _is_empty_terminal
|
|
4563
|
-
and not str(_turn_exit_reason).startswith("text_response")
|
|
4564
|
-
and len(_stripped) <= 24
|
|
4565
|
-
and _stripped[-1:] not in {".", "!", "?", "。", "!", "?", "`", ")"}
|
|
4566
|
-
)
|
|
4567
|
-
if _is_empty_terminal or _is_partial_fragment:
|
|
4568
|
-
_explanation = agent._format_turn_completion_explanation(
|
|
4569
|
-
_turn_exit_reason
|
|
4570
|
-
)
|
|
4571
|
-
if _explanation:
|
|
4572
|
-
if _is_empty_terminal:
|
|
4573
|
-
# Replace the bare "(empty)"/blank sentinel with
|
|
4574
|
-
# the actionable explanation.
|
|
4575
|
-
final_response = _explanation
|
|
4576
|
-
else:
|
|
4577
|
-
# Keep the partial fragment, append the reason so
|
|
4578
|
-
# the user sees both what arrived and why it
|
|
4579
|
-
# stopped.
|
|
4580
|
-
final_response = (
|
|
4581
|
-
_stripped + "\n\n" + _explanation
|
|
4582
|
-
)
|
|
4583
|
-
except Exception as _exp_err:
|
|
4584
|
-
logger.debug("turn-completion explainer failed: %s", _exp_err)
|
|
4585
|
-
|
|
4586
|
-
_response_transformed = False
|
|
4587
|
-
|
|
4588
|
-
# Plugin hook: transform_llm_output
|
|
4589
|
-
# Fired once per turn after the tool-calling loop completes.
|
|
4590
|
-
# Plugins can transform the LLM's output text before it's returned.
|
|
4591
|
-
# First hook to return a string wins; None/empty return leaves text unchanged.
|
|
4592
|
-
if final_response and not interrupted:
|
|
4593
|
-
try:
|
|
4594
|
-
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
|
4595
|
-
_transform_results = _invoke_hook(
|
|
4596
|
-
"transform_llm_output",
|
|
4597
|
-
response_text=final_response,
|
|
4598
|
-
session_id=agent.session_id or "",
|
|
4599
|
-
model=agent.model,
|
|
4600
|
-
platform=getattr(agent, "platform", None) or "",
|
|
4601
|
-
)
|
|
4602
|
-
for _hook_result in _transform_results:
|
|
4603
|
-
if isinstance(_hook_result, str) and _hook_result:
|
|
4604
|
-
final_response = _hook_result
|
|
4605
|
-
_response_transformed = True
|
|
4606
|
-
break # First non-empty string wins
|
|
4607
|
-
except Exception as exc:
|
|
4608
|
-
logger.warning("transform_llm_output hook failed: %s", exc)
|
|
4609
|
-
|
|
4610
|
-
# Plugin hook: post_llm_call
|
|
4611
|
-
# Fired once per turn after the tool-calling loop completes.
|
|
4612
|
-
# Plugins can use this to persist conversation data (e.g. sync
|
|
4613
|
-
# to an external memory system).
|
|
4614
|
-
if final_response and not interrupted:
|
|
4615
|
-
try:
|
|
4616
|
-
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
|
4617
|
-
_invoke_hook(
|
|
4618
|
-
"post_llm_call",
|
|
4619
|
-
session_id=agent.session_id,
|
|
4620
|
-
user_message=original_user_message,
|
|
4621
|
-
assistant_response=final_response,
|
|
4622
|
-
conversation_history=list(messages),
|
|
4623
|
-
model=agent.model,
|
|
4624
|
-
platform=getattr(agent, "platform", None) or "",
|
|
4625
|
-
)
|
|
4626
|
-
except Exception as exc:
|
|
4627
|
-
logger.warning("post_llm_call hook failed: %s", exc)
|
|
4628
|
-
|
|
4629
|
-
# Extract reasoning from the CURRENT turn only. Walk backwards
|
|
4630
|
-
# but stop at the user message that started this turn — anything
|
|
4631
|
-
# earlier is from a prior turn and must not leak into the reasoning
|
|
4632
|
-
# box (confusing stale display; #17055). Within the current turn
|
|
4633
|
-
# we still want the *most recent* non-empty reasoning: many
|
|
4634
|
-
# providers (Claude thinking, DeepSeek v4, Codex Responses) emit
|
|
4635
|
-
# reasoning on the tool-call step and leave the final-answer step
|
|
4636
|
-
# with reasoning=None, so picking only the last assistant would
|
|
4637
|
-
# silently drop legitimate same-turn reasoning.
|
|
4638
|
-
last_reasoning = None
|
|
4639
|
-
for msg in reversed(messages):
|
|
4640
|
-
if msg.get("role") == "user":
|
|
4641
|
-
break # turn boundary — don't cross into prior turns
|
|
4642
|
-
if msg.get("role") == "assistant" and msg.get("reasoning"):
|
|
4643
|
-
last_reasoning = msg["reasoning"]
|
|
4644
|
-
break
|
|
4645
|
-
|
|
4646
|
-
# Build result with interrupt info if applicable
|
|
4647
|
-
result = {
|
|
4648
|
-
"final_response": final_response,
|
|
4649
|
-
"last_reasoning": last_reasoning,
|
|
4650
|
-
"messages": messages,
|
|
4651
|
-
"api_calls": api_call_count,
|
|
4652
|
-
"completed": completed,
|
|
4653
|
-
"turn_exit_reason": _turn_exit_reason,
|
|
4654
|
-
"failed": failed,
|
|
4655
|
-
"partial": False, # True only when stopped due to invalid tool calls
|
|
4656
|
-
"interrupted": interrupted,
|
|
4657
|
-
"response_transformed": _response_transformed,
|
|
4658
|
-
"response_previewed": getattr(agent, "_response_was_previewed", False),
|
|
4659
|
-
"model": agent.model,
|
|
4660
|
-
"provider": agent.provider,
|
|
4661
|
-
"base_url": agent.base_url,
|
|
4662
|
-
"input_tokens": agent.session_input_tokens,
|
|
4663
|
-
"output_tokens": agent.session_output_tokens,
|
|
4664
|
-
"cache_read_tokens": agent.session_cache_read_tokens,
|
|
4665
|
-
"cache_write_tokens": agent.session_cache_write_tokens,
|
|
4666
|
-
"reasoning_tokens": agent.session_reasoning_tokens,
|
|
4667
|
-
"prompt_tokens": agent.session_prompt_tokens,
|
|
4668
|
-
"completion_tokens": agent.session_completion_tokens,
|
|
4669
|
-
"total_tokens": agent.session_total_tokens,
|
|
4670
|
-
"last_prompt_tokens": getattr(agent.context_compressor, "last_prompt_tokens", 0) or 0,
|
|
4671
|
-
"estimated_cost_usd": agent.session_estimated_cost_usd,
|
|
4672
|
-
"cost_status": agent.session_cost_status,
|
|
4673
|
-
"cost_source": agent.session_cost_source,
|
|
4674
|
-
"session_id": agent.session_id,
|
|
4675
|
-
}
|
|
4676
|
-
if agent._tool_guardrail_halt_decision is not None:
|
|
4677
|
-
result["guardrail"] = agent._tool_guardrail_halt_decision.to_metadata()
|
|
4678
|
-
# If a /steer landed after the final assistant turn (no more tool
|
|
4679
|
-
# batches to drain into), hand it back to the caller so it can be
|
|
4680
|
-
# delivered as the next user turn instead of being silently lost.
|
|
4681
|
-
_leftover_steer = agent._drain_pending_steer()
|
|
4682
|
-
if _leftover_steer:
|
|
4683
|
-
result["pending_steer"] = _leftover_steer
|
|
4684
|
-
agent._response_was_previewed = False
|
|
4685
|
-
|
|
4686
|
-
# Include interrupt message if one triggered the interrupt
|
|
4687
|
-
if interrupted and agent._interrupt_message:
|
|
4688
|
-
result["interrupt_message"] = agent._interrupt_message
|
|
4689
|
-
|
|
4690
|
-
# Clear interrupt state after handling
|
|
4691
|
-
agent.clear_interrupt()
|
|
4692
|
-
|
|
4693
|
-
# Clear stream callback so it doesn't leak into future calls
|
|
4694
|
-
agent._stream_callback = None
|
|
4695
|
-
|
|
4696
|
-
# Check skill trigger NOW — based on how many tool iterations THIS turn used.
|
|
4697
|
-
_should_review_skills = False
|
|
4698
|
-
if (agent._skill_nudge_interval > 0
|
|
4699
|
-
and agent._iters_since_skill >= agent._skill_nudge_interval
|
|
4700
|
-
and "skill_manage" in agent.valid_tool_names):
|
|
4701
|
-
_should_review_skills = True
|
|
4702
|
-
agent._iters_since_skill = 0
|
|
4703
|
-
|
|
4704
|
-
# External memory provider: sync the completed turn + queue next prefetch.
|
|
4705
|
-
agent._sync_external_memory_for_turn(
|
|
4706
|
-
original_user_message=original_user_message,
|
|
4464
|
+
# Post-loop turn finalization extracted to agent/turn_finalizer.finalize_turn
|
|
4465
|
+
# (god-file decomposition Phase 1 step 4). Behavior-neutral: the assembled
|
|
4466
|
+
# result dict is returned exactly as before.
|
|
4467
|
+
from agent.turn_finalizer import finalize_turn
|
|
4468
|
+
return finalize_turn(
|
|
4469
|
+
agent,
|
|
4707
4470
|
final_response=final_response,
|
|
4471
|
+
api_call_count=api_call_count,
|
|
4708
4472
|
interrupted=interrupted,
|
|
4473
|
+
failed=failed,
|
|
4709
4474
|
messages=messages,
|
|
4475
|
+
conversation_history=conversation_history,
|
|
4476
|
+
effective_task_id=effective_task_id,
|
|
4477
|
+
turn_id=turn_id,
|
|
4478
|
+
user_message=user_message,
|
|
4479
|
+
original_user_message=original_user_message,
|
|
4480
|
+
_should_review_memory=_should_review_memory,
|
|
4481
|
+
_turn_exit_reason=_turn_exit_reason,
|
|
4710
4482
|
)
|
|
4711
4483
|
|
|
4712
|
-
# Background memory/skill review — runs AFTER the response is delivered
|
|
4713
|
-
# so it never competes with the user's task for model attention.
|
|
4714
|
-
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
|
|
4715
|
-
try:
|
|
4716
|
-
agent._spawn_background_review(
|
|
4717
|
-
messages_snapshot=list(messages),
|
|
4718
|
-
review_memory=_should_review_memory,
|
|
4719
|
-
review_skills=_should_review_skills,
|
|
4720
|
-
)
|
|
4721
|
-
except Exception:
|
|
4722
|
-
pass # Background review is best-effort
|
|
4723
|
-
|
|
4724
|
-
# Note: Memory provider on_session_end() + shutdown_all() are NOT
|
|
4725
|
-
# called here — run_conversation() is called once per user message in
|
|
4726
|
-
# multi-turn sessions. Shutting down after every turn would kill the
|
|
4727
|
-
# provider before the second message. Actual session-end cleanup is
|
|
4728
|
-
# handled by the CLI (atexit / /reset) and gateway (session expiry /
|
|
4729
|
-
# _reset_session).
|
|
4730
|
-
|
|
4731
|
-
# Plugin hook: on_session_end
|
|
4732
|
-
# Fired at the very end of every run_conversation call.
|
|
4733
|
-
# Plugins can use this for cleanup, flushing buffers, etc.
|
|
4734
|
-
try:
|
|
4735
|
-
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
|
4736
|
-
_invoke_hook(
|
|
4737
|
-
"on_session_end",
|
|
4738
|
-
session_id=agent.session_id,
|
|
4739
|
-
completed=completed,
|
|
4740
|
-
interrupted=interrupted,
|
|
4741
|
-
model=agent.model,
|
|
4742
|
-
platform=getattr(agent, "platform", None) or "",
|
|
4743
|
-
)
|
|
4744
|
-
except Exception as exc:
|
|
4745
|
-
logger.warning("on_session_end hook failed: %s", exc)
|
|
4746
|
-
|
|
4747
|
-
return result
|
|
4748
|
-
|
|
4749
4484
|
|
|
4750
4485
|
|
|
4751
4486
|
__all__ = ["run_conversation"]
|