Keeping the record
Feed the transcript as the call runs, from whichever callbacks your plugin already has.role is "caller" unless you say otherwise, and it is what tells the two sides apart in the
document. A consecutive turn from the same speaker and the same role is merged into the one before
it, because a live transcript arrives as fragments and half-sentences fed to a model as separate turns
make the minutes read like a stutter. Merging never crosses speakers: filing one person’s words under
another’s name is worse than no attribution, because it is confidently wrong.
Both tracks are bounded, because a two-hour meeting sits in the memory of a process that is also carrying live audio. What survives is the tail, since the end of a meeting is what minutes are mostly about.
Here are the numbers, so “bounded” is something you can check:
MAX_TRANSCRIPT_ENTRIES, MAX_TRANSCRIPT_ENTRY_CHARS and RECAP_MIN_TURNS are on the standin barrel. The three hard holding caps are from standin.minutes import MAX_TRANSCRIPT_TURNS, MAX_TRANSCRIPT_VISUALS, MAX_TRANSCRIPT_CHARS.
Consecutive visual repeats are collapsed. The vision lane describes whatever is on screen each time it is asked, and a slide nobody changed would otherwise fill the record with the same line.
Where the minutes go
Decided once, before anything is written, and then passed to every later step.resolve_minutes_target returns a DeliveryTarget: a kind of "thread" or "caller-dm", a conversation_id and a tenant_id, and nothing else.
caller_chat comes from a PersonalChats you construct once and share with your ChatChannel, so the chat lane fills it in as messages arrive:
The SDK’s own chat lane writes to a socket and gets no status back, so it can report that a message was sent and never that it was refused. A poster built on it never advances the walk, which is the safe behaviour rather than a bug. Supply a poster that surfaces a status if you want the fallback to fire.
Writing it up
document_dir decides whether a Word document is written at all: leave it out and none is. subtitle
is the line under the document’s title, and assistant_label and caller_label name the two sides in
the attributed transcript, defaulting to "Assistant" and "Caller".
post_minutes never raises. It normally runs during teardown, where an exception takes the whole teardown with it, so every failure comes back as a sentence instead. It degrades a step at a time: a document that cannot be built sends text only, a send that fails is logged, and the spoken sentence is always there.
Two outcomes look similar and are kept apart on purpose. A call with nothing said is told it was too short. A call with plenty said but no conversation to post into is told exactly that. Conflating them tells people their conversation did not count when it did.
RecapResult is what comes back.
Your poster may answer with a plain
True or with a PostOutcome, which is ok plus the status behind it. Both are read correctly, and from standin.minutes import PostOutcome is where it lives, because it is not on the standin barrel. Do not hand back an object of your own and expect it to be truth-tested: every object is truthy, and a post StandIn rejected would report as delivered. Only a real bool is read as one.
The document
write_minutes_docx emits a Word-openable .docx with no dependencies. A document format library would be a dependency every install pays for so that the small fraction who ask for minutes get a file, which is the wrong trade for an SDK.
Give it sections from parse_minutes_sections, a list of MinutesSection, and it renders real headings instead of a flat wall of lines. A model asked for ### Key points will write ## Key points or **Key points:** depending on the model and the day, so all of those are accepted; a section that ends up with nothing in it is left out entirely rather than printed as a heading over white space.
Give it transcript and it appends an attributed transcript, which is the half a transcript-only tool cannot produce: the call gave you the real speaker per utterance. A turn that already carries its own Name: prefix is written through untouched, because relabelling it as a generic caller destroys the attribution and double-prefixing reads as a transcription fault. has_speaker_prefix is that test, exported so your own writer can make the same decision.
Delivery to the chat is text. The document is written to disk beside it, for whoever keeps the record. A meeting chat cannot be sent a file by a bot the way a person can, so a document promised into the chat would be a promise that quietly fails. Say so in the message rather than leaving it as a silent difference:
DOCUMENT_NOT_ATTACHED is that sentence.The prompt
minutes_prompt asks for Key Points, Decisions, Action Items and, when anything was shown, Presented.
The instruction not to infer what was on screen is the load-bearing one. A model handed “Sara shared a dashboard” will happily invent the numbers on it, and minutes that invent numbers are worse than minutes with a gap.
is_summary_request recognises somebody asking for the write-up in conversation. It needs both halves: “summarise” alone is asked about a document, an email, or a page the agent is looking at.
MINUTES_TOOL is the tool declaration, ready to register on CallTools. It is named post_meeting_minutes, not after the function that does the work, because a model handed two things with one name cannot tell which it is calling. It is not built in: whether there is anywhere to post depends on the call, and resolve_minutes_target is what answers that.
Answer the model the moment it calls, then run the recap. A tool call with no result stalls the turn, and the recap is a full model run plus a document write plus a send, so the caller sits in silence wondering whether anything is happening.
Next
Chat
PersonalChats, and the lane the minutes are posted down.Consulting
The slow agent that writes the recap without stalling the call.