A pricing call with Jordan
A worked Tommy tutorial for coding agents: construct a voice AI buyer, help a rep practice the call, and turn the transcript into evidence-linked coaching.
The agent is the operator
Tommy is not meant to make a salesperson operate a long CLI workflow. A coding agent operates Tommy on the salesperson’s behalf, translates ordinary conversation into structured components, and pauses only for consequential missing information or approval.
The agent begins with the guide, then asks Tommy for one state-aware action. It repeats agent next after every material change instead of remembering the workflow from chat.
tommy agent guide
tommy agent next
Show abbreviated JSON response
{
"status": "ok",
"command": "tommy agent next",
"data": {
"contract_version": "1.0",
"terminal": false,
"recommended_action": {
"id": "create_scorecard",
"cwd": "/work/ep-practice",
"argv": ["tommy", "scorecard", "create", "--id", "SCORECARD_ID", "--name", "SCORECARD_NAME"],
"mutates_local_state": true,
"requires_network": false,
"spends_money": false,
"requires_user_approval": false,
"unresolved_inputs": {
"SCORECARD_ID": {"type": "string", "description": "A short stable ID chosen by the agent."},
"SCORECARD_NAME": {"type": "string", "description": "A readable name for the scoring standard."}
},
"expected_transition": "The scorecard exists and needs at least one group."
}
}
}The action’s argv is a token array, not a shell snippet. The agent replaces its explicit placeholders using known context, runs it from the returned cwd, and asks for the next action again. The flags make external actions and paid inference visible before execution. The detailed commands below show what that agent-controlled process creates; they are not chores the salesperson is expected to type.
The situation
Alex Rivera sells Expected Parrot, an AI research platform. Tomorrow Alex will speak with Jordan Chen, Head of UX Research at Acme. Jordan has seen a demo, leads a six-person team, and is interested—but is skeptical about simulated-research validity and whether a $15,000 monthly contract can be approved.
We want Alex to practice aloud with a voice interview AI that plays Jordan. The buyer should press on validity and budget, but remain open to a technical follow-up if Alex asks useful questions and earns it. Afterward, we want a transcript-grounded score and a shorter drill aimed at the weakest part of the call.
Tommy separates three things: the scoring standard, reusable buyer behavior, and facts about this deal. We will construct each explicitly.
Start a project
The first positional value, ep-practice, is a directory name the agent chose for this tutorial. Tommy creates that directory and keeps the scenario definitions and practice history together inside it. The longer value passed to --name is a human-readable project name. Neither value comes from Expected Parrot; the agent chooses useful names from the user’s context.
After initialization we change into the new directory. Every later Tommy command discovers the project from the tommy.json marker created there.
tommy init ep-practice --name "Expected Parrot enterprise practice"
cd ep-practice
Show JSON response
{
"schema_version": 1,
"status": "ok",
"command": "tommy init",
"data": {
"project": "/work/ep-practice",
"marker": "/work/ep-practice/tommy.json"
},
"warnings": [], "errors": [], "next_steps": ["cd /work/ep-practice", "tommy guide"]
}Define what good looks like
A scorecard tells the evaluator what to look for after the call. It is independent of any one buyer, so the same scoring standard can be reused across many practices.
We are making up the IDs in this section. enterprise-call, conversation, discovery, and close are short labels the agent chooses so later commands can refer to these objects unambiguously. The salesperson should not be asked to invent them. IDs should be memorable and stable; the accompanying names and descriptions are what people will read.
First we create an empty scorecard, then a group that organizes related criteria, and finally two criteria. Each criterion is worth up to two points. Tommy will later give these definitions and the transcript to an evaluator, which must score both criteria and cite the turns supporting its judgment.
tommy scorecard create --id enterprise-call --name "Enterprise sales call"
tommy scorecard add-group \
--scorecard enterprise-call \
--id conversation \
--name "Conversation"
tommy scorecard add-criterion \
--scorecard enterprise-call \
--group conversation \
--id discovery \
--name "Discovery" \
--description "Asks useful questions before proposing a solution." \
--max-score 2
tommy scorecard add-criterion \
--scorecard enterprise-call \
--group conversation \
--id close \
--name "Next step" \
--description "Earns and clearly confirms a bounded next step." \
--max-score 2
Show JSON responses
{
"status": "ok",
"command": "tommy scorecard create",
"data": {"id": "enterprise-call", "name": "Enterprise sales call", "groups": []}
}
{
"status": "ok",
"command": "tommy scorecard add-criterion",
"data": {"scorecard_id": "enterprise-call", "group_id": "conversation", "criterion_id": "close"}
}Construct the reusable voice buyer
A template controls the simulated side of the conversation: who the buyer is, how resistant they should be, how the call begins, and what the seller must earn. It points to enterprise-call, so attempts made from this template will be evaluated with the scorecard we just defined.
Here skeptical-research-leader is another ID we chose. --mode voice tells Expected Parrot to present a spoken interview experience. --opening mid_conversation avoids spending the practice on introductions, while tough_but_winnable tells the AI not to concede too easily. These settings describe a useful rehearsal; they do not claim that the real Jordan will behave this way.
tommy template create \
--id skeptical-research-leader \
--name "Skeptical research leader" \
--call-type "Pricing follow-up" \
--scorecard enterprise-call \
--buyer-name "Jordan Chen" \
--buyer-role "Head of UX Research" \
--buyer-behavior "Direct and skeptical, but candid when the seller asks specific questions." \
--success-condition "Offer a 30-minute technical validation session if the seller earns it." \
--mode voice \
--duration-minutes 12 \
--difficulty tough_but_winnable \
--opening mid_conversation
Show JSON response
{
"schema_version": 1,
"status": "ok",
"command": "tommy template create",
"data": {
"id": "skeptical-research-leader",
"mode": "voice",
"duration_minutes": 12,
"difficulty": "tough_but_winnable",
"objections": []
},
"warnings": [], "errors": [], "next_steps": []
}The template starts with no objections. We add two now. Their names are labels for humans; each prompt is an instruction to the AI buyer about a concern it may raise. The optional follow-up gives the buyer a way to probe past a superficial answer. These are territories rather than a rigid script, so the conversation can still respond naturally to what Alex says.
tommy template add-objection \
--template skeptical-research-leader \
--name "Validity" \
--prompt "How can simulated respondents support decisions about real customers?" \
--follow-up "Ask what evidence would make the seller trust the method."
tommy template add-objection \
--template skeptical-research-leader \
--name "Budget" \
--prompt "Why should I spend this much before the team has validated it?"
Show JSON responses
{
"status": "ok",
"command": "tommy template add-objection",
"data": {"template_id": "skeptical-research-leader", "objection_count": 2}
}Add only the known deal facts
The template is reusable behavior; the deal supplies facts about this particular opportunity. Here acme-research is our chosen deal ID. The seller, offering, price, prospect, stage, objective, and history become context the AI buyer may rely on. Keeping them separate means the reusable persona does not silently become CRM history, and it makes clear which details were actually supplied.
The known budget objection belongs to this deal because it is something we know about Acme. The more general way of pressing on budget belongs to the template because it is useful behavior for similar practices.
tommy deal create \
--id acme-research \
--name "Acme research platform evaluation" \
--seller-company "Expected Parrot" \
--offering "AI-assisted research platform" \
--price '$15,000 per month' \
--prospect-company "Acme" \
--industry "Software" \
--buyer-name "Jordan Chen" \
--buyer-role "Head of UX Research" \
--stage evaluation \
--objective "Earn a technical validation session" \
--history "Jordan saw a demo and leads a six-person research team."
tommy deal add-objection \
--deal acme-research \
--text "Budget has not been approved."
Show JSON responses
{
"status": "ok",
"command": "tommy deal create",
"data": {"id": "acme-research", "prospect": {"company": "Acme", "buyer_name": "Jordan Chen"}}
}
{
"status": "ok",
"command": "tommy deal add-objection",
"data": {"deal_id": "acme-research", "objection_count": 1}
}Prepare, inspect, and launch
A practice combines the reusable template with this deal. We choose jordan-pricing as its ID. Preparing records hashes of the selected template, deal, and scorecard; if one changes afterward, Tommy refuses to build the stale practice and asks us to prepare a new one.
Building turns those components into the actual instructions for the AI buyer and a native EDSL survey. We choose runs/jordan-pricing as the visible output directory. Expected Parrot never needs to read Tommy’s private .tommy state.
tommy practice prepare \
--template skeptical-research-leader \
--deal acme-research \
--id jordan-pricing
tommy practice build \
--practice jordan-pricing \
--output-dir runs/jordan-pricing
Show JSON responses
{
"status": "ok",
"command": "tommy practice build",
"data": {
"practice_id": "jordan-pricing",
"output_dir": "/work/ep-practice/runs/jordan-pricing",
"guide": "/work/ep-practice/runs/jordan-pricing/buyer-guide.md",
"survey": "/work/ep-practice/runs/jordan-pricing/survey.ep"
}
}Read the generated buyer instructions
Before previewing or deploying, inspect the exact guide the sales target will follow. This is the combined result of the buyer template, the Acme deal facts, and the practice settings—not another hidden model call. Tommy prints the guide and also reports the exported Markdown file that contains it.
tommy practice instructions --practice jordan-pricing
Show generated instructions
{
"status": "ok",
"command": "tommy practice instructions",
"data": {
"practice_id": "jordan-pricing",
"source": "/work/ep-practice/runs/jordan-pricing/buyer-guide.md",
"instructions": "# Sales roleplay buyer guide\n\n## Character\n\nYou are **Jordan Chen**, Head of UX Research. Direct and skeptical, but candid when the seller asks specific questions.\n\n## Conversation design\n\n- Call type: Pricing follow-up\n- Opening: mid_conversation\n- Difficulty: tough_but_winnable\n- Target length: about 12 minutes\n..."
}
}Read this output as the buyer’s operating brief. It includes the supplied deal context, objection territory, success condition, and guardrails against inventing facts. If it is wrong, update the underlying template or deal and prepare a new practice rather than editing the generated guide by hand.
Preview is read-only. Tommy sends the same generated instructions into an Expected Parrot Interviewer preview so you can verify the voice experience before creating a live respondent study.
tommy practice preview --practice jordan-pricing
Show JSON response
{
"status": "ok",
"command": "tommy practice preview",
"data": {"practice_id": "jordan-pricing", "preview_url": "https://…", "external_state_created": false}
}Deployment creates a private Expected Parrot respondent study backed by those instructions. The returned respondent URL is what Alex opens to speak with the AI buyer. Run it only after the guide and preview are acceptable.
tommy practice deploy --practice jordan-pricing --confirm
Show JSON response
{
"status": "ok",
"command": "tommy practice deploy",
"data": {"practice_id": "jordan-pricing", "uuid": "<human-survey-uuid>", "respondent_url": "https://…"}
}Alex opens the respondent URL and has the voice conversation. When it is complete, fetch that response as a distinct attempt. The native response export is visible in the named run directory.
tommy attempt fetch \
--practice jordan-pricing \
--uuid <human-survey-uuid> \
--rep "Alex Rivera" \
--buyer "Jordan Chen" \
--id alex-round-1 \
--output-dir runs/alex-round-1
Show JSON response
{
"status": "ok",
"command": "tommy attempt fetch",
"data": {
"id": "alex-round-1",
"turn_count": 18,
"responses": "/work/ep-practice/runs/alex-round-1/responses.ep"
}
}If the transcript came from another recorder, import it instead. Text and JSON transcripts are accepted; importing does not call an external service.
tommy attempt import \
--practice jordan-pricing \
--transcript transcript.json \
--rep "Alex Rivera" \
--buyer "Jordan Chen" \
--id alex-round-1
Show JSON response
{
"status": "ok",
"command": "tommy attempt import",
"data": {"id": "alex-round-1", "practice_id": "jordan-pricing", "turn_count": 18}
}Evaluate the transcript
Review preparation creates native EDSL Jobs but does not run a model. The returned ep run command is the explicit inference boundary. Both Jobs and Results live in the run directory—not in .tommy.
tommy review prepare \
--attempt alex-round-1 \
--model gpt-5.4-mini \
--output-dir runs/alex-round-1
Show JSON response
{
"status": "ok",
"command": "tommy review prepare",
"data": {
"attempt_id": "alex-round-1",
"jobs": "/work/ep-practice/runs/alex-round-1/review.jobs.ep",
"expected_results": "/work/ep-practice/runs/alex-round-1/review.results.ep",
"estimated_model_calls": 1
}
}ep run \
--jobs runs/alex-round-1/review.jobs.ep \
--output runs/alex-round-1/review.results.ep
Show representative JSON output
{
"status": "completed",
"output": "runs/alex-round-1/review.results.ep"
}Register validates the review against the scorecard and verifies that every evidence reference points to a real transcript turn. Then render the standalone HTML coaching report.
tommy review register \
--attempt alex-round-1 \
--results runs/alex-round-1/review.results.ep
tommy report \
--attempt alex-round-1 \
--output-dir runs/alex-round-1
Show JSON responses
{
"status": "ok",
"command": "tommy report",
"data": {
"attempt_id": "alex-round-1",
"report": "/work/ep-practice/runs/alex-round-1/report.html",
"score": 63,
"standalone": true
}
}Practice the weakest moment again
Suppose “Next step” was the weakest criterion. Tommy can derive a five-minute practice from the reviewed attempt while preserving its provenance. Build it into a new named directory and preview it before deployment.
tommy drill prepare \
--attempt alex-round-1 \
--criterion close \
--id alex-closing-drill
tommy practice build \
--practice alex-closing-drill \
--output-dir runs/alex-closing-drill
tommy practice preview --practice alex-closing-drill
Show JSON responses
{
"status": "ok",
"command": "tommy drill prepare",
"data": {
"id": "alex-closing-drill",
"kind": "targeted_drill",
"parent_attempt_id": "alex-round-1",
"target_criterion_id": "close"
}
}After a second reviewed attempt, compare both rounds criterion by criterion.
tommy compare --attempt alex-round-1 --attempt alex-round-2
Show JSON response
{
"status": "ok",
"command": "tommy compare",
"data": {"attempt_ids": ["alex-round-1", "alex-round-2"], "overall_change": 18}
}When JSON is the better interface
The keyword commands make construction legible and are useful when an agent is learning the domain with you. Tommy also keeps bulk JSON ingestion for definitions you already maintain, or when an agent should minimize tool calls. These commands produce the same canonical components:
tommy scorecard add scorecard.json
tommy template add template.json
tommy deal add deal.json
Show JSON response
{
"status": "ok",
"command": "tommy template add",
"data": {"id": "skeptical-research-leader", "path": "/work/ep-practice/.tommy/templates/skeptical-research-leader.json"}
}The hidden path above is Tommy’s canonical project state. It is not an export path and is never passed to ep run. Runnable artifacts always go to the explicit --output-dir you chose.