Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
464 changes: 464 additions & 0 deletions src/agent/src/_index.yaml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/agent/src/agent.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ type CompiledAgentSpec = {
tools: {[string]: UnifiedTool},
memory: {string},
memory_contract: table?,
agent_options: table?,
prompt_funcs: {table},
step_funcs: {table},
tool_wrappers: {table},
bindings: {[string]: {table}}?,
}

type TokenUsage = {
Expand All @@ -48,10 +51,13 @@ type AgentRunner = {
tools: {[string]: UnifiedTool},
memory: {string},
memory_contract: table?,
agent_options: table,
system_prompt: string,
system_message: table,
prompt_funcs: {table},
step_funcs: {table},
tool_wrappers: {table},
bindings: {[string]: {table}},
total_tokens: TokenUsage,
}

Expand Down Expand Up @@ -414,9 +420,12 @@ function agent.new(compiled_spec: any): (any, string?)
tools = compiled_spec.tools or {},
memory = compiled_spec.memory or {},
memory_contract = compiled_spec.memory_contract,
agent_options = compiled_spec.agent_options or {},
system_prompt = compiled_spec.prompt or "",
prompt_funcs = compiled_spec.prompt_funcs or {},
step_funcs = compiled_spec.step_funcs or {},
tool_wrappers = compiled_spec.tool_wrappers or {},
bindings = compiled_spec.bindings or {},
total_tokens = {
prompt = 0,
completion = 0,
Expand Down
109 changes: 103 additions & 6 deletions src/agent/src/agent_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ local function define_tests()
memory = { "You are designed for testing" },
memory_contract = nil,
prompt_funcs = {},
step_funcs = {}
step_funcs = {},
tool_wrappers = {}
}

local compiled_spec_with_aliases = {
Expand Down Expand Up @@ -84,7 +85,8 @@ local function define_tests()
memory = {},
memory_contract = nil,
prompt_funcs = {},
step_funcs = {}
step_funcs = {},
tool_wrappers = {}
}

local compiled_spec_with_delegates = {
Expand Down Expand Up @@ -126,7 +128,8 @@ local function define_tests()
memory = {},
memory_contract = nil,
prompt_funcs = {},
step_funcs = {}
step_funcs = {},
tool_wrappers = {}
}

local compiled_spec_with_tool_schemas = {
Expand Down Expand Up @@ -168,7 +171,8 @@ local function define_tests()
memory = {},
memory_contract = nil,
prompt_funcs = {},
step_funcs = {}
step_funcs = {},
tool_wrappers = {}
}

local compiled_spec_with_memory = {
Expand Down Expand Up @@ -205,7 +209,48 @@ local function define_tests()
}
},
prompt_funcs = {},
step_funcs = {}
step_funcs = {},
tool_wrappers = {}
}

local compiled_spec_with_tool_wrappers = {
id = "tool-wrapper-agent",
name = "Tool Wrapper Agent",
description = "Agent with trait-owned tool wrappers",
model = "gpt-4o-mini",
prompt = "You are an agent with tool wrappers.",
tools = {},
memory = {},
memory_contract = nil,
agent_options = {
compact = {
token_threshold = 16000,
function_id = "agent.compact:test"
},
checkpoint = {
token_threshold = 32000
}
},
prompt_funcs = {},
step_funcs = {},
tool_wrappers = {
{
id = "audit_tools",
trait_id = "audit_trait",
phases = { "before_execute", "after_execute" },
binding = "test.wrapper:audit_provider",
priority = 20,
strict = true,
context = {
agent_id = "tool-wrapper-agent",
trait_id = "audit_trait",
policy = "readonly"
},
options = {
max_calls = 4
}
}
}
}

before_each(function()
Expand Down Expand Up @@ -517,6 +562,8 @@ local function define_tests()
test.eq(test_agent.system_prompt, "You are a helpful test agent.\n\n## Your memory contains:\n- You are designed for testing")
test.not_nil(test_agent.tools["calculator"])
test.not_nil(test_agent.tools["weather"])
test.not_nil(test_agent.tool_wrappers)
test.eq(#test_agent.tool_wrappers, 0)
test.eq(test_agent.total_tokens.total, 0)
end)

Expand All @@ -543,6 +590,35 @@ local function define_tests()
test.eq(test_agent.temperature, 0)
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 preserve tool wrapper specs from compiled specification", function()
local test_agent = agent.new(compiled_spec_with_tool_wrappers)

test.not_nil(test_agent)
test.not_nil(test_agent.tool_wrappers)
test.eq(#test_agent.tool_wrappers, 1)

local wrapper = test_agent.tool_wrappers[1]
test.eq(wrapper.id, "audit_tools")
test.eq(wrapper.binding, "test.wrapper:audit_provider")
test.eq(wrapper.phases[1], "before_execute")
test.eq(wrapper.phases[2], "after_execute")
test.is_true(wrapper.strict)
test.eq(wrapper.context.policy, "readonly")
test.eq(wrapper.options.max_calls, 4)
end)

it("should preserve agent options from compiled specification", function()
local test_agent = agent.new(compiled_spec_with_tool_wrappers)

test.not_nil(test_agent)
test.not_nil(test_agent.agent_options)
test.eq(test_agent.agent_options.compact.token_threshold, 16000)
test.eq(test_agent.agent_options.compact.function_id, "agent.compact:test")
test.eq(test_agent.agent_options.checkpoint.token_threshold, 32000)
end)
end)

Expand Down Expand Up @@ -682,6 +758,27 @@ local function define_tests()
test.is_nil(result.tool_calls) -- Tool calls should be cleared for delegates
end)

