standin-sdk[hermes-agent] answers Microsoft Teams calls with your Hermes Agent agent.
StandIn answers the Microsoft Teams call and dials your worker. This plugin answers that dial, connects a realtime speech-to-speech model for the conversation, and gives that model one door into Hermes, so the caller talks to the same assistant they know from chat, with their own tools, files and skills behind it.
Realtime speech-to-speech. The plugin drives a realtime model for the conversation, which is
what keeps a call at conversational latency, so a realtime provider key is required alongside the
StandIn secret. Scope has the whole boundary in one list.
Who is the brain
The realtime model is. It hears the caller and answers them, at conversational latency. Hermes is reached only when the model callshermes_agent_consult, and it never sees audio.
The model is good at conversation, Hermes is good at work. That split is the whole design.
Install
Into the Python environment that runs Hermes:hermes_agent.plugins entry point. There is no HTTP hop to Hermes, no session header, and no second service to run.
msteams_bridge, which is the name shown in hermes plugins list and used in plugins.enabled, so an operator upgrading from the standalone bridge keeps their config.yaml unchanged.
The Hermes host is deliberately not a dependency of the package. Hermes is not on PyPI: it is the application the plugin is installed into. Everything that does not need the host works without it, and everything that does raises HermesUnavailable with a sentence saying so.
Setup
9442 at the /msteams/calling path and register the public wss:// URL as your StandIn identity’s agent voice URL. Expose your agent carries the mount command and the probes, and it is the only page that does: three spellings of one URL across three pages is what caused a live incident here.
Call your StandIn number.
Check before you call
status, smoke and serve, and the set is deliberate.
Running without Hermes
hermes_agent_consult replies, in words, that it cannot reach its tools. Useful for checking a secret, a tunnel and a StandIn identity before standing up Hermes.
Configuration
Theplugins.entries.msteams_bridge.config block, with a MSTEAMS_BRIDGE_* environment variable as the fallback for each key. Both are supported because both already exist in the field.
The
realtime: sub-block configures the provider:
Three more keys tune turn detection, which is what to reach for when the assistant interrupts too eagerly or waits too long before answering:
The default
instructions are deliberately about voice behaviour and not about who the assistant is: identity comes from the operator’s own Hermes configuration through the consult boundary, so the caller talks to the same assistant they know from chat. Override it and you are replacing the voice manner, not the personality.
The listener itself belongs to the SDK: STANDIN_SECRET, STANDIN_PORT (9442), STANDIN_HOST (0.0.0.0), STANDIN_WS_PATH (/msteams/calling).
What the model can call
Neither ever raises at the model. A consult that times out, and a tool name the model invented, both
come back as a sentence it can recover from, because a tool result is fed straight into a live
conversation and an exception there leaves the caller listening to nothing.
What Hermes itself gains
Enabling the plugin also registers one Hermes tool,msteams_bridge_status, in a msteams_bridge
toolset. It takes no arguments and answers the same question as hermes msteams-bridge status, so
you can ask your assistant in chat whether a call would work right now. It is registered when the
plugin is enabled, not when the listener runs, so the answer is available in a Hermes process that
is not serving calls at all.
In a meeting
The assistant stays silent until somebody says one of the wake phrases, then keeps answering forfollow_up_window_ms without needing the name again. The machinery is the SDK’s own GroupGate, which Group calls documents for a handler you write yourself; the keys above are this plugin’s way of configuring it.
Saying “stop” or “wait” as a whole utterance cuts playback in code, whether or not the model would have stopped. Phrases ship for four languages, English, Arabic, French and German, and the match is on the whole utterance rather than a substring, so “stop by the store” is a sentence rather than an interruption. Stop means stop has the set and the normalisation.
The gate decides “is this a meeting?” from the thread id: Microsoft Teams gives a meeting or channel conversation a thread beginning 19:, and it is present in session.start on every call. The participant count is kept as a second, corroborating signal that can only add certainty, never remove it. That is a fix: the standalone bridge this plugin replaces decided from a participant count that does not arrive on the meeting-join path, so the assistant answered every turn of every meeting it was invited to.
The goodbye was the other fix. StandIn sends a closing line when it is about to end a call, so it almost always arrives while the model is mid-answer. The old path injected it with a plain say, whose response creation is guarded on “is a response already active”, so the guard swallowed it and the caller heard nothing before the line went dead. This plugin cancels the response first, then speaks.
Scope
The conversation is realtime speech-to-speech. The provider is OpenAI or Azure OpenAI out of the box, andrealtime.url points the same protocol at a socket of your own. A provider that offers only
speech to text and text to speech is not one this plugin can drive: the whole design rests on the
model hearing the caller directly, which is where the latency goes.
The model’s tool surface here is the two above. hermes_agent_consult reaches the caller’s whole
Hermes agent, so anything Hermes can do in chat it can do on the call through that one door.
That is a boundary of this plugin, not of the call. The StandIn call wire carries vision, expression,
visemes and display messages as well as audio, and this plugin already drives the expression and
viseme half itself: see On the tile. What it does not do is put the SDK’s
model-facing tools into the Hermes realtime session. Reach for VisionTools and CallTools
yourself if your agent needs them:
- looking at a screen share, and showing a picture, a page or a file: Vision and the avatar
- ringing somebody back: Reaching people, where
CALL_BACK_TOOLandCHAT_CALLBACK_TOOLship from thestandinbarrel - writing up the meeting: Meeting recap, where
MINUTES_TOOLdoes
On the tile
The caller sees StandIn’s avatar, and this plugin drives its face and its mouth without you doing anything. Two hints go out per turn:- Expression. An
ExpressionCuereads the reply text as it is being produced and sendsexpresswhen the emotion changes. It is re-read on every piece rather than once at the end, because waiting for the final transcript leaves the face wrong for the whole time the reply is being spoken. A tool call raises the thinking cue and lowers it in afinally, so a model that stays silent after a tool result does not leave the face mid-think for the rest of the call. - Visemes. A
TurnLipSyncmeasures the audio the turn actually sent and hands StandIn one mouth timeline per turn, throughsend_speech_marks, when the turn ends.
Full example
examples/hermes-msteams-connector has the .env layout and a config.yaml.example block to merge into your existing Hermes configuration. There is no agent file in it, and that is the point: Hermes already is the agent.
Next
Call handler
The seam this plugin implements.
Audio
The 16 kHz wire against a 24 kHz realtime model.
Realtime providers
The startup buffer, the echo guard and barge-in this plugin sits on.
The avatar
The expression and viseme hints this plugin sends for you.