cecli-dev 0.93.1__py3-none-any.whl
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.
- cecli/__init__.py +20 -0
- cecli/__main__.py +4 -0
- cecli/_version.py +34 -0
- cecli/args.py +1092 -0
- cecli/args_formatter.py +228 -0
- cecli/change_tracker.py +133 -0
- cecli/coders/__init__.py +38 -0
- cecli/coders/agent_coder.py +1872 -0
- cecli/coders/architect_coder.py +63 -0
- cecli/coders/ask_coder.py +8 -0
- cecli/coders/base_coder.py +3993 -0
- cecli/coders/chat_chunks.py +116 -0
- cecli/coders/context_coder.py +52 -0
- cecli/coders/copypaste_coder.py +269 -0
- cecli/coders/editblock_coder.py +656 -0
- cecli/coders/editblock_fenced_coder.py +9 -0
- cecli/coders/editblock_func_coder.py +140 -0
- cecli/coders/editor_diff_fenced_coder.py +8 -0
- cecli/coders/editor_editblock_coder.py +8 -0
- cecli/coders/editor_whole_coder.py +8 -0
- cecli/coders/help_coder.py +15 -0
- cecli/coders/patch_coder.py +705 -0
- cecli/coders/search_replace.py +757 -0
- cecli/coders/shell.py +37 -0
- cecli/coders/single_wholefile_func_coder.py +101 -0
- cecli/coders/udiff_coder.py +428 -0
- cecli/coders/udiff_simple.py +12 -0
- cecli/coders/wholefile_coder.py +143 -0
- cecli/coders/wholefile_func_coder.py +133 -0
- cecli/commands/__init__.py +192 -0
- cecli/commands/add.py +226 -0
- cecli/commands/agent.py +51 -0
- cecli/commands/architect.py +46 -0
- cecli/commands/ask.py +44 -0
- cecli/commands/chat_mode.py +0 -0
- cecli/commands/clear.py +37 -0
- cecli/commands/code.py +46 -0
- cecli/commands/command_prefix.py +44 -0
- cecli/commands/commit.py +52 -0
- cecli/commands/context.py +47 -0
- cecli/commands/context_blocks.py +124 -0
- cecli/commands/context_management.py +51 -0
- cecli/commands/copy.py +62 -0
- cecli/commands/copy_context.py +81 -0
- cecli/commands/core.py +287 -0
- cecli/commands/diff.py +68 -0
- cecli/commands/drop.py +217 -0
- cecli/commands/editor.py +78 -0
- cecli/commands/exit.py +55 -0
- cecli/commands/git.py +57 -0
- cecli/commands/help.py +140 -0
- cecli/commands/history_search.py +40 -0
- cecli/commands/lint.py +109 -0
- cecli/commands/list_sessions.py +56 -0
- cecli/commands/load.py +85 -0
- cecli/commands/load_session.py +48 -0
- cecli/commands/load_skill.py +68 -0
- cecli/commands/ls.py +75 -0
- cecli/commands/map.py +37 -0
- cecli/commands/map_refresh.py +35 -0
- cecli/commands/model.py +118 -0
- cecli/commands/models.py +41 -0
- cecli/commands/multiline_mode.py +38 -0
- cecli/commands/paste.py +91 -0
- cecli/commands/quit.py +32 -0
- cecli/commands/read_only.py +267 -0
- cecli/commands/read_only_stub.py +270 -0
- cecli/commands/reasoning_effort.py +70 -0
- cecli/commands/remove_skill.py +68 -0
- cecli/commands/report.py +40 -0
- cecli/commands/reset.py +88 -0
- cecli/commands/run.py +99 -0
- cecli/commands/save.py +49 -0
- cecli/commands/save_session.py +43 -0
- cecli/commands/settings.py +69 -0
- cecli/commands/test.py +58 -0
- cecli/commands/think_tokens.py +74 -0
- cecli/commands/tokens.py +207 -0
- cecli/commands/undo.py +145 -0
- cecli/commands/utils/__init__.py +0 -0
- cecli/commands/utils/base_command.py +131 -0
- cecli/commands/utils/helpers.py +142 -0
- cecli/commands/utils/registry.py +53 -0
- cecli/commands/utils/save_load_manager.py +98 -0
- cecli/commands/voice.py +78 -0
- cecli/commands/weak_model.py +123 -0
- cecli/commands/web.py +87 -0
- cecli/deprecated_args.py +185 -0
- cecli/diffs.py +129 -0
- cecli/dump.py +29 -0
- cecli/editor.py +147 -0
- cecli/exceptions.py +115 -0
- cecli/format_settings.py +26 -0
- cecli/help.py +119 -0
- cecli/help_pats.py +19 -0
- cecli/helpers/__init__.py +9 -0
- cecli/helpers/copypaste.py +123 -0
- cecli/helpers/coroutines.py +8 -0
- cecli/helpers/file_searcher.py +142 -0
- cecli/helpers/model_providers.py +552 -0
- cecli/helpers/plugin_manager.py +81 -0
- cecli/helpers/profiler.py +162 -0
- cecli/helpers/requests.py +77 -0
- cecli/helpers/similarity.py +98 -0
- cecli/helpers/skills.py +577 -0
- cecli/history.py +186 -0
- cecli/io.py +1782 -0
- cecli/linter.py +304 -0
- cecli/llm.py +101 -0
- cecli/main.py +1280 -0
- cecli/mcp/__init__.py +154 -0
- cecli/mcp/oauth.py +250 -0
- cecli/mcp/server.py +278 -0
- cecli/mdstream.py +243 -0
- cecli/models.py +1255 -0
- cecli/onboarding.py +301 -0
- cecli/prompts/__init__.py +0 -0
- cecli/prompts/agent.yml +71 -0
- cecli/prompts/architect.yml +35 -0
- cecli/prompts/ask.yml +31 -0
- cecli/prompts/base.yml +99 -0
- cecli/prompts/context.yml +60 -0
- cecli/prompts/copypaste.yml +5 -0
- cecli/prompts/editblock.yml +143 -0
- cecli/prompts/editblock_fenced.yml +106 -0
- cecli/prompts/editblock_func.yml +25 -0
- cecli/prompts/editor_diff_fenced.yml +115 -0
- cecli/prompts/editor_editblock.yml +121 -0
- cecli/prompts/editor_whole.yml +46 -0
- cecli/prompts/help.yml +37 -0
- cecli/prompts/patch.yml +110 -0
- cecli/prompts/single_wholefile_func.yml +24 -0
- cecli/prompts/udiff.yml +106 -0
- cecli/prompts/udiff_simple.yml +13 -0
- cecli/prompts/utils/__init__.py +0 -0
- cecli/prompts/utils/prompt_registry.py +167 -0
- cecli/prompts/utils/system.py +56 -0
- cecli/prompts/wholefile.yml +50 -0
- cecli/prompts/wholefile_func.yml +24 -0
- cecli/queries/tree-sitter-language-pack/README.md +7 -0
- cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
- cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
- cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
- cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
- cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
- cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
- cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
- cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
- cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- cecli/queries/tree-sitter-languages/README.md +24 -0
- cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
- cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
- cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
- cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
- cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
- cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
- cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
- cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
- cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
- cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
- cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
- cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
- cecli/reasoning_tags.py +82 -0
- cecli/repo.py +626 -0
- cecli/repomap.py +1368 -0
- cecli/report.py +260 -0
- cecli/resources/__init__.py +3 -0
- cecli/resources/model-metadata.json +25751 -0
- cecli/resources/model-settings.yml +2394 -0
- cecli/resources/providers.json +67 -0
- cecli/run_cmd.py +143 -0
- cecli/scrape.py +295 -0
- cecli/sendchat.py +250 -0
- cecli/sessions.py +281 -0
- cecli/special.py +203 -0
- cecli/tools/__init__.py +72 -0
- cecli/tools/command.py +103 -0
- cecli/tools/command_interactive.py +113 -0
- cecli/tools/context_manager.py +175 -0
- cecli/tools/delete_block.py +154 -0
- cecli/tools/delete_line.py +120 -0
- cecli/tools/delete_lines.py +144 -0
- cecli/tools/extract_lines.py +281 -0
- cecli/tools/finished.py +35 -0
- cecli/tools/git_branch.py +132 -0
- cecli/tools/git_diff.py +49 -0
- cecli/tools/git_log.py +43 -0
- cecli/tools/git_remote.py +39 -0
- cecli/tools/git_show.py +37 -0
- cecli/tools/git_status.py +32 -0
- cecli/tools/grep.py +242 -0
- cecli/tools/indent_lines.py +195 -0
- cecli/tools/insert_block.py +263 -0
- cecli/tools/list_changes.py +71 -0
- cecli/tools/load_skill.py +51 -0
- cecli/tools/ls.py +77 -0
- cecli/tools/remove_skill.py +51 -0
- cecli/tools/replace_all.py +113 -0
- cecli/tools/replace_line.py +135 -0
- cecli/tools/replace_lines.py +180 -0
- cecli/tools/replace_text.py +186 -0
- cecli/tools/show_numbered_context.py +137 -0
- cecli/tools/thinking.py +52 -0
- cecli/tools/undo_change.py +82 -0
- cecli/tools/update_todo_list.py +148 -0
- cecli/tools/utils/base_tool.py +64 -0
- cecli/tools/utils/helpers.py +359 -0
- cecli/tools/utils/output.py +119 -0
- cecli/tools/utils/registry.py +145 -0
- cecli/tools/view_files_matching.py +138 -0
- cecli/tools/view_files_with_symbol.py +117 -0
- cecli/tui/__init__.py +83 -0
- cecli/tui/app.py +971 -0
- cecli/tui/io.py +566 -0
- cecli/tui/styles.tcss +117 -0
- cecli/tui/widgets/__init__.py +19 -0
- cecli/tui/widgets/completion_bar.py +331 -0
- cecli/tui/widgets/file_list.py +76 -0
- cecli/tui/widgets/footer.py +165 -0
- cecli/tui/widgets/input_area.py +320 -0
- cecli/tui/widgets/key_hints.py +16 -0
- cecli/tui/widgets/output.py +354 -0
- cecli/tui/widgets/status_bar.py +279 -0
- cecli/tui/worker.py +160 -0
- cecli/urls.py +16 -0
- cecli/utils.py +499 -0
- cecli/versioncheck.py +90 -0
- cecli/voice.py +90 -0
- cecli/waiting.py +38 -0
- cecli/watch.py +316 -0
- cecli/watch_prompts.py +12 -0
- cecli/website/Gemfile +8 -0
- cecli/website/_includes/blame.md +162 -0
- cecli/website/_includes/get-started.md +22 -0
- cecli/website/_includes/help-tip.md +5 -0
- cecli/website/_includes/help.md +24 -0
- cecli/website/_includes/install.md +5 -0
- cecli/website/_includes/keys.md +4 -0
- cecli/website/_includes/model-warnings.md +67 -0
- cecli/website/_includes/multi-line.md +22 -0
- cecli/website/_includes/python-m-aider.md +5 -0
- cecli/website/_includes/recording.css +228 -0
- cecli/website/_includes/recording.md +34 -0
- cecli/website/_includes/replit-pipx.md +9 -0
- cecli/website/_includes/works-best.md +1 -0
- cecli/website/_sass/custom/custom.scss +103 -0
- cecli/website/docs/config/adv-model-settings.md +2498 -0
- cecli/website/docs/config/agent-mode.md +320 -0
- cecli/website/docs/config/aider_conf.md +548 -0
- cecli/website/docs/config/api-keys.md +90 -0
- cecli/website/docs/config/custom-commands.md +187 -0
- cecli/website/docs/config/dotenv.md +493 -0
- cecli/website/docs/config/editor.md +127 -0
- cecli/website/docs/config/mcp.md +210 -0
- cecli/website/docs/config/model-aliases.md +173 -0
- cecli/website/docs/config/options.md +890 -0
- cecli/website/docs/config/reasoning.md +210 -0
- cecli/website/docs/config/skills.md +172 -0
- cecli/website/docs/config/tui.md +126 -0
- cecli/website/docs/config.md +44 -0
- cecli/website/docs/faq.md +379 -0
- cecli/website/docs/git.md +76 -0
- cecli/website/docs/index.md +47 -0
- cecli/website/docs/install/codespaces.md +39 -0
- cecli/website/docs/install/docker.md +48 -0
- cecli/website/docs/install/optional.md +100 -0
- cecli/website/docs/install/replit.md +8 -0
- cecli/website/docs/install.md +115 -0
- cecli/website/docs/languages.md +264 -0
- cecli/website/docs/legal/contributor-agreement.md +111 -0
- cecli/website/docs/legal/privacy.md +104 -0
- cecli/website/docs/llms/anthropic.md +77 -0
- cecli/website/docs/llms/azure.md +48 -0
- cecli/website/docs/llms/bedrock.md +132 -0
- cecli/website/docs/llms/cohere.md +34 -0
- cecli/website/docs/llms/deepseek.md +32 -0
- cecli/website/docs/llms/gemini.md +49 -0
- cecli/website/docs/llms/github.md +111 -0
- cecli/website/docs/llms/groq.md +36 -0
- cecli/website/docs/llms/lm-studio.md +39 -0
- cecli/website/docs/llms/ollama.md +75 -0
- cecli/website/docs/llms/openai-compat.md +39 -0
- cecli/website/docs/llms/openai.md +58 -0
- cecli/website/docs/llms/openrouter.md +78 -0
- cecli/website/docs/llms/other.md +117 -0
- cecli/website/docs/llms/vertex.md +50 -0
- cecli/website/docs/llms/warnings.md +10 -0
- cecli/website/docs/llms/xai.md +53 -0
- cecli/website/docs/llms.md +54 -0
- cecli/website/docs/more/analytics.md +127 -0
- cecli/website/docs/more/edit-formats.md +116 -0
- cecli/website/docs/more/infinite-output.md +192 -0
- cecli/website/docs/more-info.md +8 -0
- cecli/website/docs/recordings/auto-accept-architect.md +31 -0
- cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- cecli/website/docs/recordings/index.md +21 -0
- cecli/website/docs/recordings/model-accepts-settings.md +69 -0
- cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- cecli/website/docs/repomap.md +112 -0
- cecli/website/docs/scripting.md +100 -0
- cecli/website/docs/sessions.md +213 -0
- cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
- cecli/website/docs/troubleshooting/edit-errors.md +76 -0
- cecli/website/docs/troubleshooting/imports.md +62 -0
- cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
- cecli/website/docs/troubleshooting/support.md +79 -0
- cecli/website/docs/troubleshooting/token-limits.md +96 -0
- cecli/website/docs/troubleshooting/warnings.md +12 -0
- cecli/website/docs/troubleshooting.md +11 -0
- cecli/website/docs/usage/browser.md +57 -0
- cecli/website/docs/usage/caching.md +49 -0
- cecli/website/docs/usage/commands.md +133 -0
- cecli/website/docs/usage/conventions.md +119 -0
- cecli/website/docs/usage/copypaste.md +136 -0
- cecli/website/docs/usage/images-urls.md +48 -0
- cecli/website/docs/usage/lint-test.md +118 -0
- cecli/website/docs/usage/modes.md +211 -0
- cecli/website/docs/usage/not-code.md +179 -0
- cecli/website/docs/usage/notifications.md +87 -0
- cecli/website/docs/usage/tips.md +79 -0
- cecli/website/docs/usage/tutorials.md +30 -0
- cecli/website/docs/usage/voice.md +121 -0
- cecli/website/docs/usage/watch.md +294 -0
- cecli/website/docs/usage.md +102 -0
- cecli/website/share/index.md +101 -0
- cecli_dev-0.93.1.dist-info/METADATA +549 -0
- cecli_dev-0.93.1.dist-info/RECORD +366 -0
- cecli_dev-0.93.1.dist-info/WHEEL +5 -0
- cecli_dev-0.93.1.dist-info/entry_points.txt +4 -0
- cecli_dev-0.93.1.dist-info/licenses/LICENSE.txt +202 -0
- cecli_dev-0.93.1.dist-info/top_level.txt +1 -0
cecli/sendchat.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from cecli.dump import dump # noqa: F401
|
|
2
|
+
from cecli.utils import format_messages
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sanity_check_messages(messages):
|
|
6
|
+
"""Check if messages alternate between user and assistant roles.
|
|
7
|
+
System messages can be interspersed anywhere.
|
|
8
|
+
Also verifies the last non-system message is from the user.
|
|
9
|
+
Validates tool message sequences.
|
|
10
|
+
Returns True if valid, False otherwise."""
|
|
11
|
+
last_role = None
|
|
12
|
+
last_non_system_role = None
|
|
13
|
+
i = 0
|
|
14
|
+
n = len(messages)
|
|
15
|
+
|
|
16
|
+
while i < n:
|
|
17
|
+
msg = messages[i]
|
|
18
|
+
role = msg.get("role")
|
|
19
|
+
|
|
20
|
+
# Handle tool sequences atomically
|
|
21
|
+
if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
|
|
22
|
+
# Validate tool sequence
|
|
23
|
+
expected_ids = {call["id"] for call in msg["tool_calls"]}
|
|
24
|
+
i += 1
|
|
25
|
+
|
|
26
|
+
# Check for tool responses
|
|
27
|
+
while i < n and expected_ids:
|
|
28
|
+
next_msg = messages[i]
|
|
29
|
+
if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in expected_ids:
|
|
30
|
+
expected_ids.discard(next_msg.get("tool_call_id"))
|
|
31
|
+
i += 1
|
|
32
|
+
else:
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
# If we still have expected IDs, the tool sequence is incomplete
|
|
36
|
+
if expected_ids:
|
|
37
|
+
turns = format_messages(messages)
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"Incomplete tool sequence - missing responses for tool calls:\n\n" + turns
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Continue to next message after tool sequence
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
elif role == "tool":
|
|
46
|
+
# Orphaned tool message without preceding assistant tool_calls
|
|
47
|
+
turns = format_messages(messages)
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Orphaned tool message without preceding assistant tool_calls:\n\n" + turns
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Handle normal role alternation
|
|
53
|
+
if role == "system":
|
|
54
|
+
i += 1
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if last_role and role == last_role:
|
|
58
|
+
turns = format_messages(messages)
|
|
59
|
+
raise ValueError("Messages don't properly alternate user/assistant:\n\n" + turns)
|
|
60
|
+
|
|
61
|
+
last_role = role
|
|
62
|
+
last_non_system_role = role
|
|
63
|
+
i += 1
|
|
64
|
+
|
|
65
|
+
# Ensure last non-system message is from user
|
|
66
|
+
return last_non_system_role == "user"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def clean_orphaned_tool_messages(messages):
|
|
70
|
+
"""Remove orphaned tool messages and incomplete tool sequences.
|
|
71
|
+
|
|
72
|
+
This function removes:
|
|
73
|
+
- Tool messages without a preceding assistant message containing tool_calls
|
|
74
|
+
- Assistant messages with tool_calls that don't have complete tool responses
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
messages: List of message dictionaries
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Cleaned list of messages with orphaned tool sequences removed
|
|
81
|
+
"""
|
|
82
|
+
if not messages:
|
|
83
|
+
return messages
|
|
84
|
+
|
|
85
|
+
cleaned = []
|
|
86
|
+
i = 0
|
|
87
|
+
n = len(messages)
|
|
88
|
+
|
|
89
|
+
while i < n:
|
|
90
|
+
msg = messages[i]
|
|
91
|
+
role = msg.get("role")
|
|
92
|
+
|
|
93
|
+
# If it's an assistant message with tool_calls, check if we have complete responses
|
|
94
|
+
if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
|
|
95
|
+
# Start of potential tool sequence
|
|
96
|
+
tool_sequence = [msg]
|
|
97
|
+
expected_ids = {call["id"] for call in msg["tool_calls"]}
|
|
98
|
+
j = i + 1
|
|
99
|
+
|
|
100
|
+
# Collect tool responses
|
|
101
|
+
while j < n and expected_ids:
|
|
102
|
+
next_msg = messages[j]
|
|
103
|
+
if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in expected_ids:
|
|
104
|
+
tool_sequence.append(next_msg)
|
|
105
|
+
expected_ids.discard(next_msg.get("tool_call_id"))
|
|
106
|
+
j += 1
|
|
107
|
+
else:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
# If we have all tool responses, keep the sequence
|
|
111
|
+
if not expected_ids:
|
|
112
|
+
cleaned.extend(tool_sequence)
|
|
113
|
+
i = j
|
|
114
|
+
else:
|
|
115
|
+
# Incomplete sequence - skip the entire tool sequence
|
|
116
|
+
i = j
|
|
117
|
+
# Don't add anything to cleaned
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
elif role == "tool":
|
|
121
|
+
# Orphaned tool message without preceding assistant tool_calls - skip it
|
|
122
|
+
i += 1
|
|
123
|
+
continue
|
|
124
|
+
else:
|
|
125
|
+
# Regular message - add it
|
|
126
|
+
cleaned.append(msg)
|
|
127
|
+
i += 1
|
|
128
|
+
|
|
129
|
+
return cleaned
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def ensure_alternating_roles(messages):
|
|
133
|
+
"""Ensure messages alternate between 'assistant' and 'user' roles.
|
|
134
|
+
|
|
135
|
+
Inserts empty messages of the opposite role when consecutive messages
|
|
136
|
+
of the same 'user' or 'assistant' role are found. Messages with other
|
|
137
|
+
roles (e.g. 'system', 'tool') are ignored by the alternation logic.
|
|
138
|
+
|
|
139
|
+
Also handles tool call sequences properly - when an assistant message
|
|
140
|
+
contains tool_calls, processes the complete tool sequence atomically.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
messages: List of message dictionaries with 'role' and 'content' keys.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of messages with alternating roles.
|
|
147
|
+
"""
|
|
148
|
+
if not messages:
|
|
149
|
+
return messages
|
|
150
|
+
|
|
151
|
+
# First clean orphaned tool messages
|
|
152
|
+
messages = clean_orphaned_tool_messages(messages)
|
|
153
|
+
|
|
154
|
+
result = []
|
|
155
|
+
i = 0
|
|
156
|
+
n = len(messages)
|
|
157
|
+
prev_role = None
|
|
158
|
+
|
|
159
|
+
while i < n:
|
|
160
|
+
msg = messages[i]
|
|
161
|
+
role = msg.get("role")
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
role == "assistant"
|
|
165
|
+
and not msg.get("content", None)
|
|
166
|
+
and not msg.get("tool_calls", None)
|
|
167
|
+
and not msg.get("function_call", None)
|
|
168
|
+
):
|
|
169
|
+
msg["content"] = "(empty response)"
|
|
170
|
+
|
|
171
|
+
# Handle tool call sequences atomically
|
|
172
|
+
if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
|
|
173
|
+
# Start of tool sequence - collect all related messages
|
|
174
|
+
tool_sequence = [msg]
|
|
175
|
+
expected_ids = {call["id"] for call in msg["tool_calls"]}
|
|
176
|
+
i += 1
|
|
177
|
+
|
|
178
|
+
# Collect tool responses
|
|
179
|
+
while i < n and expected_ids:
|
|
180
|
+
next_msg = messages[i]
|
|
181
|
+
if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in expected_ids:
|
|
182
|
+
tool_sequence.append(next_msg)
|
|
183
|
+
expected_ids.discard(next_msg.get("tool_call_id"))
|
|
184
|
+
i += 1
|
|
185
|
+
else:
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
# Add missing tool responses as empty
|
|
189
|
+
for tool_id in expected_ids:
|
|
190
|
+
tool_sequence.append(
|
|
191
|
+
{"role": "tool", "tool_call_id": tool_id, "content": "(empty response)"}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Add the complete tool sequence to result
|
|
195
|
+
for tool_msg in tool_sequence:
|
|
196
|
+
result.append(tool_msg)
|
|
197
|
+
|
|
198
|
+
# Update prev_role to assistant after processing tool sequence
|
|
199
|
+
prev_role = "assistant"
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Handle normal message alternation
|
|
203
|
+
if role in ("user", "assistant"):
|
|
204
|
+
if role == prev_role:
|
|
205
|
+
# Insert empty message of opposite role
|
|
206
|
+
opposite_role = "user" if role == "assistant" else "assistant"
|
|
207
|
+
result.append(
|
|
208
|
+
{
|
|
209
|
+
"role": opposite_role,
|
|
210
|
+
"content": (
|
|
211
|
+
"(empty response)"
|
|
212
|
+
if opposite_role == "assistant"
|
|
213
|
+
else "(empty request)"
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
prev_role = opposite_role
|
|
218
|
+
|
|
219
|
+
result.append(msg)
|
|
220
|
+
prev_role = role
|
|
221
|
+
else:
|
|
222
|
+
# For non-user/assistant roles, just add them directly
|
|
223
|
+
result.append(msg)
|
|
224
|
+
|
|
225
|
+
i += 1
|
|
226
|
+
|
|
227
|
+
# Consolidate consecutive empty messages in a single pass
|
|
228
|
+
consolidated = []
|
|
229
|
+
for msg in result:
|
|
230
|
+
if not consolidated:
|
|
231
|
+
consolidated.append(msg)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
last_msg = consolidated[-1]
|
|
235
|
+
current_role = msg.get("role")
|
|
236
|
+
last_role = last_msg.get("role")
|
|
237
|
+
|
|
238
|
+
# Skip consecutive empty messages with the same role
|
|
239
|
+
if (
|
|
240
|
+
current_role in ("user", "assistant")
|
|
241
|
+
and last_role in ("user", "assistant")
|
|
242
|
+
and current_role == last_role
|
|
243
|
+
and msg.get("content") in ["", "(empty response)", "(empty request)"]
|
|
244
|
+
and last_msg.get("content") in ["", "(empty response)", "(empty request)"]
|
|
245
|
+
):
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
consolidated.append(msg)
|
|
249
|
+
|
|
250
|
+
return consolidated
|
cecli/sessions.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Session management utilities for cecli."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from cecli import models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionManager:
|
|
12
|
+
"""Manages chat session saving, listing, and loading."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, coder, io):
|
|
15
|
+
self.coder = coder
|
|
16
|
+
self.io = io
|
|
17
|
+
|
|
18
|
+
def _get_session_directory(self) -> Path:
|
|
19
|
+
"""Get the session directory, creating it if necessary."""
|
|
20
|
+
session_dir = Path(self.coder.abs_root_path(".cecli/sessions"))
|
|
21
|
+
os.makedirs(session_dir, exist_ok=True)
|
|
22
|
+
return session_dir
|
|
23
|
+
|
|
24
|
+
def save_session(self, session_name: str, output=True) -> bool:
|
|
25
|
+
"""Save the current chat session to a named file."""
|
|
26
|
+
if not session_name:
|
|
27
|
+
if output:
|
|
28
|
+
self.io.tool_error("Please provide a session name.")
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
session_name = session_name.replace(".json", "")
|
|
32
|
+
session_dir = self._get_session_directory()
|
|
33
|
+
session_file = session_dir / f"{session_name}.json"
|
|
34
|
+
|
|
35
|
+
if session_file.exists():
|
|
36
|
+
if output:
|
|
37
|
+
self.io.tool_warning(f"Session '{session_name}' already exists. Overwriting.")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
session_data = self._build_session_data(session_name)
|
|
41
|
+
with open(session_file, "w", encoding="utf-8") as f:
|
|
42
|
+
json.dump(session_data, f, indent=2)
|
|
43
|
+
|
|
44
|
+
if output:
|
|
45
|
+
self.io.tool_output(f"Session saved: {session_file}")
|
|
46
|
+
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.io.tool_error(f"Error saving session: {e}")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def list_sessions(self) -> List[Dict]:
|
|
54
|
+
"""List all saved sessions with metadata."""
|
|
55
|
+
session_dir = self._get_session_directory()
|
|
56
|
+
session_files = list(session_dir.glob("*.json"))
|
|
57
|
+
|
|
58
|
+
if not session_files:
|
|
59
|
+
self.io.tool_output("No saved sessions found.")
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
sessions = []
|
|
63
|
+
for session_file in sorted(session_files, key=lambda x: x.stat().st_mtime, reverse=True):
|
|
64
|
+
try:
|
|
65
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
66
|
+
session_data = json.load(f)
|
|
67
|
+
|
|
68
|
+
session_info = {
|
|
69
|
+
"name": session_file.stem,
|
|
70
|
+
"file": session_file,
|
|
71
|
+
"model": session_data.get("model", "unknown"),
|
|
72
|
+
"edit_format": session_data.get("edit_format", "unknown"),
|
|
73
|
+
"num_messages": len(
|
|
74
|
+
session_data.get("chat_history", {}).get("done_messages", [])
|
|
75
|
+
) + len(session_data.get("chat_history", {}).get("cur_messages", [])),
|
|
76
|
+
"num_files": (
|
|
77
|
+
len(session_data.get("files", {}).get("editable", []))
|
|
78
|
+
+ len(session_data.get("files", {}).get("read_only", []))
|
|
79
|
+
+ len(session_data.get("files", {}).get("read_only_stubs", []))
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
sessions.append(session_info)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.io.tool_output(f" {session_file.stem} [error reading: {e}]")
|
|
86
|
+
|
|
87
|
+
return sessions
|
|
88
|
+
|
|
89
|
+
def load_session(self, session_identifier: str) -> bool:
|
|
90
|
+
"""Load a saved session by name or file path."""
|
|
91
|
+
if not session_identifier:
|
|
92
|
+
self.io.tool_error("Please provide a session name or file path.")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# Try to find the session file
|
|
96
|
+
session_file = self._find_session_file(session_identifier)
|
|
97
|
+
if not session_file:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
102
|
+
session_data = json.load(f)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.io.tool_error(f"Error loading session: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
# Verify session format
|
|
108
|
+
if not isinstance(session_data, dict) or "version" not in session_data:
|
|
109
|
+
self.io.tool_error("Invalid session format.")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
# Apply session data
|
|
113
|
+
return self._apply_session_data(session_data, session_file)
|
|
114
|
+
|
|
115
|
+
def _build_session_data(self, session_name) -> Dict:
|
|
116
|
+
"""Build session data dictionary from current coder state."""
|
|
117
|
+
# Get relative paths for all files
|
|
118
|
+
editable_files = [
|
|
119
|
+
self.coder.get_rel_fname(abs_fname) for abs_fname in self.coder.abs_fnames
|
|
120
|
+
]
|
|
121
|
+
read_only_files = [
|
|
122
|
+
self.coder.get_rel_fname(abs_fname) for abs_fname in self.coder.abs_read_only_fnames
|
|
123
|
+
]
|
|
124
|
+
read_only_stubs_files = [
|
|
125
|
+
self.coder.get_rel_fname(abs_fname)
|
|
126
|
+
for abs_fname in self.coder.abs_read_only_stubs_fnames
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Capture todo list content so it can be restored with the session
|
|
130
|
+
todo_content = None
|
|
131
|
+
try:
|
|
132
|
+
todo_path = self.coder.abs_root_path(".cecli.todo.txt")
|
|
133
|
+
if os.path.isfile(todo_path):
|
|
134
|
+
todo_content = self.io.read_text(todo_path)
|
|
135
|
+
if todo_content is None:
|
|
136
|
+
todo_content = ""
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.io.tool_warning(f"Could not read todo list file: {e}")
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"version": 1,
|
|
142
|
+
"session_name": session_name,
|
|
143
|
+
"model": self.coder.main_model.name,
|
|
144
|
+
"weak_model": self.coder.main_model.weak_model.name,
|
|
145
|
+
"editor_model": self.coder.main_model.editor_model.name,
|
|
146
|
+
"editor_edit_format": self.coder.main_model.editor_edit_format,
|
|
147
|
+
"edit_format": self.coder.edit_format,
|
|
148
|
+
"chat_history": {
|
|
149
|
+
"done_messages": self.coder.done_messages,
|
|
150
|
+
"cur_messages": self.coder.cur_messages,
|
|
151
|
+
},
|
|
152
|
+
"files": {
|
|
153
|
+
"editable": editable_files,
|
|
154
|
+
"read_only": read_only_files,
|
|
155
|
+
"read_only_stubs": read_only_stubs_files,
|
|
156
|
+
},
|
|
157
|
+
"settings": {
|
|
158
|
+
"auto_commits": self.coder.auto_commits,
|
|
159
|
+
"auto_lint": self.coder.auto_lint,
|
|
160
|
+
"auto_test": self.coder.auto_test,
|
|
161
|
+
},
|
|
162
|
+
"todo_list": todo_content,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def _find_session_file(self, session_identifier: str) -> Optional[Path]:
|
|
166
|
+
"""Find session file by name or path."""
|
|
167
|
+
# Check if it's a direct file path
|
|
168
|
+
session_file = Path(session_identifier)
|
|
169
|
+
if session_file.exists():
|
|
170
|
+
return session_file
|
|
171
|
+
|
|
172
|
+
# Check if it's a session name in the sessions directory
|
|
173
|
+
session_dir = self._get_session_directory()
|
|
174
|
+
|
|
175
|
+
# Try with .json extension
|
|
176
|
+
if not session_identifier.endswith(".json"):
|
|
177
|
+
session_file = session_dir / f"{session_identifier}.json"
|
|
178
|
+
if session_file.exists():
|
|
179
|
+
return session_file
|
|
180
|
+
|
|
181
|
+
session_file = session_dir / f"{session_identifier}"
|
|
182
|
+
if session_file.exists():
|
|
183
|
+
return session_file
|
|
184
|
+
|
|
185
|
+
self.io.tool_error(f"Session not found: {session_identifier}")
|
|
186
|
+
self.io.tool_output("Use /list-sessions to see available sessions.")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool:
|
|
190
|
+
"""Apply session data to current coder state."""
|
|
191
|
+
try:
|
|
192
|
+
# Clear current state
|
|
193
|
+
self.coder.abs_fnames = set()
|
|
194
|
+
self.coder.abs_read_only_fnames = set()
|
|
195
|
+
self.coder.abs_read_only_stubs_fnames = set()
|
|
196
|
+
self.coder.done_messages = []
|
|
197
|
+
self.coder.cur_messages = []
|
|
198
|
+
|
|
199
|
+
# Load chat history
|
|
200
|
+
chat_history = session_data.get("chat_history", {})
|
|
201
|
+
self.coder.done_messages = chat_history.get("done_messages", [])
|
|
202
|
+
self.coder.cur_messages = chat_history.get("cur_messages", [])
|
|
203
|
+
|
|
204
|
+
# Load files
|
|
205
|
+
files = session_data.get("files", {})
|
|
206
|
+
for rel_fname in files.get("editable", []):
|
|
207
|
+
abs_fname = self.coder.abs_root_path(rel_fname)
|
|
208
|
+
if os.path.exists(abs_fname):
|
|
209
|
+
self.coder.abs_fnames.add(abs_fname)
|
|
210
|
+
else:
|
|
211
|
+
self.io.tool_warning(f"File not found, skipping: {rel_fname}")
|
|
212
|
+
|
|
213
|
+
for rel_fname in files.get("read_only", []):
|
|
214
|
+
abs_fname = self.coder.abs_root_path(rel_fname)
|
|
215
|
+
if os.path.exists(abs_fname):
|
|
216
|
+
self.coder.abs_read_only_fnames.add(abs_fname)
|
|
217
|
+
else:
|
|
218
|
+
self.io.tool_warning(f"File not found, skipping: {rel_fname}")
|
|
219
|
+
|
|
220
|
+
for rel_fname in files.get("read_only_stubs", []):
|
|
221
|
+
abs_fname = self.coder.abs_root_path(rel_fname)
|
|
222
|
+
if os.path.exists(abs_fname):
|
|
223
|
+
self.coder.abs_read_only_stubs_fnames.add(abs_fname)
|
|
224
|
+
else:
|
|
225
|
+
self.io.tool_warning(f"File not found, skipping: {rel_fname}")
|
|
226
|
+
|
|
227
|
+
if session_data.get("model"):
|
|
228
|
+
self.coder.main_model = models.Model(
|
|
229
|
+
session_data.get("model", self.coder.args.model),
|
|
230
|
+
weak_model=session_data.get("weak_model", self.coder.args.weak_model),
|
|
231
|
+
editor_model=session_data.get("editor_model", self.coder.args.editor_model),
|
|
232
|
+
editor_edit_format=session_data.get(
|
|
233
|
+
"editor_edit_format", self.coder.args.editor_edit_format
|
|
234
|
+
),
|
|
235
|
+
verbose=self.coder.args.verbose,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Load settings
|
|
239
|
+
settings = session_data.get("settings", {})
|
|
240
|
+
if "auto_commits" in settings:
|
|
241
|
+
self.coder.auto_commits = settings["auto_commits"]
|
|
242
|
+
if "auto_lint" in settings:
|
|
243
|
+
self.coder.auto_lint = settings["auto_lint"]
|
|
244
|
+
if "auto_test" in settings:
|
|
245
|
+
self.coder.auto_test = settings["auto_test"]
|
|
246
|
+
|
|
247
|
+
# Restore todo list content if present in the session
|
|
248
|
+
if "todo_list" in session_data:
|
|
249
|
+
todo_path = self.coder.abs_root_path(".cecli.todo.txt")
|
|
250
|
+
todo_content = session_data.get("todo_list")
|
|
251
|
+
try:
|
|
252
|
+
if todo_content is None:
|
|
253
|
+
if os.path.exists(todo_path):
|
|
254
|
+
os.remove(todo_path)
|
|
255
|
+
else:
|
|
256
|
+
self.io.write_text(todo_path, todo_content)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
self.io.tool_warning(f"Could not restore todo list: {e}")
|
|
259
|
+
|
|
260
|
+
self.io.tool_output(
|
|
261
|
+
f"Session loaded: {session_data.get('session_name', session_file.stem)}"
|
|
262
|
+
)
|
|
263
|
+
self.io.tool_output(
|
|
264
|
+
f"Model: {session_data.get('model', 'unknown')}, Edit format:"
|
|
265
|
+
f" {session_data.get('edit_format', 'unknown')}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Show summary
|
|
269
|
+
num_messages = len(self.coder.done_messages) + len(self.coder.cur_messages)
|
|
270
|
+
num_files = (
|
|
271
|
+
len(self.coder.abs_fnames)
|
|
272
|
+
+ len(self.coder.abs_read_only_fnames)
|
|
273
|
+
+ len(self.coder.abs_read_only_stubs_fnames)
|
|
274
|
+
)
|
|
275
|
+
self.io.tool_output(f"Loaded {num_messages} messages and {num_files} files")
|
|
276
|
+
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
self.io.tool_error(f"Error applying session data: {e}")
|
|
281
|
+
return False
|