Changed list_models() to use the OpenAI-compatible /v1/models endpoint
instead of the Ollama-specific /api/tags endpoint. Updated ModelInfo and
ModelListResponse structs to match the OpenAI API response format.
- Remove curl/curl.h include from ollama_client.hpp
- Remove xferinfo_cb static method declaration from OllamaClient class
- Make xferinfo_cb a file-scope static function in ollama_client.cpp
Closes: #195
- Added base_url and model fields to HarnessConfig in json_types.hpp
- Removed base_url and model from OllamaConfig in ollama_client.hpp
- Updated OllamaClient constructor to accept base_url and model as separate parameters
- Updated main.cpp to use HarnessConfig for base_url and model
- Updated agent_tool.cpp to use new OllamaClient constructor
- Added test_harness_config.sh to verify the new config fields
- Updated README.md to document the config fields
Resolved merge conflicts:
- CMakeLists.txt: Added both rebase_engine/utility and base64_util sources
- src/harness.cpp: Combined Content type with proper error handling
- src/main.cpp: Merged includes, test flags, and CLI parsing
- Renamed conflicting flags: -B→-Z (build_rebase), -T→-W (edit_prompt), -b→-K (base64_decode)
- tests/CMakeLists.txt: Added all test targets from both branches
- src/rebase_engine.cpp: Fixed to use get_content_string() for Content variant type
- Updated test files to use new flag names (-Z, -W, -K)
The print_history function (used by -p flag) now properly displays multimodal
content by:
- Displaying text content parts from ContentPart arrays
- Indicating presence of image_url content parts with [Image] marker
- Rendering mixed content types in a human-readable format
All 35 tests pass.
- Add test_tool_read_multi_modal.sh with tests for:
- Successfully reading a PNG image file
- Error handling for non-existent files
- Reading a JPG file (different MIME type)
- Fix json_types.hpp to properly serialize std::optional and std::variant
types by adding serialize_to_json() helper function that dispatches to
the correct serialization method.
- Register new test in tests/CMakeLists.txt
Add a new overload to Harness::chat that takes a std::string as the user
message and a vector<std::string> const& as image paths. The method reads
the image files and converts them to data URIs internally, then constructs
the multimodal message.
This moves all the multimodal message construction logic from main.cpp
into Harness, simplifying main.cpp.
When -i flags are present, construct a multimodal message by combining
the text prompt and encoded images into a vector of ContentPart. The text
becomes a text content part and each image becomes an image_url content part.
Changes:
- Added overloaded chat(const Message&) method to Harness class
- Modified Harness::chat(const std::string&) to delegate to new method
- Updated main.cpp to construct multimodal message when images are present
- Added integration test test_multimodal_message.sh
Acceptance criteria met:
- When -i is used, Message::content is a vector<ContentPart>
- Text prompt is included as a text ContentPart
- Each image is included as an image_url ContentPart
- Backward compatible: when -i is not used, content is a plain string
- Works with -m, -d, -t, and -M flags
- Integration test verifies correct multimodal format in chat.json
- Add ImageUrl struct with url and detail fields
- Extend ContentPart with optional text and image_url fields
- Add std::optional serialization/deserialization support
- Custom to_json for ContentPart that only serializes relevant fields
- Custom json_to<ContentPart> for proper deserialization
- Add test_image_url.sh with comprehensive tests
- All 30 tests pass
- Added read_image_as_data_uri() function to base64_util for reading,
validating, and base64-encoding image files
- Added -i CLI flag to accept image file paths (multiple allowed)
- Added -I debug flag for testing image file reading
- Image files are validated at startup with clear error messages for
missing or unreadable files
- Updated help text and README.md documentation
- Added test_image_flag.sh with tests for PNG, JPEG, GIF formats,
error cases, and multiple -i flags
- Add ContentPart struct with type and text fields
- Change Message::content from std::string to std::variant<std::string, std::vector<ContentPart>>
- Add has_json_to partial specialization for std::variant
- Add helper methods get_content_string() and content_empty() to Message
- Add operator<< and append_to_variant_content() for variant content handling
- Update harness.cpp and ollama_client.cpp to use new variant content API
- All 28 existing tests pass without modification
- Add base64_encode() function to encode binary data to base64 string
- Add base64_decode() function to decode base64 string to binary data
- Add get_mime_type_from_extension() for MIME type detection based on file extension
- Add generate_data_uri() for data URI prefix generation
- Add test flags -B (encode), -b (decode), -T (mime type), -U (data URI)
- Add comprehensive tests in test_base64_encode.sh
- No new dependencies added
- Add #include <variant> header
- Add json_type_for_type() helper for type-based disambiguation
- Add to_json for std::variant that serializes active alternative
- Add variant_try_helper_impl for recursive variant deserialization
- Add json_to_impl specialization for std::variant
- Add TestVariantType (std::variant<std::string, std::vector<FunctionCall>>)
for testing variant serialization
- Register TestVariantType in json_dispatch_map for -R debug flag
- Add compile-time static_assert for TestVariantType
- Add test_json_variant.sh test suite
- Add test to CMakeLists.txt
- Changed Client::chat return type from ChatResponse to Message
- Updated OllamaClient and DebugClient to extract message from response
- Simplified Harness to work directly with Message
- ChatResponse is still parsed internally for Usage info logging
Add string_toul() function in utility.cpp that wraps std::stoul and returns
std::expected<unsigned long, std::string> instead of throwing exceptions.
Replace all uses of std::stoul with string_toul():
- src/main.cpp: 2 occurrences in test flags
- src/rebase_engine.cpp: 1 occurrence in parse_rebase_file()
The wrapper handles null input, empty input, and trailing characters,
converting any exceptions from std::stoul into error strings.
- Add test for editing a message and removing all content (empty content)
- Add test for multiple consecutive edits to the same message (changes stack)
- Add test for editing messages with special characters
- Add test for very long message content
- Add test for editing assistant messages with tool_calls
- Add test for dropping all messages (empty history result)
- Add test for unicode characters in message content
- Add test for whitespace variations in rebase file
- Add test for comments and empty lines mixed with commands
- Add test for special characters and unicode in rebase content display
- Fix apply_rebase to preserve original content when only role is changed
- Add content_edited flag to RebaseLine to track explicit edits
- Changed parse_json_safely to return std::expected<nlohmann::json, std::string>
- Updated all usages to handle the expected return type properly
- Simplified test flags in main.cpp to be minimal hooks without extra validation
- Removed try/catch blocks and explicit error handling from test flags
- All tests pass
Per CONTRIBUTING.md, test flags should be minimal hooks that:
- Accept input, call the internal function, and output the result
- Do NOT contain test logic (assertions, validation, etc.)
Changes:
1. Added json_to<RebaseCommand> and json_to<RebaseLine> serialization
so the -A flag can use json_to<std::vector<RebaseLine>> instead of
60+ lines of manual JSON parsing.
2. Simplified -P flag to output JSON directly instead of formatted
text with switch statements.
3. Added helper functions to run_test.sh for parsing rebase JSON output
(rebase_line_count, rebase_command_at, rebase_index_at, rebase_role_at).
4. Updated test_rebase_parser.sh to use JSON output and helper functions.
The -E, -e, -R, -J, -B, and -T flags are already minimal hooks and
remain unchanged.
Create new utility module (include/utility.hpp, src/utility.cpp) containing
common code that was duplicated between rebase_engine.cpp and editor.cpp:
- trim() function
- random_string() function
- random_file_name() function
- Defer RAII helper class
- spawn_editor() function
Both rebase_engine.cpp and editor.cpp now include utility.hpp instead of
duplicating these functions.
All 33 unit tests pass.
- Add edit_history_interactive() method to HistoryEditor class
- Implements the complete history editing flow:
1. Build rebase content from history
2. Spawn editor for rebase commands
3. Parse and validate rebase commands
4. For each edit command, build edit prompt and spawn editor
5. Apply all changes
- Returns expected<vector<Message>, string>
- Add content field to RebaseLine struct for storing edited content
- Update apply_rebase() to use content field when present
- Wire up -b/--edit-history flag to call edit_history_interactive()
- Program exits after successful edit and saves modified history
- Add 12 integration tests for the edit history feature:
1. Pick all messages (history unchanged)
2. Drop a single message
3. Edit message content
4. Change message role
5. Delete line equals drop (implicit pick)
6. Edit first message (no context)
7. Empty history rejected
8. Invalid command error
9. Invalid index error
10. Invalid role error
11. Multiple edits in one session
12. Edit then continue conversation normally
- Register test_edit_history in CMakeLists.txt
- Add HistoryEditor::build_edit_prompt() to format edit prompts
with previous message context and >8 separator
- Message 0 shows '(no previous message)' instead of context
- Add -T debug flag to test build_edit_prompt
- Extend mock_editor.sh to support sequential invocations via
counter file and staged input files
- Add unit tests for build_edit_prompt functionality
- Register new test in CMakeLists.txt
- Added apply_rebase() method to RebaseEngine class
- Pick commands preserve messages unchanged
- Drop commands remove messages from result
- Edit commands update role while preserving all other fields
- Messages not referenced by any command are preserved (implicit pick)
- Lenient mode: orphaned tool results are allowed
- Added -A test flag for testing apply_rebase
- Added unit tests in test_rebase_apply.sh
- Added HistoryEditor class with build_rebase_content() method
- Formats messages for rebase editor display:
- Normal messages show first line of content
- Assistant messages with tool_calls show [tool_call: function_name]
- Tool messages show first line of result content
- Empty messages show (empty)
- Added header comment block explaining pick/edit/drop commands
- Added -B CLI flag for testing
- Added unit tests covering all display format scenarios
Add rebase parser that validates rebase editor format with:
- RebaseCommand enum (Pick, Edit, Drop)
- RebaseLine struct (command, index, role)
- parse_rebase_file() with strict validation and error aggregation
Validations:
- Invalid commands (not pick/edit/drop)
- Index out of range
- Duplicate indices
- Missing/malformed role brackets
- Invalid role values (must be user/assistant/tool)
- Comment lines (#) and empty lines are skipped
Tests cover all validation scenarios via -P debug flag.
Replace std::any-based parameter passing with compile-time type-safe adapter pattern.
- Add create_tool_execute<T>() template function in tool.hpp
- Remove convert_params field from Tool struct
- Change execute field type from std::function<std::string(const std::any&)>
to std::function<std::string(const nlohmann::json&)>
- Change ToolRegistry::invoke() return type from std::expected to std::string
- Update all tool implementations to use create_tool_execute<ParamsType>()
- Remove #include <any> from tool.hpp and json_types.hpp
- Simplify harness.cpp invoke caller
All 27 tests pass.
Issue 86
1. Added try-catch in json_to_field for primitive types to catch
nlohmann::json::exception and convert to std::unexpected.
2. Added try-catch in json_to_impl::apply for primitive types to catch
nlohmann::json::exception and convert to std::unexpected.
3. Added test for deeply nested type error propagation:
Message → vector<ToolCall> → FunctionCall error propagation.
This ensures that type mismatches (e.g., string where int expected)
return descriptive error messages instead of crashing with exceptions.
Extract the duplicated has_json_to<Type> registration and json_dispatch_map
registration code from DRH_DEFINE_TYPE_REQUIRED, DRH_DEFINE_TYPE_OPTIONAL,
and DRH_DEFINE_TYPE_MIXED into two helper macros:
- DRH_JSON_REGISTER_HAS_JSON_TO(Type)
- DRH_JSON_REGISTER_DISPATCH(Type)
This eliminates code duplication while preserving identical functionality.
- Replace hand-written json_to<HarnessState>() specialization with DRH_DEFINE_TYPE_MIXED macro
- Remove hand-written to_json() free function for HarnessState
- Remove explicit has_json_to<HarnessState> trait registration (now handled by macro)
- Update test to expect new error format that includes field path (more informative)
HarnessState now uses:
DRH_DEFINE_TYPE_MIXED(HarnessState, (history, tools), (config))
All 30 tests pass.
- Remove hand-written json_to<Choice>() specialization and to_json() function
- Add DRH_DEFINE_TYPE_MIXED(Choice, (message), (finish_reason)) macro call
- Update test expectations for nested error propagation to include field path prefix
(e.g., 'field message: missing required field: role' instead of 'missing required field: role')
This change improves consistency with the PRD goal of having all json_to<T>()
implementations use macros, and provides better error messages with field path
information for debugging.
Add DRH_DEFINE_TYPE_MIXED(Type, (required_fields...), (optional_fields...))
macro that generates to_json() and json_to<Type>() for structs with mixed
required and optional JSON fields.
Features:
- Required fields: checks presence, reads via json_to_field<T>() for proper
nested type error propagation
- Optional fields: checks contains() and !is_null() before reading; uses
struct member defaults otherwise
- Generates free to_json() that sets ALL fields (required and optional)
- Automatically registers has_json_to<Type>
- Supports empty field groups: (), () for all-optional/all-required types
- Uses __VA_OPT__ (C++20+) for empty group handling
Added test structs:
- MixedTypeSmokeTest: one required + one optional field
- MixedTypeAllRequiredTest: empty optional group
- MixedTypeAllOptionalTest: empty required group
Updated tests/test_json_type_mixed.sh with smoke tests and edge case tests.
- Update DRH_JSON_FROM_REQUIRED macro to use json_to_field() for proper nested type error handling
- Update DRH_JSON_CHECK_OPTIONAL macro to use json_to_field() for proper nested type error handling
- Add DRH_JSON_FROM_OPTIONAL macro for from_json (void return type) since DRH_JSON_CHECK_OPTIONAL now returns std::expected
- Update DRH_DEFINE_TYPE_OPTIONAL to use DRH_JSON_FROM_OPTIONAL for from_json
Note: json_to_field() is used without namespace prefix to comply with CONTRIBUTING.md no-namespaces policy.
The CONTRIBUTING.md policy states: 'This project does not use namespaces
in C++ code.' Move json_to_field to global scope and update test struct
to call it directly without namespace qualification.
- Add json_to_field<T>() helper in detail namespace for compile-time dispatch
- Uses if constexpr(has_json_to_v<T>) to choose between recursive json_to<T>()
for custom types and direct .get_to() for primitives
- Custom types: calls json_to<T>() and prepends field name to error messages
- Primitive types: uses .get_to() directly (no recursion)
- Add MixedFieldsStruct test type with both primitive and custom type fields
- Add test_json_to_field.sh test suite to verify correct dispatch and error messages
- Add helper class template detail::json_to_impl with partial specialization
for std::vector<T> when T has json_to<T>() specialization
- Primary json_to<T> template delegates to helper class
- Explicit specializations (from macros/manual) take precedence
- Add json_to_vector<T>() convenience function for direct use
- Add -R vector<FunctionCall> test support in main.cpp
- Add test_json_type_vector.sh with comprehensive tests
Closes issue 66