The bug was caused by mark_tool_calls_consumed() being called after displaying each chunk, which advanced last_consumed_position to the end of the current buffer. When the next chunk arrived with JSON, the unchecked_buffer started at position 0 of the slice, causing is_on_own_line() to return true (position 0 is always "on its own line"). Removed the problematic mark_tool_calls_consumed() call from the "no tool executed" branch. The remaining call after actual tool execution is correct and necessary. Added integration test that verifies inline JSON in prose is not detected as a tool call.
255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
# Project Memory
|
||
> Updated: 2026-01-19T08:32:03Z | Size: 14.0k chars
|
||
|
||
### Remember Tool Wiring
|
||
- `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()`
|
||
- `crates/g3-core/src/tool_definitions.rs` [11000..12000] - remember tool definition in `create_core_tools()`
|
||
- `crates/g3-core/src/tool_dispatch.rs` [48] - dispatch case for "remember"
|
||
- `crates/g3-core/src/prompts.rs` [4200..6500] - Project Memory section in native prompt
|
||
- `crates/g3-cli/src/lib.rs` [1472..1495] - `read_project_memory()` loads memory at startup
|
||
|
||
### Context Window & Compaction
|
||
- `crates/g3-core/src/context_window.rs` [0..815] - `ContextWindow`, `reset_with_summary()`, `should_compact()`, `thin_context()`
|
||
- `crates/g3-core/src/lib.rs` [0..132483] - `Agent` struct, `force_compact()`, `stream_completion_with_tools()`
|
||
|
||
### Session Storage & Continuation
|
||
- `crates/g3-core/src/session_continuation.rs` [0..541] - `SessionContinuation`, `save_continuation()`, `load_continuation()`
|
||
- `crates/g3-core/src/paths.rs` [0..133] - `get_session_logs_dir()`, `get_thinned_dir()`, `get_session_file()`
|
||
- `crates/g3-core/src/session.rs` - Session logging utilities
|
||
|
||
### Tool System
|
||
- `crates/g3-core/src/tool_definitions.rs` [0..544] - `create_core_tools()`, `create_tool_definitions()`, `ToolConfig`
|
||
- `crates/g3-core/src/tool_dispatch.rs` [0..73] - `dispatch_tool()` routing
|
||
|
||
### CLI Argument Parsing
|
||
- `crates/g3-cli/src/lib.rs` [270..380] - `Cli` struct with clap derive macros
|
||
- `crates/g3-cli/src/lib.rs` [1700..2200] - `run_interactive()` with `/` command handlers
|
||
|
||
### Streaming Markdown Formatter
|
||
- `crates/g3-cli/src/streaming_markdown.rs` [21500..22500] - `format_header()` processes headers with inline formatting
|
||
- `crates/g3-cli/tests/streaming_markdown_test.rs` - Tests for markdown formatting including `test_bold_inside_header`, `test_italic_inside_header`, `test_code_inside_header`, `test_mixed_formatting_inside_header`
|
||
|
||
### Auto-Memory Feature
|
||
- `crates/g3-core/src/lib.rs` [1459..1522] - `send_auto_memory_reminder()` sends reminder to LLM after tool calls
|
||
- `crates/g3-core/src/lib.rs` [1451..1454] - `set_auto_memory()` enables/disables auto-memory
|
||
- `crates/g3-core/src/lib.rs` [116] - `tool_calls_this_turn: Vec<String>` tracks tools called per turn
|
||
- `crates/g3-cli/src/lib.rs` [393] - `auto_memory: bool` CLI flag definition
|
||
- `crates/g3-cli/src/lib.rs` [641..642, 684..685] - Flag applied to agent in console/machine modes
|
||
- `crates/g3-cli/src/lib.rs` [1340..1350, 1394..1404] - Auto-memory reminder called in single-shot mode
|
||
- `crates/g3-cli/src/lib.rs` [1758, 1931, 2216] - Auto-memory reminder called in interactive mode
|
||
|
||
### Tool Call Tracking
|
||
- `crates/g3-core/src/lib.rs` [2843..2855] - `execute_tool_in_dir()` tracks all tool calls for auto-memory
|
||
|
||
### Agent Mode
|
||
- `crates/g3-cli/src/lib.rs` [695..910] - `run_agent_mode()` handles specialized agent execution with custom prompts
|
||
- `crates/g3-cli/src/lib.rs` [820..835] - Agent creation with `Agent::new_with_custom_prompt()`
|
||
- `crates/g3-cli/src/lib.rs` [837] - `agent.set_agent_mode()` enables agent-specific session tracking
|
||
|
||
### CLI Entry Points and Modes
|
||
- `crates/g3-cli/src/lib.rs` [0..140000] - `run()` main entry, `run_agent_mode()`, `run_accumulative_mode()`, `run_autonomous()`, `run_interactive()`, `run_interactive_machine()`
|
||
- `crates/g3-cli/src/lib.rs` - `execute_task()` (~line 1990), `execute_task_machine()` (~line 2262) - duplicated retry logic
|
||
|
||
### Retry Infrastructure
|
||
- `crates/g3-core/src/retry.rs` [0..12000] - `execute_with_retry()`, `retry_operation()`, `RetryConfig`, `RetryResult` - used by g3-planner but not g3-cli
|
||
|
||
### UI Abstraction Layer
|
||
- `crates/g3-core/src/ui_writer.rs` [0..4500] - `UiWriter` trait, `NullUiWriter`
|
||
- `crates/g3-cli/src/ui_writer_impl.rs` [0..14000] - `ConsoleUiWriter` implementation
|
||
- `crates/g3-cli/src/simple_output.rs` [0..1200] - `SimpleOutput` helper (separate from UiWriter)
|
||
|
||
### Feedback Extraction
|
||
- `crates/g3-core/src/feedback_extraction.rs` [0..22000] - `extract_coach_feedback()`, `try_extract_from_session_log()`, `try_extract_from_native_tool_call()`
|
||
|
||
### Streaming Utilities
|
||
- `crates/g3-core/src/streaming.rs` [0..10000] - `truncate_line()`, `truncate_for_display()`, `log_stream_error()`, `is_connection_error()`
|
||
|
||
### Background Process Management
|
||
- `crates/g3-core/src/background_process.rs` [0..3000] - `BackgroundProcessManager`, `start()`, `list()`, `is_running()`, `get()`, `remove()`
|
||
- Design: No `stop()` method - processes are stopped via shell tool using `kill <pid>`
|
||
|
||
### Unified Diff Application
|
||
- `crates/g3-core/src/utils.rs` [5000..15000] - `apply_unified_diff_to_string()`, `parse_unified_diff_hunks()`
|
||
- Handles multi-hunk diffs, CRLF normalization, range constraints
|
||
|
||
### Error Classification
|
||
- `crates/g3-core/src/error_handling.rs` [0..567 lines] - `classify_error()`, `ErrorType`, `RecoverableError`
|
||
- Priority order: rate limit > network > server > busy > timeout > token limit > context length
|
||
- Note: "Connection timeout" classifies as NetworkError (not Timeout) due to "connection" keyword priority
|
||
|
||
### CLI Module Extractions
|
||
- `crates/g3-cli/src/metrics.rs` [0..5416] - `TurnMetrics`, `format_elapsed_time()`, `generate_turn_histogram()`
|
||
- `crates/g3-cli/src/project_files.rs` [0..5577] - `read_agents_config()`, `read_project_readme()`, `read_project_memory()`, `extract_readme_heading()`
|
||
- `crates/g3-cli/src/coach_feedback.rs` [0..4025] - `extract_from_logs()` for coach-player loop feedback extraction
|
||
|
||
### Context Compaction
|
||
- `crates/g3-core/src/compaction.rs` [0..11213] - `perform_compaction()`, `CompactionResult`, `CompactionConfig`, `calculate_capped_summary_tokens()`, `should_disable_thinking()`, `build_summary_messages()`, `apply_summary_fallback_sequence()`
|
||
- Unified compaction used by both `force_compact()` and auto-compaction in `stream_completion_with_tools()`
|
||
|
||
### Streaming Markdown Formatter (Code Blocks)
|
||
- `crates/g3-cli/src/streaming_markdown.rs` [693..735] - `flush_incomplete()` handles unclosed blocks at end of stream
|
||
- `crates/g3-cli/src/streaming_markdown.rs` [654..675] - `emit_code_block()` joins block_buffer and highlights code
|
||
- `crates/g3-cli/src/streaming_markdown.rs` [439..462] - `process_in_code_block()` detects closing fence on newline
|
||
- Bug fix: closing ``` without trailing newline must be detected in flush_incomplete(), not just process_in_code_block()
|
||
|
||
### ACD (Aggressive Context Dehydration)
|
||
- `crates/g3-core/src/acd.rs` [0..22000] - `Fragment`, `Fragment::new()`, `Fragment::save()`, `Fragment::load()`, `generate_stub()`, `list_fragments()`, `get_latest_fragment_id()`
|
||
- `crates/g3-core/src/tools/acd.rs` [0..8500] - `execute_rehydrate()` tool implementation
|
||
- `crates/g3-core/src/paths.rs` [3200..3400] - `get_fragments_dir()` returns `.g3/sessions/<session_id>/fragments/`
|
||
- `crates/g3-core/src/compaction.rs` [195..240] - ACD integration in `perform_compaction()`, creates fragment and stub when `acd_enabled`
|
||
- `crates/g3-core/src/context_window.rs` [10100..10700] - `reset_with_summary_and_stub()` adds stub before summary
|
||
- `crates/g3-cli/src/lib.rs` [157..161] - `--acd` CLI flag
|
||
- `crates/g3-cli/src/lib.rs` [1476..1525] - `/fragments` and `/rehydrate` commands
|
||
|
||
### ACD Fragment Storage Format
|
||
```json
|
||
{
|
||
"fragment_id": "abc123",
|
||
"created_at": "2026-01-11T...",
|
||
"messages": [...],
|
||
"message_count": 47,
|
||
"user_message_count": 23,
|
||
"assistant_message_count": 24,
|
||
"tool_call_summary": {"read_file": 4, "shell": 5},
|
||
"estimated_tokens": 18500,
|
||
"topics": ["implemented auth", "fixed bug"],
|
||
"preceding_fragment_id": "xyz789"
|
||
}
|
||
```
|
||
|
||
|
||
|
||
### UTF-8 Safe String Slicing Pattern
|
||
**Problem**: Rust string slices (`&s[..n]`) use byte indices, not character indices. Multi-byte UTF-8 characters (emoji, bullets `•`, `×`, `⚡`) cause panics if sliced mid-character.
|
||
|
||
**Solution**: Use `char_indices()` to find byte boundaries:
|
||
```rust
|
||
// Get byte index of the Nth character
|
||
let byte_idx = s.char_indices()
|
||
.nth(char_limit)
|
||
.map(|(i, _)| i)
|
||
.unwrap_or(s.len());
|
||
let truncated = &s[..byte_idx];
|
||
|
||
// For length checks, use chars().count() not len()
|
||
if s.chars().count() <= max_len { ... }
|
||
```
|
||
|
||
|
||
|
||
**Danger zones**: Display truncation, ACD stubs, user input handling, any string with non-ASCII characters.
|
||
|
||
### CLI Module Structure (Post-Refactor)
|
||
- `crates/g3-cli/src/lib.rs` [0..415] - Entry point, `run()`, mode dispatch, config loading
|
||
- `crates/g3-cli/src/cli_args.rs` [0..133] - `Cli` struct with clap derive macros, argument parsing
|
||
- `crates/g3-cli/src/autonomous.rs` [0..785] - `run_autonomous()`, coach-player feedback loop
|
||
- `crates/g3-cli/src/agent_mode.rs` [0..284] - `run_agent_mode()` specialized agent execution
|
||
- `crates/g3-cli/src/accumulative.rs` [0..343] - `run_accumulative_mode()` iterative requirements
|
||
- `crates/g3-cli/src/interactive.rs` [0..851] - `run_interactive()`, `run_interactive_machine()`, REPL with `/` commands
|
||
- `crates/g3-cli/src/task_execution.rs` [0..212] - `execute_task_with_retry()`, `OutputMode` enum - unified retry logic
|
||
- `crates/g3-cli/src/utils.rs` [0..91] - `display_welcome_message()`, `get_workspace_path()`
|
||
|
||
### Studio - Multi-Agent Workspace Manager
|
||
- `crates/studio/src/main.rs` [0..12500] - `cmd_run()`, `cmd_status()`, `cmd_accept()`, `cmd_discard()`, `extract_session_summary()`
|
||
- `crates/studio/src/session.rs` - `Session`, `SessionStatus`, session metadata management
|
||
- `crates/studio/src/git.rs` - `GitWorktree`, git worktree management for isolated agent sessions
|
||
|
||
**Session log format**: Session logs are stored at `<worktree>/.g3/sessions/<session_id>/session.json` with structure:
|
||
```json
|
||
{
|
||
"context_window": {
|
||
"conversation_history": [{"role": "...", "content": "..."}],
|
||
"percentage_used": 45.2,
|
||
"total_tokens": 200000,
|
||
"used_tokens": 90400
|
||
},
|
||
"session_id": "...",
|
||
"status": "...",
|
||
"timestamp": "..."
|
||
}
|
||
```
|
||
|
||
|
||
### Project Memory Location
|
||
- Memory is now stored at `analysis/memory.md` (version controlled, shared across worktrees)
|
||
- `crates/g3-core/src/tools/memory.rs` - `get_memory_path()` returns `analysis/memory.md`
|
||
- `crates/g3-cli/src/project_files.rs` - `read_project_memory()` reads from `analysis/memory.md`
|
||
|
||
### Compact Tool Output
|
||
- `crates/g3-cli/src/ui_writer_impl.rs` - `print_tool_compact()` handles compact display for file ops and other tools
|
||
- `crates/g3-core/src/streaming.rs` - `format_*_summary()` functions for each tool type
|
||
|
||
### Racket Code Search Support
|
||
Tree-sitter based syntax-aware search for Racket `.rkt` files.
|
||
|
||
- `crates/g3-core/src/code_search/searcher.rs`
|
||
- Racket parser init [~line 45] - `tree_sitter_racket::LANGUAGE`
|
||
- Extension mapping [~line 90] - `.rkt`, `.rktl`, `.rktd` → "racket"
|
||
|
||
### Auto-Memory Reminder Format
|
||
Rich few-shot prompting for higher quality memory entries with per-symbol char ranges.
|
||
|
||
- `crates/g3-core/src/lib.rs`
|
||
- `send_auto_memory_reminder()` [47800..48800] - MEMORY CHECKPOINT prompt with few-shot examples
|
||
- `crates/g3-core/src/prompts.rs`
|
||
- Memory Format section [3800..4500] - system prompt template and examples
|
||
|
||
### Language-Specific Prompt Injection
|
||
Auto-detects programming languages in workspace and injects toolchain guidance.
|
||
|
||
- `crates/g3-cli/src/language_prompts.rs`
|
||
- `LANGUAGE_PROMPTS` [12..19] - static array of (lang_name, extensions, prompt_content)
|
||
- `detect_languages()` [22..32] - scans workspace for language files
|
||
- `get_language_prompts_for_workspace()` [88..108] - returns formatted prompt for detected languages
|
||
- `scan_directory_for_extensions()` [42..77] - recursive scan with depth limit (2), skips hidden/vendor dirs
|
||
|
||
- `prompts/langs/` - directory for language prompt markdown files
|
||
- `racket.md` - Racket/raco toolchain guidance (compilation, testing, analysis, profiling)
|
||
|
||
- `crates/g3-cli/src/project_files.rs`
|
||
- `combine_project_content()` [89..106] - now accepts `language_content` parameter
|
||
|
||
To add a new language:
|
||
1. Create `prompts/langs/<lang>.md` with toolchain guidance
|
||
2. Add entry to `LANGUAGE_PROMPTS` in `language_prompts.rs` with extensions
|
||
|
||
### Agent-Specific Language Prompts
|
||
Injects agent+language-specific guidance when running in agent mode in a workspace with detected languages.
|
||
|
||
- `crates/g3-cli/src/language_prompts.rs`
|
||
- `AGENT_LANGUAGE_PROMPTS` [21..26] - static array of (agent_name, lang_name, prompt_content) tuples
|
||
- `get_agent_language_prompt()` [115..121] - looks up prompt for specific agent+lang combo
|
||
- `get_agent_language_prompts_for_workspace()` [124..137] - uses `detect_languages()` then looks up agent-specific prompts
|
||
|
||
- `crates/g3-cli/src/agent_mode.rs`
|
||
- Lines 149-159 - calls `get_agent_language_prompts_for_workspace()` and appends to system prompt
|
||
|
||
- `prompts/langs/<agent>.<lang>.md` - file naming pattern for agent+lang prompts
|
||
- `prompts/langs/carmack.racket.md` - Racket-specific guidance for carmack agent
|
||
|
||
To add a new agent+lang prompt:
|
||
1. Create `prompts/langs/<agent>.<lang>.md`
|
||
2. Add entry to `AGENT_LANGUAGE_PROMPTS` in `language_prompts.rs` with `include_str!`
|
||
|
||
### MockProvider for Testing
|
||
Configurable mock LLM provider for integration testing without real API calls.
|
||
|
||
- `crates/g3-providers/src/mock.rs`
|
||
- `MockProvider` [220..320] - mock provider with response queue, request tracking
|
||
- `MockResponse` [35..200] - configurable response with chunks and usage
|
||
- `MockChunk` [45..100] - individual streaming chunk (content, finished, tool_calls)
|
||
- `scenarios` module [410..480] - preset scenarios: `text_only_response()`, `multi_turn()`, `tool_then_response()`
|
||
|
||
- `crates/g3-core/tests/mock_provider_integration_test.rs`
|
||
- `test_butler_bug_scenario()` - reproduces consecutive user messages bug
|
||
- `test_text_only_response_saves_to_context()` - verifies text responses saved
|
||
- `test_multi_turn_text_only_maintains_alternation()` - verifies user/assistant alternation
|
||
|
||
Usage pattern:
|
||
```rust
|
||
let provider = MockProvider::new()
|
||
.with_response(MockResponse::text("Hello!"));
|
||
let mut registry = ProviderRegistry::new();
|
||
registry.register(provider);
|
||
let agent = Agent::new_for_test(config, NullUiWriter, registry).await?;
|
||
``` |