raggiecode 0.2.1__tar.gz → 0.2.2__tar.gz

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.
Files changed (130) hide show
  1. {raggiecode-0.2.1 → raggiecode-0.2.2}/PKG-INFO +11 -9
  2. {raggiecode-0.2.1 → raggiecode-0.2.2}/README.md +10 -8
  3. {raggiecode-0.2.1 → raggiecode-0.2.2}/pyproject.toml +1 -1
  4. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/agent.py +56 -108
  5. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/chat_history_db.py +84 -9
  6. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/RAG/find.py +6 -1
  7. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/todo_list.py +1 -3
  8. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/code_index_sdk.py +23 -0
  9. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/interactive.py +49 -3
  10. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggie.py +27 -0
  11. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggiecode.egg-info/PKG-INFO +11 -9
  12. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggiecode.egg-info/SOURCES.txt +1 -0
  13. raggiecode-0.2.2/tests/test_dangling_tool_resume.py +315 -0
  14. raggiecode-0.2.2/tests/test_file_references.py +58 -0
  15. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_resumability_bugs.py +28 -0
  16. raggiecode-0.2.1/tests/test_dangling_tool_resume.py +0 -98
  17. {raggiecode-0.2.1 → raggiecode-0.2.2}/setup.cfg +0 -0
  18. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/__init__.py +0 -0
  19. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/command.py +0 -0
  20. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/config.py +0 -0
  21. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/effort_levels.py +0 -0
  22. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/git_manager.py +0 -0
  23. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Agent/tools.py +0 -0
  24. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/__init__.py +0 -0
  25. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/effort.py +0 -0
  26. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/global_todo.py +0 -0
  27. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/help.py +0 -0
  28. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/reasoning.py +0 -0
  29. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/redo.py +0 -0
  30. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/reindex.py +0 -0
  31. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/shell.py +0 -0
  32. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/stream.py +0 -0
  33. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/undo.py +0 -0
  34. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/unlimited_effort.py +0 -0
  35. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Commands/window_size.py +0 -0
  36. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/RAG/__init__.py +0 -0
  37. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/RAG/document.py +0 -0
  38. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/RAG/graph.py +0 -0
  39. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/GetFileCodeStructure.py +0 -0
  40. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/GetSymbolSourceCode.py +0 -0
  41. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/__init__.py +0 -0
  42. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/ask_user.py +0 -0
  43. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/dispatch_subagent.py +0 -0
  44. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/document.py +0 -0
  45. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/edit_symbol.py +0 -0
  46. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/fuzzy_search.py +0 -0
  47. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/list_dir.py +0 -0
  48. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/read.py +0 -0
  49. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/read_image.py +0 -0
  50. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/remove.py +0 -0
  51. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/replace.py +0 -0
  52. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/search.py +0 -0
  53. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/shell.py +0 -0
  54. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/shell_kill.py +0 -0
  55. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/temp_background_service.py +0 -0
  56. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/utils.py +0 -0
  57. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/view_changes.py +0 -0
  58. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/walk_call_tree.py +0 -0
  59. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/web_fetch.py +0 -0
  60. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/web_search.py +0 -0
  61. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/Tools/write.py +0 -0
  62. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/cli.py +0 -0
  63. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/config/__init__.py +0 -0
  64. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/config/coder_system_prompt.md +0 -0
  65. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/config/roles.json +0 -0
  66. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/config/tools.json +0 -0
  67. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/__init__.py +0 -0
  68. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/cli.py +0 -0
  69. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/code_indexer.py +0 -0
  70. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/db_schema.py +0 -0
  71. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/export_to_json.py +0 -0
  72. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/extractors.py +0 -0
  73. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/file_utils.py +0 -0
  74. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/__init__.py +0 -0
  75. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/css_extractor.py +0 -0
  76. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/css_parser.py +0 -0
  77. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/css_selector_utils.py +0 -0
  78. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/edit_safety.py +0 -0
  79. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/graph.py +0 -0
  80. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/html_extractor.py +0 -0
  81. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/html_parser.py +0 -0
  82. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/jsx_extractor.py +0 -0
  83. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/location_lookup.py +0 -0
  84. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/resolver.py +0 -0
  85. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/runtime_resolver.py +0 -0
  86. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/semantic_output.py +0 -0
  87. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend/source_location.py +0 -0
  88. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend_config.py +0 -0
  89. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/frontend_models.py +0 -0
  90. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/language_config.py +0 -0
  91. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/models.py +0 -0
  92. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/node_utils.py +0 -0
  93. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/parse_worker.py +0 -0
  94. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/queries.py +0 -0
  95. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/indexing/sdk_examples.py +0 -0
  96. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggiecode.egg-info/dependency_links.txt +0 -0
  97. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggiecode.egg-info/entry_points.txt +0 -0
  98. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggiecode.egg-info/requires.txt +0 -0
  99. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/raggiecode.egg-info/top_level.txt +0 -0
  100. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/skills/__init__.py +0 -0
  101. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/skills/manager.py +0 -0
  102. {raggiecode-0.2.1 → raggiecode-0.2.2}/src/skills/tool.py +0 -0
  103. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_commands.py +0 -0
  104. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_context_budget.py +0 -0
  105. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_dependency_tracking.py +0 -0
  106. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_edit_symbol.py +0 -0
  107. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_effort_levels.py +0 -0
  108. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_event_logging.py +0 -0
  109. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_file_edit_application.py +0 -0
  110. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_fuzzy_search.py +0 -0
  111. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_get_file_code_structure.py +0 -0
  112. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_get_skill.py +0 -0
  113. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_global_todo.py +0 -0
  114. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_handover.py +0 -0
  115. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_handover_storage.py +0 -0
  116. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_integration_real_db.py +0 -0
  117. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_interactive_effort.py +0 -0
  118. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_list_dir.py +0 -0
  119. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_rejection_feedback.py +0 -0
  120. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_session_files.py +0 -0
  121. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_session_lifecycle.py +0 -0
  122. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_skills_import_export.py +0 -0
  123. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_sqlite_persistence.py +0 -0
  124. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_subagent_dispatch.py +0 -0
  125. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_subagent_session.py +0 -0
  126. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_subagent_tools.py +0 -0
  127. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_todo_list.py +0 -0
  128. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_tool_call_lifecycle.py +0 -0
  129. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_undo_redo.py +0 -0
  130. {raggiecode-0.2.1 → raggiecode-0.2.2}/tests/test_vc_rollback.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raggiecode
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: AI Coding Agent
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -28,7 +28,7 @@ Requires-Dist: ripgrep_rs
28
28
 
