Engineering noteAI & Automation
Inside Practice Companion: Rooms, Evidence and Level
Structured practice in TEEI Language: four rooms, one memory, and a level read from a written conversation three times over.
The programme
On this page
- Key facts
- Practice between conversations with people
- At a glance
- One companion, one memory
- Write, in brief
- Listen
- Say it
- How Practice Companion measures level
- Where the companion sits: language, skills, mentorship
- What comes next: an AI trainer in Skills Academy
- What we chose not to build
- Verify this
- Appendix: engineering notes
- Frequently asked questions
Practice Companion is the structured practice in TEEI Language between conversations with people. A learner keeps meeting their volunteer one to one, and other learners in group sessions. On the days in between, the companion gives them four rooms around one desk: Write, Listen, Say it and the level conversation. It remembers their level, the situation they are practising for and the lines they have chosen to keep, and every room has a way back to a person.
Practice Companion is not open to learners yet. Before the rooms open, they have to pass L2SAFE, the benchmark TEEI Research built to test whether a model understands and answers a question as safely in learner language as in fluent language. We are also tuning prompts and model settings so that learners get the best possible experience, finishing the production setup of the voice path, and deploying the level judgement behind its certification gate. AI first feedback in Skills Academy is built but not open yet either.
Our first note on the companion followed one written sentence through the writing room. This one goes wider. It starts with what the companion is for, then shows how each room is built on AWS, how the companion reads a learner’s level from a conversation, and how it compares two readings without letting a new model look like progress. It places the companion on TEEI’s path from language to skills to mentorship, with Skills Academy and TEEI Mentorship on the same engine, and ends with the one part that is still to come: an AI trainer in Skills Academy. Unless we say otherwise, it describes what is built; the closure line above says what still has to reach production before the rooms open. The engineering detail is in an appendix at the end.
Built through volunteer contributions. TEEI’s software platforms are built through volunteer work, with NOK 0 spent on paid software development. Infrastructure and third-party services are separate from that figure.
Key facts
- One companion with one memory and four rooms. A line the learner keeps in any room can travel into their next booked session, where it is the only thing from the companion the volunteer sees.
- Every model call from the written rooms goes through one AWS Lambda function in Europe (Ireland) to Claude models on Amazon Bedrock, using EU cross-Region inference. The voice runtime for Say it calls Amazon Bedrock in Europe (Ireland) itself. On both paths a Bedrock guardrail judges what reaches the learner.
- The level conversation is six written tasks. Code chooses the tasks from a versioned plan; the model only words each question.
- A scorer reads the conversation three times, quotes the learner’s own words for every criterion, and keeps the lowest band when the readings disagree. On 15 test conversations frozen before their first run, the lowest of three readings matched the intended band every time.
- Progress between two conversations is a word, up, same or down, and only when both used the same instrument and different questions.
- In Skills Academy, the skills step on TEEI’s path after language, AI first feedback on submitted work is built on the same function and not yet open. Next on the same engine: an AI trainer that gives hints, never the answer.
Practice between conversations with people
TEEI Language is conversation with people: one-to-one sessions with a volunteer by video, and group sessions such as Speaking Clubs. Practice Companion is the structured practice between those sessions. The learner keeps meeting people. On the days in between, they write, listen and speak about the situation they are practising for, get one thing back at a time, and carry what they kept into their next conversation. The companion adds structure and continuity between the sessions with people; those sessions stay the centre of the programme.
The practice is continuous rather than a set of exercises. The companion knows the learner’s level, from a conversation it has read, and the situation they are practising for, from the goal in their profile, and both shape every room. A correction is anchored in the learner’s own sentence, and the companion steers later practice towards a moment where the corrected form comes up again, around a day, a week and a month later, so a form fixed once has to be produced again without help. Each level reading names up to three things to practise next, and the writing room steers towards those. The level itself comes from the level conversation, which reads writing: six written tasks. Say it reads the learner’s spoken turns with that level in mind, and Listen groups its dialogues around it, but neither of them sets it.
The aim is language the learner can use without the companion, in a different situation and with another person. A better sentence written with help is not the measure. The same form, a week later, in their own words, is.
One learner, one week, with the situation from the fixture data in the figures below. A learner whose goal is a doctor’s appointment writes a reply to the receptionist in Write and gets one thing back: “working” to “work”, with the reason. In Listen, they hear a caller ask a clinic for a time, with the words hidden until they want them. In Say it, they talk the appointment through with a receptionist who holds two times, and keep the line they want to use. Their session with their volunteer goes ahead as booked, and the kept line is the one thing from the companion that travels into it. A week later, the writing room steers towards a moment where the corrected form comes up again, without a drill, and reads whether it is theirs now. The companion does not read what was said in the session with the volunteer, and the volunteer does not see the companion; the learner carries the practice from one to the other.
At a glance
| Room | What the learner does | What runs underneath | What is kept |
|---|---|---|---|
| Write | Writes a sentence about their own life and gets one piece of advice | The practice function on AWS Lambda; Claude Sonnet 4.6 on Amazon Bedrock with a forced tool and a guardrail | Their turns, corrections anchored in their words, and later uses of a corrected form |
| Listen | Listens to short everyday dialogues and answers three questions that are never scored | Recordings made once with ElevenLabs voices and streamed from Cloudflare R2; no live AI | Lines they choose to keep |
| Say it | Talks a situation through, by voice or in writing | ElevenLabs for speech recognition and voices; TEEI’s scenario engine on AWS Fargate; Claude Haiku 4.5 for a turn no rule can read | The transcript as text, never audio, and lines they keep |
| Level conversation | Answers six written tasks and is read, not taught | An interviewer and a scorer on Claude Sonnet 4.6, three readings per conversation | The reading, every quote behind it, its provenance and its comparability key |
One companion, one memory
The four rooms are not four apps. They share one memory: the learner’s level, the situation they are practising for, taken from the goal in their profile, and a shelf of lines they have chosen to keep. A line kept while writing is on the shelf when they open Say it, and the level from their latest conversation sets how every room writes and speaks to them.

