I often remember the useful part of a story only when someone who knows the field asks a follow-up question. A blank-page draft misses the odd detail because I do not think to include it. Fifteen years of developer relations has made me trust that moment, and the question has to arrive while the conversation is still running.
Over a day in December I wrote Interviewer, a timed, podcast-style voice call in Swift using the OpenAI Realtime API. It interviews you for fourteen minutes and turns the transcript into a draft essay when the clock runs out. Optional follow-up calls get six minutes each.
You don’t know what you know, until you are asked. The premise sits at the top of the README.
A meeting notetaker or dictation app can wait until everything is over, transcribe it, and produce a tidy summary. By then the moment to ask a better question has passed. A chatbot’s voice mode can play podcast host, but without a plan, clock, or record of earlier questions it behaves like a recorder with better manners.
The app splits that job across seven agents, and the README puts the reason bluntly: “A single intelligence, however capable, cannot simultaneously listen, remember, research, strategize, and write.” AgentCoordinator owns the conversation state and routes work to the narrower roles. Before the call, the Planner draws up the interview plan and a section structure; everything after that happens against the clock.
The failure I most wanted to avoid was an interviewer asking the same thing twice in different words. The first evening’s build had one defence: askedQuestionIds, a set of plan-question IDs in the coordinator. Closing the remaining gap took until the next afternoon.
What runs while you’re still talking
During a live conversation, processLiveUpdate runs about every ten seconds. It sends a window of recent transcript to the Note-Taker and Researcher in parallel. The Orchestrator then receives a larger window, the plan, notes, research snippets, elapsed time, target duration, asked question IDs, recent question text, and covered themes. From those inputs it chooses the phase, next question, and brief sent back to the Realtime API. None of these agents may stall a live call. If one fails, the Note-Taker returns its previous notes, the Researcher returns an empty list, and the coordinator selects the next unasked priority-1 question when the Orchestrator is unavailable.
After 85% of the interview time has passed, the coordinator locks the phase to wrap_up and overrides any other Orchestrator decision. One enthusiastic tangent near the end of a fourteen-minute call can otherwise consume the planned close.
Ending the call cleanly is its own problem. After the model delivers its thank-you, the Realtime session doesn’t know the interview is over; it waits through the silence and can end up responding to room noise. So the session manager watches the interviewer’s own speech for closing phrases like “thank you so much for” and “that’s all the time”, and the moment one appears it stops the mic and closes the WebSocket.
The whole transcript exists only after the socket closes, and only three post-call agents receive it without a window. The Analyst extracts themes, the Follow-Up agent produces targeted questions, and the Writer creates the draft. Its voice instruction in AGENT_ORCHESTRATION.md is specific: “If they say ‘gnarly problem’, don’t write ‘complex challenge’.”
The question asked twice
The first evening’s defence had a limit. askedQuestionIds knows which plan questions have been asked, but an ID set cannot detect a rephrase, and the Orchestrator may return a question without a plan ID. The current code compares that question with unasked plan questions using Jaccard similarity and accepts any match above 0.35. If it gives up, it logs “coverage tracking may be incomplete”. A comment says the threshold was lowered to 0.35 because the stricter value missed rephrased questions.
Here’s the diff of AgentCoordinator.swift between that first evening’s version and the commit that landed the next afternoon:
@@ -31,9 +31,33 @@ actor AgentCoordinator {
private var askedQuestionIds: Set<String> = []
private var finalNotes: NotesState = .empty
+ // Track recently asked question TEXTS to prevent similar questions
+ private var recentlyAskedQuestionTexts: [String] = []
+ private let maxRecentQuestions = 10
+
+ // Track question themes to prevent thematic repetition
+ private var askedQuestionThemes: Set<String> = []
+
// Agent activity tracking for UI meters
private var agentActivity: [String: Double] = [:]
+ // Transcript change detection to skip redundant processing
+ private var lastProcessedTranscriptHash: Int = 0
+ private var lastProcessedFinalCount: Int = 0 // Count of final (non-streaming) entries
+
+ // Orchestrator throttling when no content changes
+ private var lastOrchestratorRunTime: Date?
+ private var lastOrchestratorDecision: OrchestratorDecision?
+ private let orchestratorMinIntervalNoContent: TimeInterval = 30 // Min 30s between runs if no new content
+
+ // Phase locking to prevent oscillation after 85%
+ private var lockedPhase: String?
+
+ // Transcript windowing configuration to control prompt size
+ private let maxTranscriptEntriesForNotes = 20 // Note-taker gets recent context
+ private let maxTranscriptEntriesForResearch = 20 // Researcher needs enough context to identify topics
+ private let maxTranscriptEntriesForOrchestrator = 30 // Orchestrator needs more for decisions
Without those fields, the Orchestrator would work from raw transcript text alone. I have no log showing the earlier state catching a repetition during a call. Version 0.3 shipped the same evening the fields landed.
The Note-Taker feeds the same machinery. It watches the live transcript and records claims, contradictions, missing sections, quotable lines, possible titles, and coverage gaps in a structure the Orchestrator can use during the interview.
Repetition can also occur across calls. For a follow-up session, the Follow-Up agent proposes three unexplored topics. You pick one and receive six more minutes. The Realtime API is told that this is a follow-up conversation and instructed not to repeat questions already covered. Afterwards the transcripts merge in chronological order so the Analyst and Writer can use both calls.
The Researcher on a leash
Of the seven agents, I trust the Researcher least, so it has the tightest bounds. Its topic-identification prompt allows at most two topics per pass. Each must come from a specific thing the expert just said, such as a company name, jargon, or a checkable claim. The prompt uses “90% of startups fail” as its example. The main interview topic and previously attempted subjects are banned. A topic may return after five minutes if new context appears. The pass is skipped when the transcript hash has not changed, and the agent backs off after five failed cycles with the log message “too many recent failures, cooling down”.
There’s no web search behind any of this. The comment in ResearcherAgent.swift reads “web search not available in Chat Completions API”, so mid-call research is a second model asked what it already knows, under instructions to say so when it doesn’t. In an interview, research is there to sharpen the next question. It should never flood the orchestration prompt or keep rediscovering the same topic because the transcript changed by one sentence.
None of this proves the interviews are good. The repository has no benchmark for question quality, and I haven’t built one. The app requires macOS 26 (Tahoe) or iOS 26 and an OpenAI key. Audio goes to OpenAI for the live call. Sessions, plans, transcripts, and drafts remain in SwiftData on the device, with the key in the Keychain. Pick a topic you think you already understand and give it fourteen minutes.
Update, 7 December 2025: a YouTube-import commit using yt-dlp for audio and Whisper for transcription added two agents, bringing the count to nine. StyleExtractor distils past sessions into a voice guide for the Writer. ReversePlanner infers a retroactive plan from an imported recording.