Aller au contenu principal

Threads multi-utilisateurs

Un même thread Mastra peut être partagé par plusieurs utilisateurs, chacun possédant son propre nom et son propre rôle fonctionnel. L’identité du locuteur est intégrée au corps du message afin que l’agent puisse distinguer les utilisateurs tout en lisant un seul thread partagé.

Quand utiliser des threads multi-utilisateurs
Lien direct vers Quand utiliser des threads multi-utilisateurs

Utilisez des threads multi-utilisateurs lorsque plusieurs personnes collaborent sur le même sujet par l’intermédiaire d’un agent :

  • Documents collaboratifs réunissant des rédacteurs, des réviseurs et des approbateurs
  • Conversations de groupe dans lesquelles un même assistant sert plusieurs participants
  • Revues impliquant plusieurs parties prenantes, dans lesquelles les rôles disposent de niveaux d’autorité différents

Partager un même resourceId entre tous les participants
Lien direct vers share-one-resourceid-across-all-participants

Un thread appartient à un seul resourceId ; tous les participants d’un thread partagé doivent donc transmettre la même valeur. Au lieu d’utiliser un identifiant utilisateur, comme le font par défaut les applications à utilisateur unique, fondez resourceId sur la conversation elle-même, par exemple doc_${docId} pour un document partagé ou room_${roomId} pour une conversation de groupe. Lorsque tout le monde pointe vers le même resourceId, tous les participants lisent et écrivent le même historique.

Ajouter l’identité du locuteur à chaque message utilisateur
Lien direct vers Ajouter l’identité du locuteur à chaque message utilisateur

Le modèle doit savoir qui parle à chaque tour. Puisque le corps du message est le seul endroit qui persiste dans l’historique puis revient dans le contexte, enveloppez chaque message utilisateur dans une petite balise <turn> contenant l’identifiant, le nom et le rôle du locuteur. La balise reste attachée au message ; lorsque les tours précédents sont rappelés, le modèle sait donc toujours qui a dit quoi.

Construisez la balise avec une petite fonction utilitaire. L’exemple ci-dessous présente une façon de procéder : copiez-le dans votre projet et adaptez-le à la structure de vos données utilisateur :

src/mastra/identity.ts
export type Speaker = {
id: string
name: string
role: string
}

function escapeAttr(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}

export function asUserTurn(speaker: Speaker, text: string) {
const id = escapeAttr(speaker.id)
const name = escapeAttr(speaker.name)
const role = escapeAttr(speaker.role)
return {
role: 'user' as const,
content: `<turn author_id="${id}" author_name="${name}" functional_role="${role}">
${text}
</turn>`,
}
}

Apprenez à l’agent à lire la balise <turn> dans ses instructions. La propriété memory doit être configurée sur l’agent afin de pouvoir l’appeler avec un thread et une resource :

src/mastra/agents/collab.ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
},
})

export const collabAgent = new Agent({
id: 'collab',
name: 'CollabAgent',
model: 'openai/gpt-5-mini',
memory,
instructions: `
You are a collaborative document assistant. Multiple users talk to you in the SAME thread.

Every user message is wrapped in a <turn> tag carrying the user's identity:

<turn author_id="u_alice" author_name="Alice" functional_role="editor">
...message text...
</turn>

Rules:
1. Address users by their author_name.
2. Respect functional_role: editors propose changes, reviewers approve.
3. When attributing past statements, read author_name from the surrounding <turn> tag.
4. Do not echo the <turn> tags back at users.
`.trim(),
})

Appelez l’agent avec le message enveloppé. Tous les participants partagent le même thread et la même resource :

src/mastra/call.ts
import { asUserTurn } from './identity'

const docResourceId = 'doc_42'
const docThreadId = 'doc_42'

const alice = { id: 'u_alice', name: 'Alice', role: 'editor' }
const bob = { id: 'u_bob', name: 'Bob', role: 'reviewer' }

await collabAgent.generate([asUserTurn(alice, 'My favorite color is teal.')], {
memory: { thread: docThreadId, resource: docResourceId },
})

await collabAgent.generate([asUserTurn(bob, 'I want QA sign-off before publish.')], {
memory: { thread: docThreadId, resource: docResourceId },
})

La balise <turn> persiste dans le corps du message. Lorsque l’historique est rappelé lors de tours ultérieurs, le modèle sait donc toujours qui a dit quoi.

Combiner les couches de mémoire
Lien direct vers Combiner les couches de mémoire