The shelf is also the only bridge from the companion to a person. Before a booked session, the learner can bring one kept line into it. The request carries the id of the line and nothing else, so no free text can ride along, and the server checks that the session is theirs, accepted and still ahead. If more than one upcoming session could be meant, the learner is asked; the link is never guessed. On the volunteer’s side, the line is the only thing from the companion they see. A test walks the import graph from every page a volunteer can open and fails if any of them reaches the companion’s modules or tables. What the companion knows about a learner stays with the learner.
Every room has a way back to the people the learner is already practising with.
Write, in brief
The writing room is described step by step in our first note. In short: the learner writes a sentence, and our backend on Cloudflare Workers sends it with a short-lived signed token to an Amazon API Gateway endpoint, where a Lambda authorizer checks the token before the practice function runs. The function calls Claude Sonnet 4.6 with a forced tool, so the whole reply arrives as one structured answer, and any correction carries its category from a versioned list of 27. A Bedrock guardrail judges that structured answer as output before anything reaches the learner. Back in our backend, a correction has to point at the learner’s exact words before it is stored as evidence, and the reply is streamed three characters every 14 milliseconds, so it reads at a steady pace rather than in bursts.
Limits are part of the design. Each learner has an hourly and a daily message limit, and both fail closed: if the counter cannot be read, the message is not sent. A message is at most 4,000 characters, the last 40 messages are kept and the last 10 turns travel to the model. A stored correction that cannot name the model that made it does not count as evidence.
After a correction, the companion looks for the corrected form in the learner’s own later sentences, in four windows: the same session, and around a day, a week and a month later. When a window is due, the conversation steers towards a moment where the form comes up naturally, at most once a session and never as a drill.
Listen
Listen has no live AI at all. Its short English dialogues, about booking a doctor, a job interview, registering an address or viewing a flat, are written and reviewed by people and recorded once with ElevenLabs voices, then streamed from Cloudflare R2. The words stay hidden until the learner asks for them. Three questions are answered against the recording and never scored, a glossary gives each hard word in English and Ukrainian, and the dialogues are grouped around the learner’s level: at your level, a step up and easier.

