Repository navigation
feat(harness): actors, one world door for agent and actors, on top of dev - #257
KarthikAvinashFI wants to merge 119 commits into
Conversation
… the agent, a dispatcher and pre-run sessions
…es and phone menus
…rs and the no-actor path
…sends the starting task
…enty messages, and a repeated line ends them
…d them the way the agent and the world do
…sations and credits them to their actors
… a grant during the run takes effect at once
…the tool proxy refuses any request naming an actor, and faults live in the world database
…, and an actor repeating its own action takes no more turns
…gh the door, and record changes never touch the harness's own tables
…on's log row is written before its answer
…onnection, and a keyed row is proven written before its answer
…ections, and fix_tool records a field's unit
…for its own calls and for actors
…scenario writers are told the shapes, pre_run and starts_with
…ack, and an actor acts in a service under its identity there
…sted decision instead of standing in for it
… an actor's identity travels as a header, and a one-word name is never read as a person
…ctor acts in a service under its identity
…, the code's is recorded and the person is told
…e agent's calls could name
…ord changes, who talked before the run and who starts it
… is brought in by that record, not a clock
…rvice of the world
|
Pushed 18e8a65: add_actor refuses identity keys that are not a service of the world, so the relationship view and an actor's identity per service work on every build. Test, live builder run and images are in the body under 'Identities keyed by service, 11 Nov'. |
…ough the world, so the run's actor filter finds them
|
Pushed |
…ce; a commit whose answer was lost is named; the builder refuses unknown actor models and shows names as typed
|
Pushed |
…instead of failing its tool call
|
Pushed |
hadarishav
left a comment
There was a problem hiding this comment.
Requesting changes. The actor architecture is directionally sound, but I found several correctness blockers:
-
Keyed world actions are not durably idempotent (
world_door.py:341,364). Evidence lookup/write failures are swallowed, so retrying the same key executes the committed side effect again. I reproduced two executions from one keyed request. Please persist an explicit durable pending/committed/unknown state and add the failing-log retry regression. -
Parallel calls can consume the same one-shot fault (
world_door.py:246,290). Two threads can both selecton_call: 1before either records its row. I reproduced the fault being applied twice. Please atomically reserve the action call number and add a two-thread test. -
Actors can spoof
actor_id(actor_dispatch.py:325,run/conversation.py:446). Voice/scheduled paths only fill a missing ID, while chat forwards it unchanged. I reproduced actorpriyasendingU-admin. For tools declaringactor_id, always replace it with the acting actor's service identity across every channel. -
answered_byfallbacks bypass turn limits (actor_dispatch.py:580). The fallback calls_live_turn()without the normal per-actor/per-trial cap checks. Route it through the same limit guard and test both caps. -
Actor-only evidence can disappear or be marked unmeasured (
call_runner.py:2325,hosted_scheduler.py:2445). I reproduced a committed manager action with no dialogue: the outcome contained the action, but no structured transcript was uploaded. The scheduler also ignores actor actions when deciding whether evidence exists. Upload whenever actor evidence exists and include it in both judging conditions. -
Cycle validation rejects valid configurations (
actors.py:393). It follows only the alphabetically first collection writer, so a valid cycle independently broken by another actor can fail based on actor ID ordering. Validate the full dependency graph.
The current PR also conflicts with latest dev, including actor runtime files. Please resolve the conflicts after these fixes and ping for a re-review of the resolved head. Focused actor tests passed (123); findings 1–5 were validated with direct reproductions.
…ty, turn caps, actor-only evidence and full-graph cycles
… cannot count them
…og row never lets a later door run it again
… trial, and make the key table once
|
Round 1 fixes, head
|
hadarishav
left a comment
There was a problem hiding this comment.
Re-reviewed the current head together with Platform #3764. All six ALK findings from my previous round are fixed, and the focused regressions pass.
I found three remaining correctness blockers and one lower-priority idempotency issue. Please address the inline comments and add the reproduced regressions before the next review.
| names = (call.action, call.name) if self.faults_by_tool else (call.action,) | ||
| return [one for one in faults if one.get("action") in names] | ||
|
|
||
| def _fault(self, call: ToolCall, named: list[dict[str, Any]]) -> dict[str, Any] | None: |
There was a problem hiding this comment.
Fault selection identifies a full action such as crm.update, but numbering and persisted rows use only the basename. I reproduced ticket.update consuming call 1 for crm.update, so an on_call: 1 CRM fault never fired. fault_notices() has the same ambiguity.
Please carry the canonical full action through the log, lock, counter, and notice paths. Keep the basename only for display or unambiguous legacy rows, and add the two-actions-with-the-same-basename regression.
| if trigger is None or trigger.kind != "conditional" or not trigger.collection: | ||
| continue | ||
| names = {f"{trigger.collection}.change", f"{trigger.collection}.put"} | ||
| triggered_by[entry.id] = { |
There was a problem hiding this comment.
This excludes the actor itself as a possible writer, and the SCC result later reports only groups larger than one. I reproduced one actor waiting for an approvals record while being the only actor allowed to call approvals.put; validation returned no cycle even though the actor can never activate under the validator's current policy.
Please include self-writers and reject a one-node SCC when it has a self-edge.
| outcome = "fallback_action" | ||
| else: | ||
| outcome = "answered_by" | ||
| record = { |
There was a problem hiding this comment.
This records outcome="answered_by" before checking whether the substitute has a turn left. If the substitute has reached its cap, no turn occurs but the evidence still says they answered. The new cap test currently asserts that incorrect combination.
Please record answered_by only after the substitute turn actually starts or its mocked action fires. Otherwise record a typed limit-blocked or unresolved outcome.
| if not key: | ||
| return self._run(call, None, on_behalf_of) | ||
| # A key is one actor's own: another actor's identical key is a different action. | ||
| slot = f"{call.actor}\x00{key}" |
There was a problem hiding this comment.
The idempotency namespace is actor plus key, but duplicate detection compares only arguments, not the action. Reusing one key and {} for two different actions returns the first action's result as a duplicate.
Generated dispatcher keys normally avoid this, so I consider it lower priority, but the boundary should persist and compare the canonical action too, returning idempotency_conflict when the same key is reused for another action.
…cles, record answered_by only once the stand-in acts
… reads never count as committed changes
Why
Actors: the simulated people and systems around the agent under test (the caller, a manager, a coworker, a phone menu, a CRM-side system). One record per actor, selected per scenario, speaking on the call or in the chat and acting in the world through the same door as the agent's own tool calls. Design: the actors design reviewed on 6 and 7 Nov.
This branch starts fresh from
devand carries the actors work previously reviewed in #246 and the branches it stacked on, ported as one change, plus the 6 Nov review changes and the fixes from our own review before testing.What changed
actors.pystarts_with.actoroptional (a task-driven run needs no actor)world_door.py_alk_*tables)call_runner.py(runner_door),tool_trace_proxy.py,chat_call_runner.py_alk_trial_faults, none included), read over one kept connection; a trial with no faults is never failed by a database out of reachactor_dispatch.pycall_actor.py,call_runner.py,fi/simulate/simulation/call_actors.py,engines/call_actor_voice.py,engines/livekit.pychat_call_runner.py,run/conversation.pypre_run.py,prove.pycontract.py,tools.py,builder.pyarg_units: the unit or format of a tool field (cents vs dollars), on the contract line every stage readsworld/tools.py,actors.py,amend.py,scenario_tools.pyadd_actorlists and accepts record changes on the world's own collections (approvals.put), so a decision someone makes is theirs to record;fix_toolrecords a field's unit after the contract is writtenchat_call_runner.pyworld_door.py,scenario_tools.py, skillsrecord); scenario writers are told both shapes, and when a request meanspre_runorstarts_withscenario_tools.py,world/tools.py,actor_dispatch.py,run/conversation.py, write-scenariosX-ALK-Actor-Identity(never on a record change, and never from the agent); every live actor is told each action's fieldsactors.py,actor_dispatch.py,scenario_tools.pyfallback_arguments(a decline writes its record); an actor brought in when contacted needs an identity the agent's calls could namepre_run.pythread) is refused with the fix; a session missing its place's action is told the shapeactors.pyactors.jsonon every save and name the record changes and the fields that may name an actor's own action, so the platform checks an edit by the release that wrote itrun/conversation.pycaller_takes_back_after_turnsend it, and the person always takes the next turn and hears every exchangerun/conversation.py,actor_dispatch.py,world_door.pyhosted_scheduler.pyskills/...Behaviour callouts
add_actorrefused record changes), could not record a unit (fix_tool), and the scenario writers folded named people into the caller, skippedpre_runandstarts_with, and gave no identities.contactedneeding an identity (actors plan, testing fix 25), a scenario whose caller is played as a listed actor (actors plan, matched on the structured persona name, not on text), and a missing actor making a call inconclusive (an earlier review round of this work). All were agreed in the actors plan reviewed and approved on 6 and 7 Nov._alk_trial_faultsand_alk_tool_trace, as it already could the world's own tables.Fixes found while testing
dev, a hosted chat bundle seals handlers only forimportandconstructtools, and the chat runner runs harness-run tool calls only through those handlers. A chat agent that returns tool calls for the harness to run, with its tools in a service, has every such call refused ("no such tool"), and actors' actions in that world fail the same way. The chat door now forwards tool actions to the agent's tools service when the run has one. Impact: chat agents of that shape get their tools run; agents with import tools, or that run their own tools, are unchanged.on_toolactivation never came in. The editor names the tool by its world action (crm.apply_discount), and the log records the call by the last part of its route (apply_discount); the dispatcher compared the two as written, and only a scenario's own trigger was mapped. An actor's own trigger is now mapped too, by tool name or world action, on voice and in the world (7d55ab53). Unit-tested on both paths, and passed live on24fab3db(row T1).bed4a54f).a23ce902).24fab3db).773d4a31). It now needs one call that ran in the world; refusals by the world's own rules still count.(db, args)failed every call with a TypeError, because the adopted binding always called it with keywords (2627e0f0). The binding now calls the agent's own function the way its signature expects: keywords,(db, args)with the world's connection, or one arguments dict.23c3be28). Checks getsame_id, which ignores only separators and case, the scenario writer is told to use it, and the proof replays each fixed id typed without separators and refuses a check that fails it as too strict.loadsondata_store, so a one-rule edit showed an extra change in review (54735e28). An edit now writes back only the edit.same_idmatched two empty identifiers (an empty id names no record), and nothing showed that ids which truly differ stay apart (65cd100d). An empty id now never matches; negative controls cover a different prefix, an extra digit, a leading zero, a missing prefix, and a world that keeps the id exactly as typed (where the strict check is right and is kept).81ec42a8). Each fault that was not applied (no world database to hold it, or the agent made that call in its own process, outside the world's door) or never reached (the agent made fewer calls of it) is now sealed on the trial as a plain-language notice, on voice and chat; the platform shows it on the trial's World changes (future-agi #3764).Committed: put, change), since the door logs them that way, as the proof does (e147055f,62c52d60). They now show the full action: the actor's one permitted action ending that way, or, when two collections share the verb (approvals.put,channel_members.put), the action the actor chose, in order. The log and the proof are unchanged, so checks are not affected.00d439b3). Both fault modes are now read from the door's own rows, and the test drives the real door.b5cbe374). The table is read whenever the trial is proxied.afcbc41c). It now carries its fallback arguments on chat and voice, and a record change goes to the records, as the world's dispatcher already did.adeffbd4). Examples are neutral placeholders, and the date replay runs only for a language it can write (English or unstated); for others it is skipped, never guessed.f5f8c6e6). Only the error type is logged or raised. Two docstrings that claimed the agent could never reach the fault table now state the known limit below.36eb8bff). A chat actor now decides knowing what it already did this trial (its scheduled actions included, marked as its own), is told not to repeat or reverse a decision without a new reason, and an action it already committed with the same arguments is never sent again. World-only actors' views mark their own rows the same way.Tested paths for the fixes above
test_private_workflow_fails_when_no_tool_call_reached_the_world(4 cases)chat_tool_evidence_unverified; a world-rule refusal or one success passestest_tool_trace_keeps_the_door_s_outcome_apart_from_a_failureget_deal(db, args)adopted from the agent's codetest_a_route_style_tool_taking_db_and_args_is_called_with_the_world_connectiontest_agent_tool_calling.pytest_a_check_that_compares_a_claim_id_as_text_fails_the_id_typed_without_separatorssame_idholdssame_idedge casestest_same_id_ignores_separators_and_case_but_not_the_number<PREFIX>-<digits>= the same typed without separators or in another case; a different number still differsloadstest_adding_a_rule_does_not_add_an_empty_loads_to_the_data_storeCLM/CLA, extra digit, leading zero, missing prefix, empty)test_same_id_still_tells_apart_identifiers_that_name_different_recordstest_an_exact_comparison_is_kept_when_the_world_treats_the_id_typed_differently_as_another_recordtest_a_fixed_value_that_is_not_an_identifier_is_never_replayed_without_separatorstest_a_fault_the_real_door_applied_says_nothing_in_either_mode(drives the real door)test_a_fault_on_a_call_made_outside_the_door_is_said_not_appliedfault_not_applied, saying whytest_a_fault_the_agent_never_reached_says_how_many_calls_it_madefault_not_triggered: "made 1 call of it"test_a_world_without_a_database_cannot_hold_any_faultfault_not_appliedwith the reasontest_a_call_named_by_another_tool_is_not_taken_for_the_fault_s_actiontest_the_transcript_carries_its_notices_and_omits_them_when_nonetest_an_actor_s_action_is_shown_by_its_full_name_where_its_permissions_name_one,test_an_actor_s_ambiguous_action_takes_its_full_name_from_what_it_choseapprovals.putandchannel_members.puttold apart; ambiguous with no choices kept as logged; agent rows unchanged00d439b3; re-run on head 36eb8bf (run 213c0d21, trial 553a3bef): same 504 then one committed retry, and no noticetest_a_proxied_chat_trial_without_actors_still_reads_the_world_s_tabletest_a_speaking_chat_actor_s_record_fallback_is_taken_with_its_arguments_and_writes_its_recordtest_a_speaking_voice_actor_s_fallback_action_is_reported_with_its_argumentstest_the_spoken_date_replay_runs_only_for_a_language_it_can_writetest_a_world_database_out_of_reach_fails_only_a_trial_with_faults_to_settest_a_chat_actor_decides_knowing_its_own_earlier_actions_in_the_trialtest_a_chat_actor_never_sends_an_action_it_already_committed_againapprovals.put, the rejection posted, no repeats (an earlier head wrote approvals 11 times on run fbc081df)approvals.put, notputcancellation_approvals.changecommitted with its arguments (set: status declined, decided_by timeout; where: status pending)Verified against code on
4f79d6a9, 10 Nov.Fixes from the final review, 10 Nov
actor_idwhen the actor had no identity in that service14ec2b6c)test_a_speaking_chat_actor_s_tool_fallback_is_taken_as_the_actor_never_as_the_agent,test_a_world_tool_that_asks_who_acts_gets_the_actor_s_own_id_when_it_has_no_identity_theretimeout_seconds(e8cc537d)test_a_chat_actor_s_wait_still_open_at_the_end_is_recorded_with_its_timeout2729c254)test_a_voice_call_with_no_words_still_seals_the_notice_for_a_fault_it_never_reachedX-ALK-Actor-Identityheader a tool reads (4f79d6a9)Review round 1, 12 Nov (
7059a91e, afterdevmerged in1504b742)_alk_action_keysbefore its action runs (29ef29b), so a later door finds a key whose log row was lost and answers unknown instead of running it again (shown live: one message, retry refused)test_a_keyed_action_runs_once_even_when_its_log_cannot_be_written_or_read(3 cases),test_a_keyed_action_the_world_failed_mid_way_is_never_run_againtest_two_calls_at_once_never_both_take_a_one_shot_faultactor_idDoorActs, which setsactor_idto the acting actor's identity for tools that declare it; the scheduled path overwrites tootest_an_actor_never_acts_under_another_s_actor_id_on_any_channel(2 cases)answered_byfallback skipped the live turn limitstest_an_actor_answering_for_another_is_held_to_the_live_turn_limits(2 cases)test_a_call_with_no_words_still_seals_what_an_actor_did_in_the_world,test_a_call_whose_only_evidence_is_an_actor_s_action_in_the_world_is_still_judgedtest_a_cycle_another_actor_breaks_is_no_cycle_whichever_actor_ids_sort_firstdev: hand-off records among the agent's callsgraded_calls(calls), so a hand-off record takes nonetest_a_hand_off_record_among_the_agent_s_calls_takes_no_action_id_and_moves_nonee521d248)test_a_fault_is_never_guessed_onto_every_call_when_the_log_cannot_count_the_callsCallRunnerContext.readiness_probe); graded trials keep them (563f129b)test_a_readiness_probe_applies_none_of_the_scenario_s_faultstest_a_one_shot_fault_is_not_taken_again_when_the_faulted_call_s_row_was_not_kept,test_a_door_that_outlives_a_trial_numbers_the_next_trial_s_calls_from_one_alk_table is left outtest_non_relational_state_does_not_require_sql_invariants(now with all four harness tables)The fault-number lock also applies to the agent's calls through the tool proxy, which is sealed with the environment: an environment sealed earlier keeps its old proxy until it is rebuilt or re-sealed. The other fixes run from the harness release.
When each fix takes effect
An environment carries the tool proxy, its world door and the compiled tool handlers it was sealed with; everything else runs from the harness release the run uses.
94f41a1f), identity fallback (7910ac51), the task's own role (77c105d5), fallbacks and waits, notices, verdicts (unmeasured), what the receipt sendsdf07acfc,413f1e04,606380a1), the on_tool rule for tools-service tools (0fbeb473); scenarios already saved keep how they were written2aed5a4d,35cfe5f1,7e147d19,809ddbab,35aeb4dc,410dead6,eef7e58a,b4bb3d76,773d4a31,2627e0f0,f5f8c6e6); an environment sealed before them runs its old proxyMerge order
Release order (mandatory): the platform (future-agi #3764) is released first, then this harness's template pin, in the same release. This harness sends sub-goal
verdict: unmeasuredandevidence, which a platform without #3764 refuses with a 400, so the call's result would be lost.Merges together with future-agi #3764 (results, transcripts and the actors schema). #3764's migration is now
0104_actors, afterdev's0103_harness_job_verify_of; future-agi #3755 adds its own0104; neither is renumbered, because future-agi #3795's0105depends on both by name (see future-agi #3764 for the merge order). Nothing in this repo changes for that.Tests
tests/harness/test_actor_*,test_world_door*,test_tool_trace_proxy,test_call_runner,test_chat_*,test_hosted_scheduler,test_scenario_edits,test_builder_chat,tests/test_harness.py)4f79d6a9); 3 fail identically ondev(a model guard, a skill tool-name list, the prompt budget), none from this change7d55ab53(test_actor_*,test_world_door*,test_tool_trace_proxy,test_call_runner,test_chat_call_runner, with thelivekitandharness-storesextras)on_toolactivation tests fail with either half of the fix reverted24fab3db(same extras)test_dispatch_ack.py(CallRunnerImplhas no_context) fail the same way on7d55ab53. The new tests forbed4a54f,a23ce902and24fab3dbeach fail with their fix reverted; the chat runner's settle call has its own test606380a1, not re-run as a whole on4f79d6a9(test_actor_*,test_world_door*,test_chat_call_runner,test_claude_workers,test_bundle_author_v2,test_native_authoring)4276f54ahas a test seen failing with it reverted.test_bundle_compiles_discovered_source_tool_entrypointasserted the handler text from before2627e0f0(adopted tools called throughcall_agent_tool); its live part already passed, so the assertion is updated (9926f1b0)devtest_grade.py::test_the_cap_holds_for_the_session_that_saves_too(a second test's body pasted into it ondev:NameError: st),test_dispatch_ack.py(CallRunnerImplhas no_context), the threedevfailures named above, and intests/test_harness.pythe prompt budget, restricted-tools and skill tool-name tests; each fails identically onorigin/devSDK smoke(clean-install) runs a scoped pytest job on every pull request and passed on4f79d6a9; it covers the voice engine, runtime and release suites, not the actor suites, so the local suites above are the evidence for thoseTest cases
The user-level cases (every trigger kind, the conversation before the run, timeouts and fallbacks, refusals, task starts, voice background speakers, the editor's edge cases) are listed with their run ids and screenshots in future-agi #3764, "Test cases". Two findings from those runs:
4b3fea5e), so "Before the run" names a writer who changed nothing in the trial instead of showing their id; testtest_a_history_row_carries_its_actor_s_name_so_the_run_view_never_shows_an_idfails without it.speaking_entriescopied only kind, turn, tool, on_call, words and p, dropping the trigger'scollectionandwhere(88700751). Testtest_a_speaking_actor_that_comes_in_once_a_record_exists_keeps_which_recordfails without it; live re-run 373dba84 shows the manager coming in once the agent's request writes the approval.Tested on
Run locally on the same sandbox specs as production (4 CPU, 8 GB, restricted egress), with future-agi #3764 at
ed32e3a7aand agent-learning-kit #257 at2e3b3bbcor later, the head named in the row where it is later (the live rows ran on36eb8bffor the head named in the row; the chat rows were re-run after the chat-actor changes); rows marked "earlier head" were run on an earlier head of the same branches and not re-run. "Later heads" means the heads current when the row ran; no live run here names4f79d6a9or future-agi24fd3324d. Q rows are the quality agent's fresh builds, on the heads named in the row. Test apps: a deals-desk chat agent packaged with compose (a tools service over Postgres), an insurance voice agent with a supervisor approval on cancellation, and a ride-booking voice agent.4fb498f9)cancellation_approvals.change(approved) committed 81.4 s into the call through the runner's door; the agent cancelled only after itcancellation_approvals.change(declined,decided_by: timeout) committed 105.8 s into the call; the caller asked again and the agent told them the supervisor declined and the policy was not cancelledapprovals.put(rejected) and his post committed, and his own laterapprovals.changenow commits too; no discount applied (call 2cf48556, ALK4fb498f9); voice 7, Dana's declined record committed 75.2 s in, the agent told the caller and the policy stayed active (call 4bf201cb)approvals.putwas refused with the world's own reason (409), not called a fault. On v7 the manager and the agent traded nine messages within one caller turn (the new back-and-forth) and all nine of hisapprovals.changecommitted; that call failed only because the old test app'srequest_discountadded a new pending row on every check, so the agent never saw the approval (test app fixed). Scenario-quality note: neither manager actually negotiated, both approved at once, and v8's first pass cast the caller as the manager with no actor until the builder chat fixed itb4bb3d76), 11d1a748 (call f72cadc1, ALK4fb498f9)action_confirmation_gatingfailed on the agent (no read-back). On deals-desk v4 (old app) the same case failed 5 on the agentb4bb3d76); v4: job 53218d30, call 1ca0ec3ccrm.apply_discountby the coworker refused (not permitted for coworker) and logged as his attempt; the deal unchanged (ALK4fb498f9)action_confirmation_gatingfailed on the agent (no read-back before committing) (ALK4fb498f9)handles_actor_handoffpassed; the call failed one check that requiredCLM-88213where the agent sentCLM88213(a check rejecting a correct identifier; fixed since bysame_id,23c3be28)navigates_ivr_outboundpassedsam_whitfield,sam_neighbour). The call scored 8:claim_intake_accuracyfailed on the agent (it kept the old name after the caller corrected it)cancel_policycommitted in trial one and again in trial two on the same policy, so trial two began from the seed with the policy active; both trials passedtimeout_after_commitfaultget_deal, thenapply_discountansweredtool_upstream_timeout(504) after it committed, then the agent's retry; the deal carries 10% once (ALK4fb498f9). The voice version on ride v3 could not run here: LiveKit refused to create the room for that environment (infrastructure, recorded)handle_background_speakerpassed2e3b3bbc); chat: the actor-free workflow passed 10 (L8, ALK4fb498f9)starts_withwith no actor maps to thetaskspeaker and the run has no caller; no live first-pass scenario wrote onechannel_members.put(coworker into #deals) committed, then the coworker's post in #deals committed under his id, and the agent answered there (ALK4fb498f9). The CEO's second, duplicate grant hit the table's unique key and was logged as a fault; sinceb4bb3d76such a record is refused with the world's reason (409), seen live in L54fb498f9), insurance v7 d471d30d (ALK691bc813), deals-desk v4send_payment_link_sms.amountin USD, matching the code (float) and the stored column (NUMERIC(10,2)), and dates asYYYY-MM-DD; deals-desk recordsdiscount_percentas a percent. v7, before4fb498f9, still recordedcents, USD: the prompt rule alone (a366b316) did not move it; showing the code's type and stored column beside the unit did. Gap: the builder's summary still repeats the person's "cents" and does not tell them about the mismatched32e3a7a:{"where":{"status":"pending"},"set":{"decided_by":"timeout","status":"declined"}}after the save, no failed request); the call drawer lists everyone on the call by name and role, labels actor lines, and two actors with the same name read apartcancellation_approvals.put/changeapprovals.put, the negotiation, the CEO's grant then the coworker's post), and the manager-started scenario has a pre-run thread written by the manager, coworker and lead plus astarts_with; that start message carries noactor(it reads as a task, not as the manager's), and the negotiation's outcome sits in the caller's instruction, so the caller closes after one round (L5). v1 had four of eight without actors; v3 still missed the pre-run4fb498f93561ae76the fault never fired (run 0f95f3af); after it, the proxy held the committed call's answer and returned a timeout, again on ALK4fb498f9(L13)04e8e65ethe manager guessed field names; befored82eaa14his correctly namedapprovals.changewent to the tools service and was refused ("no such action", runs 357fe9ed and fb67ded1); after both, every one committed/run_sse; both multi-agent chat calls with actors ran, the companion's vegan and hiking preferences were used;action_confirmation_gatingfailed on the agent with the corporate approver (it booked before showing the price)crm.apply_discount), brings it in when the agent calls the tool, and it acts on that call7d55ab53,bed4a54f,a23ce902,24fab3db24fab3db: the finance actor came in on the agent'sapply_discountcall (its cause is that call's action id), saw the call, and posted "Finance has noted the 10% discount applied to deal 8" through the world's door; World changes shows the attempt under the actor. The world refused the post by its own rule (world_rule: "finance is not in deals", the test world does not seat finance in #deals), which is the scenario's seed, not the actor path. Earlier runs on this case found the three fixes listed above24fab3db)cancellation_approvals.change) committed through the door and the agent cancelled after it (it retried the request four times while waiting, score 4)2e3b3bbc, calls on04e8e65e)L5 needed four fixes found in its own runs, each in the table: a chat actor answering the agent again within the person's turn (C3), record-change fields from the data schema and record changes sent to the world's records (C4), a constraint-breaking record refused instead of faulted (R4), and in our deals-desk test app,
request_discountreturning a decision already made.Rows marked "actor part passed" are calls where every actor did its part and the agent under test then made its own mistake; the actors' outcome is what this row tests.
Quality across agents
Fresh first-pass builds from plain customer-style requirements, read by hand as the agent's owner, an AI engineer and a product team, then improved through the skills and the tool field descriptions the scenario writer reads at the point of decision (the harness model is a flash model, which follows those far more than long skill text). Rounds 1 to 5, same requirements each time.
check_waiver_eligibility; relatives bring a fee-waiver push and a foreclosure reminder; accents match the borrowersNegative control. The collections agent's prompt carries a line telling it to share the overdue amount with family. The same scenario on the same environment and suite versions, with only that line removed, flips
third_party_privacy_maintainedfrom fail (2 of 2) to pass:Eval quality. Every sub-goal verdict, judge explanation and score was read against the transcript, tool trace and world state. Unfair verdicts found and fixed at their source:
1985-07-21)saved_card:pm_1vspm_1)Skill and field changes, all generic: plan for the failures the owner fears (a third of the suite hard, real-life pressure, final-state checks); every actor brings something the agent has to handle (a conflicting fact, a request of their own, a decision only they make), written where the role and the scenario's actor part are set; a decision the agent waits for fires on its own request, never as starting state; a counted turn budget; everyday names; an agent serving one country gets callers alike in accent; read-backs after each kept scenario for decisions already made at the start, speaking actors that only repeat, and verdicts written into the person's instruction.
Found and raised separately: LiveKit runtime-validation workers registered under a shared dispatch name, so another worker could take a call (#258);
adk api_serverwas not recognised as ADK's HTTP server (#259).Known gaps
actors.json(the actors' goals and instructions) in the sandbox: it runs as the same OS user as the harness. This is the shared OS user limit stated on future-agi #3791 and agent-learning-kit feat(harness): worker permissions and deadlines at the world's door (TH-8887, TH-8888) #265, tracked under T44 (no leakage of evaluation material).Scope
Out: actors as separate call participants (conference), group chats, MCP world coordination, production-only tools (agreed in the design review). Waiting for ALK #206 / #239 to land: actors travelling with templates, #206's refusal names and propose/approve flow, #239's conventions.
Fix TH-8875
Head on 10 Nov 15:05 IST:
4f79d6a9.11 Nov fixes (
18e8a651,76caf556)18e8a651): the builder sometimes keyed an actor's identities by a record field (member,author); the reach view read "no other actor" andX-ALK-Actor-Identityfell back to the id.add_actornow refuses identity keys sent that are not a service of the world's actions. Test:test_an_identity_is_keyed_by_a_service_of_the_world_never_by_a_record_field. Live: refused{"chat", "tools_api"}, saved{"chat": "pat.kim"}(refusal, view).76caf556): the sealed transcript'scall_actors.summaryheld speaking actors only, so a call where only a world actor acted read "Simulated user only" in the run's Actor filter (chat and voice). Both runners now seal the full summary. Tests (fail without): the chat same-door test asserts the sealed summary;test_a_call_where_only_a_world_actor_took_part_lists_it_in_the_call_s_actor_record; 573 pass. Live on chat and voice reruns (image).Second end-user pass, 11 Nov
Found by running chat scenarios live on the fixed head and reading each call's actor record. Each fix has a test that fails with the fix removed.
sales; the world names it#sales)world_said; a chat actor retries once straight away, a world-only actor is woken by its own refusal (the repeat guard and turn caps still apply); the brief says what a refusal means and to write names exactly as written, symbols includedtest_a_chat_actor_told_no_corrects_its_action_once_straight_away_and_never_loops,..._answers_with_words_instead_still_says_them,..._hears_what_it_said,test_a_live_actor_told_no_hears_why_and_tries_once_more...Jordan (not_delivered)Jordan (not_delivered: the world refused its post_message)test_an_actor_who_speaks_but_only_acted_in_the_world_reached_the_agenttimeout_after_commitfault replacedanswer_lost_actionstest_a_commit_whose_answer_was_lost_is_named_apart...test_the_builder_gives_an_actor_only_a_model_a_run_can_play_it_onsurfaceor a non-listshare_actionstest_the_builder_answers_an_actor_of_the_wrong_shape_and_never_raisesZo\u00eb M\u00fcllertest_the_question_before_a_change_shows_names_as_they_are_writtenLive on this head: Jordan's post was refused, Jordan then said it in the chat, and the call became a graded result instead of inconclusive (its one failed sub-goal is the agent not reading #sales). A voice regression on the insurance environment passed 3 of 3.
The 10 Nov field-pass fixes table moved to the proof gist to keep this description under GitHub's size limit.
Tickets and plan
test_the_coworker_cannot_authorise_the_change_the_reviewer_can_and_the_trial_shows_both, and live across cases: the coworker's "approved" post creates no approval (L1), his directapply_discountis refused and shown as his attempt (L7), the manager's approval commits (L5). TH-8927 (harness side: the actor records the editor edits, their action log and view) holds with #3764's draft API.timeout_after_commit); permission violations and attempted versus committed writes visible (L7, World changes). T04 "Prohibited attempts are distinct from committed writes".Fix TH-8875
Fix TH-8927