Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ responses stop when the candidate interrupts. Say "can I get a hint?" when
needed. In a coding-only session Jim never ends the interview early: only the
timer or the candidate's End interview button closes it.

The media preflight offers independent choices for Jim's voice and English
accent. Voices are Puck, Charon, Kore and Orus; accents are American, British,
Australian, Canadian and Indian. Default voice uses `GEMINI_VOICE` (Puck when
unset), and Default accent leaves the interview instructions unchanged. Random
selects one of the named options independently for each control, once before
the interview token is issued. The selections stay fixed through reconnects.
Accent selection adds a speaking direction to Gemini Live; it does not change
the interview content or assessment rules.

The media preflight always requires confirmed output and a working microphone,
and its Back to lobby button releases every device without starting. For an
interview that is not recorded, a candidate may continue without a camera when
Expand Down
3 changes: 2 additions & 1 deletion docs/interview-contract-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ can select it.

## The active bundle

Bundle 33: live prompt 24, report prompt 20, rubric 1, report schema 3.
Bundle 34: live prompt 25, report prompt 20, rubric 1, report schema 3.

| Bundle | Introduced |
|---|---|
| 34 | Optional English accent directions are appended by the live prompt builder without changing the original interview instructions. Default adds no text. Voice and accent Random choices are resolved independently before token issuance and retained for the interview and reconnects. Live prompt 25; report prompt 20, rubric 1 and report schema 3 are unchanged from bundle 33. The server stamps the resolved `interviewerVoice` and `interviewerAccent` after report generation, including incomplete reports. Default voice records the configured Gemini voice; Default accent records Default. These optional provenance fields are normalized at the browser sanitizer, following bundle 7, without changing scores or existing field semantics. Reports without them remain readable without inventing missing interviewer settings. |
| 33 | The live clock policy explicitly forbids reading the TIMER label or its sentence aloud. The last platform reading remains authoritative; requested time answers and the five-minute warning retain their existing rules. Tool-assisted totals remain spend counters and never seed the comparable context peak. A total below the last completed single-generation prompt can establish a decrease; all observations in the tool turn are considered before deciding. Provisional usage received before a tool call is retracted from the comparable peak. Live instructions shorten repeated speech, evidence and interview-step guidance while retaining the three-sentence limit, safety boundaries, evidence requirements and mode-specific behavior. A paged `read_conversation` tool retrieves recorded candidate and interviewer speech omitted from local recovery tails, with substring search, byte cursors, bounded serialized pages and no platform instructions or private review notes. Recovery directs missing earlier details to that tool. Report brief 20 removes the rolling-assessment direction to report embedded instructions. Editor and board material require an exact candidate excerpt before reporting an attempted instruction; fences, headings and platform closing events do not establish one, and feedback does not describe prompt handling. Recovery removes the quoted owed event's superseded final platform clock. Additional anti-script wording was tested and withdrawn: it increased prompt tokens without preventing spoken timer leakage. Configured continuity inputs move the event clock to the final position instead of duplicating it. Configured editor, board and hint tool replies also move their redundant result clock to the authoritative continuity clock. Evidence-recording continuity omits the clock, matching ordinary evidence replies and retaining platform/read-tool time sources. A tool-response batch carries continuity and the final framework checklist once each in their final eligible responses, preserving every result and ordered state change. The final eligible coding evidence response can carry a changed, parsed editor excerpt once so speech-deferred reviews do not leave a stale editor for the tool continuation. A successful changed-code response, including a default editor read, requests the proactive review once using the final batch state, prioritizing the candidate's latest question; a default read that stops at its page limit neither carries the review nor marks the buffer shown, and only a read showing the whole buffer marks it shown, so later-line pages and requested hints whose editor page stopped at its limit do not; only a delivered review consumes an unreviewed parsed substantive revision, so explicit later-line reads, failed writes and later edits still arm the watcher. Timed replies send their private clock as non-generating ordered context, followed by the action on the same ordered channel; while the provider owes or is producing a reply, the action goes as realtime text instead, since a completed client turn would interrupt that reply even before its first output. The socket counts as owing one from detected candidate speech, its transcription, an interruption, model output or a reply-requesting send until the turn completes, and provider evidence lapses after 10 seconds while a requested reply stays owed through the longest configurable reply deadline, which the reply watchdog enforces first. Each timed reply counts as one input. Silent context and untimed input retain their transport. Timer-like candidate source text stays intact. Provider speech activity, while audio is attached and frames are not paused, defers idle prompts, checkpoint refreshes and idle assessment calls until speech ends, without treating activity as transcript evidence. Advisory transport restarts wait through the quiet transcription tail before resuming a stale checkpoint. During ongoing speech the periodic room watch checks held advisories without waiting for another provider event or stall; they finalize the partial transcript before replacing the socket, cancel its unheard reply, and restore the full quoted prefix silently before queued audio continues. The silent speech handoff omits its clock; requested-time reads retain their clock sources. A bounded 40-second microphone queue retains frames while the room loop reconnects, replacing the dependency default of 100 ms. Only a post-cancellation checkpoint can resume that handoff; a resumed socket receives the listening note alone and keeps its context baseline, and a cold one the full cold briefing before it. A platform request the replaced socket still owed is named in the note and stays owed. Events the replaced socket emits while the prefix is finalized are counted in the log. Failed writes retain local briefing debt. Coding-only Live tool declarations offer only the six REACTO phases; behavioral sessions retain all ten phase choices. Tool descriptions remove procedural rules already present in the system instructions while retaining unique paging and snapshot semantics. Whiteboard compression checkpoints omit editor and test data and request the current drawing through `read_board`; a behavioral checkpoint does not return to the drawing. Report repairs compact only whitespace outside JSON strings in complete bounded responses, preserving values and existing malformed or oversized fallback behavior. Whiteboard initial and recovery instructions describe drawing and evidence rather than editing, implementing a function or passing executed tests; coding instructions remain unchanged. Editor and requested-hint reads distinguish a stale or unknown browser-report source from current code, independently of retained Test credit; current and no-report reads remain unchanged. Editor-update outcomes, combined-mode progress and recovery also use exact displayed-report provenance while retaining edit-tolerant Test credit. Recovery briefings add only the owed-question rule and any opening or latest candidate line their transcript tail no longer holds whole, rather than repeating their own no-restart rules or asking a silent checkpoint to continue. Tool continuity follows the same plausibility gate and is omitted while the floor is held. Configured compression carries bounded continuity beside platform inputs after the opening while a cut is plausible (before any measurement, after a detected cut, or while the last comparable prompt count is in the upper half of the band between target and trigger), including the active round, evidenced steps, pending request and a bounded prior candidate opening in coding, so the first response after a context cut can continue without replaying the greeting or full problem restatement. Provider-default sessions retain their existing input path. Default editor reads can collapse identical consecutive leading comments while keeping every distinct note, original line numbers and explicit line access. Tool-assisted usage sums remain spend counters but are excluded from context-size comparisons to avoid false checkpoints. Completed coding retains the first silence check and 30-second attempt throttle, then doubles the spacing of successful same-episode nudges up to 480 seconds. Meaningful candidate activity resets the episode; failed writes do not advance its successful-send cadence. Suppressing a nudge still permits significant editor reviews, and unfinished coding, behavioral support, required replies and trusted timers retain their behavior. Active provider speech detection plus recent voiced microphone frames defers reply-timeout, generation-stall and deferred advisory restarts across watch, event and playout paths. Speech end or acoustic quiet rebases outstanding reply clocks once; stale activity edges alone cannot renew them. A provider speech start with no microphone or provider evidence for 10 seconds is treated as ended. Paused frames do not count as acoustic evidence, context resets discard the acoustic history, and the absolute interview deadline remains authoritative. During real incoming speech, a parsed substantive editor change settled for ten seconds can be sent as non-generating context. Successful delivery has its own language-qualified snapshot and becomes the baseline later editor updates diff from, without consuming optional review or reply debt; silent delivery itself diffs from the code already shown by any route and sends nothing when the model already holds the current code; failed writes remain pending and socket recovery resets or initializes that snapshot. Ordered local writes do not guarantee consumption order against realtime audio. Coding transitions are conditional on implementation not having begun; otherwise review current work. Recovery, review, silence and timer prompts can carry a bounded edits-presence note, until Coding is evidenced, without treating edits as a completed or correct implementation. Silence nudges omit the empty-editor branch when the editor holds candidate edits, and coding-only sessions omit the behavioral-question ban from nudges and reviews, which their system instruction already carries. Silence policy still selects its existing setup-error and next-step branches before that note. Existing final checklist tool responses can expose candidateEditsPresent only in the coding editor round, using the unchanged written-content heuristic; a comment or stub may qualify. Tool continuity wording is shorter with identical latest-utterance selection, trust fences, JSON data and timer. Cold and checkpoint recovery envelopes are shorter while retaining archive lookup before earlier-speech answers, reconciliation with the current local record, historical-data trust boundaries and the active-session and no-restart rules. Completed Example walkthroughs are not reopened without a relevant implementation change or a concrete unresolved concern; initial ordinary and boundary examples and execution-mode Test requirements are preserved. Outside candidate speech, judged from the microphone's own voice level or provider speech detection, each Live socket receives one camera presence frame; frames during speech keep the five-second interval, and board images are unaffected. |
| 32 | The report judges the problem's follow-ups. When the coding round completed and the interviewer was handed them, the report brief lists them numbered, and the report model returns a `followUps` entry for each: whether an interviewer line posed it, and for one that was raised, a short assessment of the answer addressed to the candidate. An entry whose `index` is missing, out of range or repeated, or whose `raised` is not a boolean, and every entry when the follow-ups were never handed over, is dropped before anything in it is checked rather than refusing the report; an assessment that is missing, blank, not a string or on an entry not raised becomes null. A kept assessment is held to a length limit and the same delivery-judgment and published-name scans as the other narrative fields, with errors named by the entry's position in the response; one still judging delivery once the repairs run out is dropped as a refused self-review check is. The server stamps each `debrief.followUps` entry as `{text, raised, assessment}`: `raised` is false when the follow-ups were never handed over, and null when they were but no report judged them, or when the report read `false` from a transcript whose opening was cut. Report prompt 19 and report schema 3; schema 2 reports, whose follow-ups are bare text, stay scored and read as unjudged. The live prompt and rubric are unchanged. |
| 31 | A candidate may disable code execution for the entire editor interview. Whiteboard sessions ignore this preference because they have no code runner. The signed participant setting persists across languages and reconnects, execution events cannot earn credit, and Test may be recorded from a hand trace of written code with source `candidate_speech`. Live prompts ask for traces rather than Run, preserve completed evidence, and distinguish the choice from a runner outage. Assessment metadata and the server-stamped optional `codeExecution: false` report field record the restriction without inventing executed or passing cases; browser reports, saved history and Markdown retain it. The field is additive under schema 2 and older renderers may ignore it. Older reports without the field retain their existing meaning. |
Expand Down
29 changes: 27 additions & 2 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ pub const THINKING_CHECK_IN_S: u64 = 120;
pub(crate) const THINKING_RELEASE_COOLDOWN: std::time::Duration =
std::time::Duration::from_secs(10);