Say it
Say it is the room where the learner talks. They open a situation, a receptionist with two appointment times, a short job interview, a new colleague on the first day, or a situation of their own built from the goal in their profile, and talk it through with a voice on the other end. They can speak or type, and ask for as much help as they want. Meaning counts and grammar does not: “No, I working morning. Can after three?” is a complete turn.

The voice path has three parts. In the browser, the ElevenLabs client connects over WebRTC to ElevenLabs, which recognises speech, handles turn-taking and speaks the replies. Each recognised turn goes over a WebSocket to TEEI’s practice-voice runtime: a Node.js 22 service on AWS Fargate behind an Application Load Balancer, in its own VPC without a NAT gateway, in Europe (Ireland). The runtime holds the conversation and writes it to our database: the session state, an append-only log of events, and the lines the learner keeps. Our backend gives the browser a token and a conversation id; the ElevenLabs key never leaves our servers. The ElevenLabs setup has its own note.
The three fixed situations are not improvised. Each one is a scenario contract: a goal, the facts the other person holds, the lines they can say, the help on offer, the rules, and the intents a model may return, with a pure step function that moves the conversation on. Rules read each turn first. Only a turn no rule can read goes to Claude Haiku 4.5 on Amazon Bedrock, which returns an intent, and only the intents the current step declares survive. A line can name a fact only by its id, so nothing the learner hears is invented.
One more check runs after the step. If the step confirmed something, say an appointment on Thursday at four, and the learner’s line contradicts it with a no, a but or a time they never chose, the reading is refused and the learner is asked again. The check covers a rule’s reading as well as the model’s. In a live simulation, a learner named the place of one lunch option and the time of the other in one sentence; this check is what keeps that from becoming a booking nobody asked for.
A situation of the learner’s own has no contract to pick lines from, so the other person’s lines are written by the model behind a Bedrock guardrail, through the practice function when the learner types and from the voice runtime when they speak, and the room will not open that situation without one. A personal detail the guardrail detects stays masked in every later request. The learner’s latest level travels into the session, so their turns are read with their level in mind; the fixed situations’ lines stay the same at every level. The transcript is stored as text. No audio is stored.
We measured the voice path on 11 September 2026, from a home connection in Norway, with a headless browser playing synthesised learner turns into a test runtime. The samples are small, and the table gives their sizes.
| Turn | Time to first audible reply | Turns measured |
|---|---|---|
| Read by a rule | 0.77 to 0.89 s | 9 |
| Read by the model, with a short bridge line (“Let me see.”) | 0.80 s to the bridge line; the answer about 1.2 s later | 1 |
| Read by the model, without a bridge line | 1.95 to 2.11 s | 2 |
| A control, such as asking for help | 0.42 to 0.65 s | 8 |
Classifying one turn with Claude Haiku 4.5 alone took a median of 1.27 seconds over 15 calls (95th percentile 1.35 s); Claude Sonnet 4.6 took 1.56 seconds for the same job. That is why the voice path uses Haiku, why rules go first, and why a model turn opens with a bridge line: a reply in under a second and a half is only possible when most turns never wait for a model.



How Practice Companion measures level
Why a conversation, not a test
TEEI Language also has a level test on the web, multiple-choice questions that someone can take before signing up: six opening questions, then eight at the level those point to. Its result is written to their account when it is created, so the companion’s first reply is already written at their level. It is a good starting point and a poor “after”: a second run asks the same questions again, so it is partly a memory test. For level and progress, the instrument is the level conversation. It asks the learner to do things with the language, in writing, and reads what they did.

