Repository navigation
feat(harness): agent reading, failure record and tool routing observations for readiness - #262
KarthikAvinashFI wants to merge 28 commits into
Conversation
…cation conversation did not call
…says leave the test world
… from what the agent reports
…the build recorded them
…ver from its message
…d at each attempt
…tools no world record could show
hadarishav
left a comment
There was a problem hiding this comment.
Requesting changes on three safety/correctness issues:
-
Synthetic readiness probing can invoke effectful tools whenever the world logs them. Logging makes an action observable, not safe. Please skip every effectful tool during synthetic probing and record why it was not asked.
-
The five-minute budget is not enforced around the whole request.
target.say()can perform up to eight wrapper calls, each receiving a fresh timeout, so one tool request can greatly exceed the shared deadline. Wrap the entiretarget.say()operation in one timeout based on the remaining budget and add a regression test. -
/work/failure.jsonshares a writable OS boundary with the tested agent, while Platform #3784 uses its domain for run gating. Until the writer is isolated or the record is authenticated, this file must remain diagnostic-only.
Please also coordinate the not_observable reason with Platform #3784's reason table.
The harness-side evidence flow is otherwise coherent, but effectful probes are a release blocker.
…nd each tool request as a whole, keep failure.json diagnostic-only
…ol runs only in the reset test world
|
Round 1 fixes, head
|
hadarishav
left a comment
There was a problem hiding this comment.
The round-one fixes are present: effectful tools are skipped, unexpected returned tools are refused, and Platform treats failure.json as diagnostic only. Three correctness issues remain.
- Service endpoints sharing a final segment produce false routing results.
chat_readiness._tool_names() maps service calls by the endpoint's final path segment and keeps the first mapping. The proxy logs both /users/search and /orders/search as search. I reproduced a real find_orders call credited to find_users, with find_orders marked not_received.
Please preserve a canonical tool identity or full endpoint in proxy records. Treat ambiguous older aliases as unobserved instead of guessing.
- The shared timeout does not stop synchronous tool handlers.
chat_readiness.py:480 wraps target.say() in asyncio.timeout(), but chat_call_runner.py:413 calls world.handle_tool_call() synchronously on the event loop. A real GeneratedWorld handler taking about 200 ms completed successfully under a 10 ms deadline and recorded routed=true.
Please enforce the remaining deadline at a supervised tool-execution boundary that can terminate blocking work. Add a blocking-handler regression; the existing slow-async-wrapper test does not cover this path.
- A repaired candidate can reuse the previous candidate's routing proof.
tool-requests.json is keyed only by tool name and survives material repair attempts in the shared authoring directory. I reproduced a first candidate using /lookup, then a repaired candidate using /changed/lookup; the second sent no request and inherited the first candidate's routed=true.
Please bind reusable evidence to the candidate or bundle fingerprint and tool definition. Keep the shared request/time budget separate. If the candidate changes after the budget is exhausted, report the tool unobserved rather than reuse stale proof.
Verification on this head covered 144 focused cases. One PostgreSQL integration case was skipped and one SQL-adaptation test lacked psycopg. GitHub workflows are green, but these three cases need regressions before approval.
…handlers by the request deadline, reuse tool answers only for the same candidate
… and whether the judge answered
…e, not the job's repository
…ares, not the harness's own
1. Why
Run readiness needs facts that only the harness has: an agent version's own tools when no build has read them, why a check failed, and which tools actually reached the test world during qualification. Without them the platform cannot see a tool a new version added, has to parse the guest log to tell whose failure a check was, and cannot tell a tool routed to the test world from one pointed somewhere else. A tool trace written by the agent itself proves only that the agent says it called a tool, not that the call arrived at the world.
The PR also fixes certify-only for environments whose saved source model was recorded by another harness version: the seed fingerprint covered the harness's own
_alk_tables, which differ by version, so a valid saved World IR was refused withsource_model_fingerprint_mismatch.Companion PR: future-agi #3784 (run readiness part 2) reads the records. Merge order does not matter: each reader falls back to today's behaviour when a file is absent.
2. What changed
harness/hosted_agent_reading.py(new)python -m fi.alk.harness.hosted_agent_reading --source --output: sets up the authoring credentials, runsunderstandon the source, then writesagent.json({sdk_version, agent}) built with the existingbuild_authored_agent. It always exits 0, and a failure leaves no documentharness/hosted_authoring_entrypoint.pyconfigure_authoring_credentials()so the reading reuses itharness/failure_record.py(new)/work/failure.json(domainharness, world or agent;phase,stage,code,process, and one redacted line of the harness's own text) from the typedProcessRuntimeErrorand the structured diagnostics the failure carries, never from the agent's output; a bare runtime error the harness raised with its own typed code text is read as the harness's. A later failure replaces it; a passing validation removes it. Diagnostic only: the tested agent shares the sandbox's OS user and can write/work, so nothing may usedomainto gate a run or decide who owns a failure until the writer is isolated or the record is authenticated; nothing in ALK reads itharness/chat_readiness.pyrecord_tool_routingwritesgeneric-harness/tool-routing-observations.jsonfrom the world's side only. A tool isrouted: truewhen the world's call log (_alk_tool_trace) or the harness's own execution of a returned call shows it arrived. A tool the agent reports calling, whose calls the world logs (a service tool behind the tool proxy, or a tool the bundle's MCP toolbox serves) but never received, isrouted: false,reason: not_received. A reported tool the world keeps no record of (the agent runs it in its own process) is listed underunobservedasno_world_record, never as routed.request_uncovered_toolsthen asks the agent once for each tool not yet shown, driving the request like a scenario turn so returned calls run at the world, and reads the result the same way. A tool the contract says leaves the test world is not asked (reaches_outside), and neither is any tool the contract says has effects (has_effects), whether or not the world logs its calls. Each request has one timeout from the check's remaining budget around the whole turn, however many times the agent is called in itharness/chat_call_runner.py_HostedChatTarget.received: the calls the harness ran at the world for the agent, exposed on the runner after a conversationharness/mcp_toolbox_adapter.pyAcceptheader does not name JSON (toolbox-coresends*/*) getsapplication/jsonadded: the JSON-response server refused every such client with 406 on the production template'smcpversion, so no toolbox agent could load its toolsharness/process_runtime.py_alk_tables as the build recorded them, or leaves them out where it recorded none (_without_later_harness_tables)harness/authoring_runtime_validation.py3. Behavior callouts
/work/failure.jsononly when a validation fails. Certified bundles carrygeneric-harness/tool-routing-observations.json._alk_tool_tracethan the running harness creates now certify instead of failingsource_model_fingerprint_mismatch. A saved model recorded for different agent tables still fails.reaches_outside), and every tool whose contract names effects (has_effects), logged by the world or not: a logged call is observable, not safe, so a synthetic request only ever reads. At most 8 tools and 5 minutes in all for the whole check (TOOL_REQUEST_LIMIT,TOOL_REQUEST_SECONDS), enforced around each whole request, not per agent call: a retry attempt reuses the earlier answers fromgeneric-harness/tool-requests.jsonand asks only within what is left. Each request is a model call on the customer's agent. Tools left over are recordednot_askedand read Partial on the platform, never Blocked._without_later_harness_tableshelper; whichever merges second resolves one identical hunk.4. Tests
Mutation checks (8 mutants on routing, 4 on the seed composition, 3 on the failure domain, each test seen failing with its change reverted) were run locally on earlier commits and are not re-run on this head. Test counts below are counted from the code on this head.
tests/harness/test_tool_routing_observations.py(14 tests, 15 cases)not_received; a tool the agent runs itself isno_world_record; each uncalled tool asked once, a returned call run at the world, a contract-outside tool never asked, a tool with effects never asked (world logs it or not), one request bounded as a whole, an unanswered or ignored request namedtests/harness/test_chat_readiness.py(1 new, 1 updated)tests/harness/test_mcp_toolbox_adapter.py(1 new with 2 cases, 1 updated)*/*or noAcceptlists the tools (real request through the adapter)tests/harness/test_process_runtime.py::test_canonical_seed_reads_the_harness_tables_as_the_build_recorded_them(5 cases)tests/harness/test_failure_record.py(10 tests, 19 cases)tests/harness/test_hosted_agent_reading.py(7)Fixed on 10 Nov after an independent review
test_a_later_attempt_of_the_check_reuses_the_answers_and_stays_within_the_limittest_a_tool_with_effects_is_never_asked_even_when_the_world_logs_its_callshas_effects(wasnot_observable), never askedFixed on 12 Nov after review (
36eaa36f, on dev merged in atd16c7884)has_effects, before any earlier answer is reusedtest_a_tool_with_effects_is_never_asked_even_when_the_world_logs_its_calls(world logs it, and no world record)asyncio.timeoutfrom the remaining budget around the wholetarget.say(); a request that runs out isrequest_failedand the restnot_askedtest_one_request_stays_within_the_budget_however_many_times_the_agent_is_called(slow agent returning a tool call every time: ends inside the 0.5 s budget)/work/failure.jsonshares the agent's OS boundaryfailure_record.pyand above; nothing in ALK readsdomainFixed on 12 Nov after a second review (
88e7204c)During a readiness tool request only the asked tool runs at the world; any other call the agent returns (for example a tool with effects) is refused, not run, and listed under
refused_during_requestsin the routing file. Test:test_a_request_runs_only_the_asked_tool_at_the_world, seen failing with the guard removed. A tool whose contract leaveseffectsempty is treated as read-only, and a callback agent runs its own tools before answering, which the harness cannot stop; the world is reset after the check. The reading module's docstring no longer says the check makes no model call.Reasons this PR writes, for the platform's reason table (future-agi #3784)
In
generic-harness/tool-routing-observations.json: undertools,routed: falsewithreason: not_received; underunobserved,reasonis one ofno_world_record,reaches_outside,has_effects,not_asked,request_failed,not_called.not_observableis no longer written;has_effectsreplaces it. future-agi #3784 maps every one of these reasons to its own words on the tool routing row. Like the tool certification beside it, this file is written inside the check's sandbox, which the tested agent shares; readiness relies on that same boundary for both.4a. What we tested on
Local platform stack on production sandbox specs (production template, 4 CPU, 8192 MB, 23 GB, restricted egress,
svc-control), this branch's 8 Nov commits (up toca45c5f3) overlaid on the template's source;47256eaband7b026f9e(the 9 and 10 Nov bound and record fixes) were not re-run live; no other local change. No run or job ids were recorded for these rows.routed: true; the platform read routing Supported and the pairing readyget_dealreported by the agent,routed: false, not_received; the other four routed. The platform blocked every case using it_alk_tool_trace(built under the actors change)source_model_fingerprint_mismatchget_orderandlist_refundsreported by the agent, no world record: bothno_world_record;issue_refundasked once, not called (before 12 Nov; it names effects, so it is nowhas_effectsand not asked). The platform read routing Partial naming each reason and the pairing ready. The environment's saved model recorded no_alk_tool_trace; it seeded without errorfind_itemsandget_itemboth in the world's call log through the toolbox; routing Supported, pairing ready. Before theAcceptfix, its build failed: the toolbox answered 406 and the agent could not load its toolsget_itempointed at a stand-in inside its own sandboxget_itemreported,routed: false, not_received;find_itemsrouted. The platform read the pairing Blocked, 3 of 3 cases held; the row namesproduction_only, because this build's contract also records a dependency the environment cannot copy for both tools, and that contract finding outranks the routing readingopenai_chatget_itemreadsnot_called: that protocol carries no completed tool results, so the agent cannot report a call the world did not receiveNot tested live: voice agents' routing (a voice check places no call, so qualification records no routing for them).
Re-checked 11 Nov on
7b026f9e(image): on the OpenAI-style catalog fixture (get_itemsent to a stand-in inside the agent's sandbox), a pairing with no certificate yet was certified on this head. Its routing record hasfind_itemsrouted (7 calls) andget_itemunderunobservedasnot_called; the platform reads tool routing Partial, "Not yet shown reaching the test world: get_item (asked, but not called)". So this head does not claim the stand-in tool reaches the world. The Supported reading seen earlier comes from the same agent's certificate recorded on 8 Nov, before this head; a Re-check of an already certified pairing does not certify again.Not tested live: the request budget (
test_requests_past_the_limit_are_recorded_as_not_asked,test_no_request_is_sent_once_the_time_is_spent) and clearing the record per attempt (test_a_record_from_an_earlier_attempt_never_speaks_for_a_later_failure): unit-tested only.Re-checked live 13 Nov with the review fixes (
36eaa36f,88e7204c,ed60aed3) on the same local stack and production sandbox specs: a new candidate was validated (certified) and saved for two chat environments, and the platform read the routing row from the new certificate.update_order_statusnames an updateupdate_order_statusunderunobservedwithhas_effects;get_order(2 calls),list_refunds(5) andissue_refund(1) routed from the qualification conversation itself; the platform row reads Partial, "its contract says it changes something, so the check did not call it"apply_discount,post_message,request_discountunderunobservedwithreaches_outside;get_deal(1 call) andread_channel(5) routed88e7204c)refused_during_requestsrefused_during_requests; unit-tested (test_a_request_runs_only_the_asked_tool_at_the_world)Verified against code on 7b026f9, 10 Nov
5. Commands to run the tests
Running
test_authoring_runtime_validation.py,test_workflow_intent.py,test_chat_readiness.pyandtest_hosted_authoring_entrypoint.pyin one session showed 8 failures and then hung in a local run, as it did on cleandev; each file passed on its own. Not re-checked on this head.6. Scope
Not in this PR: a world-side record for tools that run inside the agent's own process. The world sees their data but no call, so they stay "no world record".
7. Certificates record what made them (harness version, frameworks, judge)
The platform needs to tell a certificate made by an earlier harness, or with another framework version, from a current one, and to know whether the judge answered during the check, so judged cases are not run when it cannot grade them.
harness/certification.pyCertificationRuntime.harness_version,.frameworksandCertificationChecks.judge_reachable, serialized only when setharness/authoring_runtime_validation.py_write_generic_certificate(..., source)fills the harness version (fi.alk.__version__, none for a0.0.0+dev build) and the agent's frameworks;_with_judge_checkrunsjudge.judge_reachable()only when the catalogue has a judged sub-goal. The job's repository is read into its own name (origin), so the agent'ssourcereaches the framework readerharness/generated_runtime.pyinstalled_frameworks(source)reads the agent's.venvwhen the source has one. Otherwise it reads thepython3the agent is installed into, counting only the frameworks the agent's dependency files name (requirements*.txt,pyproject.toml,setup.py,setup.cfg,Pipfile): that interpreter also holds the harness's own dependencies.opentelemetry-apiis a span marker, not a frameworkharness/judge.pyjudge_reachable(): one 5-token request to the judge's model through the gateway, 20 s,passedorfailed; any error readsfailedBehavior callouts:
runtime.harness_version,runtime.frameworksand, with judged sub-goals only,checks.judge_reachable. One extra 5-token model call per check with judged sub-goals.claude-agent-sdk,google-adk), which would make every harness dependency bump read as the agent's framework change.Tests (each mutation-checked: dropping the harness version, recording unset fields, running the judge check without judged sub-goals, passing without a gateway, counting
opentelemetry-api, recording undeclared interpreter frameworks: all fail the test):test_certification.py::test_old_certificate_keeps_its_fingerprint::test_certificate_records_harness_and_frameworks::test_judge_check_runs_only_with_judged_sub_goals(2)::test_judge_unreachable_without_a_gatewayfailed::test_frameworks_are_read_from_the_agents_environment.venv,opentelemetry-apileft out::test_the_interpreter_counts_only_the_frameworks_the_agent_declarestest_authoring_runtime_validation.py::test_generic_certificate_combines_runtime_evidence_and_repair_history(2)Break-it pass (throwaway tests): a broken
dist-info, a source that is a file,python3returning a list or timing out, an old certificate JSON without the fields, an invalidjudge_reachablevalue: all held.Live, on the production-spec local stack: a Validate of a reviewed change (validation job
f5f21e1e) wroteruntime: {"harness_version": "0.3.7", ...}andchecks.judge_reachable: "passed"; the platform then read Stale "Checked with harness 0.3.7; runs now use 0.3.8" once a newer version was recorded, and a Re-check certified the pairing again (de67f195). That live run also showed the interpreter fallback recording the harness's ownclaude-agent-sdkandgoogle-adkfor an agent that uses neither, which the dependency-file rule above fixes.Tickets and plan
Part of TH-8874