29
29
  # Raggie Code
30
30
 
31
- > *Raggie Code v0.2.1 (beta)*
31
+ > *Raggie Code v0.2.2*
32
32
 
33
33
  <p style="padding:30px 50px;">
34
34
  <img src="Raggie.png" alt="Raggie" width="312">
@@ -134,7 +134,7 @@ Every tool call is displayed in real time with its arguments. Debug mode (`--deb
134
134
  ### From pip (GitHub)
135
135
 
136
136
  ```bash
137
- pip install git+https://github.com/Hussein-L-AlMadhachi/RaggieCode.git
137
+ pip install raggiecode
138
138
  ```
139
139
 
140
140
  ### From source
@@ -208,12 +208,13 @@ After the agent makes changes, type `/undo` as your next prompt to undo the last
208
208
  $ raggie code .
209
209
 
210
210
  Raggie Agent (code) v1.0.0 - Interactive Mode
211
- Press Esc followed by Enter to send message, or type 'exit' to quit
212
211
  --------------------------------------------------
213
212
  Indexing codebase...
214
213
 
215
214
  Effort: Zen - to change it use /effort
216
215
 
216
+ Press Esc followed by Enter to send message, or type 'exit' to quit
217
+
217
218
  You:
218
219
  > Add input validation to the login endpoint and update all callers
219
220
 
@@ -448,7 +449,7 @@ raggie/
448
449
 
449
450
  ### How the Agent Works
450
451
 
451
- 1. **Startup**: The agent loads its role config, connects to the LLM API, checks for project markers in the working directory, indexes your codebase (tree-sitter, multiprocessing) if it looks like a project, and initialises its local git repo.
452
+ 1. **Startup**: The agent loads its role config, connects to the LLM API, indexes your codebase (tree-sitter, multiprocessing), and initialises its local git repo. Before the agent is created, the CLI entry point checks for a `.raggie` folder in the working directory and prompts the user if none is found.
452
453
  2. **Prompt loop**: User sends a message → agent calls the LLM with full chat history + tool definitions → LLM responds with text and/or tool calls.
453
454
  3. **Tool execution**: Each tool call is dispatched to a registered handler. Results are fed back to the LLM as tool responses.
454
455
  4. **Re-indexing**: After each tool call, the code index is updated so the agent always has fresh context.
@@ -806,11 +807,12 @@ Languages are gracefully skipped if their tree-sitter grammar is not installed.
806
807
 
807
808
  ### Project directory detection
808
809
 
809
- Before indexing, Raggie checks whether the current directory looks like a code project by looking for project marker files (`.git`, `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, `Makefile`, `pom.xml`, `build.gradle`, `composer.json`, `Gemfile`, `mix.exs`, `build.zig`, `pubspec.yaml`, `.raggie`, `.vscode`, `.idea`, `.editorconfig`, and many more).
810
+ Before indexing, Raggie checks whether a `.raggie` folder exists in the current directory. This folder is created by Raggie on first run, so its presence means the user has already confirmed this is a project directory.
810
811
 
