From ce6b3df77b107f3238cffdbb5c6cd462a1ebe731 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Tue, 28 Jul 2026 17:15:45 -0400 Subject: [PATCH 1/2] fix(agent): omit sampling params the agent spec never set AgentRunner defaulted temperature to 0 whenever a compiled spec omitted it, and the runner type made the field non-optional, so a caller had no way to express "send no temperature at all". Claude 4.7+ and Opus/Sonnet 5 reject temperature, top_p and top_k outright with a 400 ("`temperature` is deprecated for this model"), and Anthropic's migration guidance is to omit them and steer via prompting. The fabricated default therefore makes every agent that never asked for a temperature unusable on those models. The spec type already declared temperature as optional; the runner now carries it as optional too and passes it through untouched. Lua omits a key assigned nil in a table constructor, so the options table drops the field on its own once the default stops being invented. An explicit temperature -- including an explicit 0 -- is still forwarded. Adds src/agent/test, mirroring src/llm/test, because the agent package shipped tests with no harness to run them. --- src/agent/src/agent.lua | 8 ++-- src/agent/src/agent_test.lua | 76 +++++++++++++++++++++++++++++++++++- src/agent/test/Makefile | 7 ++++ src/agent/test/_index.yaml | 8 ++++ src/agent/test/wippy.lock | 16 ++++++++ src/agent/test/wippy.yaml | 7 ++++ 6 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/agent/test/Makefile create mode 100644 src/agent/test/_index.yaml create mode 100644 src/agent/test/wippy.lock create mode 100644 src/agent/test/wippy.yaml diff --git a/src/agent/src/agent.lua b/src/agent/src/agent.lua index 1802395..5fae9d5 100644 --- a/src/agent/src/agent.lua +++ b/src/agent/src/agent.lua @@ -46,7 +46,7 @@ type AgentRunner = { description: string, model: string, max_tokens: number, - temperature: number, + temperature: number?, thinking_effort: number, tools: {[string]: UnifiedTool}, memory: {string}, @@ -67,7 +67,6 @@ local AGENT_CONFIG = { defaults = { model = "", max_tokens = 512, - temperature = 0, thinking_effort = 0 }, memory = { @@ -415,7 +414,10 @@ function agent.new(compiled_spec: any): (any, string?) description = compiled_spec.description, model = compiled_spec.model or AGENT_CONFIG.defaults.model, max_tokens = compiled_spec.max_tokens or AGENT_CONFIG.defaults.max_tokens, - temperature = compiled_spec.temperature or AGENT_CONFIG.defaults.temperature, + -- Left nil when the spec omits it: Claude 4.7+ and Opus/Sonnet 5 reject + -- temperature, top_p and top_k outright, and a value the author never + -- asked for is not ours to invent. + temperature = compiled_spec.temperature, thinking_effort = compiled_spec.thinking_effort or AGENT_CONFIG.defaults.thinking_effort, tools = compiled_spec.tools or {}, memory = compiled_spec.memory or {}, diff --git a/src/agent/src/agent_test.lua b/src/agent/src/agent_test.lua index 5dfede0..2642cee 100644 --- a/src/agent/src/agent_test.lua +++ b/src/agent/src/agent_test.lua @@ -587,13 +587,87 @@ local function define_tests() test.not_nil(test_agent) test.eq(test_agent.model, "") test.eq(test_agent.max_tokens, 512) - test.eq(test_agent.temperature, 0) + test.is_nil(test_agent.temperature) test.eq(test_agent.thinking_effort, 0) test.is_nil(next(test_agent.tools)) -- Empty tools table test.not_nil(test_agent.tool_wrappers) test.eq(#test_agent.tool_wrappers, 0) end) + it("should carry an explicit temperature through to the model options", function() + local spec = { + id = "explicit-temp-agent", + name = "Explicit Temp Agent", + description = "Sets its own temperature", + prompt = "You are explicit.", + temperature = 0.7 + } + + local test_agent = agent.new(spec) + test.eq(test_agent.temperature, 0.7) + + local seen + agent._llm = { + generate = function(messages, options) + seen = options + return { result = "done", tokens = {} } + end + } + test_agent:step(mock_prompt.new()) + test.eq(seen.temperature, 0.7) + end) + + it("should carry an explicit zero temperature rather than treating it as unset", function() + local spec = { + id = "zero-temp-agent", + name = "Zero Temp Agent", + description = "Asks for greedy decoding on purpose", + prompt = "You are deterministic.", + temperature = 0 + } + + local test_agent = agent.new(spec) + test.eq(test_agent.temperature, 0) + + local seen + agent._llm = { + generate = function(messages, options) + seen = options + return { result = "done", tokens = {} } + end + } + test_agent:step(mock_prompt.new()) + test.eq(seen.temperature, 0) + end) + + it("should omit temperature from model options when the spec does not set one", function() + -- Claude 4.7+ and Opus/Sonnet 5 reject temperature, top_p and + -- top_k outright, so a fabricated default makes every agent that + -- never asked for one unusable on those models. + local spec = { + id = "unset-temp-agent", + name = "Unset Temp Agent", + description = "Leaves sampling to the model", + prompt = "You are unset." + } + + local test_agent = agent.new(spec) + + local seen + agent._llm = { + generate = function(messages, options) + seen = options + return { result = "done", tokens = {} } + end + } + test_agent:step(mock_prompt.new()) + + test.not_nil(seen) + test.is_nil(seen.temperature) + test.is_nil(seen.top_p) + test.is_nil(seen.top_k) + end) + it("should preserve tool wrapper specs from compiled specification", function() local test_agent = agent.new(compiled_spec_with_tool_wrappers) diff --git a/src/agent/test/Makefile b/src/agent/test/Makefile new file mode 100644 index 0000000..01145a4 --- /dev/null +++ b/src/agent/test/Makefile @@ -0,0 +1,7 @@ +test: + wippy run test \ + -o wippy.llm:process_host:default=wippy.terminal:host \ + -o wippy.llm:env_storage:default=app:env_storage + +lint: + wippy lint --level error diff --git a/src/agent/test/_index.yaml b/src/agent/test/_index.yaml new file mode 100644 index 0000000..5758056 --- /dev/null +++ b/src/agent/test/_index.yaml @@ -0,0 +1,8 @@ +version: "1.0" +namespace: app + +entries: + - name: env_storage + kind: env.storage.os + meta: + type: envstorage diff --git a/src/agent/test/wippy.lock b/src/agent/test/wippy.lock new file mode 100644 index 0000000..17b8285 --- /dev/null +++ b/src/agent/test/wippy.lock @@ -0,0 +1,16 @@ +directories: + modules: .wippy + src: .. +modules: + - name: wippy/llm + version: 0.4.7 + hash: 2e55afcf55c441a8e79e3737691e5721d06138b9a3a46c7fb87199db6a5dd426 + - name: wippy/terminal + version: 0.4.2 + hash: 34b99aac2eba9c5016fd02c783117fb2b0381ce14696c7b199265458b99a3271 + - name: wippy/test + version: 0.4.10 + hash: 5bc5ffe1b71889c0610133d8843451e5bf80e93ff43712b9017c2c0955fb023d +replacements: + - from: wippy/llm + to: ../../llm/src diff --git a/src/agent/test/wippy.yaml b/src/agent/test/wippy.yaml new file mode 100644 index 0000000..ea9e75b --- /dev/null +++ b/src/agent/test/wippy.yaml @@ -0,0 +1,7 @@ +organization: wippy +module: agent-test +type: application +description: Test stub for agent module +exclude_meta: + type: + - e2e From 9c8eacf467db608f1a98e351405d5e08ff9685b0 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Tue, 28 Jul 2026 17:24:52 -0400 Subject: [PATCH 2/2] ci(agent): run the agent module in lint and test matrices The agent package shipped tests that no harness and no CI job executed. With src/agent/test in place the module joins both matrices. Linting it for the first time surfaced 7 type errors in compiler.lua, fixed here: - set_from_list and behaviors_from_trait accumulate into untyped locals, so their inferred returns ({[string]: true}, {[integer]: table}) did not match the declared ones. Both locals now carry the declared type. - normalize_raw_binding already validates that contract, binding and kind are non-empty strings, but returned an untyped table, so every field read downstream degraded to any and no BindingSpec built from one type-checked. It now returns NormalizedBinding, threaded through collect_raw_bindings and raw_bindings_from_trait. - Binding ids arrive as registry data of any type against a string? field. optional_id carries a value through as a string and leaves nil alone, shared by the three sites that build a spec. --- .github/workflows/ci.yml | 2 ++ Makefile | 2 +- src/agent/src/compiler/compiler.lua | 46 +++++++++++++++++++++-------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aaf49b3..42898db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: matrix: module: - actor + - agent - bootloader - embeddings - facade @@ -55,6 +56,7 @@ jobs: matrix: module: - actor + - agent - bootloader - embeddings - facade diff --git a/Makefile b/Makefile index 9d76fb4..cac3da0 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PACKAGES_FLAT = migration embeddings facade views security docs terminal test PACKAGES_SRC = actor agent bootloader llm relay usage # Modules that have test directories with wippy.lock -TEST_MODULES = actor bootloader embeddings facade llm migration relay usage views +TEST_MODULES = actor agent bootloader embeddings facade llm migration relay usage views .PHONY: help check-manifests run-tests run-lint install publish-all \ publish-flat-% publish-src-% diff --git a/src/agent/src/compiler/compiler.lua b/src/agent/src/compiler/compiler.lua index 9f6207a..1ef786c 100644 --- a/src/agent/src/compiler/compiler.lua +++ b/src/agent/src/compiler/compiler.lua @@ -235,7 +235,29 @@ local function normalize_binding_phases(raw_binding: any): {string} return phases :: {string} end -local function normalize_raw_binding(kind_hint: any, raw_binding: any): any? +-- Binding ids reach the compiler from registry data, so they carry whatever +-- type the author wrote. BindingSpec.id is a string, and tostring keeps the +-- authored value legible rather than discarding it. +local function optional_id(value: any): string? + if value == nil then + return nil + end + return tostring(value) +end + +type NormalizedBinding = { + id: string?, + kind: string, + contract: string, + binding: string, + phases: {string}, + context: table, + options: table, + priority: number, + strict: boolean, +} + +local function normalize_raw_binding(kind_hint: any, raw_binding: any): NormalizedBinding? if type(raw_binding) ~= "table" then return nil end @@ -255,10 +277,10 @@ local function normalize_raw_binding(kind_hint: any, raw_binding: any): any? end return { - id = raw_binding.id or raw_binding.name, - kind = kind, - contract = contract_id, - binding = binding_id, + id = optional_id(raw_binding.id or raw_binding.name), + kind = kind :: string, + contract = contract_id :: string, + binding = binding_id :: string, phases = normalize_binding_phases(raw_binding), context = type(raw_binding.context) == "table" and raw_binding.context or {}, options = type(raw_binding.options) == "table" and raw_binding.options or {}, @@ -267,7 +289,7 @@ local function normalize_raw_binding(kind_hint: any, raw_binding: any): any? } end -local function collect_raw_bindings(out: {any}, kind_hint: any, raw_bindings: any) +local function collect_raw_bindings(out: {NormalizedBinding}, kind_hint: any, raw_bindings: any) if type(raw_bindings) ~= "table" then return end @@ -293,8 +315,8 @@ local function collect_raw_bindings(out: {any}, kind_hint: any, raw_bindings: an end end -local function raw_bindings_from_trait(trait_def: any): {any} - local out = {} +local function raw_bindings_from_trait(trait_def: any): {NormalizedBinding} + local out: {NormalizedBinding} = {} if type(trait_def) ~= "table" then return out end @@ -333,7 +355,7 @@ local function list_from_map_or_array(value: any): {string} end local function set_from_list(values: {string}): {[string]: boolean} - local out = {} + local out: {[string]: boolean} = {} for _, value in ipairs(values) do out[value] = true end @@ -341,7 +363,7 @@ local function set_from_list(values: {string}): {[string]: boolean} end local function behaviors_from_trait(trait_def: any): {any} - local out = {} + local out: {any} = {} if type(trait_def) ~= "table" or type(trait_def.behaviors) ~= "table" then return out end @@ -668,7 +690,7 @@ local function append_behavior_lifecycle( local context = deep_copy(base_context) attach_options_to_context(context, options) append_binding_spec(bindings, "lifecycle", { - id = behavior.id, + id = optional_id(behavior.id), kind = "lifecycle", trait_id = trait_id, contract = "wippy.agent:lifecycle", @@ -725,7 +747,7 @@ local function append_behavior_checkpoint( local options = merge_agent_options(base_options, behavior.checkpoint) attach_options_to_context(base_context, options) append_binding_spec(bindings, "checkpoint", { - id = behavior.id, + id = optional_id(behavior.id), kind = "checkpoint", trait_id = trait_id, contract = "wippy.agent:checkpoint",