pub const INTERVIEW_CONTRACT_BUNDLE_VERSION: u32 = 33;
pub const LIVE_PROMPT_VERSION: u32 = 24;
pub const INTERVIEW_CONTRACT_BUNDLE_VERSION: u32 = 34;
pub const LIVE_PROMPT_VERSION: u32 = 25;
pub const REPORT_PROMPT_VERSION: u32 = 20;
pub const RUBRIC_VERSION: u32 = 1;
pub const REPORT_SCHEMA_VERSION: u32 = 3;
Comment thread
MorganHo001 marked this conversation as resolved.
Expand Down Expand Up @@ -2666,6 +2666,8 @@ pub fn record_hint(state: &mut RuntimeState, requested: bool) -> String {

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataConfig {
pub voice: Option<&'static str>,
pub accent: Option<&'static str>,
pub problem: &'static Problem,
pub duration_min: u32,
pub interview_loop: InterviewLoop,
Expand Down Expand Up @@ -3004,6 +3006,27 @@ pub fn with_timer(state: &RuntimeState, prompt: String) -> String {
format!("{prompt} {}", timer_line(minutes_left(state)))
}

pub const INTERVIEWER_VOICES: &[&str] = &["Puck", "Charon", "Kore", "Orus"];
pub const INTERVIEWER_ACCENTS: &[&str] =
&["American", "British", "Australian", "Canadian", "Indian"];

pub(crate) fn sanitize_interviewer_choice(
value: Option<&serde_json::Value>,
choices: &[&'static str],
) -> Option<&'static str> {
let value = value?.as_str()?;
choices.iter().copied().find(|choice| *choice == value)
}

/// Fixed speaking directions, never candidate-authored prompt text.
pub fn interviewer_accent_instruction(accent: &str) -> Option<String> {
Comment thread
ColtenOuO marked this conversation as resolved.
INTERVIEWER_ACCENTS.contains(&accent).then(|| {
format!(
"\n\n[INTERVIEWER SPEAKING STYLE]\nSpeak English with a natural {accent} accent. Apply this only to pronunciation; keep the interview content, assessment rules, and professional tone unchanged."
)
})
}

pub fn parse_participant_metadata(metadata: Option<&str>) -> MetadataConfig {
let value = metadata
.and_then(|metadata| serde_json::from_str::<serde_json::Value>(metadata).ok())
Expand All @@ -3027,6 +3050,8 @@ pub fn parse_participant_metadata(metadata: Option<&str>) -> MetadataConfig {
value.get("codeExecution") == Some(&serde_json::Value::Bool(false));

MetadataConfig {
voice: sanitize_interviewer_choice(value.get("interviewerVoice"), INTERVIEWER_VOICES),
accent: sanitize_interviewer_choice(value.get("interviewerAccent"), INTERVIEWER_ACCENTS),
problem,
duration_min,
interview_loop,
Expand Down
10 changes: 8 additions & 2 deletions src/agent/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ pub fn build_instructions_for_plan(
options: &crate::runtime::RuntimeOptions,
) -> String {
let &crate::runtime::RuntimeOptions {
voice: _,
accent,
ref profile,
ref grounding,
interview_loop,
Expand Down Expand Up @@ -528,7 +530,7 @@ policies, which come out of the conversation as they would with a person."
.filter(|policy| !policy.is_empty())
.collect::<Vec<_>>()
.join("\n\n");
format!(
let mut instructions = format!(
r#"You are {AGENT_NAME}, a senior staff software engineer running a live, spoken,
{duration_min}-minute {interview_surface} interview over video. {opening}

Expand Down Expand Up @@ -662,7 +664,11 @@ TOOLS

Be warm but rigorous: want the candidate to succeed, never do the work for them."#,
metadata.difficulty, optimal_point, pitfalls_point,
)
);
if let Some(direction) = accent.and_then(super::interviewer_accent_instruction) {
instructions.push_str(&direction);
}
instructions
}

fn grounding_policy(grounding: &InterviewGrounding) -> String {
Expand Down
2 changes: 2 additions & 0 deletions src/livekit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3080,6 +3080,8 @@ fn candidate_bootstrap<'a>(
Some(candidate.problem.id),
candidate.duration_min,
crate::runtime::RuntimeOptions {
voice: candidate.voice,
accent: candidate.accent,
profile: candidate.profile,
grounding: candidate.grounding,
interview_loop: candidate.interview_loop,
Expand Down
12 changes: 10 additions & 2 deletions src/livekit/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ fn report_value(
),
};
stamp_report_debrief(&mut report, boot, state);
stamp_report_contract(&mut report);
stamp_report_contract(&mut report, boot);
report_with_integrity_events(report, state, reason)
}

Expand Down Expand Up @@ -993,9 +993,17 @@ fn stamp_report_debrief(
}
}

fn stamp_report_contract(report: &mut serde_json::Value) {
fn stamp_report_contract(report: &mut serde_json::Value, boot: &RuntimeBootstrap<'_>) {
if let Some(object) = report.as_object_mut() {
object.insert("interviewContract".to_string(), interview_contract_json());
object.insert(
"interviewerVoice".to_string(),
serde_json::json!(boot.voice),
);
object.insert(
"interviewerAccent".to_string(),
serde_json::json!(boot.accent.unwrap_or("Default")),
);
}
}

Expand Down
Loading