Six tasks from a versioned plan
A conversation is six tasks from a fixed plan, version ltp-1. Every conversation starts with the same three core tasks. Then code chooses a branch from the learner’s own core answers: if they averaged 25 words or more, three stretch tasks follow, and otherwise three support tasks. The rule reads length, not grammar, on purpose. It is the one signal code can read without judging the learner, and it decides only which questions come next, never a band. The same answers always take the same path.
| Part | Task | What it asks the learner for | Readable from |
|---|---|---|---|
| Core | Introduce yourself | Simple sentences about themselves: where they live and what they do | A1 |
| Core | A normal day | A routine, described in order | A1 |
| Core | Something that happened | A short account of a past event, in order | A2 |
| Support | Ask for help | A request for help, with a reason | A1 |
| Support | Plans | Plans or intentions for the near future | A2 |
| Support | A preference | A preference with a simple reason | A2 |
| Stretch | Explain a problem | A problem explained with some precision, and what they did about it | A2 |
| Stretch | Compare | A comparison with a view on each side | B1 |
| Stretch | Give an opinion | An opinion held with reasons, ideally with a concession | B1 |
Every task has two versions of its question, A and B, asking for the same kind of writing at the same difficulty. A learner’s first conversation takes a variant by the toss of a coin, stored with the reading, and each later conversation takes the other one, so no two conversations in a row ask the same questions. The coin is random rather than derived from the account, so a person rating two conversations blind cannot work out which came first. The two variants are built to be comparable. Whether they are equally hard in practice has not been measured yet, and we do not claim it.
If a learner is unsure what a question wants, they can ask for one of three neutral clarifications, and only those three: that a few sentences are fine, that any example from their own life will do, and that there is no right or wrong answer. None of them gives a word, a structure or an example answer. Each one used is logged on the turn, and the scorer is told, because an answer written after help is different evidence.
The interviewer asks, and does not teach
The model never decides what to ask, only how to ask it. The interviewer receives the task and the learner’s previous answer and words the question so that it follows naturally from what they just said. It runs on Claude Sonnet 4.6 at temperature 0.3, with a forced tool, so the result is a question and nothing else. Our app then checks the question: one question, at most 240 characters, and no teaching. If the check fails, or the call does, the learner gets the task’s canonical wording instead, so the conversation never stalls on the model.

The scorer reads, and quotes
When the six tasks are done, the scorer reads the whole conversation. It runs on Claude Sonnet 4.6 at temperature 0.1, through a forced tool, against a versioned rubric with five criteria: interaction, task fulfilment, range, accuracy and coherence. For each criterion it gives a band, or says the criterion was not observed.
Every band must carry evidence: the turn and a quote from the learner’s own answer. The quote is checked word for word against the transcript twice, in the Lambda function and again in our backend, and a reading with a quote that is not there is refused. The reading also names what the learner can do, up to three things to practise next, and any quality flags. It carries no confidence number. The learner’s answers travel to Amazon Bedrock as guarded content, so the guardrail judges exactly what the learner wrote and the model reads it as text to assess, never as instructions. When there is too little writing to judge, the scorer says so, and the conversation gives no level at all rather than a guess.

