File size: 5,879 Bytes
98acb70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
-- 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