lonny-agent 0.2.6 → 0.2.7
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/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +106 -0
- package/CHANGELOG.md +24 -0
- package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
- package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
- package/dist/agent/__tests__/session-restore.test.js +260 -0
- package/dist/agent/__tests__/session-restore.test.js.map +1 -0
- package/dist/agent/commands.d.ts +26 -0
- package/dist/agent/commands.d.ts.map +1 -0
- package/dist/agent/commands.js +253 -0
- package/dist/agent/commands.js.map +1 -0
- package/dist/agent/compaction.js +8 -1
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/index.d.ts +2 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/memory.d.ts +12 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +51 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/prompt-builder.d.ts +6 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +89 -26
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/openai.d.ts +1 -0
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +16 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session-chat.d.ts +3 -0
- package/dist/agent/session-chat.d.ts.map +1 -0
- package/dist/agent/session-chat.js +384 -0
- package/dist/agent/session-chat.js.map +1 -0
- package/dist/agent/session-display.d.ts +29 -0
- package/dist/agent/session-display.d.ts.map +1 -0
- package/dist/agent/session-display.js +179 -0
- package/dist/agent/session-display.js.map +1 -0
- package/dist/agent/session-persistence.d.ts +55 -0
- package/dist/agent/session-persistence.d.ts.map +1 -0
- package/dist/agent/session-persistence.js +101 -0
- package/dist/agent/session-persistence.js.map +1 -0
- package/dist/agent/session-utils.d.ts +5 -0
- package/dist/agent/session-utils.d.ts.map +1 -0
- package/dist/agent/session-utils.js +51 -0
- package/dist/agent/session-utils.js.map +1 -0
- package/dist/agent/session.d.ts +15 -17
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +226 -608
- package/dist/agent/session.js.map +1 -1
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +67 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/index.js +24 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +168 -1
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +253 -6
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/fetch.test.js +1 -1
- package/dist/tools/__tests__/fetch.test.js.map +1 -1
- package/dist/tools/__tests__/glob.test.js +1 -1
- package/dist/tools/__tests__/glob.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +1 -1
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/ls.test.js +1 -1
- package/dist/tools/__tests__/ls.test.js.map +1 -1
- package/dist/tools/__tests__/read.test.js +1 -1
- package/dist/tools/__tests__/read.test.js.map +1 -1
- package/dist/tools/__tests__/registry.test.js +313 -0
- package/dist/tools/__tests__/registry.test.js.map +1 -1
- package/dist/tools/__tests__/write_plan.test.js +1 -1
- package/dist/tools/__tests__/write_plan.test.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +659 -39
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
- package/dist/tools/codebase/ast/adapter.d.ts +23 -0
- package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
- package/dist/tools/codebase/ast/adapter.js +11 -0
- package/dist/tools/codebase/ast/adapter.js.map +1 -0
- package/dist/tools/codebase/ast/tools.d.ts +3 -0
- package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tools.js +286 -0
- package/dist/tools/codebase/ast/tools.js.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
- package/dist/tools/codebase/ast/types.d.ts +103 -0
- package/dist/tools/codebase/ast/types.d.ts.map +1 -0
- package/dist/tools/codebase/ast/types.js +2 -0
- package/dist/tools/codebase/ast/types.js.map +1 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
- package/dist/tools/codebase/find.d.ts +3 -0
- package/dist/tools/codebase/find.d.ts.map +1 -0
- package/dist/tools/codebase/find.js +61 -0
- package/dist/tools/codebase/find.js.map +1 -0
- package/dist/tools/codebase/glob.d.ts +3 -0
- package/dist/tools/codebase/glob.d.ts.map +1 -0
- package/dist/tools/codebase/glob.js +37 -0
- package/dist/tools/codebase/glob.js.map +1 -0
- package/dist/tools/codebase/grep.d.ts +3 -0
- package/dist/tools/codebase/grep.d.ts.map +1 -0
- package/dist/tools/codebase/grep.js +169 -0
- package/dist/tools/codebase/grep.js.map +1 -0
- package/dist/tools/codebase/ls.d.ts +3 -0
- package/dist/tools/codebase/ls.d.ts.map +1 -0
- package/dist/tools/codebase/ls.js +39 -0
- package/dist/tools/codebase/ls.js.map +1 -0
- package/dist/tools/codebase/read.d.ts +10 -0
- package/dist/tools/codebase/read.d.ts.map +1 -0
- package/dist/tools/codebase/read.js +81 -0
- package/dist/tools/codebase/read.js.map +1 -0
- package/dist/tools/delete_memory.d.ts +3 -0
- package/dist/tools/delete_memory.d.ts.map +1 -0
- package/dist/tools/delete_memory.js +46 -0
- package/dist/tools/delete_memory.js.map +1 -0
- package/dist/tools/edit/diff-compute.d.ts +4 -0
- package/dist/tools/edit/diff-compute.d.ts.map +1 -0
- package/dist/tools/edit/diff-compute.js +18 -0
- package/dist/tools/edit/diff-compute.js.map +1 -0
- package/dist/tools/edit/diff-render.d.ts +18 -0
- package/dist/tools/edit/diff-render.d.ts.map +1 -0
- package/dist/tools/edit/diff-render.js +128 -0
- package/dist/tools/edit/diff-render.js.map +1 -0
- package/dist/tools/edit/edit.d.ts +4 -0
- package/dist/tools/edit/edit.d.ts.map +1 -0
- package/dist/tools/edit/edit.js +440 -0
- package/dist/tools/edit/edit.js.map +1 -0
- package/dist/tools/edit/matcher.d.ts +24 -0
- package/dist/tools/edit/matcher.d.ts.map +1 -0
- package/dist/tools/edit/matcher.js +66 -0
- package/dist/tools/edit/matcher.js.map +1 -0
- package/dist/tools/edit/parser.d.ts +10 -0
- package/dist/tools/edit/parser.d.ts.map +1 -0
- package/dist/tools/edit/parser.js +120 -0
- package/dist/tools/edit/parser.js.map +1 -0
- package/dist/tools/edit/types.d.ts +16 -0
- package/dist/tools/edit/types.d.ts.map +1 -0
- package/dist/tools/edit/types.js +2 -0
- package/dist/tools/edit/types.js.map +1 -0
- package/dist/tools/edit/write_plan.d.ts +5 -0
- package/dist/tools/edit/write_plan.d.ts.map +1 -0
- package/dist/tools/edit/write_plan.js +98 -0
- package/dist/tools/edit/write_plan.js.map +1 -0
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +66 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/execute/bash/constants.d.ts +24 -0
- package/dist/tools/execute/bash/constants.d.ts.map +1 -0
- package/dist/tools/execute/bash/constants.js +88 -0
- package/dist/tools/execute/bash/constants.js.map +1 -0
- package/dist/tools/execute/bash/errors.d.ts +39 -0
- package/dist/tools/execute/bash/errors.d.ts.map +1 -0
- package/dist/tools/execute/bash/errors.js +249 -0
- package/dist/tools/execute/bash/errors.js.map +1 -0
- package/dist/tools/execute/bash/execution.d.ts +18 -0
- package/dist/tools/execute/bash/execution.d.ts.map +1 -0
- package/dist/tools/execute/bash/execution.js +207 -0
- package/dist/tools/execute/bash/execution.js.map +1 -0
- package/dist/tools/execute/bash/index.d.ts +3 -0
- package/dist/tools/execute/bash/index.d.ts.map +1 -0
- package/dist/tools/execute/bash/index.js +117 -0
- package/dist/tools/execute/bash/index.js.map +1 -0
- package/dist/tools/execute/bash/platform.d.ts +6 -0
- package/dist/tools/execute/bash/platform.d.ts.map +1 -0
- package/dist/tools/execute/bash/platform.js +64 -0
- package/dist/tools/execute/bash/platform.js.map +1 -0
- package/dist/tools/execute/bash/security.d.ts +12 -0
- package/dist/tools/execute/bash/security.d.ts.map +1 -0
- package/dist/tools/execute/bash/security.js +26 -0
- package/dist/tools/execute/bash/security.js.map +1 -0
- package/dist/tools/execute/bash/validation.d.ts +16 -0
- package/dist/tools/execute/bash/validation.d.ts.map +1 -0
- package/dist/tools/execute/bash/validation.js +57 -0
- package/dist/tools/execute/bash/validation.js.map +1 -0
- package/dist/tools/execute/bash.d.ts +3 -0
- package/dist/tools/execute/bash.d.ts.map +1 -0
- package/dist/tools/execute/bash.js +728 -0
- package/dist/tools/execute/bash.js.map +1 -0
- package/dist/tools/execute/git.d.ts +3 -0
- package/dist/tools/execute/git.d.ts.map +1 -0
- package/dist/tools/execute/git.js +183 -0
- package/dist/tools/execute/git.js.map +1 -0
- package/dist/tools/execute/task_complete.d.ts +7 -0
- package/dist/tools/execute/task_complete.d.ts.map +1 -0
- package/dist/tools/execute/task_complete.js +38 -0
- package/dist/tools/execute/task_complete.js.map +1 -0
- package/dist/tools/fetch.d.ts.map +1 -1
- package/dist/tools/fetch.js +2 -0
- package/dist/tools/fetch.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +2 -0
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +2 -0
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/glob.d.ts.map +1 -1
- package/dist/tools/glob.js +2 -0
- package/dist/tools/glob.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +2 -0
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/install/clawhub.d.ts +26 -0
- package/dist/tools/install/clawhub.d.ts.map +1 -0
- package/dist/tools/install/clawhub.js +182 -0
- package/dist/tools/install/clawhub.js.map +1 -0
- package/dist/tools/install/constants.d.ts +5 -0
- package/dist/tools/install/constants.d.ts.map +1 -0
- package/dist/tools/install/constants.js +5 -0
- package/dist/tools/install/constants.js.map +1 -0
- package/dist/tools/install/install_skill.d.ts +3 -0
- package/dist/tools/install/install_skill.d.ts.map +1 -0
- package/dist/tools/install/install_skill.js +132 -0
- package/dist/tools/install/install_skill.js.map +1 -0
- package/dist/tools/install/npm.d.ts +8 -0
- package/dist/tools/install/npm.d.ts.map +1 -0
- package/dist/tools/install/npm.js +52 -0
- package/dist/tools/install/npm.js.map +1 -0
- package/dist/tools/install/skill-content.d.ts +9 -0
- package/dist/tools/install/skill-content.d.ts.map +1 -0
- package/dist/tools/install/skill-content.js +89 -0
- package/dist/tools/install/skill-content.js.map +1 -0
- package/dist/tools/install_skill.d.ts.map +1 -1
- package/dist/tools/install_skill.js +2 -0
- package/dist/tools/install_skill.js.map +1 -1
- package/dist/tools/list_memory.d.ts +3 -0
- package/dist/tools/list_memory.d.ts.map +1 -0
- package/dist/tools/list_memory.js +55 -0
- package/dist/tools/list_memory.js.map +1 -0
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +2 -0
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/memory/delete_memory.d.ts +3 -0
- package/dist/tools/memory/delete_memory.d.ts.map +1 -0
- package/dist/tools/memory/delete_memory.js +46 -0
- package/dist/tools/memory/delete_memory.js.map +1 -0
- package/dist/tools/memory/list_memory.d.ts +3 -0
- package/dist/tools/memory/list_memory.d.ts.map +1 -0
- package/dist/tools/memory/list_memory.js +55 -0
- package/dist/tools/memory/list_memory.js.map +1 -0
- package/dist/tools/memory/save_memory.d.ts +3 -0
- package/dist/tools/memory/save_memory.d.ts.map +1 -0
- package/dist/tools/memory/save_memory.js +45 -0
- package/dist/tools/memory/save_memory.js.map +1 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +2 -0
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -2
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +112 -17
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/save_memory.d.ts +3 -0
- package/dist/tools/save_memory.d.ts.map +1 -0
- package/dist/tools/save_memory.js +45 -0
- package/dist/tools/save_memory.js.map +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +2 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/tree.d.ts +12 -0
- package/dist/tools/tree.d.ts.map +1 -0
- package/dist/tools/tree.js +87 -0
- package/dist/tools/tree.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web/fetch.d.ts +3 -0
- package/dist/tools/web/fetch.d.ts.map +1 -0
- package/dist/tools/web/fetch.js +49 -0
- package/dist/tools/web/fetch.js.map +1 -0
- package/dist/tools/web/search.d.ts +3 -0
- package/dist/tools/web/search.d.ts.map +1 -0
- package/dist/tools/web/search.js +134 -0
- package/dist/tools/web/search.js.map +1 -0
- package/dist/tools/write_plan.d.ts.map +1 -1
- package/dist/tools/write_plan.js +2 -0
- package/dist/tools/write_plan.js.map +1 -1
- package/dist/tui/commands.d.ts +4 -0
- package/dist/tui/commands.d.ts.map +1 -0
- package/dist/tui/commands.js +118 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/components/colors.d.ts +36 -0
- package/dist/tui/components/colors.d.ts.map +1 -0
- package/dist/tui/components/colors.js +48 -0
- package/dist/tui/components/colors.js.map +1 -0
- package/dist/tui/components/header-bar.d.ts +26 -0
- package/dist/tui/components/header-bar.d.ts.map +1 -0
- package/dist/tui/components/header-bar.js +78 -0
- package/dist/tui/components/header-bar.js.map +1 -0
- package/dist/tui/components/index.d.ts +9 -0
- package/dist/tui/components/index.d.ts.map +1 -0
- package/dist/tui/components/index.js +9 -0
- package/dist/tui/components/index.js.map +1 -0
- package/dist/tui/components/landing-screen.d.ts +12 -0
- package/dist/tui/components/landing-screen.d.ts.map +1 -0
- package/dist/tui/components/landing-screen.js +53 -0
- package/dist/tui/components/landing-screen.js.map +1 -0
- package/dist/tui/components/pixel-logo.d.ts +5 -0
- package/dist/tui/components/pixel-logo.d.ts.map +1 -0
- package/dist/tui/components/pixel-logo.js +27 -0
- package/dist/tui/components/pixel-logo.js.map +1 -0
- package/dist/tui/components/plan-utils.d.ts +11 -0
- package/dist/tui/components/plan-utils.d.ts.map +1 -0
- package/dist/tui/components/plan-utils.js +62 -0
- package/dist/tui/components/plan-utils.js.map +1 -0
- package/dist/tui/components/plans-list.d.ts +18 -0
- package/dist/tui/components/plans-list.d.ts.map +1 -0
- package/dist/tui/components/plans-list.js +46 -0
- package/dist/tui/components/plans-list.js.map +1 -0
- package/dist/tui/components/rich-footer.d.ts +29 -0
- package/dist/tui/components/rich-footer.d.ts.map +1 -0
- package/dist/tui/components/rich-footer.js +124 -0
- package/dist/tui/components/rich-footer.js.map +1 -0
- package/dist/tui/components/todo-panel.d.ts +14 -0
- package/dist/tui/components/todo-panel.d.ts.map +1 -0
- package/dist/tui/components/todo-panel.js +83 -0
- package/dist/tui/components/todo-panel.js.map +1 -0
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +14 -7
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/index.d.ts +2 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +99 -505
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/overlays.d.ts +28 -0
- package/dist/tui/overlays.d.ts.map +1 -0
- package/dist/tui/overlays.js +154 -0
- package/dist/tui/overlays.js.map +1 -0
- package/dist/tui/themes.d.ts +5 -0
- package/dist/tui/themes.d.ts.map +1 -0
- package/dist/tui/themes.js +38 -0
- package/dist/tui/themes.js.map +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +60 -122
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/index.html +1 -1
- package/dist/web/public/messages.js +41 -25
- package/dist/web/public/state.js +72 -72
- package/dist/web/public/style.css +1126 -996
- package/dist/web/public/websocket.js +267 -267
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +82 -3
- package/dist/web/session-bridge.js.map +1 -1
- package/docs/ast-tools.md +117 -0
- package/package.json +60 -55
- package/scripts/check-line-count.mjs +53 -0
- package/scripts/copy-ast-wasm.mjs +32 -0
- package/src/agent/__tests__/session-restore.test.ts +284 -0
- package/src/agent/commands.ts +274 -0
- package/src/agent/compaction.ts +9 -2
- package/src/agent/index.ts +30 -25
- package/src/agent/memory.ts +60 -0
- package/src/agent/prompt-builder.ts +100 -26
- package/src/agent/providers/anthropic.ts +179 -179
- package/src/agent/providers/google.ts +210 -210
- package/src/agent/providers/ollama.ts +186 -186
- package/src/agent/providers/openai.ts +18 -3
- package/src/agent/session-chat.ts +480 -0
- package/src/agent/session-display.ts +195 -0
- package/src/agent/session-persistence.ts +146 -0
- package/src/agent/session-utils.ts +54 -0
- package/src/agent/session.ts +261 -718
- package/src/cli/index.ts +73 -4
- package/src/config/index.ts +342 -342
- package/src/index.ts +30 -3
- package/src/tools/__tests__/bash.test.ts +187 -1
- package/src/tools/__tests__/edit.test.ts +275 -11
- package/src/tools/__tests__/fetch.test.ts +1 -1
- package/src/tools/__tests__/glob.test.ts +1 -1
- package/src/tools/__tests__/grep.test.ts +1 -1
- package/src/tools/__tests__/ls.test.ts +1 -1
- package/src/tools/__tests__/read.test.ts +1 -1
- package/src/tools/__tests__/registry.test.ts +332 -0
- package/src/tools/__tests__/write_plan.test.ts +1 -1
- package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
- package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
- package/src/tools/codebase/ast/adapter.ts +44 -0
- package/src/tools/codebase/ast/tools.ts +322 -0
- package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
- package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
- package/src/tools/codebase/ast/types.ts +102 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
- package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
- package/src/tools/{find.ts → codebase/find.ts} +4 -2
- package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
- package/src/tools/{grep.ts → codebase/grep.ts} +4 -2
- package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
- package/src/tools/{read.ts → codebase/read.ts} +5 -3
- package/src/tools/edit/diff-compute.ts +21 -0
- package/src/tools/edit/diff-render.ts +145 -0
- package/src/tools/edit/edit.ts +468 -0
- package/src/tools/edit/matcher.ts +70 -0
- package/src/tools/edit/parser.ts +131 -0
- package/src/tools/edit/types.ts +19 -0
- package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
- package/src/tools/execute/bash/constants.ts +93 -0
- package/src/tools/execute/bash/errors.ts +304 -0
- package/src/tools/execute/bash/execution.ts +211 -0
- package/src/tools/execute/bash/index.ts +131 -0
- package/src/tools/execute/bash/platform.ts +65 -0
- package/src/tools/execute/bash/security.ts +28 -0
- package/src/tools/execute/bash/validation.ts +78 -0
- package/src/tools/execute/git.ts +209 -0
- package/src/tools/execute/task_complete.ts +40 -0
- package/src/tools/install/clawhub.ts +222 -0
- package/src/tools/install/constants.ts +4 -0
- package/src/tools/install/install_skill.ts +150 -0
- package/src/tools/install/npm.ts +60 -0
- package/src/tools/install/skill-content.ts +108 -0
- package/src/tools/memory/delete_memory.ts +46 -0
- package/src/tools/memory/list_memory.ts +53 -0
- package/src/tools/memory/save_memory.ts +45 -0
- package/src/tools/registry.ts +119 -19
- package/src/tools/tree.ts +103 -0
- package/src/tools/types.ts +13 -0
- package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
- package/src/tools/{search.ts → web/search.ts} +4 -2
- package/src/tui/commands.ts +135 -0
- package/src/tui/components/colors.ts +52 -0
- package/src/tui/components/header-bar.ts +88 -0
- package/src/tui/components/index.ts +8 -0
- package/src/tui/components/landing-screen.ts +67 -0
- package/src/tui/components/pixel-logo.ts +33 -0
- package/src/tui/components/plan-utils.ts +69 -0
- package/src/tui/components/plans-list.ts +55 -0
- package/src/tui/components/rich-footer.ts +146 -0
- package/src/tui/components/todo-panel.ts +95 -0
- package/src/tui/index.ts +335 -786
- package/src/tui/overlays.ts +203 -0
- package/src/tui/themes.ts +41 -0
- package/src/web/index.ts +64 -127
- package/src/web/public/index.html +1 -1
- package/src/web/public/messages.js +41 -25
- package/src/web/public/state.js +72 -72
- package/src/web/public/style.css +1126 -996
- package/src/web/public/websocket.js +267 -267
- package/src/web/session-bridge.ts +272 -193
- package/AGENT.md +0 -99
- package/src/tools/bash.ts +0 -125
- package/src/tools/edit.ts +0 -747
- package/src/tools/git.ts +0 -76
- package/src/tools/install_skill.ts +0 -539
- package/src/tui/components.ts +0 -635
package/src/agent/session.ts
CHANGED
|
@@ -1,261 +1,40 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import * as fs from 'node:fs'
|
|
3
|
-
import * as os from 'node:os'
|
|
4
2
|
import * as path from 'node:path'
|
|
5
3
|
import type { Config } from '../config/index.js'
|
|
6
|
-
import { saveTokenUsage } from '../config/tokens.js'
|
|
7
4
|
import { FileReadTracker } from '../diff/apply.js'
|
|
8
|
-
import { fmtErr } from '../tools/errors.js'
|
|
9
5
|
import { ToolRegistry } from '../tools/registry.js'
|
|
10
|
-
import type { ToolCall
|
|
11
|
-
import { compact, estimateMessagesTokens, shouldCompact } from './compaction.js'
|
|
12
|
-
import { EventChannels, getGlobalEventBus } from './event-bus.js'
|
|
6
|
+
import type { ToolCall } from '../tools/types.js'
|
|
13
7
|
import type { LLMMessage, LLMProvider } from './llm.js'
|
|
14
8
|
import { buildSystemPrompt } from './prompt-builder.js'
|
|
15
9
|
import { AnthropicProvider } from './providers/anthropic.js'
|
|
16
10
|
import { GoogleProvider } from './providers/google.js'
|
|
17
11
|
import { OllamaProvider } from './providers/ollama.js'
|
|
18
12
|
import { OpenAIProvider } from './providers/openai.js'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function getSessionDir(): string {
|
|
37
|
-
return path.join(os.homedir(), '.lonny', 'sessions')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getSessionFilePath(cwd: string): string {
|
|
41
|
-
const absPath = path.resolve(cwd)
|
|
42
|
-
const hash = createHash('sha256').update(absPath, 'utf-8').digest('hex').slice(0, 12)
|
|
43
|
-
const dirName = path.basename(absPath)
|
|
44
|
-
const safeName = dirName.replace(/[<>:"/\\|?*]/g, '_')
|
|
45
|
-
return path.join(getSessionDir(), `${safeName}-${hash}.json`)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function ensureDir(dir: string): void {
|
|
49
|
-
if (!fs.existsSync(dir)) {
|
|
50
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Colors ─────────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
const CY = '\x1b[36m'
|
|
57
|
-
const GR = '\x1b[32m'
|
|
58
|
-
const YE = '\x1b[33m'
|
|
59
|
-
const RE = '\x1b[31m'
|
|
60
|
-
const MG = '\x1b[35m'
|
|
61
|
-
const GY = '\x1b[90m'
|
|
62
|
-
const RS = '\x1b[0m'
|
|
63
|
-
const BLD = '\x1b[1m'
|
|
64
|
-
const TH = '\x1b[48;2;22;22;32m\x1b[38;2;150;150;170m' // dark bg + dim fg for thinking
|
|
65
|
-
|
|
66
|
-
/** Get terminal width (columns), default to 80. */
|
|
67
|
-
function termWidth(): number {
|
|
68
|
-
return process.stdout.columns ?? 80
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Visible width of a string (strip ANSI codes). ASCII=1, CJK/non-ASCII=2. */
|
|
72
|
-
function visibleWidth(s: string): number {
|
|
73
|
-
let w = 0
|
|
74
|
-
for (let i = 0; i < s.length; i++) {
|
|
75
|
-
if (s.charCodeAt(i) === 0x1b) {
|
|
76
|
-
// Skip past escape sequence
|
|
77
|
-
while (i < s.length && s[i] !== 'm') i++
|
|
78
|
-
continue
|
|
79
|
-
}
|
|
80
|
-
w += s.charCodeAt(i) > 0x7e ? 2 : 1
|
|
81
|
-
}
|
|
82
|
-
return w
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Visible prefix width for thinking box lines: " │" = 3 */
|
|
86
|
-
const THINK_PREFIX_WIDTH = 3
|
|
87
|
-
|
|
88
|
-
/** Build the top border of the thinking box */
|
|
89
|
-
function thinkTopBorder(): string {
|
|
90
|
-
return `\n ${GY}╭───────${RS}${TH} Think ${GY}────────────────────${RS}\n`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Build the bottom border of the thinking box */
|
|
94
|
-
function thinkBottomBorder(): string {
|
|
95
|
-
return ` ${GY}╰${'─'.repeat(42)}${RS}\n\n`
|
|
96
|
-
}
|
|
13
|
+
import { runChat } from './session-chat.js'
|
|
14
|
+
import {
|
|
15
|
+
ensureDir,
|
|
16
|
+
findLegacySessionFile,
|
|
17
|
+
generateId,
|
|
18
|
+
getSessionDir,
|
|
19
|
+
getSessionFilePath,
|
|
20
|
+
getSessionFilesForCwd,
|
|
21
|
+
migrateSessionData,
|
|
22
|
+
type SessionData,
|
|
23
|
+
type SessionInfo,
|
|
24
|
+
} from './session-persistence.js'
|
|
25
|
+
import { sanitizeMessages } from './session-utils.js'
|
|
26
|
+
|
|
27
|
+
export { formatToolInput } from './session-display.js'
|
|
28
|
+
|
|
29
|
+
// ── Session output interface ────────────────────────────────────────────────
|
|
97
30
|
|
|
98
31
|
export interface SessionOutput {
|
|
99
32
|
write: (text: string) => void
|
|
100
|
-
/** When true, tool invocation/result formatting is skipped (TUI handles it via event bus) */
|
|
101
33
|
suppressToolOutput?: boolean
|
|
102
|
-
/**
|
|
103
|
-
* When autoApprove is false, called before dispatching write-type tool calls.
|
|
104
|
-
* Return true to allow execution, false to reject. Tools are batched — one confirmation per turn.
|
|
105
|
-
*/
|
|
106
34
|
confirmTool?: (toolCalls: ToolCall[]) => Promise<boolean>
|
|
107
35
|
}
|
|
108
36
|
|
|
109
|
-
|
|
110
|
-
if (output) {
|
|
111
|
-
output.write(text)
|
|
112
|
-
} else {
|
|
113
|
-
process.stdout.write(text)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function printUserMessage(prompt: string, output?: SessionOutput): void {
|
|
118
|
-
// When suppressToolOutput is true (Web UI mode), the frontend already
|
|
119
|
-
// displays the user message, so skip sending it as a chunk.
|
|
120
|
-
if (output?.suppressToolOutput) return
|
|
121
|
-
const line = ` ${GY}┃${RS} ${BLD}${CY}You${RS}`
|
|
122
|
-
writeOut(`\n${line} ${prompt}\n\n`, output)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function printToolInvocation(tc: ToolCall, output?: SessionOutput): void {
|
|
126
|
-
const detail = formatToolInput(tc)
|
|
127
|
-
const isWrite = tc.name === 'write_plan' || tc.name === 'edit'
|
|
128
|
-
const icon = isWrite ? `${YE}◆${RS}` : `${GR}◇${RS}`
|
|
129
|
-
const label = isWrite ? `${YE}${tc.name}${RS}` : `${GR}${tc.name}${RS}`
|
|
130
|
-
writeOut(`\n ${GY}│${RS} ${icon} ${label}${detail ? ` ${GY}${detail}${RS}` : ''}\n`, output)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function printToolResult(tc: ToolCall, result: ToolResult, output?: SessionOutput): void {
|
|
134
|
-
if (!result.success) {
|
|
135
|
-
writeOut(` ${GY}│${RS} ${RE}✖${RS} ${RE}${result.error}${RS}\n`, output)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
if (tc.name === 'read') {
|
|
139
|
-
const fileCount = (result.output.match(/^=== /gm) || []).length
|
|
140
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} read ${fileCount} file(s)\n`, output)
|
|
141
|
-
for (const line of result.output.split('\n')) {
|
|
142
|
-
if (line.startsWith('=== ')) {
|
|
143
|
-
const fp = line.slice(4, line.includes(' ===') ? line.indexOf(' ===') + 4 : undefined)
|
|
144
|
-
writeOut(` ${GY}│${RS} ${GY}${fp}${RS}\n`, output)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
} else if (tc.name === 'glob') {
|
|
148
|
-
const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length
|
|
149
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} glob ${count} match(es)\n`, output)
|
|
150
|
-
} else if (tc.name === 'grep') {
|
|
151
|
-
const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length
|
|
152
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} grep ${count} match(es)\n`, output)
|
|
153
|
-
} else if (tc.name === 'bash') {
|
|
154
|
-
const outLines = result.output.split('\n')
|
|
155
|
-
const summary = outLines.length > 1 ? `(${outLines.length} lines)` : ''
|
|
156
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} bash ${summary}\n`, output)
|
|
157
|
-
} else if (tc.name === 'edit') {
|
|
158
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} edit\n`, output)
|
|
159
|
-
if (result.output) {
|
|
160
|
-
for (const l of result.output.split('\n')) {
|
|
161
|
-
if (l.trim()) writeOut(` ${GY}│${RS} ${l.trim()}\n`, output)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} else if (tc.name === 'write_plan') {
|
|
165
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} ${result.output || tc.name}\n`, output)
|
|
166
|
-
} else if (tc.name === 'search') {
|
|
167
|
-
writeOut(
|
|
168
|
-
` ${GY}│${RS} ${GR}✔${RS} search: ${String(tc.input.query || '').slice(0, 80)}\n`,
|
|
169
|
-
output,
|
|
170
|
-
)
|
|
171
|
-
} else {
|
|
172
|
-
writeOut(` ${GY}│${RS} ${GR}✔${RS} ${tc.name}\n`, output)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
interface SingleEditShape {
|
|
177
|
-
file_path: string
|
|
178
|
-
old_string: string
|
|
179
|
-
new_string: string
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isSingleEditShape(v: unknown): v is SingleEditShape {
|
|
183
|
-
return (
|
|
184
|
-
typeof v === 'object' &&
|
|
185
|
-
v !== null &&
|
|
186
|
-
'file_path' in v &&
|
|
187
|
-
'old_string' in v &&
|
|
188
|
-
'new_string' in v
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function formatToolInput(tc: ToolCall): string {
|
|
193
|
-
const parts: string[] = []
|
|
194
|
-
if (tc.name === 'read' && Array.isArray(tc.input.paths)) {
|
|
195
|
-
parts.push(tc.input.paths.join(', '))
|
|
196
|
-
} else if (tc.name === 'glob' && typeof tc.input.pattern === 'string') {
|
|
197
|
-
parts.push(tc.input.pattern)
|
|
198
|
-
} else if (tc.name === 'grep') {
|
|
199
|
-
if (typeof tc.input.pattern === 'string') parts.push(`/${tc.input.pattern}/`)
|
|
200
|
-
if (typeof tc.input.include === 'string') parts.push(`in:${tc.input.include}`)
|
|
201
|
-
} else if (tc.name === 'ls') {
|
|
202
|
-
parts.push(typeof tc.input.path === 'string' ? tc.input.path : '.')
|
|
203
|
-
} else if (tc.name === 'bash') {
|
|
204
|
-
const cmd = typeof tc.input.command === 'string' ? tc.input.command : ''
|
|
205
|
-
parts.push(cmd.length > 80 ? `${cmd.slice(0, 80)}\u2026` : cmd)
|
|
206
|
-
} else if (tc.name === 'search') {
|
|
207
|
-
if (typeof tc.input.query === 'string') parts.push(tc.input.query.slice(0, 120))
|
|
208
|
-
} else if (tc.name === 'write_plan') {
|
|
209
|
-
if (typeof tc.input.filename === 'string') parts.push(tc.input.filename)
|
|
210
|
-
} else if (tc.name === 'edit') {
|
|
211
|
-
if (Array.isArray(tc.input.edits) && tc.input.edits.every(isSingleEditShape)) {
|
|
212
|
-
const paths = tc.input.edits.map(e => e.file_path)
|
|
213
|
-
parts.push(paths.join(', '))
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return parts.join(' \u2502 ')
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function printTokenStats(
|
|
220
|
-
turnIn: number,
|
|
221
|
-
turnOut: number,
|
|
222
|
-
totalIn: number,
|
|
223
|
-
totalOut: number,
|
|
224
|
-
turnApi: number,
|
|
225
|
-
totalApi: number,
|
|
226
|
-
output?: SessionOutput,
|
|
227
|
-
turnCacheHit?: number,
|
|
228
|
-
turnCacheMiss?: number,
|
|
229
|
-
totalCacheHit?: number,
|
|
230
|
-
totalCacheMiss?: number,
|
|
231
|
-
): void {
|
|
232
|
-
const bus = getGlobalEventBus()
|
|
233
|
-
bus.emit(EventChannels.TOKEN_STATS, {
|
|
234
|
-
turnIn,
|
|
235
|
-
turnOut,
|
|
236
|
-
totalIn,
|
|
237
|
-
totalOut,
|
|
238
|
-
turnApi,
|
|
239
|
-
totalApi,
|
|
240
|
-
turnCacheHit,
|
|
241
|
-
turnCacheMiss,
|
|
242
|
-
totalCacheHit,
|
|
243
|
-
totalCacheMiss,
|
|
244
|
-
})
|
|
245
|
-
// Skip terminal output in Web UI mode
|
|
246
|
-
if (output?.suppressToolOutput) return
|
|
247
|
-
const total = totalIn + totalOut
|
|
248
|
-
let msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`
|
|
249
|
-
// Show cache hit rate if available
|
|
250
|
-
const cacheHit = totalCacheHit ?? 0
|
|
251
|
-
const cacheMiss = totalCacheMiss ?? 0
|
|
252
|
-
const cacheTotal = cacheHit + cacheMiss
|
|
253
|
-
if (cacheTotal > 0) {
|
|
254
|
-
const pct = Math.round((cacheHit / cacheTotal) * 100)
|
|
255
|
-
msg += ` ${GY}cached${RS} ${pct}%`
|
|
256
|
-
}
|
|
257
|
-
writeOut(`\n${msg}\n`, output)
|
|
258
|
-
}
|
|
37
|
+
// ── Session class ────────────────────────────────────────────────────────────
|
|
259
38
|
|
|
260
39
|
export class Session {
|
|
261
40
|
messages: LLMMessage[]
|
|
@@ -265,12 +44,10 @@ export class Session {
|
|
|
265
44
|
config: Config
|
|
266
45
|
output?: SessionOutput
|
|
267
46
|
private _onPlanWritten?: (display: string) => void
|
|
268
|
-
|
|
47
|
+
|
|
269
48
|
set onPlanWritten(cb: ((display: string) => void) | undefined) {
|
|
270
49
|
this._onPlanWritten = cb
|
|
271
|
-
// Update context so setMode() picks it up too
|
|
272
50
|
this.registry.updateContext({ onPlanWritten: cb })
|
|
273
|
-
// Re-register write_plan tool with the new callback
|
|
274
51
|
if (this.registry.has('write_plan')) {
|
|
275
52
|
this.registry.reRegisterWritePlan(this.config.cwd, cb)
|
|
276
53
|
}
|
|
@@ -288,8 +65,8 @@ export class Session {
|
|
|
288
65
|
turnCacheMissTokens: number = 0
|
|
289
66
|
totalCacheHitTokens: number = 0
|
|
290
67
|
totalCacheMissTokens: number = 0
|
|
291
|
-
|
|
292
|
-
|
|
68
|
+
stopped: boolean = false
|
|
69
|
+
abortController: AbortController | null = null
|
|
293
70
|
|
|
294
71
|
constructor(config: Config, output?: SessionOutput) {
|
|
295
72
|
this.config = config
|
|
@@ -321,25 +98,37 @@ export class Session {
|
|
|
321
98
|
this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model)
|
|
322
99
|
}
|
|
323
100
|
|
|
324
|
-
// Placeholder until initSystemPrompt() is called
|
|
325
101
|
this.messages = [{ role: 'system', content: '' }]
|
|
326
|
-
// Initialize system prompt asynchronously
|
|
327
102
|
this.initSystemPrompt(config)
|
|
328
103
|
}
|
|
329
104
|
|
|
330
|
-
/** Initialize the system prompt asynchronously */
|
|
331
105
|
private initSystemPrompt(config: Config): void {
|
|
332
|
-
buildSystemPrompt(config).then(prompt => {
|
|
333
|
-
this.messages
|
|
106
|
+
buildSystemPrompt(config, this.registry.getDefinitions()).then(prompt => {
|
|
107
|
+
if (this.messages.length <= 1) {
|
|
108
|
+
this.messages = [{ role: 'system', content: prompt }]
|
|
109
|
+
}
|
|
334
110
|
})
|
|
335
111
|
}
|
|
336
112
|
|
|
337
|
-
|
|
113
|
+
sessionId: string = generateId()
|
|
114
|
+
sessionTitle: string = ''
|
|
115
|
+
sessionCreatedAt: string = new Date().toISOString()
|
|
116
|
+
|
|
338
117
|
save(): void {
|
|
339
118
|
const dir = getSessionDir()
|
|
340
119
|
ensureDir(dir)
|
|
341
|
-
const filePath = getSessionFilePath(this.config.cwd)
|
|
120
|
+
const filePath = getSessionFilePath(this.config.cwd, this.sessionId)
|
|
121
|
+
const now = new Date().toISOString()
|
|
122
|
+
let title = this.sessionTitle
|
|
123
|
+
if (!title) {
|
|
124
|
+
const firstUserMsg = this.messages.find(m => m.role === 'user')
|
|
125
|
+
if (firstUserMsg && typeof firstUserMsg.content === 'string') {
|
|
126
|
+
title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
342
129
|
const data: SessionData = {
|
|
130
|
+
id: this.sessionId,
|
|
131
|
+
title: title || undefined,
|
|
343
132
|
cwd: path.resolve(this.config.cwd),
|
|
344
133
|
messages: this.messages,
|
|
345
134
|
totalInputTokens: this.totalInputTokens,
|
|
@@ -350,42 +139,105 @@ export class Session {
|
|
|
350
139
|
mode: this.config.mode,
|
|
351
140
|
model: this.config.model,
|
|
352
141
|
provider: this.config.provider,
|
|
353
|
-
|
|
142
|
+
createdAt: this.sessionCreatedAt,
|
|
143
|
+
updatedAt: now,
|
|
354
144
|
}
|
|
355
145
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
356
146
|
}
|
|
357
147
|
|
|
358
|
-
/** Try to load a saved session for the given cwd. Returns null if none exists. */
|
|
359
148
|
static async load(config: Config, output?: SessionOutput): Promise<Session | null> {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
149
|
+
const cwdSessions = getSessionFilesForCwd(config.cwd)
|
|
150
|
+
console.log(
|
|
151
|
+
`[session] Found ${cwdSessions.length} session files for cwd "${config.cwd}":`,
|
|
152
|
+
cwdSessions
|
|
153
|
+
.map(
|
|
154
|
+
f =>
|
|
155
|
+
`${f.fileName} (updatedAt=${f.data.updatedAt?.slice(0, 19)}, messages=${f.data.messages?.length || 0})`,
|
|
156
|
+
)
|
|
157
|
+
.join(', '),
|
|
158
|
+
)
|
|
159
|
+
if (cwdSessions.length > 0) {
|
|
160
|
+
cwdSessions.sort((a, b) => {
|
|
161
|
+
const aLen = a.data.messages?.length || 0
|
|
162
|
+
const bLen = b.data.messages?.length || 0
|
|
163
|
+
if (aLen !== bLen) return bLen - aLen
|
|
164
|
+
return b.data.updatedAt.localeCompare(a.data.updatedAt)
|
|
165
|
+
})
|
|
166
|
+
console.log(`[session] Loading session file: ${cwdSessions[0].fileName}`)
|
|
167
|
+
return Session.loadFromData(cwdSessions[0].data, config, output)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const legacyPath = findLegacySessionFile(config.cwd)
|
|
171
|
+
if (legacyPath) {
|
|
172
|
+
try {
|
|
173
|
+
const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8')) as Record<string, unknown>
|
|
174
|
+
const data = migrateSessionData(raw)
|
|
175
|
+
const session = await Session.loadFromData(data, config, output)
|
|
176
|
+
session.save()
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(legacyPath)
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
return session
|
|
183
|
+
} catch {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
366
186
|
}
|
|
367
187
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static async loadById(
|
|
192
|
+
sessionId: string,
|
|
193
|
+
config: Config,
|
|
194
|
+
output?: SessionOutput,
|
|
195
|
+
): Promise<Session | null> {
|
|
196
|
+
const allSessions = Session.listSessions()
|
|
197
|
+
const target = allSessions.find(s => s.id === sessionId)
|
|
198
|
+
if (!target) return null
|
|
199
|
+
const legacyPath = findLegacySessionFile(config.cwd)
|
|
200
|
+
if (legacyPath) {
|
|
201
|
+
try {
|
|
202
|
+
const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8')) as Record<string, unknown>
|
|
203
|
+
const data = migrateSessionData(raw)
|
|
204
|
+
if (data.id === sessionId) {
|
|
205
|
+
return Session.loadFromData(data, config, output)
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
/* ignore */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const filePath = getSessionFilePath(target.cwd, sessionId)
|
|
212
|
+
try {
|
|
213
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>
|
|
214
|
+
return Session.loadFromData(migrateSessionData(raw), config, output)
|
|
215
|
+
} catch {
|
|
372
216
|
return null
|
|
373
217
|
}
|
|
218
|
+
}
|
|
374
219
|
|
|
220
|
+
private static async loadFromData(
|
|
221
|
+
data: SessionData,
|
|
222
|
+
config: Config,
|
|
223
|
+
output?: SessionOutput,
|
|
224
|
+
): Promise<Session> {
|
|
375
225
|
const session = new Session(config, output)
|
|
376
|
-
// Restore messages (replace the default system prompt with the saved one)
|
|
377
226
|
session.messages = data.messages
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
data.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
227
|
+
console.log(`[session] Loading session ${data.id}: ${data.messages.length} messages from disk`)
|
|
228
|
+
session.messages = sanitizeMessages(session.messages)
|
|
229
|
+
console.log(
|
|
230
|
+
`[session] After sanitize: ${session.messages.length} messages (removed ${data.messages.length - session.messages.length})`,
|
|
231
|
+
)
|
|
232
|
+
session.sessionId = data.id
|
|
233
|
+
session.sessionTitle = data.title || ''
|
|
234
|
+
session.sessionCreatedAt = data.createdAt
|
|
235
|
+
config.mode = data.mode
|
|
236
|
+
session.registry.setMode(data.mode)
|
|
237
|
+
if (data.model !== config.model || data.provider !== config.provider) {
|
|
238
|
+
const prompt = await buildSystemPrompt(config, session.registry.getDefinitions())
|
|
386
239
|
session.messages[0] = { role: 'system', content: prompt }
|
|
387
240
|
}
|
|
388
|
-
// Restore token stats
|
|
389
241
|
session.totalInputTokens = data.totalInputTokens
|
|
390
242
|
session.totalOutputTokens = data.totalOutputTokens
|
|
391
243
|
session.totalApiCalls = data.totalApiCalls
|
|
@@ -394,475 +246,166 @@ export class Session {
|
|
|
394
246
|
return session
|
|
395
247
|
}
|
|
396
248
|
|
|
397
|
-
/** Remove the saved session file for the given cwd. */
|
|
398
249
|
static clearSavedSession(cwd: string): void {
|
|
399
|
-
const
|
|
250
|
+
const cwdSessions = getSessionFilesForCwd(cwd)
|
|
251
|
+
for (const { fileName } of cwdSessions) {
|
|
252
|
+
try {
|
|
253
|
+
fs.unlinkSync(path.join(getSessionDir(), fileName))
|
|
254
|
+
} catch {
|
|
255
|
+
/* ignore */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const legacyPath = findLegacySessionFile(cwd)
|
|
259
|
+
if (legacyPath) {
|
|
260
|
+
try {
|
|
261
|
+
fs.unlinkSync(legacyPath)
|
|
262
|
+
} catch {
|
|
263
|
+
/* ignore */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static listSessions(maxCount?: number): SessionInfo[] {
|
|
269
|
+
const dir = getSessionDir()
|
|
270
|
+
try {
|
|
271
|
+
if (!fs.existsSync(dir)) return []
|
|
272
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'))
|
|
273
|
+
const sessions: SessionInfo[] = []
|
|
274
|
+
for (const fileName of files) {
|
|
275
|
+
try {
|
|
276
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, fileName), 'utf-8')) as Record<
|
|
277
|
+
string,
|
|
278
|
+
unknown
|
|
279
|
+
>
|
|
280
|
+
const data = migrateSessionData(raw)
|
|
281
|
+
const firstUserMsg = data.messages.find(m => m.role === 'user')
|
|
282
|
+
let title = data.title || ''
|
|
283
|
+
if (!title && firstUserMsg && typeof firstUserMsg.content === 'string') {
|
|
284
|
+
title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
|
|
285
|
+
}
|
|
286
|
+
sessions.push({
|
|
287
|
+
id: data.id,
|
|
288
|
+
cwd: data.cwd,
|
|
289
|
+
title,
|
|
290
|
+
messageCount: data.messages.length,
|
|
291
|
+
mode: data.mode,
|
|
292
|
+
model: data.model,
|
|
293
|
+
provider: data.provider,
|
|
294
|
+
totalInputTokens: data.totalInputTokens,
|
|
295
|
+
totalOutputTokens: data.totalOutputTokens,
|
|
296
|
+
totalApiCalls: data.totalApiCalls,
|
|
297
|
+
createdAt: data.createdAt,
|
|
298
|
+
updatedAt: data.updatedAt,
|
|
299
|
+
fileName,
|
|
300
|
+
})
|
|
301
|
+
} catch {
|
|
302
|
+
// Skip corrupted files
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
306
|
+
if (maxCount && maxCount > 0) {
|
|
307
|
+
return sessions.slice(0, maxCount)
|
|
308
|
+
}
|
|
309
|
+
return sessions
|
|
310
|
+
} catch {
|
|
311
|
+
return []
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
static deleteSession(id: string): boolean {
|
|
316
|
+
const sessions = Session.listSessions()
|
|
317
|
+
const target = sessions.find(s => s.id === id)
|
|
318
|
+
if (!target) return false
|
|
400
319
|
try {
|
|
401
|
-
fs.unlinkSync(
|
|
320
|
+
fs.unlinkSync(path.join(getSessionDir(), target.fileName))
|
|
321
|
+
return true
|
|
402
322
|
} catch {
|
|
403
|
-
|
|
323
|
+
return false
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
clearSavedSession(): void {
|
|
328
|
+
Session.clearSavedSession(this.config.cwd)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fork(): Session {
|
|
332
|
+
const forked = new Session(this.config, this.output)
|
|
333
|
+
forked.messages = [...this.messages]
|
|
334
|
+
forked.onPlanWritten = this.onPlanWritten
|
|
335
|
+
const baseTitle = this.sessionTitle || 'forked session'
|
|
336
|
+
forked.sessionTitle = `${baseTitle} (fork)`
|
|
337
|
+
forked.sessionId = generateId()
|
|
338
|
+
forked.sessionCreatedAt = new Date().toISOString()
|
|
339
|
+
forked.save()
|
|
340
|
+
return forked
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
exportSession(filePath?: string): string {
|
|
344
|
+
const dir = filePath || path.join(getSessionDir(), `export-${this.sessionId}.json`)
|
|
345
|
+
const now = new Date().toISOString()
|
|
346
|
+
let title = this.sessionTitle
|
|
347
|
+
if (!title) {
|
|
348
|
+
const firstUserMsg = this.messages.find(m => m.role === 'user')
|
|
349
|
+
if (firstUserMsg && typeof firstUserMsg.content === 'string') {
|
|
350
|
+
title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const exportData = {
|
|
354
|
+
id: this.sessionId,
|
|
355
|
+
title: title || '(untitled)',
|
|
356
|
+
cwd: path.resolve(this.config.cwd),
|
|
357
|
+
mode: this.config.mode,
|
|
358
|
+
model: this.config.model,
|
|
359
|
+
provider: this.config.provider,
|
|
360
|
+
totalInputTokens: this.totalInputTokens,
|
|
361
|
+
totalOutputTokens: this.totalOutputTokens,
|
|
362
|
+
totalApiCalls: this.totalApiCalls,
|
|
363
|
+
totalCacheHitTokens: this.totalCacheHitTokens,
|
|
364
|
+
totalCacheMissTokens: this.totalCacheMissTokens,
|
|
365
|
+
createdAt: this.sessionCreatedAt,
|
|
366
|
+
exportedAt: now,
|
|
367
|
+
messages: this.messages.map(m => ({
|
|
368
|
+
role: m.role,
|
|
369
|
+
content: typeof m.content === 'string' ? m.content : null,
|
|
370
|
+
tool_calls: m.tool_calls
|
|
371
|
+
? m.tool_calls.map(tc => ({
|
|
372
|
+
id: tc.id,
|
|
373
|
+
name: tc.name,
|
|
374
|
+
input: tc.input,
|
|
375
|
+
}))
|
|
376
|
+
: undefined,
|
|
377
|
+
reasoning_content: m.reasoning_content,
|
|
378
|
+
})),
|
|
404
379
|
}
|
|
380
|
+
const fullPath = path.resolve(dir)
|
|
381
|
+
ensureDir(path.dirname(fullPath))
|
|
382
|
+
fs.writeFileSync(fullPath, JSON.stringify(exportData, null, 2), 'utf-8')
|
|
383
|
+
return fullPath
|
|
405
384
|
}
|
|
406
385
|
|
|
407
|
-
async setMode(mode: 'code' | 'plan' | 'ask'): Promise<void> {
|
|
386
|
+
async setMode(mode: 'code' | 'plan' | 'ask' | 'loop'): Promise<void> {
|
|
408
387
|
this.config.mode = mode
|
|
409
|
-
const prompt = await buildSystemPrompt(this.config)
|
|
410
|
-
this.messages[0] = { role: 'system', content: prompt }
|
|
411
388
|
this.registry.setMode(mode)
|
|
389
|
+
const prompt = await buildSystemPrompt(this.config, this.registry.getDefinitions())
|
|
390
|
+
this.messages[0] = { role: 'system', content: prompt }
|
|
412
391
|
this.save()
|
|
413
392
|
}
|
|
414
393
|
|
|
415
|
-
/** Stop the current conversation gracefully */
|
|
416
394
|
stop(): void {
|
|
417
395
|
this.stopped = true
|
|
418
|
-
// Abort any in-flight LLM stream to stop token consumption immediately
|
|
419
396
|
this.abortController?.abort()
|
|
420
397
|
}
|
|
421
398
|
|
|
422
|
-
/** Check if the session was stopped */
|
|
423
399
|
isStopped(): boolean {
|
|
424
400
|
return this.stopped
|
|
425
401
|
}
|
|
426
402
|
|
|
427
|
-
/** Reset the stopped flag for a new conversation */
|
|
428
403
|
resetStopped(): void {
|
|
429
404
|
this.stopped = false
|
|
430
|
-
// Create new AbortController for next conversation
|
|
431
405
|
this.abortController = new AbortController()
|
|
432
406
|
}
|
|
433
407
|
|
|
434
408
|
async chat(userPrompt: string): Promise<void> {
|
|
435
|
-
|
|
436
|
-
const out = this.output
|
|
437
|
-
printUserMessage(userPrompt, out)
|
|
438
|
-
this.messages.push({ role: 'user', content: userPrompt })
|
|
439
|
-
// Save immediately after user message so page refresh doesn't lose it
|
|
440
|
-
this.save()
|
|
441
|
-
|
|
442
|
-
// Reset per-turn counters
|
|
443
|
-
this.turnInputTokens = 0
|
|
444
|
-
this.turnOutputTokens = 0
|
|
445
|
-
this.turnApiCalls = 0
|
|
446
|
-
this.turnCacheHitTokens = 0
|
|
447
|
-
this.turnCacheMissTokens = 0
|
|
448
|
-
|
|
449
|
-
let iterations = 0
|
|
450
|
-
const maxIterations = 30
|
|
451
|
-
|
|
452
|
-
// Reset stopped flag for new conversation
|
|
453
|
-
this.resetStopped()
|
|
454
|
-
// Create a new AbortController for this chat invocation
|
|
455
|
-
// (a new controller is needed each time because abort() is one-shot)
|
|
456
|
-
this.abortController = new AbortController()
|
|
457
|
-
|
|
458
|
-
// Declare toolCalls outside the loop so we can reference it in stop check
|
|
459
|
-
let toolCalls: ToolCall[] = []
|
|
460
|
-
|
|
461
|
-
while (iterations < maxIterations) {
|
|
462
|
-
// Check if stop was requested
|
|
463
|
-
if (this.isStopped()) {
|
|
464
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
465
|
-
this.save()
|
|
466
|
-
return
|
|
467
|
-
}
|
|
468
|
-
iterations++
|
|
469
|
-
this.turnApiCalls++
|
|
470
|
-
this.totalApiCalls++
|
|
471
|
-
toolCalls = []
|
|
472
|
-
let fullResponse = ''
|
|
473
|
-
let reasoningContent: string | undefined
|
|
474
|
-
let reasoningOutput = false
|
|
475
|
-
let reasoningLineStart = false
|
|
476
|
-
|
|
477
|
-
bus.emit(EventChannels.TURN_START, { prompt: userPrompt, iteration: iterations })
|
|
478
|
-
bus.emit(EventChannels.LLM_STREAM_START, { iteration: iterations })
|
|
479
|
-
|
|
480
|
-
const stream = this.provider.chat(
|
|
481
|
-
this.messages,
|
|
482
|
-
this.registry.getDefinitions(),
|
|
483
|
-
this.abortController.signal,
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
for await (const chunk of stream) {
|
|
488
|
-
if (chunk.reasoning_content) {
|
|
489
|
-
reasoningContent = chunk.reasoning_content
|
|
490
|
-
// Stream reasoning content in real-time (only when no text in same chunk)
|
|
491
|
-
if (!chunk.text) {
|
|
492
|
-
// Emit thinking via EventBus for Web UI
|
|
493
|
-
bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content })
|
|
494
|
-
if (!reasoningOutput) {
|
|
495
|
-
reasoningOutput = true
|
|
496
|
-
reasoningLineStart = true
|
|
497
|
-
if (!out?.suppressToolOutput) {
|
|
498
|
-
writeOut(thinkTopBorder(), out)
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
// Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
|
|
502
|
-
if (!out?.suppressToolOutput) {
|
|
503
|
-
// Track column position on current line for wrapping
|
|
504
|
-
let thinkCol = 0
|
|
505
|
-
// Handle newlines in streamed content - add left border on each new line
|
|
506
|
-
// Also manually wrap long lines so wrapped lines keep the │ prefix.
|
|
507
|
-
let remaining = chunk.reasoning_content
|
|
508
|
-
const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH
|
|
509
|
-
while (remaining.length > 0) {
|
|
510
|
-
if (reasoningLineStart) {
|
|
511
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
512
|
-
reasoningLineStart = false
|
|
513
|
-
thinkCol = 0
|
|
514
|
-
}
|
|
515
|
-
const nlIdx = remaining.indexOf('\n')
|
|
516
|
-
if (nlIdx === -1) {
|
|
517
|
-
// No newline — write as much as fits on current line, wrap if needed
|
|
518
|
-
while (remaining.length > 0) {
|
|
519
|
-
const segWidth = visibleWidth(remaining)
|
|
520
|
-
const avail = maxContentWidth - thinkCol
|
|
521
|
-
if (segWidth <= avail) {
|
|
522
|
-
// Fits entirely on current line
|
|
523
|
-
writeOut(remaining, out)
|
|
524
|
-
thinkCol += segWidth
|
|
525
|
-
remaining = ''
|
|
526
|
-
} else if (avail <= 0) {
|
|
527
|
-
// Current line is full, wrap to next
|
|
528
|
-
writeOut(`${RS}\n`, out)
|
|
529
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
530
|
-
thinkCol = 0
|
|
531
|
-
} else {
|
|
532
|
-
// Write first part that fits, then wrap
|
|
533
|
-
// Find character boundary that fits within avail
|
|
534
|
-
let cut = avail
|
|
535
|
-
while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail) cut--
|
|
536
|
-
if (cut <= 0) cut = 1
|
|
537
|
-
writeOut(remaining.slice(0, cut), out)
|
|
538
|
-
writeOut(`${RS}\n`, out)
|
|
539
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
540
|
-
thinkCol = 0
|
|
541
|
-
remaining = remaining.slice(cut)
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
} else {
|
|
545
|
-
// Has newline — process the segment up to newline
|
|
546
|
-
const segment = remaining.slice(0, nlIdx)
|
|
547
|
-
const segWidth = visibleWidth(segment)
|
|
548
|
-
const avail = maxContentWidth - thinkCol
|
|
549
|
-
if (segWidth <= avail) {
|
|
550
|
-
// Segment fits on current line
|
|
551
|
-
writeOut(segment, out)
|
|
552
|
-
writeOut(`${RS}\n`, out)
|
|
553
|
-
reasoningLineStart = true
|
|
554
|
-
thinkCol = 0
|
|
555
|
-
} else {
|
|
556
|
-
// Segment too long — write what fits, wrap, then rest
|
|
557
|
-
let rest = segment
|
|
558
|
-
// Write remainder of current line
|
|
559
|
-
if (avail > 0) {
|
|
560
|
-
let cut = avail
|
|
561
|
-
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail) cut--
|
|
562
|
-
if (cut <= 0) cut = 1
|
|
563
|
-
writeOut(rest.slice(0, cut), out)
|
|
564
|
-
rest = rest.slice(cut)
|
|
565
|
-
}
|
|
566
|
-
writeOut(`${RS}\n`, out)
|
|
567
|
-
reasoningLineStart = true
|
|
568
|
-
thinkCol = 0
|
|
569
|
-
// Write rest of segment on continuation line(s)
|
|
570
|
-
if (rest.length > 0) {
|
|
571
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
572
|
-
reasoningLineStart = false
|
|
573
|
-
while (rest.length > 0) {
|
|
574
|
-
const rw = visibleWidth(rest)
|
|
575
|
-
if (rw <= maxContentWidth) {
|
|
576
|
-
writeOut(rest, out)
|
|
577
|
-
thinkCol = rw
|
|
578
|
-
rest = ''
|
|
579
|
-
} else {
|
|
580
|
-
let cut = maxContentWidth
|
|
581
|
-
while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
|
|
582
|
-
cut--
|
|
583
|
-
if (cut <= 0) cut = 1
|
|
584
|
-
writeOut(rest.slice(0, cut), out)
|
|
585
|
-
writeOut(`${RS}\n`, out)
|
|
586
|
-
writeOut(` ${GY}│${RS}${TH}`, out)
|
|
587
|
-
rest = rest.slice(cut)
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
writeOut(`${RS}\n`, out)
|
|
592
|
-
reasoningLineStart = true
|
|
593
|
-
thinkCol = 0
|
|
594
|
-
}
|
|
595
|
-
remaining = remaining.slice(nlIdx + 1)
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (chunk.type === 'text' && chunk.text) {
|
|
602
|
-
if (reasoningOutput) {
|
|
603
|
-
bus.emit(EventChannels.THINKING_END, {})
|
|
604
|
-
if (!out?.suppressToolOutput) {
|
|
605
|
-
writeOut(`${RS}\n`, out)
|
|
606
|
-
writeOut(thinkBottomBorder(), out)
|
|
607
|
-
}
|
|
608
|
-
reasoningOutput = false
|
|
609
|
-
reasoningLineStart = false
|
|
610
|
-
}
|
|
611
|
-
fullResponse += chunk.text
|
|
612
|
-
writeOut(chunk.text, out)
|
|
613
|
-
} else if (chunk.type === 'tool_use' && chunk.tool_call) {
|
|
614
|
-
toolCalls.push(chunk.tool_call)
|
|
615
|
-
} else if (chunk.type === 'complete') {
|
|
616
|
-
if (chunk.usage) {
|
|
617
|
-
this.turnInputTokens += chunk.usage.input_tokens
|
|
618
|
-
this.turnOutputTokens += chunk.usage.output_tokens
|
|
619
|
-
this.totalInputTokens += chunk.usage.input_tokens
|
|
620
|
-
this.totalOutputTokens += chunk.usage.output_tokens
|
|
621
|
-
// Accumulate DeepSeek cache hit/miss tokens
|
|
622
|
-
if (chunk.usage.prompt_cache_hit_tokens != null) {
|
|
623
|
-
this.turnCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
|
|
624
|
-
this.totalCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
|
|
625
|
-
}
|
|
626
|
-
if (chunk.usage.prompt_cache_miss_tokens != null) {
|
|
627
|
-
this.turnCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
|
|
628
|
-
this.totalCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
|
|
632
|
-
if (toolCalls.length === 0) {
|
|
633
|
-
const finalAssistantMsg: LLMMessage = {
|
|
634
|
-
role: 'assistant',
|
|
635
|
-
content: fullResponse || null,
|
|
636
|
-
reasoning_content: reasoningContent,
|
|
637
|
-
}
|
|
638
|
-
this.messages.push(finalAssistantMsg)
|
|
639
|
-
printTokenStats(
|
|
640
|
-
this.turnInputTokens,
|
|
641
|
-
this.turnOutputTokens,
|
|
642
|
-
this.totalInputTokens,
|
|
643
|
-
this.totalOutputTokens,
|
|
644
|
-
this.turnApiCalls,
|
|
645
|
-
this.totalApiCalls,
|
|
646
|
-
out,
|
|
647
|
-
this.turnCacheHitTokens,
|
|
648
|
-
this.turnCacheMissTokens,
|
|
649
|
-
this.totalCacheHitTokens,
|
|
650
|
-
this.totalCacheMissTokens,
|
|
651
|
-
)
|
|
652
|
-
writeOut('\n\n', out)
|
|
653
|
-
saveTokenUsage(
|
|
654
|
-
this.config.cwd,
|
|
655
|
-
this.turnInputTokens,
|
|
656
|
-
this.turnOutputTokens,
|
|
657
|
-
this.turnApiCalls,
|
|
658
|
-
)
|
|
659
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
660
|
-
this.save()
|
|
661
|
-
return
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
} catch (e) {
|
|
667
|
-
const errMsg = fmtErr(e)
|
|
668
|
-
const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)'
|
|
669
|
-
if (!out?.suppressToolOutput) {
|
|
670
|
-
writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out)
|
|
671
|
-
writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out)
|
|
672
|
-
}
|
|
673
|
-
console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent)
|
|
674
|
-
if (reasoningOutput) {
|
|
675
|
-
bus.emit(EventChannels.THINKING_END, {})
|
|
676
|
-
if (!out?.suppressToolOutput) {
|
|
677
|
-
writeOut(`${RS}\n`, out)
|
|
678
|
-
writeOut(thinkBottomBorder(), out)
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 })
|
|
682
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
683
|
-
|
|
684
|
-
// If there are pending toolCalls that weren't executed due to abort,
|
|
685
|
-
// add an assistant message so the model knows they weren't executed
|
|
686
|
-
if (toolCalls.length > 0) {
|
|
687
|
-
const interruptedMsg: LLMMessage = {
|
|
688
|
-
role: 'assistant',
|
|
689
|
-
content: null,
|
|
690
|
-
tool_calls: toolCalls,
|
|
691
|
-
reasoning_content: reasoningContent,
|
|
692
|
-
}
|
|
693
|
-
this.messages.push(interruptedMsg)
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
saveTokenUsage(
|
|
697
|
-
this.config.cwd,
|
|
698
|
-
this.turnInputTokens,
|
|
699
|
-
this.turnOutputTokens,
|
|
700
|
-
this.turnApiCalls,
|
|
701
|
-
)
|
|
702
|
-
this.save()
|
|
703
|
-
return
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
bus.emit(EventChannels.LLM_STREAM_END, {
|
|
707
|
-
iteration: iterations,
|
|
708
|
-
toolCallCount: toolCalls.length,
|
|
709
|
-
})
|
|
710
|
-
|
|
711
|
-
// Close reasoning display if still open (model ended with tool calls, no text)
|
|
712
|
-
if (reasoningOutput) {
|
|
713
|
-
bus.emit(EventChannels.THINKING_END, {})
|
|
714
|
-
if (!out?.suppressToolOutput) {
|
|
715
|
-
writeOut(`${RS}\n`, out)
|
|
716
|
-
writeOut(thinkBottomBorder(), out)
|
|
717
|
-
}
|
|
718
|
-
reasoningOutput = false
|
|
719
|
-
reasoningLineStart = false
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (toolCalls.length === 0) {
|
|
723
|
-
if (fullResponse) {
|
|
724
|
-
const finalAssistantMsg: LLMMessage = {
|
|
725
|
-
role: 'assistant',
|
|
726
|
-
content: fullResponse,
|
|
727
|
-
reasoning_content: reasoningContent,
|
|
728
|
-
}
|
|
729
|
-
this.messages.push(finalAssistantMsg)
|
|
730
|
-
printTokenStats(
|
|
731
|
-
this.turnInputTokens,
|
|
732
|
-
this.turnOutputTokens,
|
|
733
|
-
this.totalInputTokens,
|
|
734
|
-
this.totalOutputTokens,
|
|
735
|
-
this.turnApiCalls,
|
|
736
|
-
this.totalApiCalls,
|
|
737
|
-
out,
|
|
738
|
-
this.turnCacheHitTokens,
|
|
739
|
-
this.turnCacheMissTokens,
|
|
740
|
-
this.totalCacheHitTokens,
|
|
741
|
-
this.totalCacheMissTokens,
|
|
742
|
-
)
|
|
743
|
-
writeOut('\n\n', out)
|
|
744
|
-
}
|
|
745
|
-
saveTokenUsage(
|
|
746
|
-
this.config.cwd,
|
|
747
|
-
this.turnInputTokens,
|
|
748
|
-
this.turnOutputTokens,
|
|
749
|
-
this.turnApiCalls,
|
|
750
|
-
)
|
|
751
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
|
|
752
|
-
this.save()
|
|
753
|
-
return
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const assistantMsg: LLMMessage = {
|
|
757
|
-
role: 'assistant',
|
|
758
|
-
content: fullResponse || null,
|
|
759
|
-
tool_calls: toolCalls,
|
|
760
|
-
reasoning_content: reasoningContent,
|
|
761
|
-
}
|
|
762
|
-
this.messages.push(assistantMsg)
|
|
763
|
-
this.save()
|
|
764
|
-
|
|
765
|
-
// ── User confirmation for write-type tool calls ──
|
|
766
|
-
if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
|
|
767
|
-
const writeTools = ['edit', 'bash', 'write_plan', 'install_skill']
|
|
768
|
-
const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name))
|
|
769
|
-
if (needsConfirm.length > 0) {
|
|
770
|
-
const approved = await this.output.confirmTool(needsConfirm)
|
|
771
|
-
if (!approved) {
|
|
772
|
-
const rejectMsg: LLMMessage = {
|
|
773
|
-
role: 'tool',
|
|
774
|
-
content:
|
|
775
|
-
'USER_REJECTED: The user declined to execute the requested tool calls. Try a different approach.',
|
|
776
|
-
tool_call_id: needsConfirm[0].id,
|
|
777
|
-
name: 'user_feedback',
|
|
778
|
-
}
|
|
779
|
-
this.messages.push(rejectMsg)
|
|
780
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
781
|
-
continue
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
787
|
-
const tc = toolCalls[i]
|
|
788
|
-
// Check if stop was requested after each tool call
|
|
789
|
-
if (this.isStopped()) {
|
|
790
|
-
// Add assistant message with remaining tool_calls so model knows they weren't executed
|
|
791
|
-
const remainingToolCalls = toolCalls.slice(i)
|
|
792
|
-
if (remainingToolCalls.length > 0) {
|
|
793
|
-
const interruptedMsg: LLMMessage = {
|
|
794
|
-
role: 'assistant',
|
|
795
|
-
content: null,
|
|
796
|
-
tool_calls: remainingToolCalls,
|
|
797
|
-
}
|
|
798
|
-
this.messages.push(interruptedMsg)
|
|
799
|
-
}
|
|
800
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
801
|
-
this.save()
|
|
802
|
-
return
|
|
803
|
-
}
|
|
804
|
-
bus.emit(EventChannels.TOOL_CALL, { name: tc.name, input: tc.input, id: tc.id })
|
|
805
|
-
if (!out?.suppressToolOutput) {
|
|
806
|
-
printToolInvocation(tc, out)
|
|
807
|
-
}
|
|
808
|
-
const result: ToolResult = await this.registry.dispatch(tc)
|
|
809
|
-
if (result.success) {
|
|
810
|
-
bus.emit(EventChannels.TOOL_RESULT, { name: tc.name, id: tc.id, output: result.output })
|
|
811
|
-
} else {
|
|
812
|
-
bus.emit(EventChannels.TOOL_ERROR, { name: tc.name, id: tc.id, error: result.error })
|
|
813
|
-
}
|
|
814
|
-
if (!out?.suppressToolOutput) {
|
|
815
|
-
printToolResult(tc, result, out)
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const resultMsg: LLMMessage = {
|
|
819
|
-
role: 'tool',
|
|
820
|
-
content: result.success ? result.output : `ERROR: ${result.error}`,
|
|
821
|
-
tool_call_id: tc.id,
|
|
822
|
-
name: tc.name,
|
|
823
|
-
}
|
|
824
|
-
this.messages.push(resultMsg)
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
this.save()
|
|
828
|
-
|
|
829
|
-
// End the current turn before next iteration — frontend creates a new message
|
|
830
|
-
bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
|
|
831
|
-
|
|
832
|
-
// Check if compaction is needed
|
|
833
|
-
if (shouldCompact(this.messages, this.config.contextWindow)) {
|
|
834
|
-
const before = this.messages.length
|
|
835
|
-
const result = compact(this.messages, this.config.contextWindow)
|
|
836
|
-
if (result.compressed) {
|
|
837
|
-
this.messages = result.messages
|
|
838
|
-
bus.emit(EventChannels.COMPACTION_TRIGGERED, { before, after: result.newCount })
|
|
839
|
-
if (out && !out.suppressToolOutput) {
|
|
840
|
-
out.write(
|
|
841
|
-
`\n ${GY}┃${RS} ${GY}📦 Compressed context: ${before} → ${result.newCount} messages${RS}\n`,
|
|
842
|
-
)
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (iterations >= maxIterations) {
|
|
849
|
-
printTokenStats(
|
|
850
|
-
this.turnInputTokens,
|
|
851
|
-
this.turnOutputTokens,
|
|
852
|
-
this.totalInputTokens,
|
|
853
|
-
this.totalOutputTokens,
|
|
854
|
-
this.turnApiCalls,
|
|
855
|
-
this.totalApiCalls,
|
|
856
|
-
out,
|
|
857
|
-
this.turnCacheHitTokens,
|
|
858
|
-
this.turnCacheMissTokens,
|
|
859
|
-
this.totalCacheHitTokens,
|
|
860
|
-
this.totalCacheMissTokens,
|
|
861
|
-
)
|
|
862
|
-
writeOut('\nAgent reached maximum iterations. Stopping.\n', out)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls)
|
|
866
|
-
this.save()
|
|
409
|
+
return runChat(this, userPrompt)
|
|
867
410
|
}
|
|
868
411
|
}
|