Mezera v historii konverzací v M365 Agents SDK (a jak jsme ji vyplnili)
M365 Agents SDK nepodporuje načítání minulých aktivit. Zde je, jak jsme pro zákazníka vyřešili obnovení konverzace a práci s historií v multi-agentní aplikaci WebChat.
Update (březen 2026): Od vydání tohoto článku oficiální adaptér pro M365 Agents SDK už umí předat
conversationIda navázat tak na předchozí konverzaci. Ano, podařilo se to protlačit. Podkladové API ale pořád neumí načíst starší aktivity ani vypsat konverzace pro dané conversation ID, takže přístup s ukládáním aktivit popsaný níže je stále potřeba.
Tohle je přímo z praxe. Teams a Microsoft 365 jsou de facto hlavní „povrchy“ pro nasazení Copilot Studio agentů – a dává to smysl. Ne každá organizace se ale chce zastavit právě tam. Některé si chtějí postavit vlastní portál, vlastní UX a vlastní branding. Chtějí mít pod kontrolou celý zážitek.
Jeden zákazník, se kterým Microsoft spolupracuje, jde přesně touto cestou. Staví vlastní zaměstnanecký portál, kde má každé oddělení svého AI specialistu. HR politiky, IT podpora, schvalování ve financích, právní doporučení – vše dostupné v jedné aplikaci, přičemž každý agent je „grounded“ ve svém doménovém know-how.
Zaměstnanec přepíná mezi HR agentem a IT helpdeskem a zase zpět, aniž by opustil aplikaci. Zážitek je plynulý a plně v brandu.
Pro samotnou chatovou část používají BotFramework WebChat, který Vám dá plnou kontrolu nad message pipeline, a přitom řeší veškerou renderovací komplexitu, kterou nechcete znovu stavět od nuly. Pro napojení backendu si vybrali M365 Agents SDK (@microsoft/agents-copilotstudio-client) místo Direct Line. Proč? Dva hlavní důvody:
- Podpora streamingu. SDK používá streaming přes SSE, takže odpovědi agenta přicházejí postupně v reálném čase – bez pollingu. Přesně ten moderní chatový zážitek, který uživatelé očekávají. Kdo by chtěl čekat dlouhé sekundy, než odpověď dorazí?
- Tenant Graph Grounding. SDK funguje s Authenticate with Microsoft, což umožňuje Tenant Graph Grounding – agenti pak mají sémantické vyhledávání nad daty ze SharePointu a Copilot Connectorů.
Zatím vše vypadalo dobře. Pak ale narazili na zásadní limit.
Co tu chybí
U M365 Agents SDK je jedna nepříjemná věc, na kterou Vás nikdo nepřipraví, dokud nejste po uši v implementaci: podkladové API neumí načíst starší aktivity.
U Direct Line máte endpoint getActivities. Můžete se znovu připojit ke konverzaci, stáhnout kompletní historii aktivit a WebChat všechno bez problémů vykreslí, jako by uživatel nikdy neodešel. Je to přesně ten typ funkce, kterou berete jako samozřejmost — dokud ji nemáte.
SDK? Tam máte smůlu. Jakmile se stránka reloadne (nebo uživatel odejde jinam a vrátí se), konverzace je prostě… pryč. Ne že by zmizela na serveru. Na straně Copilot Studio pořád existuje i s celým kontextem. Jenže neexistuje způsob, jak se SDK zeptat „co se v téhle konverzaci zatím stalo?“
Aby bylo jasno: nejde o „paměť“ ve stylu ChatGPT. Agent při navázání konverzace pořád ví, o čem jste se bavili. Problém je v tom, že klient nemá žádné API, kterým by si stáhl starší aktivity nebo vypsal předchozí konverzace. Když se uživatel vrátí, WebChat nemá co vykreslit a ani se nemá odkud zeptat.
Co to má znamenat, Microsofte? (Ano, vím, že to říkám sám sobě. Jsme na jedné straně. Stejně to ale bylo potřeba říct.)
Dva problémy, ne jeden
Když jsme si k tomu sedli, došlo nám, že ve skutečnosti řešíme dva oddělené problémy:
Problém 1: Obnovení konverzace ✅
Oficiální metoda CopilotStudioWebChat.createConnection() v SDK nepřijímá parametr conversationId. Pokaždé, když vytvoříte connection, začne úplně novou konverzaci. Neexistuje způsob, jak říct „připoj mě zpátky ke konverzaci abc-123“.
Tohle už je vyřešené. Oficiální adapter teď parametr conversationId podporuje. Muahahah. Když tenhle článek původně vznikal, klient v SDK sice uměl předat ID konverzace, ale WebChat adapter to nenabízel. Teď už ano. Náš vlastní adapter navíc umožňuje řídit, jestli se má poslat úvodní pozdrav — ne každá aplikace totiž chce, aby agent při každém startu nové konverzace posílal stejnou uvítací zprávu.
Problém 2: Historie aktivit
I když vyřešíte problém 1 (a Microsoft ho vyřešil), pořád nedokážete načíst starší aktivity ani vypsat předchozí konverzace. Neexistuje nic jako „dej mi aktivity pro konverzaci abc-123“ ani „dej mi seznam konverzací tohoto uživatele“. Konverzace se sice na straně serveru obnoví, takže agent má kontext, ale WebChat vykreslí prázdné okno chatu. Uživatel tak vidí prázdnou obrazovku a musí si pamatovat, kde skončil.
Řešení: vlastní WebChat adapter
Abychom vyřešili oba problémy, rozšířili jsme adapter, který je už součástí SDK. M365 Agents SDK obsahuje CopilotStudioWebChat.createConnection() – DirectLine-kompatibilní shim, který implementuje connectionStatus$, activity$, postActivity() a end(), jak je vidět v oficiálním webclient sample. Náš vlastní adapter na stejný vzor navazuje a doplňuje chybějící možnosti pro správu konverzací.
Adapter je open source a najdete ho na github.com/adilei/copilot-webchat-adapter.
Pozor: Tenhle adapter je open source a je k dispozici „as-is“. Microsoft ho nepodporuje. Pokud ho použijete, je to Váš kód a Vaše odpovědnost.
Architektura vypadá takto:
flowchart LR
WC[WebChat] -->|postActivity| AD[createConnection<br/>DirectLine shim]
AD -->|activity$| WC
AD -->|sendActivityStreaming| SDK[M365 Agents SDK]
SDK -->|yield Activity| AD
SDK -->|HTTP| CS[Copilot Studio]
CS -->|SSE| SDK
Adapter pro real-time streaming používá async generator metody ze SDK (startConversationStreaming() a sendActivityStreaming()).
Jak vyřešit navázání na předchozí konverzaci
Adapter umí přijmout volbu conversationId. Když ji zadáte, udělá následující:
- Přeskočí volání
startConversationStreaming()(žádné duplicitní uvítání) - Předá
conversationIddo každého volánísendActivityStreaming() - Po celou dobu spojení si hlídá aktuální ID konverzace
1
2
3
4
5
6
7
8
9
10
import { createConnection } from 'copilot-webchat-adapter'
// New conversation (default behavior)
const directLine = createConnection(client, { showTyping: true })
// Resume existing conversation
const directLine = createConnection(client, {
conversationId: savedConversationId,
showTyping: true,
})
Ano, proměnná se jmenuje directLine. Není to omyl — je to záměr. WebChat očekává prop directLine, který implementuje connectionStatus$, activity$, postActivity() a end(). Neřeší (a ani nemusí vědět), že transport pod tím ve skutečnosti není Direct Line. Adapter mluví stejným protokolem.
Řešení historie aktivit
Obnovení konverzace je jen polovina problému. Bez předchozích zpráv uživatel po návratu vidí prázdné chat okno. Jak mu tedy vrátit i kontext?
V celé skládačce jsou dvě role: adapter (náš DirectLine shim) a consumer (Vaše aplikace – kód, který adapter používá a vykresluje WebChat).
Adapter má na starosti načíst historii při obnovení a znovu ji „přehrát“ do WebChat. Přijímá volitelný callback getHistoryFromExternalStorage, který se (pokud ho dodáte) zavolá při připojení: vrátí dřívější aktivity a adapter je ještě před příchodem nových zpráv pošle přes activity$. Adapter zároveň řeší číslování sekvence, aby WebChat vykreslil vše ve správném pořadí.
Consumer má naopak na starosti aktivity průběžně ukládat a přes tento callback je při obnovení předat zpět adapteru. Adapter je úplně nezávislý na tom, jak a kam aktivity ukládáte. Potřebuje jen funkci, která pro dané conversation ID vrátí pole aktivit.
getHistoryFromExternalStorageje záměrně volitelný. Až SDK nebo jeho podkladové API jednou nabídne nativní načítání aktivit, adapter si je bude moct načítat interně sám a tento callback bude sloužit spíš jako možnost přepsání chování než jako povinná součást.
Jak úložiště implementujete, je čistě na Vás. Náš zákazník používá databázi. Ukázka v adapteru používá jednoduché úložiště nad localStorage. Můžete použít IndexedDB, server-side API – cokoliv, co Vám dává smysl. Jediná „smlouva“ je funkce getActivities(conversationId), kterou adapter zavolá při obnovení:
1
2
3
4
5
6
7
import { createConnection } from 'copilot-webchat-adapter'
const directLine = createConnection(client, {
conversationId: savedConversationId,
showTyping: true,
getHistoryFromExternalStorage: (id) => activityStore.getActivities(id),
})
Jenže jak se aktivity do úložiště vůbec dostanou? Tady přichází na řadu Redux middleware ve WebChat:
1
2
3
4
5
6
7
8
9
10
11
const store = WebChat.createStore({}, () => next => action => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
const { activity } = action.payload
if (activity.type === 'message' && directLine.conversationId) {
activityStore.saveActivity(directLine.conversationId, activity)
}
}
return next(action)
})
WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'))
Každá příchozí message activity se hned po doručení uloží. Když se uživatel vrátí, adapter zavolá getActivities, znovu je „přehraje“ do WebChat a konverzace pokračuje přesně tam, kde skončila.
Jak to vypadá v praxi
Testovací stránka adaptéru sice nevyhraje soutěž o nejlepší design, ale důležité je, co umí ukázat.
Tady je první konverzace. Uživatel se připojí, pozdrav se postupně „streamuje“ a pak položí dotaz na firemní politiky. Všimněte si conversation ID ve stavovém řádku:
Teď uživatel stránku obnoví, vloží conversation ID a znovu se připojí. Uložená historie se zobrazí hned a může pokračovat přesně tam, kde skončil. Agent má pořád kompletní kontext, takže navazující otázka dostane odpověď odpovídající předchozí konverzaci:
Konverzace po refreshi pokračuje
Žádné opakované přehrání pozdravu. Žádná prázdná obrazovka. Konverzace jednoduše naváže tam, kde skončila.
Co ukázka (zatím) nepokrývá
Redux middleware v ukázce ukládá jen aktivity, kde type === 'message'. Je to zjednodušení. V produkční aplikaci budete nejspíš chtít ukládat víc věcí – třeba odeslání z adaptive cards, aby se po obnovení historie dříve „prokliknuté“ karty zobrazily jako už odeslané (nebo byly deaktivované). Ukázka to zatím neřeší, ale princip je stejný: v middleware zachytit relevantní typy aktivit a uložit je do store.
Samotný adapter je v tomhle neutrální. Přehrává jednoduše to, co vrátí Vaše funkce getHistoryFromExternalStorage.
Graceful Degradation
Pokud getHistoryFromExternalStorage vyhodí chybu (vymazaný localStorage, překročená kvóta, poškozená data), adapter ji potichu „spolkne“ a pokračuje bez historie. Místo pádu aplikace dostanete prázdný chat, který ale dál funguje.
Omezení a háčky
Je fér říct, co tohle neřeší:
Expirace konverzace. Konverzace v Copilot Studio netrvají věčně. Microsoft zatím důkladně neotestoval, co se stane, když se pokusíte navázat na expirovanou konverzaci – jestli SDK vyhodí výjimku, vrátí chybovou aktivitu, nebo tiše založí novou. Je to na seznamu věcí k prověření.
Žádná historie napříč zařízeními. Při výchozí implementaci přes localStorage je historie konverzace navázaná na konkrétní prohlížeč. Otevřete jiný prohlížeč nebo zařízení a konverzaci sice navážete (agent má kontext), ale neuvidíte předchozí zprávy. Pokud to potřebujete, implementujte server-side úložiště.
Hlavní body
- M365 Agents SDK umožňuje streaming a tenant Graph grounding pro Copilot Studio agenty. Oficiální adapter už podporuje obnovení konverzace přes
conversationId, ale pořád mu chybí API pro načtení aktivit z minulých konverzací. - V praxi to znamená, že WebChat po reloadu stránky přijde o celou viditelnou historii konverzace, i když agent si kontext na serveru zachová.
- copilot-webchat-adapter tenhle zbytek mezery doplňuje: funguje jako DirectLine-kompatibilní shim s možností napojit vlastní úložiště aktivit.
- Aktivity se ukládají přes WebChat Redux middleware a při opětovném připojení se přehrají přes callback
getHistoryFromExternalStoragev adapteru. - Rozhraní
ActivityStoreje abstraktní, takže můžete localStorage snadno vyměnit za libovolnou perzistenci podle Vašich požadavků.
Co dál
Jsme zhruba v polovině. Oficiální adapter už umí obnovení konverzace (problém č. 1), což je skvělá zpráva. Jenže podkladové API stále nenabízí endpoint typu getActivities() ani žádný způsob, jak uživateli vypsat jeho předchozí konverzace. Dokud tohle nepřibude, zůstává popsaný pattern s ukládáním aktivit nejpraktičtější cestou, jak uživatelům po reconnectu vrátit viditelnou historii chatu.
Adapter je na tuhle budoucnost připravený: jakmile SDK přidá načítání historie, getHistoryFromExternalStorage se z hlavního mechanismu změní na volitelný override.
Narazili jste na tuhle mezeru také? Používáte M365 Agents SDK s WebChat, nebo jste stále na Direct Line? Budu rád, když se podělíte o zkušenosti v komentářích.
Další čtení
Tento článek vznikl s využitím materiálu z microsoft.github.io. Osobní postřehy a komentáře jsou moje vlastní.