Three readings, and the lowest band
Our first scorer read each conversation once, and in certification it sometimes read a learner one band too high. The cause was the judgement itself, not chance in the sampling: the same transcript was read one band apart from one call to the next, in both directions, with valid quotes every time, so checking the quotes could not catch it. Setting the temperature to 0 did not help; at 0 the readings still disagreed on 5 of 31 conversations and read high more often. In one diagnosis, a reading gave four of the five criteria B1 and set the level at B1, while the other readings gave A2 on every criterion. The disagreement sat in the criteria.
So the scorer reads every conversation three times, independently and in parallel, and when the readings disagree it keeps the lowest band. Readings on both sides of a boundary are evidence that the learner is between two bands, and between two bands the rubric’s rule is the lower one. The stored reading is one of the three, whole: its band, its criteria and every quote come from the same reading, never a blend. If the readings disagreed, the stored reading is flagged readings_disagreed. If most of them found too little to judge, the conversation gets no level.
We tried the median first. It failed its own certification run, so it is not what runs.
What we measured
We certify the scorer against written test conversations, each written for one intended band: a development set of 31, a regression set of 14 and a holdout set of 15. The holdout set was written after the change and frozen, with a recorded hash, before its first run. Its design was fixed before any answer was written: five typical conversations, four built to tempt a high reading, four built to tempt a low one, and two that test the mechanics. The intended bands are the test writers’, not ratings by independent raters, so these numbers show that the scorer reads conversations the way they were designed. They do not show agreement with people.
| Test set and scorer | Conversations | Exact | High | Low | Over-reads on conversations built to tempt one |
|---|---|---|---|---|---|
| Development, one reading | 31 | 91.3% | 5.9% | 2.8% | 5 |
| Development, lowest of three | 31 | 94.2% | 0.6% | 5.2% | 0 |
| Regression, one reading | 14 | 92.8% | 7.2% | 0% | 7 |
| Regression, lowest of three | 14 | 100% | 0% | 0% | 0 |
| Holdout, one reading | 15 | 96.4% | 3.2% | 0% | 5 |
| Holdout, lowest of three | 15 | 100% | 0% | 0% | 0 |
The price shows in the development set: more readings one band low, from 2.8% to 5.2%, all on conversations built to invite a low reading. No threshold was moved, no label changed and no conversation was dropped to get these numbers; the old failures stay in the sets as regressions. The holdout set is small. With 15 conversations, 15 of 15 is consistent with a true match rate as low as about 78 percent (95 percent confidence interval), so the holdout row shows that the scorer did not fail on conversations it had never seen, not that it is right 100 percent of the time.
Scored five more times, three readings each time, no conversation moved by more than one band between repeats. All five repeats agreed on 90% of the development conversations and on every regression and holdout conversation.
Three readings take time, though they run in parallel and finish within a second or two of each other. Over 34 readings of the longest development conversations, the median was 17.2 seconds and the 90th percentile 27.1 seconds. The guardrail took 0.4 seconds; most of the rest was writing about 1,200 tokens of output, and its speed moved with load on Amazon Bedrock. Our app waits 28 seconds, inside the 30-second ceiling of Amazon API Gateway. Two of the 34 went over. A reading that times out is released, and the learner can finish the conversation again; nothing half-read is stored.
A receipt for the scorer
A scorer that changed without being certified again would make every number above meaningless. So the certification run writes a receipt that names exactly what passed: a fingerprint of the scorer, a SHA-256 over its code and its rubric, together with the model, the temperature, the guardrail version and its policy, and a hash of every test file. When our infrastructure is built, it compares the scorer about to ship with the receipt and refuses a different scorer, a different model or a different guardrail. The same fingerprint is stored with every reading, so each stored band names the judgement that produced it. The writing room’s reply prompt has a receipt of its own, keyed on a hash of the prompt.
Comparing two conversations
A stored reading carries a comparability key: a SHA-256 over the language, the modality, the task plan, the rubric, the system prompt, the tool schema, the model and the scorer’s fingerprint. If any one of them changes, the key changes, so a new prompt or a new model can never look like progress.
Two readings are put side by side only when they pass every check: both come from conversations rather than the web test, they share a comparability key, and they used different variants, so the second conversation did not repeat the first one’s questions. The result is then a word, up, same or down, from one band to another. Never a percentage, because CEFR bands are not a scale you can do arithmetic on. And the result is labelled uncalibrated unless both readings are calibrated.

