Much of what happens in a room is long and written a piece at a time: a
model's answer, an agent's report, a summary of a board, an import's log.
A stream of a room (Issho.Streams) is that output. Any process of your
app writes it; the pages and agents allowed to see it get it as it comes;
and Issho keeps it while it is written and for a while after, so that a
person who goes to another page, opens another tab, or loses their
connection finds it again where it got to, not where they left it.
Issho keeps the chunks and sends them on. It does not read them: what a stream is, a component or your app decides. Kotoba can show one as a suggestion in a text, a canvas as a card waiting to be placed, your app as a chat's answer or a log.
Writing one
{:ok, stream} =
Issho.Streams.start(MyApp.Issho, "notebook:7",
kind: "summary",
for: ada,
place: %{"doc" => "notebook:7/intro", "at" => %{"node" => block_id}},
by: assistant
)
for chunk <- MyApp.Model.stream(prompt), do: :ok = Issho.Streams.append(stream, chunk)
:ok = Issho.Streams.finish(stream)The process that starts it writes it. If that process exits while the
stream is written, the stream fails ("writer_exited"), or finishes with
on_exit: :finish, so no stream is left being written forever. Run the
model in a process of its own (a Task, an agent's work with
Issho.Agents.run/5), not in the LiveView that asked for it: the person
may leave the page, and the answer goes on.
Who sees it
:for is required:
- a person (an actor, or a key): a private draft, an answer to what they asked. Their agents see it too, and so does its writer.
:room: everyone who may read the room. An agent's report, a proposal for everyone to take or leave.:policy: whomever your policy allows:read_streamin the room (Issho.Policy), for a rule of your own.
Reading one
In a LiveView in the room (Issho.Live.join/4):
def mount(_params, _session, socket) do
{:ok, outputs} = if connected?(socket), do: Issho.Live.streams(room), else: {:ok, []}
{:ok, assign(socket, :outputs, Map.new(outputs, &{&1.id, &1}))}
end
def handle_info({Issho, :stream, _id, {:started, stream}}, socket),
do: {:noreply, update(socket, :outputs, &Map.put(&1, stream.id, stream))}
def handle_info({Issho, :stream, id, {:chunk, text}}, socket),
do: {:noreply, update(socket, :outputs, &Map.update!(&1, id, fn s -> %{s | content: s.content <> text} end))}
def handle_info({Issho, :stream, id, message}, socket), do: ...Issho.Live.streams/1 gives the streams the person may see, each with
what it holds so far, and from then on sends the LiveView what comes, in
order, with nothing missed or sent twice: {:chunk, data}, :finished,
{:failed, reason}, {:closed, outcome}, :gone, and {:started, stream} for a new one. A page that mounts while a stream is written
picks it up where it got to. (These are not LiveView's own streams of a
list, and :streams is LiveView's assign.)
Elsewhere (a job, a participant, another node), Issho.Streams.watch/3
and Issho.Streams.subscribe/4 do the same for any process.
Its end
Finished or failed, a stream is kept for :keep milliseconds (the
instance's :stream_keep, five minutes), then goes. close/2 ends it at
once with an outcome you name: a suggestion accepted or rejected, a
report dismissed. From a person's page, Issho.Live.close_stream/3 first
checks that they may see it:
def handle_event("accept", %{"id" => id}, socket) do
{:ok, stream} = Issho.Streams.get(MyApp.Issho, id)
:ok = MyApp.Notes.insert(stream.content)
:ok = Issho.Live.close_stream(room, id, :accepted)
{:noreply, socket}
endEach start, finish, failure and close is an event of the room:
"stream.started", "stream.finished", "stream.failed" and
"stream.closed", with the stream's id, kind, whom it is for, and the
reason or the outcome. An agent waiting on them learns whether its draft
was taken.
Agents
An agent writes its long answers to a stream for the person it works
for: the start_stream, append_to_stream and finish_stream room
tools. Its stream lives while the agent is in the room.
Limits
A stream holds at most :stream_max_bytes (1 MB; one that would hold more
fails, "too_large"), and a room has at most :room_max_streams (32) at
once. Streams are kept in memory, as presence is: a stream does not
outlive its node. Each is a process, named in the cluster, so a page on
any node reads it.