it("should keep tool wrapper metadata stable when a delegate redirect is emitted", function()
local redirect_agent_spec = {}
for k, v in pairs(compiled_spec_with_delegates) do
redirect_agent_spec[k] = v
end
redirect_agent_spec.tool_wrappers = compiled_spec_with_tool_wrappers.tool_wrappers
redirect_agent_spec.agent_options = compiled_spec_with_tool_wrappers.agent_options

local test_agent = agent.new(redirect_agent_spec)
local prompt_builder = mock_prompt.new()

prompt_builder:add_user("Please analyze this sales data for trends")
local result = test_agent:step(prompt_builder)

test.not_nil(result.delegate_calls)
test.eq(#result.delegate_calls, 1)
test.eq(#test_agent.tool_wrappers, 1)
test.eq(test_agent.tool_wrappers[1].binding, "test.wrapper:audit_provider")
test.eq(test_agent.agent_options.compact.token_threshold, 16000)
end)

it("should preserve tool calls when no delegates are triggered", function()
local test_agent = agent.new(compiled_spec_with_delegates)
local prompt_builder = mock_prompt.new()
Expand Down Expand Up @@ -1228,4 +1325,4 @@ local function define_tests()
end)
end

return require("test").run_cases(define_tests)
return require("test").run_cases(define_tests)
181 changes: 181 additions & 0 deletions src/agent/src/checkpoint_runtime.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
local contract = require("contract")

type CheckpointBindingSpec = {
id: string?,
trait_id: string?,
kind: string?,
contract: string?,
binding: string?,
context: table?,
options: table?,
priority: number?,
strict: boolean?,
order: number?,
}

type CheckpointPayload = {
host: table,
agent: table?,
reason: string?,
selector: table?,
refs: table?,
run_context: table?,
options: table?,
context: table?,
}

local checkpoint_runtime = {
CONTRACT_ID = "wippy.agent:checkpoint",
}

checkpoint_runtime._contract = nil

local function get_contract(): any
return checkpoint_runtime._contract or contract
end

local function normalize_bindings(bindings: any): {CheckpointBindingSpec}
local raw = bindings
if type(raw) == "table" and type(raw.checkpoint) == "table" then
raw = raw.checkpoint
end

local normalized: {CheckpointBindingSpec} = {}
for _, binding in ipairs(raw or {}) do
if type(binding) == "table" then
normalized[#normalized + 1] = binding
end
end

table.sort(normalized, function(a, b)
local ap = tonumber(a.priority) or 100
local bp = tonumber(b.priority) or 100
if ap == bp then
return (tonumber(a.order) or 0) < (tonumber(b.order) or 0)
end
return ap < bp
end)

return normalized :: {CheckpointBindingSpec}
end

local function copy_payload(payload: CheckpointPayload?): CheckpointPayload
local out = {}
for k, v in pairs(payload or {}) do
out[k] = v
end
return out :: CheckpointPayload
end

local function checkpoint_text(result: any): string?
if type(result) ~= "table" then
return nil
end
local value = result.memory or result.summary
if type(value) == "string" and value ~= "" then
return value
end
return nil
end

local function call_binding(binding_spec: CheckpointBindingSpec, payload: CheckpointPayload): (table?, string?)
local binding = binding_spec.binding
if type(binding) ~= "string" or binding == "" then
return nil, "checkpoint binding is required"
end

local contract_id = binding_spec.contract
if type(contract_id) ~= "string" or contract_id == "" then
contract_id = checkpoint_runtime.CONTRACT_ID
end
if contract_id ~= checkpoint_runtime.CONTRACT_ID then
return nil, "unsupported checkpoint contract: " .. tostring(contract_id)
end

local contract_def, contract_err = get_contract().get(checkpoint_runtime.CONTRACT_ID)
if contract_err or not contract_def then
return nil, "failed to load checkpoint contract: " .. tostring(contract_err or "not found")
end

local opener = contract_def
if opener.with_context then
opener = opener:with_context(binding_spec.context or {})
end

local instance, open_err = opener:open(tostring(binding))
if open_err or not instance then
return nil, "failed to open checkpoint binding: " .. tostring(open_err or "no instance")
end

local result, apply_err = instance:create(payload)

if apply_err then
return nil, tostring(apply_err)
end

if type(result) ~= "table" then
return nil, "checkpoint binding returned no result"
end

if not checkpoint_text(result) then
return nil, "checkpoint binding returned empty memory"
end

return result :: table, nil
end

function checkpoint_runtime.create(bindings: any, payload: any): (table, string?)
local base_payload: CheckpointPayload = copy_payload(payload :: CheckpointPayload?)
local normalized = normalize_bindings(bindings)

local summary = {
applied = 0,
skipped = 0,
result = nil :: table?,
metadata = {},
errors = {},
}

if #normalized == 0 then
return summary, nil
end

if type(base_payload.host) ~= "table" or type(base_payload.host.kind) ~= "string" or base_payload.host.kind == "" then
return summary, "checkpoint host is required"
end

for _, binding in ipairs(normalized) do
local current_payload: CheckpointPayload = copy_payload(base_payload)
current_payload.options = binding.options or {}
current_payload.context = binding.context or {}

local result, err = call_binding(binding, current_payload :: CheckpointPayload)
if err then
local checkpoint_error = {
binding_id = binding.id,
trait_id = binding.trait_id,
error = tostring(err),
strict = binding.strict == true,
}
summary.errors[#summary.errors + 1] = checkpoint_error
if binding.strict == true then
return summary, tostring(err)
end
else
summary.applied = summary.applied + 1
summary.result = result
if result.metadata ~= nil then
summary.metadata[#summary.metadata + 1] = {
binding_id = binding.id,
trait_id = binding.trait_id,
metadata = result.metadata,
}
end
return summary, nil
end
end

return summary, nil
end

return checkpoint_runtime
Loading
Loading