Issho.Agents (Issho v0.4.0)

Copy Markdown View Source

Agents in rooms for front ends without a lasting connection: an MCP server, whose every tool call is its own request, a webhook, a chat bot. An agent joins a room on its first call and stays, present and where it last pointed, between calls; it leaves after :idle milliseconds with no call (ten minutes), or when told.

{:ok, claude} = Issho.Agents.join(MyApp.Issho, agent, "board:42", on_behalf_of: ada)
{:ok, _snapshot} = Issho.Participant.open(claude, "board:42")
Issho.Participant.place(claude, %{"doc" => "board:42", "at" => %{"node" => "n7"}})

join/4 gives the agent's Issho.Participant, the same one each time for the same actor and room, and starts the idle time again. People see the agent as they see anyone: its name, its place, its changes.

An agent in a room hears its events (Issho.Event), but those it made happen itself, and keeps the last 100 of them until it takes them (wait/5, the wait_for_events tool): what people did between its calls, and what it waits for (someone mentions it, a document changes).

An agent is one in the cluster (:global, as the documents' authorities are): a call on any node finds it, so an MCP endpoint behind a load balancer needs no session affinity.

Its work

Your app's own agent (a model your app calls, not an MCP client) can run its work under Issho: run/5 runs a function as the agent, in a process linked to it. When a person stops the agent (Issho.Work), the process is ended at once, wherever it is (waiting on the model, in the middle of a tool), not at its next change; a pause still lets it run, and refuses its changes. When it crashes, its task says it failed and the room hears "agent.failed" (Issho.Work.failed/2).

Each agent in a room is a state machine (:gen_statem): listening, or waiting (a call of wait/6 that has nothing yet), with its idle time and a wait's time as its timeouts. An agent idle for a while hibernates: it takes little memory until it is called.

Summary

Functions

A value the agent kept in the room, or default.

The agent's participant in the room, joining it on the first call. Options: those of Issho.join_as/4 (:can, :on_behalf_of, :claims) and :idle.

Takes the agent out of the room.

The participant of the agent with this key in the room, if it is there.

Keeps a value for the agent in the room, for its later calls (what it copied, what it was asked to remember), until it leaves.

The rooms the agent with this key is in, in the cluster.

Runs the agent's work in the room, joining it as join/4 does: fun, given the agent's participant, in a process linked to the agent, which stays in the room while it runs (its idle time starts when the work ends). One work at a time: {:error, :running} while one runs.

The events of the room since the agent last took them, whose names are one of names (Issho.Events.match?/2: a list of names, "kind.*", or :all), oldest first; when there is none yet, waits up to timeout milliseconds for one. With mentions_me: true, only those whose data mentions the agent (mentions, a comment's). Gives {:ok, events}, an empty list when none came, or {:error, :not_there} for an agent not in the room.

Functions

get(instance, key, room, name, default \\ nil)

@spec get(atom(), String.t(), String.t(), term(), term()) :: term()

A value the agent kept in the room, or default.

join(instance, actor, room, opts \\ [])

@spec join(atom(), Issho.Actor.t(), String.t(), keyword()) ::
  {:ok, pid()} | {:error, term()}

The agent's participant in the room, joining it on the first call. Options: those of Issho.join_as/4 (:can, :on_behalf_of, :claims) and :idle.

leave(instance, key, room)

@spec leave(atom(), String.t(), String.t()) :: :ok

Takes the agent out of the room.

participant(instance, key, room)

@spec participant(atom(), String.t(), String.t()) :: pid() | nil

The participant of the agent with this key in the room, if it is there.

put(instance, key, room, name, value)

@spec put(atom(), String.t(), String.t(), term(), term()) ::
  :ok | {:error, :not_there}

Keeps a value for the agent in the room, for its later calls (what it copied, what it was asked to remember), until it leaves.

rooms(instance, key)

@spec rooms(atom(), String.t()) :: [String.t()]

The rooms the agent with this key is in, in the cluster.

run(instance, actor, room, fun, opts \\ [])

@spec run(atom(), Issho.Actor.t(), String.t(), (pid() -> term()), keyword()) ::
  {:ok, pid()} | {:error, term()}

Runs the agent's work in the room, joining it as join/4 does: fun, given the agent's participant, in a process linked to the agent, which stays in the room while it runs (its idle time starts when the work ends). One work at a time: {:error, :running} while one runs.

{:ok, work} =
  Issho.Agents.run(MyApp.Issho, assistant, "board:42", fn claude ->
    :ok = Issho.Work.start(claude, "Group the cards", ["Read", "Group"])
    # ... the model, the tools ...
    Issho.Work.finish(claude, "Grouped 14 cards.")
  end, on_behalf_of: ada)

Gives {:ok, pid}. Monitor it to know how it ended: :normal; {:shutdown, {:stopped, by}}, a person stopped the agent (by, who); {:shutdown, :left}, the agent left the room; or the reason it crashed, when the agent's task says it failed (Issho.Work.failed/2).

wait(instance, key, room, names, timeout, opts \\ [])

@spec wait(
  atom(),
  String.t(),
  String.t(),
  :all | [String.t()],
  non_neg_integer(),
  keyword()
) ::
  {:ok, [Issho.Event.t()]} | {:error, :not_there}

The events of the room since the agent last took them, whose names are one of names (Issho.Events.match?/2: a list of names, "kind.*", or :all), oldest first; when there is none yet, waits up to timeout milliseconds for one. With mentions_me: true, only those whose data mentions the agent (mentions, a comment's). Gives {:ok, events}, an empty list when none came, or {:error, :not_there} for an agent not in the room.