99 Commits

Author SHA1 Message Date
hal9000 afbf1c5484 issue 215: Use OpenAI official API to list available models
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.
2026-08-12 19:21:00 -05:00
darthrubik 6c1906679d Merge remote-tracking branch 'origin/master' into feature/save-model-between-chats 2026-08-12 15:41:23 -05:00
hal9000 a3e044712f Move xferinfo_cb to ollama_client.cpp to avoid exposing libcurl in header
- 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
2026-08-06 21:22:10 +00:00
darthrubik 618976e316 Adding the ability to cancel an agent midstream 2026-08-05 18:11:32 -05:00
hal9000 3e99b37683 Move base_url and model into HarnessConfig (issue 186)
- 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
2026-08-04 15:13:37 -05:00
darthrubik d655a9d953 Merge remote-tracking branch 'origin/master' into feature/rebase 2026-07-23 16:59:40 -05:00
darthrubik 11484fec1e Adding the ability to handle non utf8 encodings 2026-07-10 15:29:08 -05:00
hal9000 da919f9f6e Merge origin/master into work branch (issue #169)
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)
2026-06-15 22:52:29 +00:00
hal9000 030e3f7656 Implement issue 167: Handle multimodal content in print_history pretty-print mode
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.
2026-06-15 17:31:12 -05:00
hal9000 2824b89643 Implement issue 161: Add unit tests for read_multi_modal tool
- 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
2026-06-15 22:02:58 +00:00
darthrubik 0020669eb0 Adding read multi-modal tool 2026-06-15 16:53:42 -05:00
darthrubik 79218deb2f Refactor to use Content for tool calls 2026-06-15 16:53:42 -05:00
hal9000 0772fd5fe8 Implement issue 159: Add new overload to Harness::chat for multimodal input
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.
2026-06-15 16:28:38 -05:00
darthrubik 45ff22302e Removing custom json parser/exporter 2026-06-15 15:53:57 -05:00
hal9000 7d374a4172 Implement multimodal message construction for -i flag (#155)
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
2026-06-12 07:17:11 -05:00
hal9000 0ca0305ebf feat: Add ImageUrl struct and image_url support to ContentPart (#152)
- 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
2026-06-12 07:17:08 -05:00
hal9000 068557b06c Implement issue 154: CLI -i flag with image file reading
- 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
2026-06-12 07:16:40 -05:00
hal9000 ae0462baea Implement issue 151: Message::content as Variant (text-only ContentPart)
- 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
2026-06-12 07:16:40 -05:00
hal9000 ce40d1fe9d Issue 153: Add base64 encoding utility
- 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
2026-06-12 07:16:40 -05:00
hal9000 ffcc907a99 Add std::variant JSON serialization infrastructure (issue 150)
- 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
2026-06-11 21:13:10 +00:00
darthrubik ea8582644e Merge branch 'master' into feature/rebase (issue #140) 2026-06-04 16:07:42 -05:00
darthrubik 9a29bf727c Fixing usage 2026-06-04 15:48:07 -05:00
darthrubik 2cdb06cffe Fixup tool calling 2026-06-04 15:23:32 -05:00
darthrubik a06aa531f6 Refactor (partially broken) to stream 2026-06-03 17:12:36 -05:00
darthrubik 2f0a2cb089 Refactor Client::chat to return Message instead of ChatResponse
- 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
2026-06-03 16:14:07 -05:00
darthrubik eee99a53c9 Replace std::stoul with custom string_toul wrapper
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.
2026-06-01 19:11:32 -05:00
darthrubik 7fe1656c7d Add edge case tests for history editing feature (issue 114)
- 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
2026-05-30 19:49:05 -05:00
darthrubik 51c911df8d Fix issue 112: Make test flags minimal hooks
- 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
2026-05-30 18:14:15 -05:00
darthrubik e129fcc99a Issue #110: Remove thick debug flags from main.cpp
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.
2026-05-30 14:38:20 -05:00
darthrubik 271ac96100 Refactor: Extract shared utilities from rebase_engine and editor
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.
2026-05-29 16:30:54 -05:00
darthrubik b507436801 Implement issue #100: Full Interactive Edit Flow and Integration Tests
- 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
2026-05-29 16:18:00 -05:00
darthrubik 1d799cc05d Implement build_edit_prompt for history editing (issue #99)
- 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
2026-05-29 15:43:47 -05:00
darthrubik a1aa63b260 Implement RebaseEngine::apply_rebase() for issue #98
- 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
2026-05-29 15:31:49 -05:00
darthrubik a1e0a299cd Implement HistoryEditor::build_rebase_content() for issue 96
- 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
2026-05-29 15:13:25 -05:00
darthrubik adc4be09ce Implement RebaseEngine::parse_rebase_file() for issue #95
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.
2026-05-29 15:05:57 -05:00
darthrubik b327df20a2 refactor: remove std::any from codebase (issue #90)
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.
2026-05-29 14:11:09 -05:00
darthrubik 8f12972492 Fix: Exception safety in json_to helpers and add deeply nested type test
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.
2026-05-28 15:57:17 -05:00
darthrubik 73cc7668e2 Remove duplicated logic in JSON macros (issue 85)
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.
2026-05-28 15:43:02 -05:00
darthrubik c0d14de8d8 Implement json_dispatch_map and macro auto-registration (issue #80)
- Add #include <unordered_map> and #include <functional> to json_types.hpp
- Declare inline dispatch map for -R debug flag
- Add auto-registration IIFE to DRH_DEFINE_TYPE_REQUIRED macro
- Add auto-registration IIFE to DRH_DEFINE_TYPE_OPTIONAL macro
- Add auto-registration IIFE to DRH_DEFINE_TYPE_MIXED macro
- Add manual registration for vector<FunctionCall> (cannot use macros)
- Replace ~140-line if-else chain in main.cpp with simple map lookup

All 30 tests pass.
2026-05-28 15:23:42 -05:00
darthrubik 2fbccadab8 issue 76: Convert HarnessState to use DRH_DEFINE_TYPE_MIXED macro
- 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.
2026-05-28 14:20:53 -05:00
darthrubik 3461a92ce2 Replace hand-written json_to<Choice>() with DRH_DEFINE_TYPE_MIXED macro
- 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.
2026-05-28 14:15:51 -05:00
darthrubik a741891048 Issue #74: Convert Message to use DRH_DEFINE_TYPE_MIXED macro
- Remove hand-written json_to<Message>() template specialization
- Remove custom to_json() member method from Message struct
- Add DRH_DEFINE_TYPE_MIXED(Message, (role), (content, reasoning, reasoning_content, tool_call_id, tool_calls))
- Preserve dump() method as out-of-line member function that delegates to free to_json()
- All 30 tests pass
2026-05-28 14:08:46 -05:00
darthrubik 3b4ecb0cbc Issue #73: Convert MixedFieldsStruct to use DRH_DEFINE_TYPE_MIXED macro
- Replace hand-written json_to<MixedFieldsStruct>() specialization with DRH_DEFINE_TYPE_MIXED macro
- Add tests for valid MixedFieldsStruct deserialization
- Add tests for missing required field errors (name, count, func)
- Add test for nested type error propagation (FunctionCall inside MixedFieldsStruct)
2026-05-28 13:58:04 -05:00
darthrubik de78daa6f7 Implement DRH_DEFINE_TYPE_MIXED macro with tests (issue 72)
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.
2026-05-28 13:47:47 -05:00
darthrubik ba92c6fabd Issue #70: Replace hand-written json_to<ChatResponse> with DRH_DEFINE_TYPE_REQUIRED
- Remove hand-written json_to<ChatResponse>, json_to_ChatResponse, and to_json for ChatResponse
- Replace with DRH_DEFINE_TYPE_REQUIRED(ChatResponse, choices, usage)
- Add has_json_to partial specialization for std::vector<T> when T has json_to
- Remove from_json generation from macros (uses .get_to() which doesn't work with nested custom types)
- Add tests for ChatResponse:
  - Valid choices array and usage succeeds
  - Missing required field returns error
  - Invalid nested Choice in vector returns element-index error
  - Invalid Usage returns field-contextualized error
  - Empty choices array succeeds
2026-05-28 10:19:42 -05:00
darthrubik 6a0dec9854 Implement issue 69: Replace hand-written json_to<ToolCall> with DRH_DEFINE_TYPE_REQUIRED
- Remove hand-written to_json, from_json for ToolCall
- Remove hand-written json_to<ToolCall> specialization
- Remove hand-written has_json_to<ToolCall> specialization
- Add DRH_DEFINE_TYPE_REQUIRED(ToolCall, id, type, function) macro
- Update test to expect field-contextualized error for nested FunctionCall

This validates that the updated macros correctly handle structs with
nested custom types (FunctionCall) with proper error propagation.
2026-05-28 09:35:33 -05:00
darthrubik d3b4b08a43 Issue 68: Update DRH_JSON_FROM_REQUIRED and DRH_JSON_CHECK_OPTIONAL macros to use json_to_field()
- 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.
2026-05-28 09:24:41 -05:00
darthrubik 4b8636d174 fix: remove namespace detail from json_to_field to comply with no-namespace 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.
2026-05-28 09:00:19 -05:00
darthrubik 4cc328a6fc feat: add json_to_field<T>() helper with if constexpr dispatch (issue #67)
- 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
2026-05-28 08:56:28 -05:00
darthrubik a245ab571f feat: Add json_to<std::vector<T>> specialization for recursive nested types
- 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
2026-05-28 08:48:02 -05:00