Checking the scorer against people
Certification tests the scorer against conversations written for a band. Whether its bands agree with experienced human raters is a different question, and the platform holds the instrument for it. Raters score conversations blind: they do not see the AI’s band, the account or the date, and because variants come from a coin, they cannot tell an earlier conversation from a later one. Agreement is computed three ways: exact, within one band, and as linearly weighted Cohen’s kappa. After a learner’s first English conversation with a volunteer, the volunteer can also give a blind estimate of their level, as a second, independent signal.
Until those ratings show how far the scorer and people agree, every reading is stored as uncalibrated, and we make no claim about agreement with human raters.
What the level does, and never does
The level calibrates the companion. It sets how the writing room writes to the learner from the first reply, groups the listening dialogues around them, and tells the voice runtime how to read their turns. The things to practise named in their latest reading decide what the writing room steers towards, unless a chance from an earlier correction is due, which comes first. The level is never a requirement for a room, never a right to anything and never a certificate, and the companion never shows a score, a percentage or a streak.
Where the companion sits: language, skills, mentorship
TEEI’s programmes form one path, from language to skills to mentorship, and a person can start at any step. TEEI Language and TEEI Mentorship are two products on one platform: one codebase, built with React Router and deployed as two Cloudflare Workers, one for each product’s address, and Skills Academy is served from the same platform under its own address. They share code. They do not share an audience, a promise, a profile or words: a learner is not a mentee, a participant in a class is neither, and a conversation partner is not a mentor.
Oksana T. TEEI Language Graduate
Film, 1 min 21 s, with captions
At the heart of both is a conversation between two people. Those conversations run on Amazon Chime SDK, and sessions and verified hours are settled from its meeting events, as our note on Amazon Chime SDK describes. Practice Companion adds no hours and no sessions. It lives between the conversations, and its shelf is how something from the days in between reaches the next one. Practice Now, which matches a learner with a person for a live conversation, is a different thing: it is matching between people, not the companion.
Skills Academy is the skills step: its own programme, at its own address, where volunteers with professional skills teach live workshops, clinics, expert Q&As, webinars and classes, and a TEEI Language learner finds it from their own desk. It shares the companion’s engine. Work handed in for a class goes to the same Lambda function, at its own route, for AI first feedback, built and not yet open, under the rules the level conversation keeps:
- Every score quotes the participant’s work, and the quote must appear in it exactly once. A single quote that fails refuses the whole reading, so no score is ever stored without its reason.
- The work travels to the model as guarded content, and our logs keep lengths, counts and reason codes, never the text, a quote or a comment.
- The rubric has three criteria, each scored from 1 to 5. The participant scores their own work first, and a reviewer does not see those scores until they have saved their own. There is no total, no grade and no pass or fail.
- AI gives the first feedback, one or two sentences to act on per criterion. A person on the class team reads it before the scores count.
- Temperature 0.2, a short window for a second attempt, and an hourly limit per person.
TEEI Mentorship is the last step on the path, and its help between sessions is a career companion on its own Lambda function, behind the same authorizer. It gives feedback on a CV, helps prepare the next session and answers a career question, in at most 250 words, with an hourly limit per person. It refers back to the mentor by name and never claims a relationship of its own: the mentor is the person, and the companion is the help in between.
| Piece of the engine | TEEI Language | Skills Academy |
|---|---|---|
| One Lambda function behind a signed bridge, Claude on Amazon Bedrock and a guardrail | Write and the level conversation; Say it reaches Bedrock and the guardrail from its voice runtime | AI first feedback on submitted work |
| Every finding quotes the person’s own words, or it is refused | Corrections and level readings | Every rubric score |
| Provenance stored with each reading | Model, prompt, schema, rubric, scorer fingerprint | Model, prompt, schema, rubric |
| A chance to show it again without help, after a day, a week and a month | A corrected form in their own later sentences | A new task without hints, from the AI trainer |
| A shelf that carries one thing to a person | A kept line, into a booked session | The work and the criterion it is stuck on, into a clinic or to a mentor |
What comes next: an AI trainer in Skills Academy
This is the one section of this note about what comes next rather than what is built. Everything in Skills Academy is professional learning, with AI in every field of it. AI and data is a field of its own, from AI foundations and prompt practice to responsible AI, and in every other field AI is part of how participants learn and how their work is read. The next part of Practice Companion is the trainer for that: the companion’s loop, applied to professional work, in Skills Academy, on the engine above.
Hints, never the answer. A participant takes a task from a class, and the AI trainer helps them through it. Its tool has a field for a hint and a field for the next move, and no field for a solution, so the rule lives in the function, the way anchoring does, and not only in the prompt. Every hint is short and anchored in the work the participant has handed in, and a Bedrock guardrail judges it before they see it.
Their own score first. The participant hands in the work and scores it first. AI gives the first feedback and quotes their own words for every score, and a person on the class team reads it before it counts. Anything AI has scored is labelled AI-assessed; only work a person has reviewed is labelled verified.
A new task, without hints. A criterion the participant has not yet shown without help becomes a learning item, and every piece of evidence records how much help was in front of them. Then comes the companion’s chance, in its professional form: a new task without hints, a day, a week and a month later. A new variant rather than the same task, because a skill shown once on a familiar task is not yet a skill. The difference between how someone does with help and how they do on a new task without it is the measure we watch most closely. We call it the crutch gap.
When AI is the skill. When using AI is what the participant is learning, the AI tool is allowed in the assessment too. What changes is the trainer’s role: it does not lead them through the task they are assessed on.
Stuck points go to a person. Like a kept line in TEEI Language, the work and the criterion it is stuck on travel with the participant to the instructor’s clinic, or on to the next step of the path, a mentor. After a class, “Keep going with a mentor” opens the TEEI Mentorship directory filtered to the subject. It proposes nobody, books nothing and sends nothing; the participant chooses.
The skills underneath come from open frameworks. ESCO, the European classification of skills and occupations, is the backbone, with labels in Ukrainian and Norwegian. For working with AI, the classes use the AI Fluency framework by Rick Dakan, Joseph Feller and Anthropic, published under CC BY-NC-SA 4.0, and the first class follows its Discernment part: judging whether what an AI produced is good enough to use.
What we chose not to build
- No persona. The companion has no name, no face and no avatar.
- No score for the learner to chase. No counters, streaks, points or mastery percentages, and progress is a word, never a number.
- No level as a gate. The level calibrates the companion; it never decides access and is never a certificate.
- No blended reading. The stored reading is one whole reading, with its own quotes, never an average of three.
- No model choosing the questions. The task plan is code; the model only words a question.
- No free text to the volunteer. The shelf carries an id, and the volunteer sees one line.
- No audio. Say it stores the transcript as text.
Verify this
- Read about TEEI Language, the programme the companion belongs to.
- Read our first note on the companion, which follows one written sentence through the writing room.
- Read about L2SAFE, the benchmark the rooms have to pass before they open.
- Try the English level test that comes before sign-up.
- Read how Amazon Chime SDK carries TEEI’s conversations and turns them into verified hours.
- See Skills Academy, the skills step, where AI is part of every field, and TEEI Mentorship, the last step on the path.
- If you are building with TEEI, talk to us and we will walk you through the code.
Appendix: engineering notes
For developers who want the detail behind the sections above.
The branch rule
This is the rule that picks the second half of the level conversation, from the task plan’s own file:
export const STRETCH_MEAN_WORDS = 25;
export const wordCount = (text: string): number =>
text.split(/\s+/).filter(Boolean).length;
/** Which three tasks follow the core, from their core answers alone.
* Skipped core tasks count 0. */
export function branchFor(coreAnswers: readonly string[]): LevelBranch {
if (coreAnswers.length === 0) return 'support';
const mean =
coreAnswers.reduce((sum, a) => sum + wordCount(a), 0) /
coreAnswers.length;
return mean >= STRETCH_MEAN_WORDS ? 'stretch' : 'support';
}
It reads length because length is the one thing code can read without judging. It chooses the next three questions and never a band.
Combining three readings
This is the function in the practice Lambda that turns three readings into one. It keeps a whole reading, never a blend:
export function combineReadings(
readings: ValidatedAssessment[],
): ValidatedAssessment | null {
if (readings.length === 0) return null;
const scored = readings.filter(
(r): r is ValidatedAssessment & { level: AssessedBand } =>
r.status === 'scored' && r.level !== null,
);
const insufficientCount = readings.length - scored.length;
const outcomes = readings.map((r) =>
r.status === 'scored' ? r.level : 'insufficient',
);
const disagreed = new Set(outcomes).size > 1;
const mark = (r: ValidatedAssessment): ValidatedAssessment =>
disagreed && !r.quality_flags.includes('readings_disagreed')
? {
...r,
quality_flags: [...r.quality_flags, 'readings_disagreed'],
}
: r;
if (scored.length === 0 || insufficientCount >= scored.length) {
const unplaced =
readings.find((r) => r.status === 'insufficient') ?? readings[0];
return unplaced ? mark(unplaced) : null;
}
const lowest = scored.reduce((low, r) =>
BAND_RANK[r.level] < BAND_RANK[low.level] ? r : low,
);
return mark(lowest);
}
Comparing two readings
This is the function in TEEI Language that decides whether two readings may be shown as progress, with its comments shortened:
export function progressBetween(
earlier: LanguageAssessment | null,
later: LanguageAssessment | null,
): ProgressVerdict {
if (!earlier || !later) {
return { comparable: false, reason: 'too_few' };
}
if (earlier.source !== later.source) {
return { comparable: false, reason: 'different_sources' };
}
// Two web tests ask the same questions: the second is partly a memory
// test.
if (earlier.source === 'web_mcq') {
return { comparable: false, reason: 'not_repeatable' };
}
if (
!earlier.comparabilityKey ||
!later.comparabilityKey ||
earlier.comparabilityKey !== later.comparabilityKey
) {
return { comparable: false, reason: 'different_instrument' };
}
// The same questions twice is partly a memory test too.
const va = earlier.provenance?.variant ?? null;
const vb = later.provenance?.variant ?? null;
if (va !== null && va === vb) {
return { comparable: false, reason: 'same_variant' };
}
const a = rank(earlier.level);
const b = rank(later.level);
return {
comparable: true,
direction: b > a ? 'up' : b < a ? 'down' : 'same',
from: earlier.level,
to: later.level,
empirical:
earlier.validationStatus === 'calibrated' &&
later.validationStatus === 'calibrated'
? 'calibrated'
: 'uncalibrated',
};
}
What a stored reading carries
Each level reading is stored with the provenance that produced it, so a band can always be traced back to its judgement: the model, the Region, the temperature, the token limit, the versions of the system prompt, the rubric and the tool schema, the guardrail, the scorer’s fingerprint, the task plan, the variant, and whether the band came from the model or from the writing floor. Its comparability key is a hash of the language, the modality and six of those fields: the task plan, the rubric, the system prompt, the tool schema, the model and the scorer’s fingerprint. The rest is stored as provenance, and its validation status starts as uncalibrated.
The AWS pieces
| AWS service | Region | What it does for the companion |
|---|---|---|
| Amazon API Gateway (HTTP API) | Europe (Ireland) | The one entrance from our backend into AWS, with a Lambda authorizer that checks the signed token. The same API carries the meeting joins in our Amazon Chime SDK note |
| AWS Lambda | Europe (Ireland) | One practice function for the writing room, turn reading, the interviewer, the scorer and Skills Academy feedback: Node.js 22 on Arm-based AWS Graviton, 30-second timeout, a reserved-concurrency cap, traced with AWS X-Ray. The career companion runs as a function of its own |
| Amazon Bedrock | EU cross-Region inference | Claude Sonnet 4.6 for replies, questions, readings and feedback; Claude Haiku 4.5 in Say it, for a turn no rule can read and for the other person’s lines in a situation of the learner’s own |
| Amazon Bedrock Guardrails | Europe (Ireland) | Content and personal-data policy on what the learner sends and what the model returns; pinned to a version the scorer’s receipt names |
| AWS Fargate (Amazon ECS) | Europe (Ireland) | The practice-voice runtime for Say it, a WebSocket server behind an Application Load Balancer, in its own VPC without a NAT gateway |
Outside AWS, our backend runs on Cloudflare Workers, the Listen recordings stream from Cloudflare R2, and ElevenLabs recognises speech and speaks in Say it.
Frequently asked questions
Can learners use Practice Companion today?
Not yet. Before the rooms open, they have to pass L2SAFE, the benchmark TEEI Research built to test whether a model understands and answers a question as safely in learner language as in fluent language. We are also tuning prompts and model settings so that learners get the best possible experience, finishing the production setup of the voice path, and deploying the level judgement behind its certification gate. AI first feedback in Skills Academy is built but not open yet either.
Is the level a test result or a certificate?
No. It sets how the companion writes and speaks to a learner. It is never a requirement for a room, a right to anything or a certificate.
Does an AI decide the level on its own?
A scorer reads the conversation three times and keeps the lowest band when the readings disagree, and every band has to quote the learner. Readings stay marked as uncalibrated until blind ratings by people show how far they agree.
Which AI models does it use?
Claude Sonnet 4.6 on Amazon Bedrock for the writing room, the interviewer and the scorer, and Claude Haiku 4.5 for a spoken turn no rule can read, all inside TEEI's own AWS account. ElevenLabs recognises speech and provides the voices.
Does the companion record audio?
No. In Say it, the transcript is stored as text and no audio is kept.
Does it replace the volunteer?
No. Conversation with people, one to one with a volunteer and in group sessions, is the point of TEEI Language, and it continues throughout. The companion is the structured practice on the days in between, and every room has a way back to a person.