Merge pull request 'feature/save-model-between-chats' (#52) from feature/save-model-between-chats into master

Reviewed-on: https://192.168.8.63/git/darthrubik/drharness/pulls/52
This commit was merged in pull request #52.
This commit is contained in:
2026-08-12 15:41:38 -05:00
9 changed files with 108 additions and 22 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ to it)
* **`Client Layer`**:
* `OllamaClient`: Handles the actual communication with an Ollama-compatible API server via HTTP (using `libcurl`).
* `DebugClient`: A CLI-friendly mock client that reads a mock JSON response from `stdin`, allowing for deterministic testing of harness logic without relying on an external LLM.
* **State Management**: Conversations are serialized to and deserialized from JSON files. This includes the full message history (`history`), the active tool registry (`tools`), and harness configuration (`config`).
* **State Management**: Conversations are serialized to and deserialized from JSON files. This includes the full message history (`history`), the active tool registry (`tools`), and harness configuration (`config` including `base_url`, `model`, `max_tool_iterations`, `verbose`, and `depth`).
## 🛠️ How to Build
+3 -1
View File
@@ -362,8 +362,10 @@ struct HarnessConfig {
size_t max_tool_iterations = 16;
bool verbose = false;
size_t depth = 0;
std::string base_url = "https://192.168.8.63/ollama";
std::string model = "qwen3.6:35b-a3b-128k";
};
DRH_DEFINE_TYPE_OPTIONAL(HarnessConfig, max_tool_iterations, verbose, depth)
DRH_DEFINE_TYPE_OPTIONAL(HarnessConfig, max_tool_iterations, verbose, depth, base_url, model)
struct FunctionCall {
std::string name = "";
+3 -3
View File
@@ -9,15 +9,13 @@
using http_req_cb_t = std::function<std::string(std::string const& old, std::string const& new_str)>;
struct OllamaConfig {
std::string base_url = "https://192.168.8.63/ollama";
std::string model = "qwen3.6:35b-a3b-128k";
std::string auth_token = "";
int timeout_s = 60*10;
};
class OllamaClient : public Client {
public:
explicit OllamaClient(OllamaConfig config = {});
explicit OllamaClient(std::string base_url, std::string model, OllamaConfig config = {});
~OllamaClient() override;
std::expected<Message, std::string> chat(const std::vector<Message>& messages,
@@ -26,6 +24,8 @@ public:
std::expected<std::vector<std::string>, std::string> list_models() const override;
private:
std::string base_url_;
std::string model_;
OllamaConfig config_;
nlohmann::json build_body(const std::vector<Message>& messages,
const std::vector<Tool>& tools) const;
+13 -8
View File
@@ -140,9 +140,13 @@ static HarnessState read_harness_state_or_exit(const char* file_path) {
int main(int argc, char** argv) {
setup_signal_handler();
HarnessConfig harnessConfig = HarnessConfig{
.max_tool_iterations = 1000,
.verbose = true,
};
OllamaConfig cfg;
if (const char* m = std::getenv("OLLAMA_MODEL")) cfg.model = m;
if (const char* u = std::getenv("OLLAMA_URL")) cfg.base_url = u;
if (const char* m = std::getenv("OLLAMA_MODEL")) harnessConfig.model = m;
if (const char* u = std::getenv("OLLAMA_URL")) harnessConfig.base_url = u;
if (const char* a = std::getenv("OLLAMA_AUTHORIZATION_TOKEN")) cfg.auth_token = a;
bool use_debug = false;
@@ -183,7 +187,7 @@ int main(int argc, char** argv) {
// Add more base tools here:
for (const auto& profile : profiles)
expect_or_exit(master_registery.register_tool(make_agent_tool(profile, master_registery, cfg.base_url)));
expect_or_exit(master_registery.register_tool(make_agent_tool(profile, master_registery, harnessConfig.base_url)));
auto consume_arg = [&](){
argc--;
@@ -247,7 +251,7 @@ int main(int argc, char** argv) {
else if (argv[0] == std::string("-M"))
{
consume_arg();
cfg.model = consume_arg();
harnessConfig.model = consume_arg();
}
else if (argv[0] == std::string("-d"))
{
@@ -460,7 +464,7 @@ int main(int argc, char** argv) {
auto create_client = [&](bool debug) -> std::unique_ptr<Client> {
if (debug)
return std::make_unique<DebugClient>();
return std::make_unique<OllamaClient>(cfg);
return std::make_unique<OllamaClient>(harnessConfig.base_url, harnessConfig.model, cfg);
};
// Scan for skills
@@ -690,16 +694,17 @@ int main(int argc, char** argv) {
}
}
Harness harness(create_client(use_debug), agent_tools, {.max_tool_iterations = 1000, .verbose = true});
Harness harness(create_client(use_debug), agent_tools, harnessConfig);
if (!start_new)
{
HarnessState state_data = read_harness_state_or_exit(file_to_open);
harnessConfig = state_data.config;
harness = Harness(create_client(use_debug), master_registery, state_data);
}
std::cerr << "drharness v0.3 | model: " << cfg.model
<< " | " << cfg.base_url << "\n\n";
std::cerr << "drharness v0.3 | model: " << harnessConfig.model
<< " | " << harnessConfig.base_url << "\n\n";
std::string final_prompt;
+5 -4
View File
@@ -8,7 +8,8 @@
static int ollama_clients = 0;
OllamaClient::OllamaClient(OllamaConfig config) : config_(std::move(config)) {
OllamaClient::OllamaClient(std::string base_url, std::string model, OllamaConfig config)
: base_url_(std::move(base_url)), model_(std::move(model)), config_(std::move(config)) {
if (ollama_clients == 0)
curl_global_init(CURL_GLOBAL_DEFAULT);
@@ -121,7 +122,7 @@ std::expected<std::string, std::string> OllamaClient::http_request(const std::st
nlohmann::json OllamaClient::build_body(const std::vector<Message>& messages,
const std::vector<Tool>& tools) const {
nlohmann::json body = {
{"model", config_.model},
{"model", model_},
{"stream", true},
{"messages", messages},
{"stream_options", {
@@ -142,7 +143,7 @@ std::expected<Message, std::string> OllamaClient::chat(const std::vector<Message
const std::vector<Tool>& tools) const {
nlohmann::json body = build_body(messages, tools);
std::string body_str = body.dump();
std::string url = config_.base_url + "/v1/chat/completions";
std::string url = base_url_ + "/v1/chat/completions";
Message total{};
Usage usage{};
@@ -250,7 +251,7 @@ std::expected<Message, std::string> OllamaClient::chat(const std::vector<Message
}
std::expected<std::vector<std::string>, std::string> OllamaClient::list_models() const {
std::string url = config_.base_url + "/api/tags";
std::string url = base_url_ + "/api/tags";
auto response_result = http_request(url, "GET", "");
if (!response_result) {
return std::unexpected(response_result.error());
+3
View File
@@ -162,3 +162,6 @@ add_dr_test(test_print_history_multimodal)
# Add encoding tests
add_dr_test(test_encoding)
# Add harness config tests (base_url and model in HarnessConfig)
add_dr_test(test_harness_config)
+14
View File
@@ -455,6 +455,20 @@ function dr_content_text() {
}
export -f dr_content_text
# Get base_url from config in chat file
function dr_config_base_url() {
local chat_file="$1"
jq -r '.config.base_url' "$chat_file"
}
export -f dr_config_base_url
# Get model from config in chat file
function dr_config_model() {
local chat_file="$1"
jq -r '.config.model' "$chat_file"
}
export -f dr_config_model
# Resolve paths relative to this script
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# test_harness_config.sh - Tests for HarnessConfig with base_url and model fields
# Test 1: Verify config is saved with base_url and model in new conversations
echo "$(be_done "Response")" | "${DRHARNESS}" \
-F "${TEST_TEMP_DIR}/chat.json" \
-n \
-d \
-m "test" 2>/dev/null
# Verify config contains base_url and model
BASE_URL=$(dr_config_base_url "${TEST_TEMP_DIR}/chat.json")
MODEL=$(dr_config_model "${TEST_TEMP_DIR}/chat.json")
if [ -z "$BASE_URL" ] || [ "$BASE_URL" = "null" ]; then
echo "FAIL: Expected config.base_url to be set"
exit 1
fi
if [ -z "$MODEL" ] || [ "$MODEL" = "null" ]; then
echo "FAIL: Expected config.model to be set"
exit 1
fi
# Test 2: Verify -M flag sets model in config
echo "$(be_done "Response")" | "${DRHARNESS}" \
-F "${TEST_TEMP_DIR}/chat2.json" \
-n \
-d \
-M "custom-model" \
-m "test" 2>/dev/null
MODEL2=$(dr_config_model "${TEST_TEMP_DIR}/chat2.json")
if [ "$MODEL2" != "custom-model" ]; then
echo "FAIL: Expected config.model to be 'custom-model', got '${MODEL2}'"
exit 1
fi
# Test 3: Verify backward compatibility - loading file without base_url and model
create_state "${TEST_TEMP_DIR}/old_format.json" user "hello"
echo "$(be_done "Response")" | "${DRHARNESS}" \
-F "${TEST_TEMP_DIR}/old_format.json" \
-d \
-m "test" 2>/dev/null
# Verify the saved file now has base_url and model with defaults
SAVED_BASE_URL=$(dr_config_base_url "${TEST_TEMP_DIR}/old_format.json")
SAVED_MODEL=$(dr_config_model "${TEST_TEMP_DIR}/old_format.json")
if [ -z "$SAVED_BASE_URL" ] || [ "$SAVED_BASE_URL" = "null" ]; then
echo "FAIL: Expected saved config.base_url to be set"
exit 1
fi
if [ -z "$SAVED_MODEL" ] || [ "$SAVED_MODEL" = "null" ]; then
echo "FAIL: Expected saved config.model to be set"
exit 1
fi
echo "All harness config tests passed"
+5 -5
View File
@@ -48,14 +48,14 @@ Tool make_agent_tool(const AgentProfile& profile,
}
ToolRegistry sub_registry = std::move(sub_reg_result.value());
Harness harness(std::make_unique<OllamaClient>(OllamaConfig{
.base_url = base_url,
.model = profile.model,
Harness harness(std::make_unique<OllamaClient>(base_url, profile.model, OllamaConfig{
.timeout_s = 120
}), sub_registry, {
}), sub_registry, HarnessConfig{
.max_tool_iterations = 16,
.verbose = true,
.depth = depth
.depth = depth,
.base_url = base_url,
.model = profile.model
});
auto result = harness.chat(task);