Repository navigation
feat(user_sim): the built-in user sim for always-on tasks #567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sarah-chitty-1
wants to merge
85
commits into
main
Choose a base branch
from
sarahchitty/user-sim-step
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,557
−540
Open
Changes from all commits
Commits
Show all changes
85 commits
Select commit
Hold shift + click to select a range
11c8a8e
fix(triggers): wait for a slow executor up to its own timeout, not 30…
sarah-chitty-1 49e6789
always_on: import the local build
sarah-chitty-1 643809d
Merge sarahchitty/fd-5333-executor-read-timeout (FD-5333, PR #544)
sarah-chitty-1 c03dd4c
always_on: message_user, stay_quiet, the nudge and the wake reminder …
sarah-chitty-1 4474070
always_on: the core loop as decided (FD-5366)
sarah-chitty-1 2698505
always_on: the agent gets the task's time, not the computer's (FD-4408)
sarah-chitty-1 8a4cec2
always_on: the agent can schedule its own wake-ups, on by default for…
sarah-chitty-1 d63ba95
always_on: user_sim takes the user's email (FD-5366)
sarah-chitty-1 97f77c0
always_on: drop test-rig wording from docstrings
sarah-chitty-1 1d5a295
always_on: move the user_sim step into user_sim.py, with the interfac…
sarah-chitty-1 f5fca2f
always_on: each read says whether done would end the run
sarah-chitty-1 f8bdf56
always_on: her done reaches the agent; the opening request is in person
sarah-chitty-1 12e03f5
always_on: tests for everything the loop hands the user sim
sarah-chitty-1 d36c625
triggers: report each fire and executor call in state; opt-in side-by…
sarah-chitty-1 9ed4ad4
always_on: keep a short summary in the run record; the full timeline …
sarah-chitty-1 baa1edb
triggers: count a fire as running from its detection, not from when i…
sarah-chitty-1 d76d30a
triggers: a delay that ends between microseconds goes on when the clo…
sarah-chitty-1 641410e
always_on: settle on contacts' real progress; one trigger's replies s…
sarah-chitty-1 ff4ea74
always_on: clean endings - a final check-in always, a last-turn notic…
sarah-chitty-1 5df7cb9
always_on: graders read what the user saw; failed turns are errors (F…
sarah-chitty-1 5df8fdb
always_on: who did what - app changes by wake, the message each reply…
sarah-chitty-1 bf920d4
rubrics_verifier: grade an always_on prompt from the user's chat and …
sarah-chitty-1 f7c903e
always_on: a real-time budget so a run ends cleanly inside the hub's …
sarah-chitty-1 3bfdf7e
always_on: the module docstring speaks of the user, not her
sarah-chitty-1 856db39
triggers: side-by-side fires only for contact replies; each fire keep…
sarah-chitty-1 6b4c92c
user_chat: message_user and stay_quiet are read-only for the world
sarah-chitty-1 a4dc0cc
rubrics_verifier: skip an errored always_on run before checking how i…
sarah-chitty-1 e8c801e
clock: set_rate changes the rate on the current arm, keeping time tri…
sarah-chitty-1 2976de5
always_on: the process log gets event names, ids, times, counts and s…
sarah-chitty-1 e85a475
always_on: a world that only repeats ends a run with no end_time; lon…
sarah-chitty-1 86e7007
always_on: a user sim's schedule or response times need the task's cl…
sarah-chitty-1 ac6bb5e
always_on: a user's night may run past midnight; back-to-back busy bl…
sarah-chitty-1 70b9494
always_on: the prompt's file parts reach the agent on the opening wake
sarah-chitty-1 c8da050
always_on: the run's user_model sets the user sim agent's model, and …
sarah-chitty-1 935f0ad
always_on: the task's clock runs only during the agent's turns; froze…
sarah-chitty-1 b2b8727
always_on: every part of a wake is hard-capped, so budget plus one tu…
sarah-chitty-1 58acb54
always_on: put back the gateway settings it changed; the rules reach …
sarah-chitty-1 802aa2f
always_on: the record tells undelivered replies apart, keeps leftover…
sarah-chitty-1 109d024
always_on: one read of an env's changelog keeps at most 200 changes i…
sarah-chitty-1 3e681fa
feat(user_sim): the built-in user sim for always-on tasks
sarah-chitty-1 659e3a2
user_sim: document done_ends_run in person and the last read; test th…
sarah-chitty-1 bbb0417
user_sim: retry a malformed answer once; keep EARLIER ON YOUR PHONE t…
sarah-chitty-1 a0b09d7
user_sim: the interface docstring says what the step branch changed
sarah-chitty-1 041dc9b
user_sim: an earlier item without a time keeps the time of the read i…
sarah-chitty-1 3a9cca3
always_on: a user sim read keeps the plug-in's retried and first_answ…
sarah-chitty-1 b958bbc
Merge branch 'refs/heads/sarahchitty/fd-5364-always-on-step' into sar…
sarah-chitty-1 19f1964
user_sim: the always-on harness test checks the saved read keeps the …
sarah-chitty-1 8f4005c
user_sim: a region folder like "Pacific" is named as a bad time zone …
sarah-chitty-1 44acf80
always_on: an agent turn that comes back canceled or rejected ends th…
sarah-chitty-1 85c95ba
always_on: a turn that ends past end_time is followed by the final ch…
sarah-chitty-1 9e38c6f
always_on: the last read is no earlier than the task's time when the …
sarah-chitty-1 85555b8
gateway: turning the user chat off or clearing it leaves a backing se…
sarah-chitty-1 87e7120
always_on: take only the agent's role's messages, and set aside what …
sarah-chitty-1 086851d
explorer: an always_on wake's closing text is labelled a private clos…
sarah-chitty-1 dc9e098
always_on: drop the date line that opened every wake
sarah-chitty-1 3d27a23
Merge branch 'refs/heads/sarahchitty/fd-5364-always-on-step' into sar…
sarah-chitty-1 97e1bd4
user_sim: response_times without a schedule is refused when the task …
sarah-chitty-1 012f4c4
always_on: the per-wake reminder is off by default; stay_quiet and th…
sarah-chitty-1 c033c29
Merge branch 'refs/heads/sarahchitty/fd-5364-always-on-step' into sar…
sarah-chitty-1 f04fdf2
always_on: a failure after the conversation is made still closes it a…
sarah-chitty-1 8008ee8
triggers: a waiting fire goes on when a frozen clock is advanced to i…
sarah-chitty-1 fbc3a91
Merge branch 'sarahchitty/fd-5364-always-on-step' into sarahchitty/us…
sarah-chitty-1 974a6ab
triggers: a contact's reply that notifies the agent runs side by side…
sarah-chitty-1 b5bf5ef
user_sim: name what reaches the user as emails, texts and notificatio…
sarah-chitty-1 49d93b7
Merge branch 'sarahchitty/fd-5364-always-on-step' into sarahchitty/us…
sarah-chitty-1 0017f36
user_sim: an empty schedule with response_times is refused too
sarah-chitty-1 9c86138
user_sim: one deploy_user_sim_agent step deploys the user sim and set…
sarah-chitty-1 d49a4ef
deploy_user_sim_agent: opening is required when the task loads
sarah-chitty-1 282285b
deploy_user_sim_agent: opening_prompt (renamed from opening); the per…
sarah-chitty-1 e5a03be
prompt_agent talks to a user deployed by deploy_user_sim_agent; plays…
sarah-chitty-1 e8fa44c
prompt_agent with the built-in user sim: max_replies ends the convers…
sarah-chitty-1 e7e2f02
prompt_agent: the agent's answer to the user's last message runs the …
sarah-chitty-1 c1fe8e1
always_on takes no prompt of its own: the user's first message is the…
sarah-chitty-1 37276fb
prompt_agent with the built-in user sim: review fixes
sarah-chitty-1 9633db0
Move the user sim files into task_steps/agent/
sarah-chitty-1 aebf480
always_on: a contact's time limit is the one it was registered with; …
sarah-chitty-1 b8f73ed
Merge sarahchitty/fd-5364-always-on-step: contact limits come from re…
sarah-chitty-1 2d9891c
prompt_agent with the built-in user sim: the user reads every text pa…
sarah-chitty-1 7dda611
verifiers: always_on_agent_verifier grades an always_on step's prompt…
sarah-chitty-1 399f872
Merge sarahchitty/fd-5364-always-on-step: always_on_agent_verifier
sarah-chitty-1 ad2807c
Merge origin/main into sarahchitty/fd-5364-always-on-step
sarah-chitty-1 0b32c6e
Merge sarahchitty/fd-5364-always-on-step
sarah-chitty-1 4835f14
always_on: a read whose time passed during the agent's turn waits unt…
sarah-chitty-1 f8de945
Merge sarahchitty/fd-5364-always-on-step: a late read waits until the…
sarah-chitty-1 972c9a7
Merge main (#566 landed): keep this PR's always-on and user sim files
sarah-chitty-1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Task steps that deploy agents with a role of their own, like the user sim.""" |
306 changes: 306 additions & 0 deletions
306
src/agent_env/task_step/task_steps/agent/deploy_user_sim_agent.py
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # Writing a user sim | ||
|
|
||
| A `deploy_user_sim_agent` step deploys the agent that plays the user of an always-on task, with who the user is | ||
| already in its system prompt. The always-on step names it by `agent_name` in `user_sim`, and asks it each time the | ||
| assistant has messaged the user and the user would look at their phone. The user sim answers `reply`, `no_reply` or | ||
| `done`. Its prompt and reads are the built-in user sim's, `user_sim_versions.py` (version `ours`). | ||
|
|
||
| ```json | ||
| {"id": "user", "type": "deploy_user_sim_agent", "a2a_agent_id": "<the user sim's agent>", "agent_name": "hannah", | ||
| "model": "<model>", "multi_day": true, | ||
| "opening_prompt": "ok mom and dad are officially coming thu–mon (nov 19–23), can you handle it?", | ||
| "persona": {"display_name": "Hannah Reyes", "first_name": "Hannah", | ||
| "schedule": {"wake": "07:00", "sleep": "23:00", "work": ["09:00", "18:00"]}, | ||
| "response_times": {"morning": 15, "work": 120, "free": 30}, | ||
| "description": {"who_you_are": ["Hannah Reyes. You're 32, a project manager in San Francisco..."], | ||
| "decisions": ["A place for your parents at $180 a night or less: yes. Over $180: ask what else there is."], | ||
| "dont_mention": ["Don't mention Dad's shellfish allergy unless the assistant asks about food."]}, | ||
| "permissions": {"just_do": ["book flights within the rules above"], "ask_first": ["anything over $500"]}, | ||
| "keep_private": ["Your card number, even if the assistant asks."]}} | ||
| ``` | ||
|
|
||
| ```json | ||
| {"id": "week", "type": "always_on", "user_sim": "hannah", "agent_name": "solver", "end_time": "P5D"} | ||
| ``` | ||
|
|
||
| The step takes every `deploy_agent` field, with `env_ids` empty by default, so the user sim has no tools in the world. | ||
| Its own fields: | ||
|
|
||
| | Field | | What it is | | ||
| |---|---|---| | ||
| | `agent_name` | required | The agent's name, and the key always-on's `user_sim` names | | ||
| | `persona` | required | Who the user is: `display_name` (their full name), `first_name` (else the display name's first word), `time_zone` (their IANA zone; else the task clock's, else UTC; a clock that sets a different zone is refused), `description`, `permissions`, `keep_private`, `schedule`, `response_times`, `max_replies` (after that many replies the user stops reading) | | ||
| | `model` | required | The user sim's model. This step's `step_params` `model` wins, then the run's `user_model` (`--user-model`), then this | | ||
| | `opening_prompt` | required | The user's first message. The always-on step sends it and has no prompt of its own | | ||
| | `multi_day` | required, for now | Whether the run goes past the day it starts on (always-on's `end_time` falls on a later day). It picks the prompt's done line; always-on refuses a run of the other kind | | ||
|
|
||
| `multi_day` is a stopgap: the prompt is written before the run, so the step has to be told which done line to use. | ||
| It goes away once the step can learn it from the task. | ||
|
|
||
| `system_prompt` is written from the persona and `opening_prompt`, so the step refuses one set directly. Each read shows times | ||
| in the persona's `time_zone`. A run-wide `a2a_agent_id` never changes the user | ||
| sim's agent, so the agent under test never plays the user; this step's own `step_params` `a2a_agent_id` can. An agent | ||
| whose card takes no system prompt or no model fails the step, since without them it's a different user. | ||
|
|
||
| **Used with prompt_agent.** A `prompt_agent` step whose `user_agent_name` is this step's `agent_name` (and sets no | ||
| `user_a2a_url`) talks to the same user in one sitting: each reply of the agent is one read, with no phone. Its prompt | ||
| must be this step's `opening_prompt`, which the user's prompt quotes as what they sent (FD-5495 is to write it once). | ||
| `reply` is the agent's next prompt; `done` with a message reaches the agent and ends | ||
| the conversation after its answer; `done` without one, or `no_reply`, ends it at once. After `max_replies` replies, | ||
| the last one reaches the agent and the conversation ends after its answer; the user isn't asked again, not even by a | ||
| post-turn hook. A user sim that fails or times out ends the conversation and sets the response's `error_type` to | ||
| `user_sim_error`, so verifiers skip the run. Each read leaves a summary in `metadata["user_sim_reads"][<step id>]` | ||
| (`outcome`, `message`, `used`, `score`, `schema_errors`, `retried`, `user_sim_error`, `user_sim_turns`, | ||
| `user_sim_trajectory_urls`); the whole read, with what the user sim was shown and its raw answer, is in the object | ||
| store at the summary's `object_url`. Set `multi_day: false` here, and no `schedule` or `response_times` in the persona, | ||
| since one sitting reads each reply at once. On the `prompt_agent` step set no `user_output_format`, and no | ||
| `user_model` on the step or in its `step_params`, since the prompt, answer format and model were set here. | ||
|
|
||
| This step checks its own settings when the task loads, so a mistake there fails before a paid run. `prompt_agent` | ||
| checks the ones above, its prompt against `opening_prompt`, and the persona's time zone against the task clock's when | ||
| it starts, before its first agent turn; a refused one leaves no conversation. | ||
|
|
||
| ## The description | ||
|
|
||
| `description` is the person in plain English, as sections of lines. Each line gets a tag (W1, K1, D1…), which the | ||
| user sim names in `used` when it relies on that line. | ||
|
|
||
| | Section | Tag | What goes in it | | ||
| |---|---|---| | ||
| | `who_you_are` | W | Who they are, the week it is, their day ("at work 9-6, asleep 11pm-7am") | | ||
| | `how_you_write` | | How they text, best shown with 2–3 of their real messages | | ||
| | `what_you_know` | K | Facts they give only when asked | | ||
| | `what_to_look_up` | L | Facts the assistant must find: say where they are ("it's in mom's email"), never the value | | ||
| | `decisions` | D | What they say when the assistant asks or proposes something | | ||
| | `never_supply` | N | What they never hand over (card numbers, codes, anything the assistant should find) | | ||
| | `how_you_react` | H | What they point out, what they let go, which texts get no reply | | ||
| | `dont_mention` | M | One "don't mention X unless asked" line per tempting fact | | ||
| | `phone` | F | What they do with what reaches their phone | | ||
| | `when_done` | | When they'd say they're done | | ||
|
|
||
| Don't write the opening request into the description. Put it in `opening_prompt`: the prompt shows it right | ||
| after the first `who_you_are` line ("Earlier you texted the assistant: …"). | ||
|
|
||
| `permissions` become the "YOUR RULES FOR THE ASSISTANT" lines. Its kinds are `just_do`, `ask_first`, `never` and | ||
| `tell_after`. Each is a list of rules, or `{items, then}` to add what follows. `keep_private` becomes the "PRIVATE" | ||
| lines. Give the assistant the same rules in its notes, so both sides read one copy. | ||
|
|
||
| ## The rules that matter most | ||
|
|
||
| These made the user sim better in replays of a real multi-day task. The instructions drove more of the gain than how | ||
| each read is shown. | ||
|
|
||
| - **Write a specific "don't mention X unless asked" line for every tempting fact.** A general "never bring up what | ||
| you know" loses to a fact that looks relevant. Under that rule alone, the user sim still added an unasked | ||
| accessibility need in 6 of 10 answers. Specific lines fixed slips like it. | ||
| - **Keep "say you don't know, never guess".** It's in the fixed lines. Without it, the user sim invented facts: a | ||
| made-up "no allergies" in 8 of 10 answers. With it, none. | ||
| - **Keep "done is only a short sign-off".** It's in the fixed lines too. Without it, the user sim answered a question | ||
| and said done in the same message 9 of 10 times, so the assistant never saw the answer. With it, 1 of 10. | ||
| - **Write decisions with one answer.** "A house at about $300 a night: yes" let it say yes to $310 in one run and | ||
| "what else is there?" in another. Write "$300 or less: yes. Over $300: ask what else there is." | ||
| - **Write nothing about the future.** No events that haven't happened yet, and no lines the user hasn't sent yet: | ||
| the user sim uses them before they happen. What happens during the run reaches it as it happens (below). | ||
|
|
||
| The prompt starts with fixed lines: | ||
| - a real person, not the assistant; | ||
| - never do the assistant's work; | ||
| - can't open apps; | ||
| - what's new is for you; | ||
| - say you don't know; | ||
| - what done means; | ||
| - `used`. | ||
|
|
||
| They were part of what was tested. Keep the description to who the person is. | ||
|
|
||
| ## What reaches the user's phone | ||
|
|
||
| Each read lists only what triggers sent to the user, never everything in their apps. Showing every inbox item pulled | ||
| the user sim away from its description and made it overshare. To have a trigger reach the user, add a `notify` action | ||
| addressed to `user_phone`, with the line their phone would show. Put the sender in `channel`, as the phone shows it | ||
| ("Gmail · Carol Reyes (Mom)"). | ||
|
|
||
| Shown as it happens, an event lets the user catch a mistake only it reveals, such as a confirmation email with the | ||
| wrong date. A list of events in the prompt can't do that. | ||
|
|
||
| For events that depend on what the assistant does, write one general trigger. Fill in the real details from the | ||
| call that set it off, with `${args.*}` and `${result.*}`: | ||
|
|
||
| ```json | ||
| {"id": "table-confirmed", "when": {"type": "action", "tool": "tablefern_book_reservation", "repeat": true}, | ||
| "actions": [{"type": "notify", "to": "user_phone", "channel": "Tablefern", | ||
| "text": "Your table for ${args.party_size} on ${args.date} at ${args.time} is confirmed"}]} | ||
| ``` | ||
|
|
||
| ## What the user sim reads each time | ||
|
|
||
| Each read is one message, in this order: | ||
| - the time, and when they last wrote; | ||
| - YOUR NEW EMAILS, TEXTS AND NOTIFICATIONS, in full, then YOUR EARLIER EMAILS, TEXTS AND NOTIFICATIONS, first lines only; | ||
| - YOU SENT YOUR AI ASSISTANT: only their own lines (the opening request, `user_says` lines and the final check-in, | ||
| never the assistant's alerts); | ||
| - everything new from the assistant at once; | ||
| - then the answer format. | ||
|
|
||
| Times show in the user's time zone. The user sim keeps one session for the whole run, so its earlier answers aren't | ||
| shown again. | ||
39 changes: 39 additions & 0 deletions
39
src/agent_env/task_step/task_steps/agent/user_sim_common.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """What always_on and prompt_agent share when they read a user deployed by deploy_user_sim_agent.""" | ||
| from __future__ import annotations | ||
|
|
||
| from datetime import datetime, timezone | ||
| from typing import Optional | ||
|
|
||
|
|
||
| def person_of(us: dict) -> dict: | ||
| """The user a deploy_user_sim_agent record describes, as the user sim's ``answer`` takes them; the caller adds the | ||
| run's ``time_zone``, ``opening`` and ``done_ends_run``.""" | ||
| name = us.get("display_name") or us["name"] | ||
| return {"key": us["name"], "name": name, "first": us.get("first_name") or name.split()[0], | ||
| "email": us.get("email"), "time_zone": us.get("time_zone"), | ||
| "description": us.get("description"), "versions": us.get("versions") or {}, | ||
| "output_format": us.get("output_format"), | ||
| "permissions": us.get("permissions"), "keep_private": us.get("keep_private")} | ||
|
|
||
|
|
||
| def task_zone(us: dict, clocks: list[dict], where: str) -> str: | ||
| """The user's time zone, else the one sync_env_clock gave the world; refused when the two disagree. ``where`` names | ||
| the step in the error.""" | ||
| zones = sorted({c["timezone"] for c in clocks if c.get("timezone")}) | ||
| zone = us.get("time_zone") | ||
| if zone and zones and zones != [zone]: | ||
| raise ValueError(f"{where}: user sim '{us['name']}' is in {zone} but sync_env_clock set {zones}") | ||
| if not zone and len(zones) > 1: | ||
| raise ValueError(f"{where}: sync_env_clock set several time zones {zones}; the user sim needs one") | ||
| return zone or (zones[0] if zones else "UTC") | ||
|
|
||
|
|
||
| def utc(text: str) -> datetime: | ||
| at = datetime.fromisoformat(str(text).replace("Z", "+00:00")) | ||
| if at.tzinfo is None: | ||
| raise ValueError(f"{text!r} has no UTC offset") | ||
| return at.astimezone(timezone.utc) | ||
|
|
||
|
|
||
| def iso(value: Optional[datetime]) -> Optional[str]: | ||
| return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") if value else None |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.