-- The web adapter only supplies a backend and an isolated filesystem. -- Parsing, dispatch, approval, recovery, repetition and eviction remain Lua. local Agent = require('agent') local tools = require('tools') local trace = require('trace') local protocol = require('protocol') local tasks = require('eval.tasks.tasks').tasks local M = {} function M.tasks_json() local out = {} for _, t in ipairs(tasks) do out[#out + 1] = {id=t.id, prompt=t.prompt, tools=t.tools, budget=t.budget or 4096, approve=t.approve == true, script=t.script} end return trace.encode(out) end function M.registry(names, files) local reg = tools.registry() for _, name in ipairs(names) do local spec = {} for k, v in pairs(assert(tools[name], 'unknown tool')) do spec[k] = v end if name == 'read_file' then spec.run = function(a) local body = files[a.path] if body == nil then error('cannot open ' .. a.path .. ': No such file', 0) end if #body > tools.MAX_OBSERVATION then return body:sub(1, tools.MAX_OBSERVATION) .. '\n[truncated]' end return body end elseif name == 'write_file' then spec.run = function(a) -- These keys never become host filesystem paths. if #a.path > 200 or #a.text > 8192 then error('virtual file too large', 0) end local count = 0 for _ in pairs(files) do count = count + 1 end if count >= 32 and files[a.path] == nil then error('virtual file limit reached', 0) end files[a.path] = a.text return string.format('wrote %d bytes to %s', #a.text, a.path) end elseif name == 'list_dir' then spec.run = function(a) local prefix = a.path or '.' prefix = prefix:gsub('^%./', ''):gsub('/$', '') if prefix == '.' then prefix = '' end if prefix ~= '' then prefix = prefix .. '/' end local names, seen = {}, {} for path in pairs(files) do if path:sub(1, #prefix) == prefix then local name = path:sub(#prefix + 1):match('^[^/]+') if name and not seen[name] then names[#names+1] = name; seen[name] = true end end end table.sort(names) return table.concat(names, '\n') end elseif name == 'calc' then local calculate = spec.run spec.run = function(a) if #a.expr > 512 then error('expression exceeds 512 characters', 0) end return calculate(a) end end reg:add(spec) end return reg end function M.run(config_json, files_json, factory) local o = assert(trace.decode(config_json)) local files = assert(trace.decode(files_json)) local task for _, t in ipairs(tasks) do if t.id == o.task_id then task = t end end local names = o.tool_names or (task and task.tools) or {'calc','read_file','list_dir','write_file'} local reg = M.registry(names, files) local backend, tokenizer if o.model == 'scripted' then assert(task and o.prompt == task.prompt, 'Scripted replay requires an unchanged preset task.') local i = 0 backend = {complete=function() i = i + 1 return task.script[i] or 'CALL: {"tool":"answer","args":{"text":"(script exhausted)"}}', {} end} tokenizer = require('bpe').approximate() else backend, tokenizer = factory(reg, config_json) end local old_render = protocol.render_system if o.model == 'tiny' then protocol.render_system = protocol.render_compact end local fatal_on = {} if o.error_recovery == false or o.parse_recovery == false then fatal_on.parse_error = true end if o.error_recovery == false or o.tool_recovery == false then fatal_on.tool_error = true; fatal_on.timeout = true end local approvals = {} local agent = Agent.new({backend=backend, tools=reg, tokenizer=tokenizer, max_steps=o.max_steps or 10, max_tokens=o.context_limit or 4096, repeat_threshold=o.loop_detector == false and 10000 or 3, policy=o.policy or 'elide_observations', fatal_on=fatal_on, approve=function(call) local allowed = o.approval_gate == false or o.approve_write == true approvals[#approvals+1] = {call=call, allowed=allowed, gate_enabled=o.approval_gate ~= false} return allowed end}) local answer, reason, steps = agent:run(o.prompt) protocol.render_system = old_render local evicted = false for _, s in ipairs(steps) do if s.evicted then evicted = true end end local passed, why if task and o.prompt == task.prompt and not o.decoder_trial then -- Preserve the original judges, redirecting their out.txt checks to -- the same in-memory files used by the public tools. local old_open, old_remove = io.open, os.remove io.open = function(path) if files[path] == nil then return nil end return {read=function() return files[path] end, close=function() end} end os.remove = function(path) files[path] = nil; return true end local judge = o.model == 'scripted' and task.check or task.solved local ok, a, b = pcall(judge, answer, steps, {evicted=evicted, reason=reason}) io.open, os.remove = old_open, old_remove if not ok then error(a) end passed, why = a, b end return trace.encode({answer=answer, reason=reason, steps=steps, passed=passed, judge_note=why, evicted=evicted, approvals=approvals, files=files}) end return M