811
- - **If project markers are found**: indexing proceeds automatically as normal.
812
- - **If no project markers are found**: Raggie warns that the directory doesn't look like a project and asks whether to index anyway. This prevents accidentally scanning unrelated files (e.g. if you run `raggie code .` in your home directory). If you decline, Raggie exits and suggests you `cd` into your project directory or start a new one with `raggie code <project-name>`.
813
- - **Subagent sessions**: indexing is skipped silently (subagents can't prompt interactively).
812
+ - **`.raggie` exists**: indexing proceeds automatically as normal.
813
+ - **No `.raggie` folder, but directory has no subdirectories**: indexing proceeds without prompting — a flat directory is small enough to be safe.
814
+ - **No `.raggie` folder, directory has subdirectories**: Raggie warns that the directory doesn't look like a project and asks whether to index anyway. This prevents accidentally scanning unrelated files (e.g. if you run `raggie code .` in your home directory). If you decline, Raggie exits and suggests you `cd` into your project directory or start a new one with `raggie code <project-name>`.
815
+ - **Subagent sessions**: subagents inherit the parent's `.raggie` folder (same working directory), so they always index normally.
814
816
 
815
817
  You can manually trigger re-indexing at any time with the `/reindex` command.
816
818
 
@@ -1,6 +1,6 @@
1
1
  # Raggie Code
2
2
 
3
- > *Raggie Code v0.2.1 (beta)*
3
+ > *Raggie Code v0.2.2*
4
4
 
5
5
  <p style="padding:30px 50px;">
6
6
  <img src="Raggie.png" alt="Raggie" width="312">
@@ -106,7 +106,7 @@ Every tool call is displayed in real time with its arguments. Debug mode (`--deb
106
106
  ### From pip (GitHub)
107
107
 
108
108
  ```bash
109
- pip install git+https://github.com/Hussein-L-AlMadhachi/RaggieCode.git
109
+ pip install raggiecode
110
110
  ```
111
111
 
112
112
  ### From source
@@ -180,12 +180,13 @@ After the agent makes changes, type `/undo` as your next prompt to undo the last
180
180
  $ raggie code .
181
181
 
182
182
  Raggie Agent (code) v1.0.0 - Interactive Mode
183
- Press Esc followed by Enter to send message, or type 'exit' to quit
184
183
  --------------------------------------------------
185
184
  Indexing codebase...
186
185
 
187
186
  Effort: Zen - to change it use /effort
188
187
 
188
+ Press Esc followed by Enter to send message, or type 'exit' to quit
189
+
189
190
  You:
190
191
  > Add input validation to the login endpoint and update all callers
191
192
 
@@ -420,7 +421,7 @@ raggie/
420
421
 
421
422
  ### How the Agent Works
422
423
 
423
- 1. **Startup**: The agent loads its role config, connects to the LLM API, checks for project markers in the working directory, indexes your codebase (tree-sitter, multiprocessing) if it looks like a project, and initialises its local git repo.
424
+ 1. **Startup**: The agent loads its role config, connects to the LLM API, indexes your codebase (tree-sitter, multiprocessing), and initialises its local git repo. Before the agent is created, the CLI entry point checks for a `.raggie` folder in the working directory and prompts the user if none is found.
424
425
  2. **Prompt loop**: User sends a message → agent calls the LLM with full chat history + tool definitions → LLM responds with text and/or tool calls.
425
426
  3. **Tool execution**: Each tool call is dispatched to a registered handler. Results are fed back to the LLM as tool responses.
426
427
  4. **Re-indexing**: After each tool call, the code index is updated so the agent always has fresh context.
@@ -778,11 +779,12 @@ Languages are gracefully skipped if their tree-sitter grammar is not installed.
778
779
 
779
780
  ### Project directory detection
780
781
 
781
- Before indexing, Raggie checks whether the current directory looks like a code project by looking for project marker files (`.git`, `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, `Makefile`, `pom.xml`, `build.gradle`, `composer.json`, `Gemfile`, `mix.exs`, `build.zig`, `pubspec.yaml`, `.raggie`, `.vscode`, `.idea`, `.editorconfig`, and many more).
782
+ Before indexing, Raggie checks whether a `.raggie` folder exists in the current directory. This folder is created by Raggie on first run, so its presence means the user has already confirmed this is a project directory.
782
783
 
783
- - **If project markers are found**: indexing proceeds automatically as normal.
784
- - **If no project markers are found**: Raggie warns that the directory doesn't look like a project and asks whether to index anyway. This prevents accidentally scanning unrelated files (e.g. if you run `raggie code .` in your home directory). If you decline, Raggie exits and suggests you `cd` into your project directory or start a new one with `raggie code <project-name>`.
785
- - **Subagent sessions**: indexing is skipped silently (subagents can't prompt interactively).
784
+ - **`.raggie` exists**: indexing proceeds automatically as normal.
785
+ - **No `.raggie` folder, but directory has no subdirectories**: indexing proceeds without prompting — a flat directory is small enough to be safe.
786
+ - **No `.raggie` folder, directory has subdirectories**: Raggie warns that the directory doesn't look like a project and asks whether to index anyway. This prevents accidentally scanning unrelated files (e.g. if you run `raggie code .` in your home directory). If you decline, Raggie exits and suggests you `cd` into your project directory or start a new one with `raggie code <project-name>`.
787
+ - **Subagent sessions**: subagents inherit the parent's `.raggie` folder (same working directory), so they always index normally.
786
788
 
787
789
  You can manually trigger re-indexing at any time with the `/reindex` command.
788
790
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "raggiecode"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "AI Coding Agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,4 @@
1
1
  import os
2
- import glob
3
- import sys
4
2
  import json
5
3
  import shutil
6
4
  import platform
@@ -13,7 +11,7 @@ from rich.markdown import Markdown
13
11
  from .config import load_roles, load_tools, load_keys
14
12
  from .tools import ToolRegistry
15
13
  from .command import CommandRegistry
16
- from .chat_history_db import init_db, get_or_create_session, load_messages, save_message, update_chat_title, generate_title, get_active_todo_list, get_todo_tasks, create_session, set_redirect_session_id, save_handover, get_old_session_ids, is_subagent_session, get_session_effort, get_session_info, resolve_session_id, migrate_todo_lists
14
+ from .chat_history_db import init_db, get_or_create_session, load_messages, save_message, update_chat_title, generate_title, get_active_todo_list, get_todo_tasks, create_session, set_redirect_session_id, save_handover, get_old_session_ids, is_subagent_session, get_session_effort, get_session_info, resolve_session_id, migrate_todo_lists, repair_message_sequence
17
15
  from .git_manager import GitManager
18
16
  from skills import SkillManager
19
17
  from indexing.code_index_sdk import CodeIndexSDK
@@ -99,84 +97,13 @@ class Agent:
99
97
  # Initialize git manager for commit/undo/redo operations
100
98
  self.git_manager = GitManager(root_dir=os.getcwd())
101
99
 
102
- # Check if this looks like a project directory before indexing
103
- cwd = os.getcwd()
104
- project_markers = [
105
- # VCS
106
- ".git", ".hg", ".svn",
107
- # Python
108
- "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt",
109
- "Pipfile", "poetry.lock", "uv.lock", "tox.ini", "MANIFEST.in",
110
- # JavaScript / TypeScript
111
- "package.json", "tsconfig.json", "yarn.lock", "pnpm-lock.yaml",
112
- "package-lock.json", "bower.json", ".npmrc", "deno.json",
113
- # Go
114
- "go.mod", "go.sum", "go.work",
115
- # Rust
116
- "Cargo.toml",
117
- # C / C++
118
- "CMakeLists.txt", "Makefile", "Makefile.am", "configure.ac",
119
- "meson.build", "BUCK", "BUILD", "BUILD.bazel", "WORKSPACE",
120
- # C# / .NET
121
- "Directory.Build.props", "global.json",
122
- # Java / Kotlin
123
- "pom.xml", "build.gradle", "build.gradle.kts",
124
- "settings.gradle", "settings.gradle.kts", "gradle.properties",
125
- # PHP
126
- "composer.json", "artisan",
127
- # Ruby
128
- "Gemfile", "Rakefile", ".rspec",
129
- # Elixir
130
- "mix.exs",
131
- # Zig
132
- "build.zig",
133
- # Dart / Flutter
134
- "pubspec.yaml",
135
- # Lua
136
- "rockspec",
137
- # Generic / editor
138
- ".raggie", ".vscode", ".idea", ".editorconfig",
139
- ]
140
- is_project = any(os.path.exists(os.path.join(cwd, m)) for m in project_markers)
141
- if not is_project:
142
- is_project = bool(glob.glob(os.path.join(cwd, "*.csproj")) or glob.glob(os.path.join(cwd, "*.sln")))
143
-
144
- if is_project:
145
- try:
146
- with self.console.status("[bold green]Indexing codebase...", spinner="dots"):
147
- self.code_indexer.index_directory()
148
- except (KeyboardInterrupt, EOFError):
149
- self.console.print("\n[yellow]Indexing interrupted. Using existing index.[/yellow]")
150
- self.code_indexer._connect()
151
- elif self.is_subagent:
152
- self.console.print(
153
- f"[yellow]Warning:[/yellow] '{cwd}' doesn't look like a project directory. "
154
- f"Skipping indexing."
155
- )
156
- else:
157
- self.console.print(
158
- f"[yellow]Warning:[/yellow] '{cwd}' doesn't look like a project directory "
159
- f"(no .git, pyproject.toml, package.json, go.mod, Cargo.toml, etc. found).\n"
160
- f"Indexing here may scan unrelated files and take a long time."
161
- )
162
- try:
163
- response = input("\nIndex anyway? (y/N): ").strip().lower()
164
- except (EOFError, KeyboardInterrupt):
165
- response = "n"
166
-
167
- if response in ("y", "yes"):
168
- try:
169
- with self.console.status("[bold green]Indexing codebase...", spinner="dots"):
170
- self.code_indexer.index_directory()
171
- except (KeyboardInterrupt, EOFError):
172
- self.console.print("\n[yellow]Indexing interrupted. Using existing index.[/yellow]")
173
- self.code_indexer._connect()
174
- else:
175
- self.console.print(
176
- "Please cd into your project directory and try again.\n"
177
- "To start a new project: raggie code <project-name>"
178
- )
179
- sys.exit(0)
100
+ # Index the codebase at the start of each conversation
101
+ try:
102
+ with self.console.status("[bold green]Indexing codebase...", spinner="dots"):
103
+ self.code_indexer.index_directory()
104
+ except (KeyboardInterrupt, EOFError):
105
+ self.console.print("\n[yellow]Indexing interrupted. Using existing index.[/yellow]")
106
+ self.code_indexer._connect()
180
107
 
181
108
  # Display previous chat history if it exists (skip for subagents — they
182
109
  # share the chat_id but don't need the parent's conversation printed)
@@ -378,12 +305,38 @@ class Agent:
378
305
  "content": message,
379
306
  }
380
307
 
308
+ @staticmethod
309
+ def _normalize_message_content(msg):
310
+ """Ensure message content is a string or content-block list, as the API requires."""
311
+ content = msg.get("content")
312
+ if content is not None and not isinstance(content, (str, list)):
313
+ msg["content"] = json.dumps(content)
314
+
315
+ def _create_completion(self, **kwargs):
316
+ """Single choke point for chat.completions.create.
317
+
318
+ Repairs the tool_call/tool-response sequence and normalizes content on
319
+ the in-memory history right before sending, so the API never receives
320
+ an invalid conversation regardless of how the history was produced.
321
+ """
322
+ self.chat_history = repair_message_sequence(self.chat_history)
323
+ for msg in self.chat_history:
324
+ self._normalize_message_content(msg)
325
+ kwargs["messages"] = self.chat_history
326
+ return self.client.chat.completions.create(**kwargs)
327
+
381
328
 
382
329
 
383
330
  def _has_dangling_tool_work(self):
384
331
  if not self.chat_history:
385
332
  return False
386
333
 
334
+ # An assistant tool_calls message with missing responses anywhere in the
335
+ # history (not just at the tail — a crash can be followed by stored user
336
+ # messages) makes the sequence invalid for the API.
337
+ if self._get_pending_toolcalls():
338
+ return True
339
+
387
340
  last_msg = self.chat_history[-1]
388
341
  if last_msg.get("role") == "tool":
389
342
  return True
@@ -392,7 +345,7 @@ class Agent:
392
345
  if last_msg.get("role") == "user" and last_msg.get("tool_call_id"):
393
346
  return True
394
347
 
395
- return last_msg.get("role") == "assistant" and bool(last_msg.get("tool_calls"))
348
+ return False
396
349
 
397
350
  def _get_pending_toolcalls(self):
398
351
  """Find tool calls from the last assistant message that don't have tool responses yet.
@@ -438,6 +391,9 @@ class Agent:
438
391
  tool_output = self.tool_registry.call(
439
392
  tool_name, args_dict, toolcall_id, self.session_id
440
393
  )
394
+ # A tool returning non-string content (e.g. a dict) would make the
395
+ # next API call fail; normalize before it enters the history.
396
+ self._normalize_message_content(tool_output)
441
397
 
442
398
  if self.debug:
443
399
  print(f"\n[DEBUG] Tool output for {tool_name}:")
@@ -479,24 +435,20 @@ class Agent:
479
435
  if not self._has_dangling_tool_work():
480
436
  return
481
437
 
482
- last_msg = self.chat_history[-1]
483
- if last_msg.get("role") == "assistant" and last_msg.get("tool_calls"):
484
- for toolcall in last_msg["tool_calls"]:
485
- function = toolcall.get("function", {})
486
- yield from self._execute_tool_call(
487
- toolcall.get("id"),
488
- function.get("name"),
489
- function.get("arguments", "{}"),
490
- )
491
- elif last_msg.get("tool_call_id"):
492
- pending = self._get_pending_toolcalls()
493
- for toolcall in pending:
494
- function = toolcall.get("function", {})
495
- yield from self._execute_tool_call(
496
- toolcall.get("id"),
497
- function.get("name"),
498
- function.get("arguments", "{}"),
499
- )
438
+ # Re-execute every tool call that never got a response, wherever the
439
+ # dangling assistant message sits in the history.
440
+ for toolcall in self._get_pending_toolcalls():
441
+ function = toolcall.get("function", {})
442
+ yield from self._execute_tool_call(
443
+ toolcall.get("id"),
444
+ function.get("name"),
445
+ function.get("arguments", "{}"),
446
+ )
447
+
448
+ # Executed tool responses are appended to the end of the history; pull
449
+ # them up next to their assistant message so the sequence sent to the
450
+ # API stays valid.
451
+ self.chat_history = repair_message_sequence(self.chat_history)
500
452
 
501
453
  yield from self.start()
502
454
 
@@ -530,9 +482,8 @@ class Agent:
530
482
  tool_calls_accum = []
531
483
 
532
484
  try:
533
- stream = self.client.chat.completions.create(
485
+ stream = self._create_completion(
534
486
  model=model,
535
- messages=self.chat_history,
536
487
  tools=self._get_tools(self.agent_role),
537
488
  stream=True,
538
489
  stream_options={"include_usage": True},
@@ -595,9 +546,8 @@ class Agent:
595
546
 
596
547
  try:
597
548
  with self.console.status("[bold green]Thinking...", spinner="dots"):
598
- chat = self.client.chat.completions.create(
549
+ chat = self._create_completion(
599
550
  model=model,
600
- messages=self.chat_history,
601
551
  tools=self._get_tools(self.agent_role),
602
552
  )
603
553
  except Exception as err:
@@ -652,9 +602,8 @@ class Agent:
652
602
  self._handover_text = None
653
603
  handover_text = ""
654
604
  try:
655
- stream = self.client.chat.completions.create(
605
+ stream = self._create_completion(
656
606
  model=model,
657
- messages=self.chat_history,
658
607
  stream=True,
659
608
  )
660
609
  for chunk in stream:
@@ -687,9 +636,8 @@ class Agent:
687
636
  self._handover_text = None
688
637
  try:
689
638
  with self.console.status("[bold green]Generating handover instructions...", spinner="dots"):
690
- chat = self.client.chat.completions.create(
639
+ chat = self._create_completion(
691
640
  model=model,
692
- messages=self.chat_history,
693
641
  )
694
642
  except Exception as err:
695
643
  self.chat_history.pop()
@@ -493,29 +493,104 @@ def save_message(session_id: int, message: Dict):
493
493
  conn.close()
494
494
 
495
495
 
496
+ def repair_message_sequence(messages: List[Dict]) -> List[Dict]:
497
+ """Move each tool response to directly follow the assistant message that requested it.
498
+
499
+ A crash or interrupt can leave a session where an assistant message with
500
+ tool_calls is followed by user/other messages, with the tool responses
501
+ appended later. The chat API requires every tool response to directly
502
+ follow its assistant tool_calls message, so pull each one forward.
503
+ """
504
+ result = []
505
+ owner = {} # tool_call_id -> position of owning assistant message in result
506
+ placed = {} # assistant position -> number of its tool responses already placed
507
+ for msg in messages:
508
+ tool_call_id = msg.get("tool_call_id")
509
+ if tool_call_id is not None and tool_call_id in owner:
510
+ asst_pos = owner.pop(tool_call_id)
511
+ insert_at = asst_pos + 1 + placed.get(asst_pos, 0)
512
+ result.insert(insert_at, msg)
513
+ placed = {k + 1 if k >= insert_at else k: v for k, v in placed.items()}
514
+ placed[asst_pos] = placed.get(asst_pos, 0) + 1
515
+ owner = {k: v + 1 if v >= insert_at else v for k, v in owner.items()}
516
+ else:
517
+ if msg.get("role") == "assistant" and msg.get("tool_calls"):
518
+ asst_pos = len(result)
519
+ for tc in msg["tool_calls"]:
520
+ tcid = tc.get("id")
521
+ if tcid:
522
+ owner[tcid] = asst_pos
523
+ placed[asst_pos] = 0
524
+ result.append(msg)
525
+ return result
526
+
527
+
528
+ def validate_message_sequence(messages: List[Dict]) -> List[str]:
529
+ """Return a list of API-contract violations in the message sequence (empty if valid).
530
+
531
+ Checks the two invariants the chat API enforces:
532
+ - content must be a string or a content-block list
533
+ - every assistant tool_calls entry must be answered by a tool response
534
+ (role "tool", or role "user" with tool_call_id for vision outputs) before
535
+ any other message follows
536
+ """
537
+ errors = []
538
+ pending: List[str] = []
539
+ for i, msg in enumerate(messages):
540
+ content = msg.get("content")
541
+ if content is not None and not isinstance(content, (str, list)):
542
+ errors.append(f"messages[{i}]: content is {type(content).__name__}, expected str or list")
543
+
544
+ tool_call_id = msg.get("tool_call_id")
545
+ if tool_call_id and msg.get("role") in ("tool", "user"):
546
+ if tool_call_id in pending:
547
+ pending.remove(tool_call_id)
548
+ else:
549
+ errors.append(f"messages[{i}]: tool response without matching tool_call")
550
+ continue
551
+
552
+ if pending:
553
+ errors.append(f"messages[{i}]: {msg.get('role')} message arrived before tool responses to {pending}")
554
+ pending = []
555
+
556
+ if msg.get("role") == "assistant" and msg.get("tool_calls"):
557
+ pending = [tc.get("id") for tc in msg["tool_calls"]]
558
+
559
+ if pending:
560
+ errors.append(f"unanswered tool_calls at end of history: {pending}")
561
+ return errors
562
+
563
+
496
564
  def load_messages(session_id: int) -> List[Dict]:
497
- """Load all messages for a given session, ordered by timestamp."""
565
+ """Load all messages for a given session in insertion order."""
498
566
  conn = _get_conn()
499
567
  cursor = conn.cursor()
500
-
568
+
569
+ # Order by id, not timestamp: timestamps can tie (same-second inserts),
570
+ # which makes the ordering of tool_calls vs tool responses nondeterministic.
501
571
  cursor.execute("""
502
572
  SELECT role, content, tool_calls, tool_call_id
503
573
  FROM messages
504
574
  WHERE session_id = ?
505
- ORDER BY timestamp ASC
575
+ ORDER BY id ASC
506
576
  """, (session_id,))
507
577
 
508
578
  messages = []
509
579
  for row in cursor.fetchall():
510
580
  content_str = str(row[1]) if row[1] is not None else ""
581
+ content = content_str
511
582
  try:
512
583
  parsed = json.loads(content_str)
513
- if isinstance(parsed, (list, dict)):
584
+ # Only restore structured content for vision-style content-block
585
+ # lists (e.g. [{"type": "text", ...}, {"type": "image_url", ...}]).
586
+ # Any other JSON value (a tool result that happens to be valid JSON,
587
+ # like a package.json dump) must stay a plain string, or the API
588
+ # rejects it with "content should be a string or a list".
589
+ if (isinstance(parsed, list) and parsed
590
+ and all(isinstance(item, dict) and "type" in item for item in parsed)):
514
591
  content = parsed
515
- else:
516
- content = content_str
517
592
  except (json.JSONDecodeError, TypeError):
518
- content = content_str
593
+ pass
519
594
 
520
595
  message = {
521
596
  "role": str(row[0]) if row[0] is not None else "user",
@@ -538,9 +613,9 @@ def load_messages(session_id: int) -> List[Dict]:
538
613
  continue
539
614
 
540
615
  messages.append(message)
541
-
616
+
542
617
  conn.close()
543
- return messages
618
+ return repair_message_sequence(messages)
544
619
 
545
620
 
546
621
  def delete_chat(chat_id: int):
@@ -392,7 +392,12 @@ def find_symbol_implementation(symbol_name: str, file_path: str = None) -> str:
392
392
  class_body = sdk.get_class_body(symbol_name, file_path)
393
393
  if class_body:
394
394
  return class_body
395
-
395
+
396
+ # Try to find as variable
397
+ variable_body = sdk.get_variable_body(symbol_name, file_path)
398
+ if variable_body:
399
+ return variable_body
400
+
396
401
  # Search for similar symbols by name and description
397
402
  matches = sdk.search_symbols(symbol_name, limit=10)
398
403
 
@@ -116,9 +116,7 @@ def handle_get_todo_list(arguments, toolcall_id, parent_session_id=None):
116
116
  if task.get('cancel_reason'):
117
117
  output += f"{YELLOW} Cancel reason: {task['cancel_reason']}{RESET}\n"
118
118
  output += "\n"
119
-
120
- print(output)
121
-
119
+
122
120
  return {
123
121
  "role": "tool",
124
122
  "tool_call_id": toolcall_id,
@@ -804,6 +804,29 @@ class CodeIndexSDK(QueryMixin, DescriptionMixin):
804
804
 
805
805
  return ""
806
806
 
807
+ def get_variable_body(self, variable_name: str, file_path: Optional[str] = None) -> Optional[str]:
808
+ """Get the declaration source lines of a variable by name.
809
+
810
+ Args:
811
+ variable_name: Name of the variable.
812
+ file_path: Optional relative file path to disambiguate same-name variables.
813
+
814
+ Returns:
815
+ Source code string of the declaration, or None if not found / file unresolvable.
816
+ """
817
+ file_id = None
818
+ if file_path:
819
+ f = self.get_file_by_path(file_path)
820
+ if f:
821
+ file_id = f.id
822
+
823
+ matches = self.get_variable_by_name(variable_name, file_id)
824
+ if not matches:
825
+ return None
826
+
827
+ var = matches[0]
828
+ return self._read_source_lines(var.file_id, var.location.start_line, var.location.end_line)
829
+
807
830
  def get_class_body(self, class_name: str, file_path: Optional[str] = None) -> Optional[str]:
808
831
  """Get the source body of a class by name.
809
832
 
@@ -3,8 +3,10 @@ Agent run loops for Raggie (interactive and non-interactive modes).
3
3
  """
4
4
 
5
5
  import json
6
+ import re
6
7
  import sys
7
8
  from importlib.metadata import version
9
+ from pathlib import Path
8
10
  from prompt_toolkit import prompt
9
11
  from rich.console import Console
10
12
  from rich.markdown import Markdown
@@ -18,6 +20,50 @@ GREEN = "\033[32m"
18
20
  DIM = "\033[2m"
19
21
  RESET = "\033[0m"
20
22
 
23
+ # Matches @path/to/file.x:23 and @path/to/file.x:23-43. The lookbehind rejects
24
+ # @-mentions embedded in emails/handles, and the mandatory :N suffix ensures
25
+ # only explicit line references are expanded.
26
+ _FILE_REF_RE = re.compile(r'(?<![\w.@-])@([\w./\\-]+\.[A-Za-z0-9]+):(\d+)(?:-(\d+))?')
27
+
28
+
29
+ def expand_file_references(text):
30
+ """Append referenced code snippets for @path/file.x:N[-M] mentions in user input.
31
+
32
+ References pointing to non-existent files are left untouched so emails,
33
+ handles, and plain text pass through unchanged.
34
+ """
35
+ snippets = []
36
+ seen = set()
37
+ for m in _FILE_REF_RE.finditer(text):
38
+ ref = m.group(0)
39
+ if ref in seen:
40
+ continue
41
+ seen.add(ref)
42
+
43
+ path_str, start, end = m.group(1), int(m.group(2)), m.group(3)
44
+ end = int(end) if end else start
45
+ if end < start:
46
+ start, end = end, start
47
+
48
+ path = Path(path_str)
49
+ if not path.is_file():
50
+ continue
51
+
52
+ try:
53
+ lines = path.read_text(errors="replace").splitlines()
54
+ except OSError:
55
+ continue
56
+ if start > len(lines):
57
+ continue
58
+
59
+ end = min(end, len(lines))
60
+ chunk = "\n".join(f"{i}: {lines[i - 1]}" for i in range(start, end + 1))
61
+ snippets.append(f"--- {path_str}:{start}-{end} ---\n{chunk}")
62
+
63
+ if not snippets:
64
+ return text
65
+ return text + "\n\n<referenced_code>\n" + "\n\n".join(snippets) + "\n</referenced_code>"
66
+
21
67
 
22
68
  def _prompt_effort(session_id, value=None):
23
69
  """Ask the user to pick an effort level and store it on the session."""
@@ -167,7 +213,6 @@ def run_interactive(agent, role):
167
213
  """
168
214
  model_name = agent.roles[role].get("model", "unknown")
169
215
  print(f"{GREEN}Raggie Agent ({role}) v{version('raggiecode')} - Interactive Mode{RESET}")
170
- print(f"{GREEN}Press Esc followed by Enter to send message, or type 'exit' to quit{RESET}")
171
216
  print("-" * 50)
172
217
  print("\nuse /help to see all available commands")
173
218
 
@@ -190,6 +235,7 @@ def run_interactive(agent, role):
190
235
  set_session_effort(agent.session_id, DEFAULT_EFFORT)
191
236
  current = DEFAULT_EFFORT
192
237
  print(f"\n{DIM}Effort: {effort_name(current)} - to change it use /effort{RESET}")
238
+ print(f"{DIM}Press Esc followed by Enter to send message, or type 'exit' to quit{RESET}")
193
239
  print(f"{GREEN}\n\nYou:{RESET}")
194
240
  user_input = prompt("> ", multiline=True)
195
241
  except (EOFError, KeyboardInterrupt):
@@ -206,7 +252,7 @@ def run_interactive(agent, role):
206
252
  state = StreamState()
207
253
  depth = get_session_depth(agent.session_id)
208
254
  model_name = agent.roles[role].get("model", "unknown")
209
- for event in agent.start(user_input):
255
+ for event in agent.start(expand_file_references(user_input)):
210
256
  state = _print_event(event, state, depth, model_name)
211
257
  except KeyboardInterrupt:
212
258
  print("\n[interrupted]")
@@ -233,7 +279,7 @@ def run_non_interactive(agent, prompt_text, effort=None):
233
279
 
234
280
  try:
235
281
  state = StreamState()
236
- for event in agent.start(prompt_text):
282
+ for event in agent.start(expand_file_references(prompt_text)):
237
283
  if event[0] == "error":
238
284
  state._stop_live()
239
285
  if state.reasoning_started: