Skip to content

feat(harness): actors, one world door for agent and actors, on top of dev - #257

Open
KarthikAvinashFI wants to merge 119 commits into
devfrom
feat/TH-8875-actors
Open

KarthikAvinashFI wants to merge 119 commits into
devfrom
feat/TH-8875-actors

Conversation

@KarthikAvinashFI

@KarthikAvinashFI KarthikAvinashFI commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

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 dev and 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

Area Files What
Actor record actors.py kinds, activation, permissions, limits (incl. live turn caps), the cast a scenario selects; starts_with.actor optional (a task-driven run needs no actor)
One door world_door.py agent tool calls and actor actions through one rule set and one action log: permission, faults, idempotency (scoped per actor, row written before a keyed answer), record changes (never to _alk_* tables)
Where actors act call_runner.py (runner_door), tool_trace_proxy.py, chat_call_runner.py actors act through a door built in the runner's own process; the tool proxy refuses any request naming an actor; every trial behind a tool proxy writes its own faults to the world database (_alk_trial_faults, none included), read over one kept connection; a trial with no faults is never failed by a database out of reach
Dispatcher actor_dispatch.py due, decide, deliver; per-actor and per-trial live turn caps; a repeated action is a loop and ends the actor's turns; the old HTTP token path is removed (actors act only through the door)
Voice call_actor.py, call_runner.py, fi/simulate/simulation/call_actors.py, engines/call_actor_voice.py, engines/livekit.py hand-offs, background voices, phone menus; actions handed to the runner
Chat chat_call_runner.py, run/conversation.py the same actors, door in process; task-driven start
Before the run pre_run.py, prove.py conversations actors write among themselves (20 messages by default, a repeated line ends them), replayed into the world before the run and before every proof, credited to their actors
Contract contract.py, tools.py, builder.py arg_units: the unit or format of a tool field (cents vs dollars), on the contract line every stage reads
Builder world/tools.py, actors.py, amend.py, scenario_tools.py a recorded unit is shown beside the field's declared type and stored column; the cast read-back says how each actor plays; add_actor lists and accepts record changes on the world's own collections (approvals.put), so a decision someone makes is theirs to record; fix_tool records a field's unit after the contract is written
Chat tools service chat_call_runner.py only the contract's service routes go to the agent's tools service, with their method, for its harness-run calls and for actors' actions; a call the service never answered is still the agent's recorded attempt; World changes read this trial's rows from the world's table, where the tool proxy logs the agent's own calls
Record changes world_door.py, scenario_tools.py, skills a put takes the record's fields as written (or under record); scenario writers are told both shapes, and when a request means pre_run or starts_with
Scenario cast scenario_tools.py, world/tools.py, actor_dispatch.py, run/conversation.py, write-scenarios a scenario whose caller is played as a listed actor (by the persona's name field) is refused with the fix; every kept scenario reads its cast back: each action as a record change or a message, who talked before the run, who starts it, and the listed actors it leaves out; an actor needs an identity where its action's tool names who acts; an actor's identity goes to the service as X-ALK-Actor-Identity (never on a record change, and never from the agent); every live actor is told each action's fields
Fallbacks and triggers actors.py, actor_dispatch.py, scenario_tools.py a fallback action is taken with fallback_arguments (a decline writes its record); an actor brought in when contacted needs an identity the agent's calls could name
Pre-run pre_run.py a session none of whose actors can write (all mocked, no thread) is refused with the fix; a session missing its place's action is told the shape
Sealed rules actors.py the actor rules are written beside actors.json on 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 it
Chat back-and-forth run/conversation.py an actor the agent answered is asked again within the person's turn, so the two can trade several messages; the actor's live turns, the trial's live turns and caller_takes_back_after_turns end it, and the person always takes the next turn and hears every exchange
Actors' record changes run/conversation.py, actor_dispatch.py, world_door.py a chat actor's permitted record change goes to the world's records as a scheduled one does; actors are told each record change's fields from the contract's data schema; a record that breaks a world constraint is refused (409) with the world's reason instead of being called a fault
Scheduler hosted_scheduler.py an actor who speaks but only acted in the world counts as having reached the agent
Skills skills/... actors, access as world records, task-driven starts, units (the code's unit wins over the person's words, and the person is told), decisions as record changes, pre-run and starts_with, saying so when the app has no place for a requested decision

Behaviour callouts

  • Sub-goal verdicts: a sub-goal that depends on an actor reads not applicable when that actor never came in, inconclusive when it came in and never acted, and unmeasured when it is judged and the call left no words, actions or recording to judge it from. Checks still grade the agent only.
  • Behaviour change without actors: a chat agent whose tools are a service now has its harness-run tool calls answered by that service (see Fixes found while testing). The actor-free chat runs below cover it.
  • Found by building from plain requirements, fixed, and checked with fresh builds from the same requirements (rows B1, B2): the builder gave an actor the agent's own action and nobody the approval change (add_actor refused record changes), could not record a unit (fix_tool), and the scenario writers folded named people into the caller, skipped pre_run and starts_with, and gave no identities.
  • Behaviour change for chat runs with speaking actors: an actor the agent answers may answer again before the person's next turn, up to its live turns (10 by default, 20 per trial), so those transcripts can carry more actor lines than before. Runs without speaking actors are unchanged.
  • Access (who is in a channel, who may see a record) is world data the world's own rules read on every call; an actor's grant is a record change through the door.
  • New refusals and who agreed them: the permission conflict before saving (the product plan's world journey; decision Q6 of the actors plan), an impossible trigger refused when written (the product plan's "impossible triggers"), contacted needing 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.
  • Limitation: the agent's own tools service holds the world database URL by design (it is the agent's code), so it could touch _alk_trial_faults and _alk_tool_trace, as it already could the world's own tables.

Fixes found while testing

  • Chat agents whose tools are a service. On dev, a hosted chat bundle seals handlers only for import and construct tools, 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.
  • Chat agents that run their own tools never met a scenario's faults. Only the voice runner wrote the trial's faults where the tool proxy reads them. Chat trials behind a tool proxy now write them too.
  • An actor's own on_tool activation 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 on 24fab3db (row T1).
  • The agent's calls through its tool proxy were invisible to chat actors. A chat trial's actors read only the runner's own action log, so an on_tool trigger never saw the agent's proxied calls; they now read the world's own log from the trial's start (bed4a54f).
  • An actor woken by the agent's call could not see that call. Its view held only rows addressed to it or actions it may take, so it waited for something that had already happened; it now also sees the call its own on_tool trigger watches, while when it comes in stays its trigger's to decide (a23ce902).
  • A trial's end dropped an actor's turn. When the conversation ended just after the agent's call, the actors' task was cancelled mid-turn and the actor was recorded nowhere. On voice and chat the dispatcher now settles at the end (up to 30 s): a cut turn is taken again and a call that landed at the end still wakes its actor; a turn still unfinished then is recorded as such (24fab3db).
  • Fixed after the first round of testing (each with a test that fails without its fix):
    • The chat readiness probe passed when every tool call was refused or broke on our side (773d4a31). It now needs one call that ran in the world; refusals by the world's own rules still count.
    • A tool written as a route handler taking (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.
    • A generated check compared a claim number as text and failed an id typed without its separators (23c3be28). Checks get same_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.
    • Every contract edit from the builder chat also wrote empty fields the stored contract never had, such as loads on data_store, so a one-rule edit showed an extra change in review (54735e28). An edit now writes back only the edit.
    • same_id matched 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).
    • A fault the scenario set that did not take effect was only logged, so a trial ran without it and nothing said so (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).
    • An actor's committed actions read by their last part in a trial's world changes and transcript (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.
  • Fixed after the final independent review (each with a test that fails without its fix):
    • An after-commit fault the door applied was reported as "could not be applied", because the door keeps that row as committed with the answer lost (00d439b3). Both fault modes are now read from the door's own rows, and the test drives the real door.
    • A proxied chat trial without actors never read the world's action table, so its fault notices and world changes missed the agent's own calls (b5cbe374). The table is read whenever the trial is proxied.
    • A speaking actor's timeout fallback was sent with no arguments, and on chat as a tool call, so a decline never wrote its record (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.
    • The id comparison's examples named a real run's claim id, and the spoken-date proof replayed English month names for any language (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.
    • A failure to write a trial's faults could carry the database error's text, which can hold the world's password (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.
    • A speaking chat actor never saw its own actions, so on a run where the agent kept saying "no approval yet" the manager wrote approvals again and again, after his own scheduled rejection (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.
  • Found, not fixed here: the simulated caller can go silent when asked to hold; the first builder turn of a new interactive build can fail with a connection error. Each is raised for its own work.
  • Chat actors could not hold a conversation with the agent. A chat actor wrote at most once per person turn, so it could never trade several messages with the agent; it is now asked again after the agent answers it, within the live limits.
  • A chat actor's own record change never reached the world. It went to the agent's tools service as a tool call and was refused ("no such action"); a scheduled one went to the records. Both now take the same route, and actors are told the fields.
  • A record that broke a unique key was logged as the world being down. It is now refused with the world's reason.
  • The unit rule alone did not hold. A fresh build from requirements saying "cents" still recorded cents on a dollar field; showing the code's type and stored column beside the unit fixed it on the next build.

Tested paths for the fixes above

Path How Result
Chat agent whose only tool calls were refused before running (unknown tool, not permitted) or broke on our side Unit, test_private_workflow_fails_when_no_tool_call_reached_the_world (4 cases) Probe fails with chat_tool_evidence_unverified; a world-rule refusal or one success passes
Tool trace keeps the door's outcome apart from a failure Unit, test_tool_trace_keeps_the_door_s_outcome_apart_from_a_failure A fault is no longer read as a refusal
Route-style tool get_deal(db, args) adopted from the agent's code Unit, test_a_route_style_tool_taking_db_and_args_is_called_with_the_world_connection Returns the deal from the world; failed with a TypeError before
Keyword tool, arguments-dict tool, state-first tool Unit, test_agent_tool_calling.py Each still gets what it takes; a wrong keyword still fails loudly
Check comparing a claim id as text Unit, proof replay test_a_check_that_compares_a_claim_id_as_text_fails_the_id_typed_without_separators Refused as too strict; the same check with same_id holds
same_id edge cases Unit, test_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 differs
Builder chat adds a rule to a contract stored without loads Unit, test_adding_a_rule_does_not_add_an_empty_loads_to_the_data_store Only the rule changes; a field the contract already had is kept
Identifiers that truly differ (CLM/CLA, extra digit, leading zero, missing prefix, empty) Unit, test_same_id_still_tells_apart_identifiers_that_name_different_records Never match; an empty id matched another empty one before
A world that stores the id exactly as typed Unit, proof test_an_exact_comparison_is_kept_when_the_world_treats_the_id_typed_differently_as_another_record The strict check is not called too strict
Free text and dates among fixed values Unit, test_a_fixed_value_that_is_not_an_identifier_is_never_replayed_without_separators Only the id is replayed without separators
Fault applied by the door, both modes Unit, test_a_fault_the_real_door_applied_says_nothing_in_either_mode (drives the real door) No notice before or after commit
Fault on a call the agent made in its own process Unit, test_a_fault_on_a_call_made_outside_the_door_is_said_not_applied Notice fault_not_applied, saying why
Fault on call 2 when the agent made one call Unit, test_a_fault_the_agent_never_reached_says_how_many_calls_it_made Notice fault_not_triggered: "made 1 call of it"
Voice world with no database Unit, test_a_world_without_a_database_cannot_hold_any_fault Notice fault_not_applied with the reason
Call to a different tool Unit, test_a_call_named_by_another_tool_is_not_taken_for_the_fault_s_action Not counted as the fault's call
Notices in the sealed transcript Unit, test_the_transcript_carries_its_notices_and_omits_them_when_none Present when there are notices, absent otherwise
An actor's action named in world changes Unit, test_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_chose approvals.put and channel_members.put told apart; ambiguous with no choices kept as logged; agent rows unchanged
Fault notice live on chat Live, deals-desk #9 (timeout_after_commit on crm.apply_discount), run c3b01b2d The fault took effect (first apply_discount got 504, the retry committed once), but a "could not be applied" notice showed. Fixed in 00d439b3; re-run on head 36eb8bf (run 213c0d21, trial 553a3bef): same 504 then one committed retry, and no notice
Proxied chat trial with faults and no actors Unit, test_a_proxied_chat_trial_without_actors_still_reads_the_world_s_table The world's table is read; without a proxy or actors it is not
Speaking actor's record fallback on chat Unit, test_a_speaking_chat_actor_s_record_fallback_is_taken_with_its_arguments_and_writes_its_record Sent with its fallback arguments as a record change; the outcome is on the timeout event
Speaking actor's fallback on voice Unit, test_a_speaking_voice_actor_s_fallback_action_is_reported_with_its_arguments Reported with its fallback arguments
Spoken-date proof in another language Unit, test_the_spoken_date_replay_runs_only_for_a_language_it_can_write Skipped for a language it can't write; English and unstated still replayed
Fault table out of reach with faults to set Unit, test_a_world_database_out_of_reach_fails_only_a_trial_with_faults_to_set Fails naming the error type only; the connection string never appears
Chat actor knows its own earlier actions Unit, test_a_chat_actor_decides_knowing_its_own_earlier_actions_in_the_trial Its scheduled rejection is in its brief, marked as its own
Chat actor asked again after the agent says "not yet" Unit, test_a_chat_actor_never_sends_an_action_it_already_committed_again Sent once; the two repeats are recorded, not sent
Manager asked again after a scheduled rejection, live Live, deals-desk #2 on head 36eb8bf (run 213c0d21, trial f3c7ca2f) One approvals.put, the rejection posted, no repeats (an earlier head wrote approvals 11 times on run fbc081df)
An actor's action named in full, live Same trial, World changes and transcript Reads approvals.put, not put
Speaking actor's approval times out on voice, live Live, insurance #2 cancellation-supervisor-timeout on head 36eb8bf (run 6caddb9a, call b3e3220c) Passed; after 30 s the supervisor's fallback cancellation_approvals.change committed with its arguments (set: status declined, decided_by timeout; where: status pending)
Proxied chat trial without actors, live Live, deals-desk #7 on head 36eb8bf (run 9922ea9c, call 3b5af555) Completed, no notice. No local scenario has a fault and no actors, so that pairing stays unit-only (row above)
Speaking actor's fallback on chat, live Not run No chat environment here has a speaking actor with a timeout fallback; unit-only (row above)

Verified against code on 4f79d6a9, 10 Nov.

Fixes from the final review, 10 Nov

Finding Fix Test (fails without the fix)
A chat actor's tool fallback was sent with no identity, so the world credited it to the agent; a tool that asks who acts got no actor_id when the actor had no identity in that service Both carry the actor as itself, its own id when it has no identity there (14ec2b6c) 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_there
A chat actor's wait still open when the conversation ended was dropped from what the actors chose, and chat timeouts lacked the wait they were given (voice kept both) Recorded like voice, with timeout_seconds (e8cc537d) test_a_chat_actor_s_wait_still_open_at_the_end_is_recorded_with_its_timeout
A voice call with no words sealed no transcript, so a fault that did not take effect was never said The notices are sealed even with no words (2729c254) test_a_voice_call_with_no_words_still_seals_the_notice_for_a_fault_it_never_reached
Builders were not told how a service learns which actor is acting The actors guidance names the X-ALK-Actor-Identity header a tool reads (4f79d6a9) guidance text

Review round 1, 12 Nov (7059a91e, after dev merged in 1504b742)

Finding Fix Test (fails without the fix)
A keyed action ran twice when the action log could not be read or written The door keeps each actor key's state (pending, committed, unknown when the world failed mid-action) beside the log; a log that cannot say refuses the key instead of running it ; each key is also claimed in the world's _alk_action_keys before 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_again
Two calls at once both took a one-shot fault The call number is taken under one lock per faulted action, and that call's row is written before the next is counted test_two_calls_at_once_never_both_take_a_one_shot_fault
An actor could send another's actor_id Every channel's actions enter through DoorActs, which sets actor_id to the acting actor's identity for tools that declare it; the scheduled path overwrites too test_an_actor_never_acts_under_another_s_actor_id_on_any_channel (2 cases)
An answered_by fallback skipped the live turn limits It goes through the same per-actor and per-trial guard test_an_actor_answering_for_another_is_held_to_the_live_turn_limits (2 cases)
An actor-only call sealed no structured transcript and its judged sub-goals read unmeasured The transcript is sealed whenever actors acted or took part; actor actions count as evidence for judging 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_judged
A cycle another actor breaks was refused, depending on id order Cycles are found over the whole graph of the scenario's actors test_a_cycle_another_actor_breaks_is_no_cycle_whichever_actor_ids_sort_first
Merge with dev: hand-off records among the agent's calls Action marks pair with graded_calls(calls), so a hand-off record takes none test_a_hand_off_record_among_the_agent_s_calls_takes_no_action_id_and_moves_none
Found in our own pass after the review: when the action log could not count the agent's calls, every call of a faulted action was faulted as its first The door applies no fault when it cannot tell which call this is; the trial's fault notice then says the fault was not applied (e521d248) test_a_fault_is_never_guessed_onto_every_call_when_the_log_cannot_count_the_calls
Found in live verification: a scenario's fault was applied during the readiness probe, so a fault on the probe's tool call could fail certification and block Validate and Save The probe runs with no scenario faults (CallRunnerContext.readiness_probe); graded trials keep them (563f129b) test_a_readiness_probe_applies_none_of_the_scenario_s_faults
Found in the final pre-review pass: when the faulted call's log row was lost, the next call took the same one-shot fault again The door also numbers each faulted action's calls itself and takes the higher of its count and the log's; a door that outlives a trial (the tool proxy) starts again at each trial, which it reads from the version of the trial's faults row test_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
Same pass: two first claims at once could race on creating the keys table and refuse a fresh key The table is made once per log, and a create that loses the race is retried covered by the proxy suite on a real Postgres
Same pass: the harness's own keys and trial-faults tables could count as business tables in the data checks Every _alk_ table is left out test_non_relational_state_does_not_require_sql_invariants (now with all four harness tables)
Same pass: the cycle refusal did not say the agent and the seed are not counted as writing a record The refusal says so wording only
Same pass: two docstrings said the agent could not write the voice actions file; on the hosted sandbox it runs as the same OS user The docstrings now say what is enforced and what is not wording only

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.

Takes effect Fixes
On the next run, with the new template pinned Actor decisions and lines (chat and voice), model routing and the no-tools session (94f41a1f), identity fallback (7910ac51), the task's own role (77c105d5), fallbacks and waits, notices, verdicts (unmeasured), what the receipt sends
On the next build or builder-chat edit (scenario writing) Read-backs and writing rules (df07acfc, 413f1e04, 606380a1), the on_tool rule for tools-service tools (0fbeb473); scenarios already saved keep how they were written
Only after the environment is rebuilt or re-sealed Changes in the tool proxy, world door and compiled handlers sealed into a tools-service bundle (2aed5a4d, 35cfe5f1, 7e147d19, 809ddbab, 35aeb4dc, 410dead6, eef7e58a, b4bb3d76, 773d4a31, 2627e0f0, f5f8c6e6); an environment sealed before them runs its old proxy

Merge 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: unmeasured and evidence, 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, after dev's 0103_harness_job_verify_of; future-agi #3755 adds its own 0104; neither is renumbered, because future-agi #3795's 0105 depends on both by name (see future-agi #3764 for the merge order). Nothing in this repo changes for that.

Tests

Suite Result
Actor, door, dispatcher, proxy, runners, scheduler, builder and scenario tools (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) 1,115 passed (local run, head not recorded; not re-run on 4f79d6a9); 3 fail identically on dev (a model guard, a skill tool-name list, the prompt budget), none from this change
Actor suites on 7d55ab53 (test_actor_*, test_world_door*, test_tool_trace_proxy, test_call_runner, test_chat_call_runner, with the livekit and harness-stores extras) 470 passed; the new on_tool activation tests fail with either half of the fix reverted
Actor, door, dispatcher, proxy, chat and voice runner suites on 24fab3db (same extras) 519 passed. 4 failures in test_dispatch_ack.py (CallRunnerImpl has no _context) fail the same way on 7d55ab53. The new tests for bed4a54f, a23ce902 and 24fab3db each fail with their fix reverted; the chat runner's settle call has its own test
Mutation checks every fix since the review has a test that fails with the fix reverted (record changes, flat puts, service routing, failed-call recording, identity header and the proxy dropping an agent-sent one, folded caller, contacted needs an identity, fallback arguments, pre-run writers, per-trial faults, world-changes rows, units read-back)
Head 606380a1, not re-run as a whole on 4f79d6a9 (test_actor_*, test_world_door*, test_chat_call_runner, test_claude_workers, test_bundle_author_v2, test_native_authoring) 224 + 254 + 70 passed. Each fix after 4276f54a has a test seen failing with it reverted. test_bundle_compiles_discovered_source_tool_entrypoint asserted the handler text from before 2627e0f0 (adopted tools called through call_agent_tool); its live part already passed, so the assertion is updated (9926f1b0)
Known failures shared with dev test_grade.py::test_the_cap_holds_for_the_session_that_saves_too (a second test's body pasted into it on dev: NameError: st), test_dispatch_ack.py (CallRunnerImpl has no _context), the three dev failures named above, and in tests/test_harness.py the prompt budget, restricted-tools and skill tool-name tests; each fails identically on origin/dev
CI SDK smoke (clean-install) runs a scoped pytest job on every pull request and passed on 4f79d6a9; it covers the voice engine, runtime and release suites, not the actor suites, so the local suites above are the evidence for those

Test 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:

  • A pre-run message now carries its actor's name (4b3fea5e), so "Before the run" names a writer who changed nothing in the trial instead of showing their id; test test_a_history_row_carries_its_actor_s_name_so_the_run_view_never_shows_an_id fails without it.
  • An actor who speaks and comes in once a record exists never came in: speaking_entries copied only kind, turn, tool, on_call, words and p, dropping the trigger's collection and where (88700751). Test test_a_speaking_actor_that_comes_in_once_a_record_exists_keeps_which_record fails 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 ed32e3a7a and agent-learning-kit #257 at 2e3b3bbc or later, the head named in the row where it is later (the live rows ran on 36eb8bff or 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 names 4f79d6a9 or future-agi 24fd3324d. 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.

# Situation Case Env (build) How tested Result Evidence (job / call ids, screenshots)
L1 Coworker says "approved"; only the manager's action counts (chat) live case L1 deals-desk v3 ba9ca65b (compose chat, first pass) live chat passed, 9: the coworker's posts in #sales committed under his id; no approval record existed and the agent only requested one (ALK 4fb498f9) job 4d9c8dec, call d088538c
L1v Supervisor approves a cancellation; the agent completes it (voice) live case L1 insurance v6 ad4658e6 (gated app, first pass) live voice passed, 8: Dana's cancellation_approvals.change (approved) committed 81.4 s into the call through the runner's door; the agent cancelled only after it job 911dea9d, call 76fa0c02
L2 Contacted approver times out, fallback declined live case L2 insurance v6 ad4658e6 live voice passed, 5 (no sub-goal failed; the score is the judge's overall rating): the supervisor came in on the pending request and never answered; her 30 s timeout fired and her fallback cancellation_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 cancelled job 911dea9d, call df2d0fc9
L3 Approval refused; nothing committed live case L3 deals-desk v3 (chat), insurance v6 (voice) live chat + voice passed: chat 10, the manager's approvals.put (rejected) and his post committed, and his own later approvals.change now commits too; no discount applied (call 2cf48556, ALK 4fb498f9); voice 7, Dana's declined record committed 75.2 s in, the agent told the caller and the policy stayed active (call 4bf201cb) jobs 4d9c8dec, 911dea9d
L4 Manager approves, lead objects live case L4 deals-desk v3 live chat actor part passed: both decision records committed; the call failed on the agent (it reset the discount without consent and did not read #deals) run beb74200, call d22e1798 (earlier head, not re-run on the later heads)
L5 Live manager negotiates over several messages live case L5 deals-desk v8 81df4bd7 #4 (fixed test app; #4 corrected in the builder chat), deals-desk v7 13d46401 #4 live chat passed, 10 on v8: the account executive asked for a discount without knowing the outcome, the manager answered in the chat with a 12% approval and recorded it, the agent read the deal and applied 12%; his duplicate approvals.put was 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 his approvals.change committed; that call failed only because the old test app's request_discount added 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 it jobs dfb75ee5 (call 0992c334, ALK b4bb3d76), 11d1a748 (call f72cadc1, ALK 4fb498f9)
L6 Event-started by the manager, pre-run thread written by actors, manager keeps acting live case L6, R6 deals-desk v8 81df4bd7 #5 (fixed test app) live chat + proof passed, 7: the pre-run thread (coworker asks, manager approves, lead agrees) was replayed into #sales before the run, the run started from the manager's message credited to him, and the agent read the deal and #sales, applied 10% and posted; action_confirmation_gating failed on the agent (no read-back). On deals-desk v4 (old app) the same case failed 5 on the agent job e436e8a5, call f6ea91ac (ALK b4bb3d76); v4: job 53218d30, call 1ca0ec3c
L7 Coworker tries a write it may not; refused and shown live case L7 deals-desk v3 live chat passed, 9: crm.apply_discount by the coworker refused (not permitted for coworker) and logged as his attempt; the deal unchanged (ALK 4fb498f9) job 4d9c8dec, call 331273d9
L8 One workflow across CRM and channel, consistent live case L8 deals-desk v3 live chat passed, 10: the agent applied the 10% discount and posted the matching update in #sales through its own tools service; action_confirmation_gating failed on the agent (no read-back before committing) (ALK 4fb498f9) job 4d9c8dec, call 2ce4a94a
L9 Background voice plus hand-off with barge-in live case L9 insurance v4 48d0eb01 live voice passed (actor part): the spouse came in on turn 2 and delivered the claim number, handles_actor_handoff passed; the call failed one check that required CLM-88213 where the agent sent CLM88213 (a check rejecting a correct identifier; fixed since by same_id, 23c3be28) run 64e29242, call 58bb8031 (earlier head, not re-run on the later heads)
L10 Phone menu on an outbound call (IVR) live case L10, R10 insurance v4 48d0eb01 live voice passed, 9: the phone menu answered first, the agent got through and verified the policyholder; navigates_ivr_outbound passed run 64e29242, call b0f0072b (earlier head, not re-run on the later heads)
L11 Dropped actor; two actors with the same name live case L11 insurance v5 (builder chat), insurance v6 #10 (live) builder chat + live voice passed: dropping an actor in use refused with the scenarios named (builder chat, run 4f298278); live on the later heads, both "Sam Whitfield"s were triggered on turn 1 and spoke, and each transcript line carries its own actor id (sam_whitfield, sam_neighbour). The call scored 8: claim_intake_accuracy failed on the agent (it kept the old name after the caller corrected it) job f9ef8d1a, call 08bd616f
L12 Trial two starts from the seed after trial one changed a record live case L12 insurance v4 48d0eb01 live voice, TRIALS=2 passed: cancel_policy committed 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 passed run 6136e011, calls 074166c7, 01745dd8 (earlier head, not re-run on the later heads)
L13 Change commits, answer lost; a retry does not apply it twice live case L13 deals-desk v4 5462ff96 #9 (compose chat) live chat with a timeout_after_commit fault passed, 10: the tool trace shows get_deal, then apply_discount answered tool_upstream_timeout (504) after it committed, then the agent's retry; the deal carries 10% once (ALK 4fb498f9). The voice version on ride v3 could not run here: LiveKit refused to create the room for that environment (infrastructure, recorded) job 53218d30, call 5506965d
L14 Delayed supervisor approval inside the timeout live case L14 insurance v6 #7 live voice passed, 8: Dana's approval committed 85.3 s into the call while the agent waited, and the agent cancelled after it job 911dea9d, call 6494d77b
L15 Background voice from the start live case L15 insurance v6 #4 live voice passed, 10: the spouse spoke on turn 1 in the background mix ("remember to ask them for the adjuster's phone number"), the agent stayed with the caller, handle_background_speaker passed job f9ef8d1a, call acf2e6b7
L16 Prebuilt Slack world (needs #161/#210 on dev) live case L16 not possible: #161/#210 are not on dev not run
L17 No-actor regression, voice and chat live case L17 ride 49046d00 (voice), deals-desk v3 #7 (chat) live voice: 3 of 3 calls completed, scores 9 / 8 / 6, and none of their 38 transcript lines carries an actor id or speaker name; the 6 is the agent not answering an interruption (ALK 2e3b3bbc); chat: the actor-free workflow passed 10 (L8, ALK 4fb498f9) jobs 7189ca48 (calls b4f1fa4e, 5d3ec264, 097ad0f8), 4d9c8dec (call 2ce4a94a)
R2 Task-driven run, no actor review R2 unit passed (unit): a starts_with with no actor maps to the task speaker and the run has no caller; no live first-pass scenario wrote one test_actor_runtime, platform speaker map
R4 Access granted mid-run takes effect review R4 deals-desk v3 #8 live chat passed, 10: the CEO's channel_members.put (coworker into #deals) committed, then the coworker's post in #deals committed under his id, and the agent answered there (ALK 4fb498f9). The CEO's second, duplicate grant hit the table's unique key and was logged as a fault; since b4bb3d76 such a record is refused with the world's reason (409), seen live in L5 job 4d9c8dec, call 4bb942df
R5 Actor-to-actor conversation cap (20) and loop stop review R5 deals-desk v4 #5 pre-run unit + live pre-run session passed: unit tests for the 20-message cap and a repeated line ending the session; live, the three actors wrote 4 messages and stopped on agreement, under the cap (again on the later heads, job d20ca91b) test_actor_plan; suite be817e7b
R6 Proof baseline includes pre-run sessions review R6 deals-desk v4 #5 unit + proof at seal passed: unit test that proof replays pre-run messages credited to their actors; #5 was re-proved with its written thread before saving test_actor_runtime; builder turn 19:59 (earlier head, not re-run on the later heads)
R11 Field units reach the actor and the contract review R11 insurance v8 5cf4d3a4 (fresh build, ALK 4fb498f9), insurance v7 d471d30d (ALK 691bc813), deals-desk v4 fresh builds + bundle read passed on v8: from the same requirements that say "cents", the contract now records send_payment_link_sms.amount in USD, matching the code (float) and the stored column (NUMERIC(10,2)), and dates as YYYY-MM-DD; deals-desk records discount_percent as a percent. v7, before 4fb498f9, still recorded cents, 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 mismatch bundles 5cf4d3a4, d471d30d, 5462ff96
UI Actors tab, actor form, limits, transcripts with actor lines, world changes live case UI insurance v6, deals-desk v3 browser (Vite on the PR head), 26 screenshots, GIF and MP4 passed: every page loaded with no failed API call and no console error; the Actors tab lists each actor with what it may do, when it comes in and its fallback (an action fallback reads "is taken for them", record changes read as actions); Limits opens; the form shows "Taken with (JSON)" for an action fallback and keeps it after a role edit is saved (re-checked on ed32e3a7a: {"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 apart walk video and screenshots attached below
VD An actor's action during a live voice call goes through the runner's own door review R1, R3 insurance v6 live voice passed: the supervisor's four record changes committed during the calls with her id in the action log, while the agent's calls went through its tool proxy job 911dea9d (calls 76fa0c02, df2d0fc9, 4bf201cb, 6494d77b)
B1 First pass: permissions, decisions as record changes, identities, units found in testing deals-desk v2 33e2361d, insurance v6 ad4658e6 fresh build from the original requirements passed: manager, lead and CEO got exactly the record changes asked for, the coworker only posting, no actor the agent's own action; identities matched the seeded members; the supervisor's decisions were cancellation_approvals.put/change bundles 33e2361d, ad4658e6 (earlier head, not re-run on the later heads)
B2 First pass: each named person is in the scenario that names them found in testing deals-desk v1 to v6 fresh build from the original eight requirements each time passed on v6, with two gaps: every person the requirements name acts in their scenario (the manager's rejection and both conflicting decisions as approvals.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 a starts_with; that start message carries no actor (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-run suite 7c549211 (v6, earlier head, not re-run on the later heads)
C1 Chat agent whose tools are a service, no actors (dev fix) found in testing deals-desk v1, v3 (compose chat) live chat passed, 10 / 10: the actor-free workflow ran the agent's service tools and its calls were recorded for grading; again on ALK 4fb498f9 runs fc357ba8 (call e447bf50); job 4d9c8dec (call 2ce4a94a)
C2 Faults on a chat agent that runs its own tools found in testing deals-desk v4 #9 live chat passed: before 3561ae76 the fault never fired (run 0f95f3af); after it, the proxy held the committed call's answer and returned a timeout, again on ALK 4fb498f9 (L13) runs 0f95f3af; job 53218d30 (call 5506965d)
C3 A chat actor answers the agent again within the person's turn found in testing (L5) deals-desk v7 #4 unit + live chat passed: unit tests for the back-and-forth up to the actor's live turns, the person taking the next turn and hearing every exchange, and the trial's limits; live, the manager and the agent traded nine messages within one caller turn, capped by the actor's live turns job 11d1a748, call f72cadc1
C4 A chat actor's own record change reaches the world, with its fields named found in testing (L5) deals-desk v7 #4 unit + live chat passed: before 04e8e65e the manager guessed field names; before d82eaa14 his correctly named approvals.change went to the tools service and was refused ("no such action", runs 357fe9ed and fb67ded1); after both, every one committed jobs fb67ded1 (call 8a83c413), 11d1a748 (call f72cadc1)
Q1 Fresh build, multi-agent travel concierge, chat with actors (Google ADK) quality round travel 44e0a3bf, 625610f5 fresh build + live chat passed: both builds completed and served on /run_sse; both multi-agent chat calls with actors ran, the companion's vegan and hiking preferences were used; action_confirmation_gating failed on the agent with the corporate approver (it booked before showing the price) builds 44e0a3bf, 625610f5; job 1e0e3c20 (built on a local merge of #258, #259, #260 over the actors head, not the final head)
T1 An actor's own on_tool activation, saved by the editor as the world action (crm.apply_discount), brings it in when the agent calls the tool, and it acts on that call live check for 7d55ab53, bed4a54f, a23ce902, 24fab3db deals-desk v8: an actor added through the actor editor, scenario #10 with no scenario trigger live chat passed on 24fab3db: the finance actor came in on the agent's apply_discount call (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 above runs bd469d20 (call b1bb8bde), 5e894011 (call d4e97bdd), 94fe9fa8 (call 22b83929), ef14e73f (call dcdf6791)
L1v recheck Supervisor approval on voice after the end-of-trial settle (24fab3db) regression check insurance v6 ad4658e6 #2 live voice passed: both sub-goals held; Dana did not answer inside her 30 s wait, so her fallback approval (cancellation_approvals.change) committed through the door and the agent cancelled after it (it retried the request four times while waiting, score 4) run c736be22, call 438ed7c5
Q2 Fresh build, ride booking, voice with actors quality round ride 4cce31e3 (clean rebuild on ALK 2e3b3bbc, calls on 04e8e65e) fresh build + live voice passed, 3 of 3: a friend in the background corrects the pickup to the side door (8), a friend takes the phone to name the exact entrance (10), a teenager pushes for an XL twice (9); each verdict read against the transcript. Verdict: stay. Weak spot: the actors give in at once (the parent never resists the XL) build 4cce31e3; job 7be54ee9 (calls 12ed48db, 123df4e5, 90016acb)
Q3 Fresh build, outbound voice (alderway) quality round alderway 576ab89f fresh build + live voice build passed and the run's own worker took the call (agent joined 2.7 s after dispatch); the calls then stalled because the speech provider returned HTTP 402 on the stored key, so no verdict build 576ab89f; jobs a72f0c57, 36b83c8a (local merge of #258, #259, #260)

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_discount returning 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.

A voice call: two people with the same name in the background, each line attributed to its own actor

A voice call: the supervisor's approval recorded through the runner's door, then the agent cancels

A chat run: the CEO grants channel access mid-run, the coworker's post is credited to the coworker, the agent answers in the channel

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.

Agent kind Round 1 Latest Verdict
Outbound collections, voice 2 of 8 scenarios with something to decide; approver acted at the start with the decision made; accents spread across Indian borrowers 7 of 8; the supervisor's waiver decision fires on the agent's own check_waiver_eligibility; relatives bring a fee-waiver push and a foreclosure reminder; accents match the borrowers Stay: caught a debt disclosed to the borrower's sister (2 of 2 runs) and a missing call log
Multi-agent returns, chat (OpenAI Agents SDK) 1 of 8; setups invented orders the agent could never see, so every run failed unfairly 5 of 8; actors speak in the chat instead of writing records the agent never reads Stay: caught the coordinator claiming a refund before its worker had run
Outbound insurance intake, voice 0 of 8; partner and co-owner repeat what the caller holds 7 of 8; a co-owner corrects the roof age, a partner asks for something out of scope, a driving caller presses for a price Suites stay; calls blocked by #258
Ride booking, voice 1 of 8; the corporate approver asked for is in no scenario 4 of 8 with someone to handle: a friend corrects the pickup, a friend takes the phone for the exact entrance, a teenager pushes for an XL; 3 of 3 calls fair (8, 10, 9) Stay: the actors bring real conflicts, though they give in at once
Multi-agent travel concierge, chat (Google ADK) 2 of 8; approver at the start Builds blocked by #259 Pending

Negative 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_maintained from fail (2 of 2) to pass:

As shipped: the agent tells the borrower's sister the overdue EMI; the privacy sub-goal fails with the reason

The same scenario with the one prompt line removed: the agent declines to share the account and the sub-goal passes

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:

Unfair verdict Fix
Records an actor or setup writes for an agent that keeps its data in its own process (the agent never sees them; a damaged-item refund passed every check) Refused at write time, with the fix in the message; for agents that load data once at start, only changes after the start are refused
A check compared a spoken date as text ("21 July 1985" vs 1985-07-21) The proof now replays every fixed ISO date in spoken form and calls a check that fails it too strict
A check compared a prefixed id the world resolves the same (saved_card:pm_1 vs pm_1) The proof replays the bare id and calls the check too strict when the world ends in the same state
"Tomorrow" fixed as a calendar date, failing a run on another day A relative day is never fixed as a date
A caller acting on someone else's record, so a correct refusal failed The writer is told whose record it is after the submit

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_server was not recognised as ADK's HTTP server (#259).

Known gaps

  • The tested agent can read 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).
  • A first pass can still write a negotiation whose caller states the figure the actor settles on (build 74644905's Feature/adding pull request template #4 says "an 18% discount is approved"). The read-back names it and the skill says not to; refusing such a scenario outright is a new check on scenario writing, waiting for a decision.

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)

  • Identities keyed by service (18e8a651): the builder sometimes keyed an actor's identities by a record field (member, author); the reach view read "no other actor" and X-ALK-Actor-Identity fell back to the id. add_actor now 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).
  • Actor filter misses world-only actors (76caf556): the sealed transcript's call_actors.summary held 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.

Case Before Now Test
A live actor's action refused by the world (Jordan posted to sales; the world names it #sales) The actor never heard why and took no second try, so the call was inconclusive Its own refused rows carry 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 included test_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...
Why an actor never reached the agent 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_agent
A commit whose answer a timeout_after_commit fault replaced Not marked in the trial's world changes answer_lost_actions test_a_commit_whose_answer_was_lost_is_named_apart...
An unknown model given to an actor from the builder chat Saved; each of its turns would fail Refused with the models on offer test_the_builder_gives_an_actor_only_a_model_a_run_can_play_it_on
A builder call to add_actor with a non-text surface or a non-list share_actions The tool raised Answered with an error the builder can act on; 1,372 malformed add_actor and drop_actor calls on chat and voice, none raise test_the_builder_answers_an_actor_of_the_wrong_shape_and_never_raises
The builder's confirm question for "Zoë Müller" Zo\u00eb M\u00fcller Written as typed test_the_question_before_a_change_shows_names_as_they_are_written

Live 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.

Lost answer in world changes

The 10 Nov field-pass fixes table moved to the proof gist to keep this description under GitHub's size limit.

Tickets and plan

  • Linear: Fix TH-8875 and TH-8927, with future-agi #3764. Part of TH-8651 and TH-8650 (earlier sub-actor and actor-editor PRs folded in here), under TH-8920, whose cited change review is future-agi #3795. TH-8875's "Done when" holds in one trial in 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 direct apply_discount is 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.
  • Product plan shared on 1 Nov: T15, all four acceptance lines: distinct knowledge and actions per actor; one workflow updates two systems consistently (L8, CRM and channel); a forced timeout cannot duplicate a committed action (L13, timeout_after_commit); permission violations and attempted versus committed writes visible (L7, World changes). T04 "Prohibited attempts are distinct from committed writes".
  • Built for: TH-8875 "Per-actor permitted actions and tool permissions", approver and system actors (one actor kind, with or without a channel), "Enforcement in the world", "Who-did-what shown per actor on the trial", requirements 1 to 3, and the pre-run conversations written by actors planned under it; TH-8927 read, create, update and remove actors in a draft, each actor's view without evaluator-only material, edits shown as proposed changes.
  • Considered: one world door for every agent shape (permission, faults, idempotency and the log in one place); every check grades the agent, never an actor, and a check that depends on an actor reads not applicable when that actor never came in; every limit is a setting, not a constant; conversations before the run are written by the actors and never graded.

Fix TH-8875
Fix TH-8927

@KarthikAvinashFI KarthikAvinashFI self-assigned this Nov 7, 2026
…enty messages, and a repeated line ends them
… 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
…onnection, and a keyed row is proven written before its answer
…ections, and fix_tool records a field's unit
…scenario writers are told the shapes, pre_run and starts_with
…ack, and an actor acts in a service under its identity there
… an actor's identity travels as a header, and a one-word name is never read as a person
…, the code's is recorded and the person is told
…ord changes, who talked before the run and who starts it
@KarthikAvinashFI
KarthikAvinashFI marked this pull request as ready for review November 10, 2026 12:16
@KarthikAvinashFI

Copy link
Copy Markdown
Member Author

New since the last review: 8870075 keeps the record trigger for actors who speak (they never came in before), and 9f8e9a2 tells the scenario writer to bring in an action that depends on another actor's action by that record, not a clock. Both checked live (runs 373dba84, 76d60e59).

@KarthikAvinashFI

Copy link
Copy Markdown
Member Author

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
@KarthikAvinashFI

Copy link
Copy Markdown
Member Author

Pushed 76caf556: a call's sealed actor record now lists actors who act only through the world, so the run's Actor filter finds calls where only such an actor acted (chat and voice). Tests fail without the fix; live reruns in the body under "11 Nov fixes".

…ce; a commit whose answer was lost is named; the builder refuses unknown actor models and shows names as typed
@KarthikAvinashFI

Copy link
Copy Markdown
Member Author

Pushed 36b0c8af: an actor the world refuses now hears why and tries again once, a commit whose answer was lost is named, and the builder refuses unknown actor models and shows names as typed. Each fix has a test that fails without it, with live chat and voice runs on this head. Details in the description under "Second end-user pass, 11 Nov".

@KarthikAvinashFI

Copy link
Copy Markdown
Member Author

Pushed 41ec2785: the builder's add_actor now answers a non-text surface or a non-list share_actions with an error instead of raising. Found by fuzzing add_actor and drop_actor with 1,372 malformed calls on chat and voice; none raise now. Test fails without the fix.

@hadarishav hadarishav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The actor architecture is directionally sound, but I found several correctness blockers:

  1. 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.

  2. Parallel calls can consume the same one-shot fault (world_door.py:246,290). Two threads can both select on_call: 1 before either records its row. I reproduced the fault being applied twice. Please atomically reserve the action call number and add a two-thread test.

  3. 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 actor priya sending U-admin. For tools declaring actor_id, always replace it with the acting actor's service identity across every channel.

  4. answered_by fallbacks 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.

  5. 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.

  6. 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.

@KarthikAvinashFI

Copy link
Copy Markdown
Member Author

Round 1 fixes, head 730386d77:

  1. Keyed actions keep a pending, committed or unknown state, and the key is claimed before the action runs (7059a91e, 29ef29b0). Test: test_a_keyed_action_runs_once_even_when_its_log_cannot_be_written_or_read.
  2. A one-shot fault's call number is reserved under a lock and counted even when its log row is lost (7059a91e, 66cc5f44); the docstring says numbering holds per door (730386d7). Test: test_two_calls_at_once_never_both_take_a_one_shot_fault.
  3. A tool declaring actor_id always gets the acting actor's identity on every channel (7059a91e). Test: test_an_actor_never_acts_under_another_s_actor_id_on_any_channel.
  4. answered_by fallbacks go through the same per-actor and per-trial caps (7059a91e). Test: test_an_actor_answering_for_another_is_held_to_the_live_turn_limits.
  5. Actor-only evidence is uploaded and counted in both judging conditions (7059a91e). Tests: 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_judged.
  6. Cycles are checked over the full graph (7059a91e). Test: test_a_cycle_another_actor_breaks_is_no_cycle_whichever_actor_ids_sort_first.
    Dev conflicts resolved in 1504b742. Deferred: the tested agent can read actors.json in the sandbox, tracked under T44.

@hadarishav hadarishav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants