participant_pair chat sub-mode: simulated person versus simulated person, with no chatbot endpoint in the loop. To rehearse against a bot you point at, see guides/chat-studies (the external_chatbot sub-mode). For how chat fits the modality registry, see concepts/study.
Cardinality is always two. No three-or-more party group chats, and no marking one side as the live human (live human-versus-AI rehearsal lives in
me.ishlabs.io and is not on the MCP or CLI surface yet).The asymmetry contract
Each side carries its ownscenario with its own goal embedded in it, and the partner never sees the other side’s scenario. That asymmetry is the design intent. Without it the two simulated people collude toward whichever objective is more legible and the rehearsal collapses. Write each scenario as if briefing one person who has not spoken to the other.
How sides pair up
group_a and group_b pair 1:1 by index. Side A’s first person talks to side B’s first person, the second to the second, and so on.
[tp-aaa] against [tp-ccc, tp-ddd] makes two conversations that share tp-aaa. That is the 1xN rehearsal, fix one side and vary the other. Any other mismatch is rejected.
You rarely list person IDs by hand. The recommended path is criteria-driven: hand each side a RoleCriteria filter and the backend resolves the pool when the iteration is created. Reach for explicit group_a / group_b only when you already have a hand-curated panel from concepts/people.
Steps
Create the study with side-tagged assignments
A pair study persists two
Assignment rows, one per role. Each carries the role label (name), the scenario (instructions), an optional goal (test_context), and the side discriminator (a or b). This is the same shape the product writes, so a study created here opens correctly in the launcher.chat_mode is required when modality is chat. Set it to participant_pair.On the MCP path, do not pass iteration content to
study_create. Pair iterations are deferred to study_add_iteration so you can pick the audience selection first. The CLI can build iteration A inline when you pass the pair flags to ish study create (shown below), or you can keep the two steps separate with ish iteration create.Scout pool sizes before committing (recommended)
person_get speaks the same demographic vocabulary as RoleCriteria, so the cheapest pre-flight is a list call with limit=10. The envelope carries total (the full match count) plus a small sample to eyeball. If either side returns total: 0, mint the missing pool with person_generate before you create the iteration.RoleCriteria accepts occupation, age band, gender, country, five enum filters (education_level_in, household_in, locale_type_in, income_level_in, employment_status_in), and five coarse accessibility booleans (requires_captions, uses_screen_reader, prefers_reduced_motion, prefers_high_contrast, has_any_accessibility_need). The full enum values are in the tools-study reference.Create the pair iteration, criteria-driven
Hand each side a Each side requires either a non-empty
RoleCriteria filter and let the backend resolve the pool at create time. You never need to know which person IDs exist. Echo the scenarios here so the run-time path has them; the goal stays on the Assignment.test_context you set in step 1. initiator_side decides who speaks turn 0 (default a).RoleCriteria or an explicit audience. An empty filter on one side is rejected up front rather than accepted and failed at run time. Scenarios must be non-empty on both sides.Or pin an explicit panel
Use explicit audiences when you already have specific participants in mind, for example a hand-curated panel from Side A here is one person broadcast against two on side B: two conversations that share the sales rep, varying the CTO. Passing both criteria and an explicit audience is allowed; the backend then validates the explicit list against the criteria. Once the iteration exists the resolved audiences are baked in. You do not pass them again at run time.
person_get. Pass person IDs or tp- aliases for each side.Dispatch the run
Pair iterations carry both rosters, so Both sides consume one model call per turn, so the cost is roughly the per-turn chat cost times two per pair. See
study_run needs only the study and iteration. The audience argument is rejected here (the audience lives in the iteration). config_id is optional: omit it and study_run auto-infers from the first group_a person’s simulation_config_id. If that person has no config assigned, the dispatch returns a validation_error naming the missing field rather than silently picking one; pass config_id explicitly or assign a config to the source person.concepts/credits-and-limits for the model. Prefer the default non-blocking dispatch and poll the returned hint; the study_run reference covers wait, timeout, and the client transport ceiling. For the run-versus-ask choice, see concepts/run-vs-ask.Read the transcripts
Pair runs return one transcript per conversation, not per participant. Each transcript interleaves both sides’ turns by timestamp and carries a The MCP
conversation_summary with who_steered, dominant_dynamic, momentum_shifts, and a summary prose field. The end_reason is stamped on the conversation itself and surfaced alongside the summary, not inside the summary blob.view="transcripts" returns every conversation in one call. The CLI --transcript projection takes one participant; to slice the aggregate by role, filter with --side a or --side b. For how reactions and signals are structured, see concepts/reactions-and-results.End reasons
Each conversation ends for one of these reasons, surfaced onend_reason:
| Value | Meaning |
|---|---|
MAX_TURNS | Hit the per-iteration turn cap. |
MUTUAL_DISENGAGE | Both sides’ last turn reported they did not want to continue. |
MUTUAL_GOAL | Both sides’ last turn reported their goal was achieved. |
MIXED_END | One side achieved its goal; the other disengaged. |
NO_PROGRESS | An action cycle was detected; the conversation looped. |
ERROR | A model or worker failure. |
The two chat modes side by side
external_chatbot | participant_pair | |
|---|---|---|
| Who talks to whom | simulated person versus your chatbot endpoint | simulated person (side A) versus simulated person (side B) |
| Iteration input | endpoint or chatbot_endpoint_id | chat_pair=ChatPairConfig(...) |
| Cost per turn | one model call | two model calls |
| Transcript granularity | one per participant | one per conversation (both sides) |
| Audience selection | study_run(audience=...) | baked into the iteration; audience rejected at run time |
Reference
study tools
study_create, study_add_iteration, study_run, study_get, and the ChatPairConfig / RoleCriteria / Assignment shapes.ish study create
Every flag for the CLI study create, including the pair-mode and inline-iteration shortcuts.
ish study run
Dispatch flags, config override, and pair-mode behavior.
ish study results
Transcript projection and the
--side / --turn slice flags.Related
Probe a chatbot
The
external_chatbot mode: endpoint setup, smoke tests, slot bindings.People
How audiences and criteria resolve to the simulated people who play each side.
Iteration
The iteration shape and where
mode_details lives.Reactions and results
How transcripts, summaries, and signals are structured.