Le modèle d’étiquetage des utilisateurs se combine avec toutes les couches de mémoire. Choisissez la couche selon la durée pendant laquelle la conversation doit mémoriser les faits propres à chaque utilisateur :

  • Conversations courtes, limitées à une seule session ou à un thread assez petit pour tenir dans lastMessages, ou situations nécessitant un enregistrement mot pour mot de chaque intervention : utilisez uniquement l’historique des messages. Les balises utilisateur de l’historique suffisent ; aucune couche de mémoire supplémentaire n’est nécessaire.
  • Threads de longue durée, c’est-à-dire des conversations qui dépassent lastMessages et dont les faits propres à chaque utilisateur doivent survivre à l’éviction de l’historique : utilisez la mémoire observationnelle.
  • Besoin d’une liste structurée de participants, ou adaptateur de stockage incompatible avec OM, qui nécessite LibSQL, PG ou MongoDB : utilisez la mémoire de travail.

Nous recommandons d’utiliser soit la mémoire observationnelle, soit la mémoire de travail, car elles répondent à des besoins qui se recoupent. Les exécuter ensemble augmente la latence et le coût en tokens sans apporter beaucoup d’avantages.

Historique des messages uniquement
Lien direct vers Historique des messages uniquement

Pour les conversations courtes, ou lorsque vous avez besoin d’un enregistrement mot pour mot de chaque intervention, les balises utilisateur de l’historique suffisent. lastMessages réinjecte les tours précédents dans le contexte en conservant leur attribution :

src/mastra/agents/collab-basic.ts
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
},
})

Le modèle lit l’identité dans la balise <turn> du message actuel ainsi que dans les messages précédents balisés, réinjectés au moyen de lastMessages.

La mémoire observationnelle (OM) extrait les faits propres à chaque utilisateur dans un journal en arrière-plan sans consommer le budget d’outils de l’agent. Le modèle Observer par défaut lit nativement les balises <turn> et produit des attributions telles que Alice stated her favorite color is teal. et Bob asked for QA sign-off before publish.

Préférez OM à la mémoire de travail pour les threads multi-utilisateurs lorsque votre stockage le prend en charge. OM extrait automatiquement les faits, s’adapte à n’importe quel nombre de participants et ne nécessite aucune maintenance de modèle. Activez-la sans remplacement :

src/mastra/agents/collab-om.ts
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
observationalMemory: true,
},
})

OM nécessite un adaptateur de stockage qui la prend en charge : @mastra/libsql, @mastra/pg, @mastra/mongodb ou @mastra/oracledb.

remarque

Si vous remplacez le modèle Observer par un modèle moins performant et que les faits se réduisent à un terme générique User, utilisez observation.instruction pour apprendre à l’Observer à lire la balise <turn>.

Avec la mémoire de travail
Lien direct vers Avec la mémoire de travail

Utilisez la mémoire de travail lorsque OM n’est pas envisageable, par exemple si votre adaptateur de stockage ne prend pas OM en charge, ou lorsque vous avez besoin d’une liste de participants structurée et déterministe que l’agent peut lire et modifier à chaque tour.

Le modèle de mémoire de travail par défaut suppose un utilisateur par thread (« Prénom », « Nom », etc.). Pour les threads multi-utilisateurs, fournissez un modèle comportant une liste de participants :

src/mastra/agents/collab-wm.ts
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
workingMemory: {
enabled: true,
scope: 'thread',
template: `# Document Collaboration State

## Participants
<!-- One entry per known collaborator. Use author_id as the stable key. -->
<!-- - **<author_name>** (<author_id>, <functional_role>): <their position> -->

## Open Questions

## Decisions
`,
},
},
})

Définissez scope: 'thread' afin que la liste des participants appartienne au document et non à un utilisateur individuel. Ajoutez une instruction demandant à l’agent d’ajouter chaque nouveau participant à la liste lorsqu’un nouvel author_id apparaît dans une balise <turn>.

Pour en savoir plus sur les modèles, consultez la page Modèles personnalisés.

Sécurité
Lien direct vers Sécurité

Définissez le speaker à partir du contexte de requête authentifié, jamais à partir du corps de la requête. Si un client peut choisir son propre author_id, un utilisateur peut usurper l’identité d’un autre. Utilisez le contexte de requête pour lire l’utilisateur vérifié depuis votre couche d’authentification et construire la balise <turn> sur le serveur avant d’appeler l’agent.