Perceptions & InteractionRequests

Perceptions and InteractionRequests

It is possible to send _metadata as part of your DeveloperDispatchedPerception and use that metadata in your MentalProcess. It is also possible to dispatch InteractionRequest objects with _metadata attached from the Soul Engine for use in your client side applications.

Example Soul Client Dispatch

// from your application
  name: "kafischer",
  action: "chatted",
  content: "Hi there!",
  _metadata: {
    discordMessage: {
      id: 123,
      userId: 123,
      guildId: 123,
      guildName: "Open Souls",

And that metadata is useable from your MentalProcess as part of both the step's memory _metadata and the invokingPerception metadata.

import { usePerceptions, useActions } from "@opensouls/engine"
const exampleProcess: MentalProcess = async ({ workingMemory }) => {
  import { log } from useActions()
  // get information about the incoming perceptions
  const { invokingPerception, pendingPerceptions } = usePerceptions()
  log(`The soul received a discord message from the guild: ${invokingPerception._metadata.discordMessage.guildName}`)
  // rest of the mental process
  // ...
  return workingMemory
export default exampleProcess

Example Mental Process Dispatch

Your MentalProcess can dispatch _metadata as well. dispatch is available from the useActions hook.

import { usePerceptions, useActions } from "@opensouls/engine"
const heartReactor: MentalProcess = async ({ workingMemory }) => {
  import { log, dispatch } from useActions()
  // get information about the incoming perceptions
  const { invokingPerception, pendingPerceptions } = usePerceptions()
    action: "emojiReacts",
    content: "❤️",
    _metadata: {
  return workingMemory
export default heartReactor

This is useable on the Soul client side through an ActionEvent:

import { ActionEvent, Soul, SoulEvent } from "@opensouls/soul";
// ... the rest of your app
soul.on("emojiReacts", (evt: ActionEvent) => {
  console.log("reacts!", evt)
  const message = findMessage(evt._metadata.messageId)
  this.message.react(await evt.content()) 


A Perception (what goes in to the Soul Engine) and an InteractionRequest (what comes out of the Soul Engine) are both instances of a SoulEvent which has this shape:

export enum SoulEventKinds {
  Perception = "perception",
  InteractionRequest = "interactionRequest",
  System = "system",
export interface SoulEvent {
  _id: string
  _kind: SoulEventKinds
  _timestamp: number // milliseconds since epoch
  _metadata?: Record<string, Json>
  _pending?: boolean
  internal?: boolean
  _mentalProcess?: {
    name: string
    params: Json
  action: string
  content: string
  name?: string,
export type Json =
  | { [key: string]: Json | undefined }
  | Json[]
  | boolean
  | null
  | number
  | string
  | undefined

On the Soul client side, we have a special event for an InteractionRequest which handles streaming, etc for you. This is an ActionEvent:

 * `ActionEvent` is designed to be isomorphic between streaming and non-streaming actions.
 * When an event is not streaming,
 * the `content` will be immediately available as a `Promise<string>`. The `stream`
 * will be an `AsyncIterable<string>` that yields a single value if the event
 * is not streaming, allowing for consistent handling of event data.
 * If the event *is* streaming then content will resolve when the stream is complete.
export type ActionEvent = {
  content: () => Promise<string>
  isStreaming: boolean
  stream: () => AsyncIterable<string>
  action: string
  name?: string,
  _metadata: InteractionRequest['_metadata']
  _timestamp: InteractionRequest['_timestamp']
  // @deprecated perception is deprecated, use interactionRequest
  perception: InteractionRequest
  interactionRequest: InteractionRequest

It's also possible to listen to all SoulEvent dispatches through the Events.newSoulEvent event (but this will not automatically handle streaming for you).

soul.on(Events.newSoulEvent, (evt: SoulEvent) => {