Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions docs/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ type Message =
from: string; // the participant whose presence changed — stamped by the runtime
identity?: string; // on 'arrived' and 'seated': how the room knew them
by?: string; // on 'seated': the assistant, when it did the seating
attention?: Attention; // on 'seated': what wakes the seat; absent means 'broadcast'
preferences?: string; // on 'arrived': how the person reads, when they said so
}
| {
kind: 'summary';
Expand Down Expand Up @@ -534,22 +536,34 @@ reads takes the narrower type and cannot start anything by accident.
One file per concern, in layers an import points down through, and
`session.ts` is the room that composes them ([`toolchain.md`](toolchain.md)
§1 names the layers, and Biome holds them): the
log in [`log.ts`](../packages/ambion/src/log/log.ts), who is here in
[`presence.ts`](../packages/ambion/src/presence.ts), a seat, what wakes it
log in [`log.ts`](../packages/ambion/src/log/log.ts), every fact folded
over it in [`fold.ts`](../packages/ambion/src/room/fold.ts), who is here in
[`presence.ts`](../packages/ambion/src/room/presence.ts), a seat, what wakes it
and the seat's side of the wire in
[`seat.ts`](../packages/ambion/src/seat/seat.ts), one activation in
[`activation.ts`](../packages/ambion/src/seat/activation.ts), the hands it
holds in [`hands.ts`](../packages/ambion/src/seat/hands.ts), an activation's
id in [`lease.ts`](../packages/ambion/src/room/lease.ts), the exchange in
[`exchange.ts`](../packages/ambion/src/exchange.ts), what the assistant
[`exchange.ts`](../packages/ambion/src/room/exchange.ts), what the assistant
writes in [`assistant.ts`](../packages/ambion/src/room/assistant.ts), what
crosses between a seat and its room in
[`wire.ts`](../packages/ambion/src/wire.ts), what an
[`wire.ts`](../packages/ambion/src/wire.ts), what an activation is given
in [`view.ts`](../packages/ambion/src/room/view.ts), what an
agent's tools reach into in
[`workspace.ts`](../packages/ambion/src/tools/workspace.ts), what a host
owns in [`runtime.ts`](../packages/ambion/src/host/runtime.ts), and what
any of them reads in [`render.ts`](../packages/ambion/src/render.ts).

**The log holds what the room does not hold in memory.** The roster, the
reserve, the people, the open exchange and the closes are each a fold over
the log ([`fold.ts`](../packages/ambion/src/room/fold.ts)). Three kinds of
entry hold them, in the room's one Pi session: `ambion/message`,
`ambion/close` and `ambion/composition`. Every entry beside a message
carries `after`, the last message seq when it was written. A stopped room
reads back the roster its run left, and a run starts from the composition
row it writes. The leases the room holds and the summaries it owes stay in
memory.

**A seat is seated for the run. An activation lasts seconds.** What an
activation has heard, what landed while it worked, and whether it left a mark
belong to the activation and end with it. Rule 5's `readThrough` is an
Expand Down
23 changes: 11 additions & 12 deletions docs/assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ answers for anyone.

The unit is the **exchange**, and it belongs to the core:
[`exchange.md`](exchange.md) specifies it, and
[`exchange.ts`](../packages/ambion/src/exchange.ts) is where it lives. A
[`exchange.ts`](../packages/ambion/src/room/exchange.ts) is where it lives. A
question, and everything the room does until it goes quiet again. A
person's question opens one; quiescence closes it; what lands in between
steers the seats already working and changes nothing.
Expand Down Expand Up @@ -269,8 +269,8 @@ writes one message per exchange, to one person, and the exchange says whom
**A person's exchange outlives their visit.** Priya may ask and walk out
before the room settles. The exchange is still hers, it still closes, and
the assistant still writes its summary — addressed to her, the way she
reads, waiting for her. How she reads is run state the room keeps after she
leaves.
reads, waiting for her. How she reads is on the record, with her arrival,
so the room keeps it after she leaves.

**Sam gets no summary for a question he did not ask.** His message into
Priya's exchange steers whoever is working and owns nothing. His own next
Expand Down Expand Up @@ -526,11 +526,9 @@ assistant does not mean it always writes — it means somebody is always
there to judge whether writing would help.

**A restarted room seats it again with the agents.** The assistant is
composition, like an agent. How each person reads is run state, like an
exchange (§6): a person known from a replayed record has no preferences on
file until they visit in the new run. No question can be asked without a
visit, so the assistant never writes for somebody whose preferences the room
has not seen.
composition, like an agent. How each person reads is on the record, with
their latest arrival, so a person known from a replayed record reads the
way they last said they do.

**An agent-only room pays for one idle seat.** A room nobody visits seats
the assistant, lists it in every roster, and never activates it. That is one
Expand Down Expand Up @@ -735,10 +733,11 @@ message stands for work somebody stopped. `stopSession` is the other case,
below.

**A run that stops mid-exchange writes no summary.** `stopSession` aborts
the activations in flight, so the exchange never closes. It aborts a draft in
flight for the same reason, and a draft that does finish after the stop
commits nothing. The person asked and heard nothing, and the record shows a
question, some work and a shutdown. Accepted.
the activations in flight and writes no close, so the exchange stays open
on the log. It aborts a draft in flight for the same reason, and a draft
that does finish after the stop commits nothing. The next run over the same
log closes the exchange at its start ([`exchange.md`](exchange.md) §5) and
writes what it owes then. Accepted.

**A widened range is bounded by a race, and nothing else.** A summary
covers one exchange, so the only thing that can make a range large is what
Expand Down
73 changes: 52 additions & 21 deletions docs/exchange.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This document is the design contract for the exchange: the room's own unit
of work. It is shipped. The code lives in
[`exchange.ts`](../packages/ambion/src/exchange.ts), and
[`exchange.ts`](../packages/ambion/src/room/exchange.ts), and
[`session.ts`](../packages/ambion/src/session.ts) opens and closes one as
the room runs. Read [`agent.md`](agent.md) first: an exchange is made of
the activations that document specifies, and it changes none of the eight
Expand Down Expand Up @@ -40,7 +40,7 @@ in a sentence a model reads.
## 2. The shape

A room is a sequence of exchanges, and the exchanges have one shape
([`exchange.ts`](../packages/ambion/src/exchange.ts)):
([`exchange.ts`](../packages/ambion/src/room/exchange.ts)):

```ts
interface Exchange {
Expand Down Expand Up @@ -76,13 +76,18 @@ the exchange itself, for the case where the room is busy and has no owner:
somebody arrives, the seat that watches the door wakes, and a question lands
on top of work nobody asked for. That question still owns what follows.

**Quiescence closes it.** The room settles when no agent is active, and a
**Quiescence closes it.** The room settles when nothing is live, and a
room that settles has finished. A seat that says something wakes its
readers inside its own `say`, before its own activation ends, so the room is
never briefly empty in the middle of a burst. What is running is read off
the seats, because a seat holds the activation it is taking, so there is no
count beside them to keep in step. `through` is the record as it stood at
that moment, so a closed exchange names the range it turned out to hold.
readers inside its own `say`, before its own lease ends, so the room is
never briefly empty in the middle of a burst. What is live is read off the
leases held and the wakes still pending, so there is no count beside them
to keep in step. The room writes a close row, and `through` is the record
as it stood at the moment the room went quiet, so a closed exchange names
the range it turned out to hold. A quiet the room observed on one
exchange closes that exchange alone. A question that lands after that
moment and before the row is written opens the next exchange. The host
hears `exchange_opened` for it once the row is on the log, and an exchange
nobody works on closes at once, the way a question that wakes nobody does.

A question that wakes no seat has no seat to stop, so the room runs the
same check once the question is routed: nothing is working, so the
Expand Down Expand Up @@ -129,18 +134,23 @@ into a quiet room, opens his own exchange.

---

## 5. Run state
## 5. A fold over the log

An exchange belongs to a running room. `Exchanges` holds the open one in
memory, and a restart begins with none. That is right for a room
mid-question: the record keeps what was said, and nobody is mid-question
after a restart. A person whose question the room was working on asks
again, and that question opens a new exchange.
An exchange is a fold over the log. The open exchange is the first
question a person asked after the last close row's `through`
(`openExchange` in [`exchange.ts`](../packages/ambion/src/room/exchange.ts)). A
close is a row on the log beside the messages: `{ owner, from, through,
at }`. It takes no seq; `through` orders it. `messages()` returns the
messages alone, and their seqs stay `1..n`.

A closed exchange is an owner and a range, so it is derivable from the
record. Nothing derives it today; a host that wants a history of exchanges
records the `exchange_closed` events as they arrive.
[`planning/backlog.md`](../planning/backlog.md) holds the work.
A run that starts over a log with an exchange open closes it first. The
seats that worked on it went with the run that started it, so nothing is
live, and the close row lands before the room takes its first message.
The host of the new run hears `exchange_closed` for it, and the assistant
writes what that exchange owes.

Every closed exchange is on the log, so a host that wants a history of
exchanges reads the close rows off the room's Pi session.

---

Expand All @@ -167,6 +177,10 @@ controls):
the assistant owes nobody one. That is the moment a host waits for when it
wants the one message a person reads.

Both wait for the room to be up first: a call made right after
`startSession` answers after the replay, and after the close of an
exchange the last run left open (§5).

The two differ because the assistant is a seat like any other, and its
activation counts. The assistant writing about an exchange is not the room
still working on it, so a drafting activation closes no exchange, and the
Expand All @@ -182,9 +196,15 @@ summary is drafted, and that window is the one place it can.

**An aborted exchange still closes.** `abort()` cancels the activations in
flight and the room settles, so the exchange closes with the range it
reached. **A run that stops mid-exchange closes nothing.** `stopSession`
aborts the activations in flight and takes the room down, and the exchange
never closes: the next run begins with none.
reached. **A stopped room closes nothing.** `stopSession` aborts the
activations in flight and writes no close row. A release that lands after
the stop must not write into a log the next run has started over. The
exchange stays open on the log, and the next run closes it at its start
(§5). A run that dies without `stop` leaves the exchange open the same way.

**A close the storage refuses leaves the exchange open.** `settled()` and
`quiet()` still answer, and the room still says `quiet`; the next time the
seats stop, the room writes the close again.

---

Expand Down Expand Up @@ -245,9 +265,20 @@ The exchange is proved beside the assistant that first reads one, in
- the person whose question opened the exchange owns it, and a second
person speaking into it owns nothing (§4);
- an exchange outlives its owner's visit (§4);
- an exchange closes at the quiet the room observed, and a question that
lands before the row is written opens the next (§3);
- a quiet observed on one exchange never closes the next, and a question
the assistant already woke on composes nothing and closes at once (§3);
- an exchange closes before anything is written about it, and the room
settles before it goes quiet (§6).

[`restart.test.ts`](../packages/ambion/test/restart.test.ts) proves that a
stopped room writes no close, and that the next run closes the exchange
before `quiet()` answers (§5, §6).
[`presence.test.ts`](../packages/ambion/test/presence.test.ts) proves that
a close the storage refuses leaves the exchange open, and that whoever
waits still hears the room (§6).

All in-process, in vitest, on a scripted stream.

The live run is
Expand Down
21 changes: 14 additions & 7 deletions docs/presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,10 @@ opened one, and two tabs of one person do not make two people. The host
decides when that person is gone; the room takes its word for it.

A second `visitSession` with the same name and a different identity is
refused: one name is one identity for the life of the opening. The
alternative is a roster that changes under the agents reading it.
refused while the person is present: one name is one identity for as long
as they are in the room. The alternative is a roster that changes under
the agents reading it. An absent person may return under a new identity,
and their next `arrived` carries it.

`deliver` on a visit that left throws, and so does it on a visit whose run
was stopped. A handle to a finished visit is a stale handle, and the
Expand Down Expand Up @@ -210,6 +212,8 @@ interface Presence {
from: string;
/** How the room knew them, on `arrived` alone. */
identity?: string;
/** How they read, on `arrived`, when they said so. */
preferences?: string;
}

export type Message = Spoken | Presence | Summary;
Expand Down Expand Up @@ -325,11 +329,14 @@ forget.** An agent that reads `andrei (present)` and calls
lands the message. So does an agent in a session reopened next week,
because replaying the record replays the arrivals.

Presence itself is live: it is a fact about a running room, and it dies
with the process. What survives is the record of how it changed, and that
rebuilds everything — who has ever been here, who was here last, when, and
where each of them stopped reading. Presence is kept in one place, and the
place is the record.
Presence is a fold over the record (`foldPeople` in
[`presence.ts`](../packages/ambion/src/room/presence.ts)): a person is present
from their last `arrived` until their next `left`. The record rebuilds
everything — who has ever been here, who is here now, when, and where each
of them stopped reading. Presence is kept in one place, and the place is
the record. A run that dies writes no `left`, so a person stays present
until the host says they left: a visit on the next run hands back a handle
and commits nothing, and `leave()` on it writes the `left`.

---

Expand Down
45 changes: 26 additions & 19 deletions docs/roster.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,10 @@ open wakes it the same way, and the runtime hands it one tool, `seat`,
bound to the reserve. The assistant bookends the exchange: it composes the
room at the open and consolidates what the room said at the close.

The order inside `publish` is what makes it parallel. A question lands, the
room opens the exchange and activates the assistant, then it routes the
question and activates the seats. The assistant reads the question while
the seats do.
The order inside the commit is what makes it parallel. A question lands,
the room sees the exchange it opened and wakes the assistant, then it
routes the question and wakes the seats. The assistant reads the question
while the seats do.

**What the assistant is handed.** The same context every seat reads, and
two things more: the reserve (§2) as a second roster, and the ask at the
Expand Down Expand Up @@ -299,11 +299,13 @@ Unseating is the direction the room cannot take back, so the assistant
holds no tool for it. [`planning/backlog.md`](../planning/backlog.md) holds the
argument for giving it one.

**`stop` unseats what the run added.** `stopSession` commits `left` for
every person present ([`presence.md`](presence.md) §8). It commits
`unseated`, in the same way and without routing, for every seat the run
added after it started. The next run begins from the composition
`startSession` was given, and the record says who was seated in between.
**`stop` leaves the roster to the next composition.** `stopSession`
aborts every activation in flight and commits `left` for every person
present ([`presence.md`](presence.md) §8). It writes no `unseated`. The
next `startSession` writes its own composition row, the roster folds from
that row and the seatings after it, and the record says who was seated in
between. A read of the stopped room (`readSession`) folds the roster the
run left.

**A seat that leaves keeps its downstream session.** Rule 8 puts every
activation's turns in `<room>:<agent>`. An agent seated, unseated and
Expand All @@ -321,9 +323,9 @@ until something unrelated activated and ended.

This case exists today, in a room where every seat is `named` and a
question is undirected. It is common once a room may start with the
assistant alone and an empty reserve. So `publish` runs the same check the end
of an activation runs: after routing, if nothing is working, the room
settles and the exchange closes. The exchange holds one message, the
assistant alone and an empty reserve. So the room runs the same check the
end of an activation runs once the question is committed: after routing,
if nothing is working, the room settles and the exchange closes. The exchange holds one message, the
question, and the assistant writes nothing for it, because an exchange the
agents said nothing into writes nothing ([`assistant.md`](assistant.md)
§4). The host hears `exchange_opened`, `exchange_closed` and `quiet`, in
Expand All @@ -343,10 +345,13 @@ Each boundary is stated so a later change has to argue with it.
- **The assistant never defines an agent.** It seats from the reserve, and
the host decides what is in it by writing `available`. §2.
- **A seat never reads the reserve.** §2.
- **A seating is on the record, and the starting composition is not.** The
record holds what happened in the run. What the run started with is the
run's, as `agent.md` §5 says of the roster and `presence.md` says of the
people.
- **A seating is on the record, and so is the composition.** Every
`startSession` writes a composition row beside the messages: the
assistant, the goal, the agents seated and the agents in reserve, each
with its name, its identity and its attention. The roster folds from the
latest row and the seatings and unseatings after it, so a stopped room
reads back. A read from a process that holds no definition reports every
identity off the log.
- **The threshold reads the record.** The rule that a summary is written
when the agents said more than one thing counts messages from any name
that is not a person and not the assistant, so an agent that spoke and
Expand Down Expand Up @@ -384,9 +389,11 @@ this document makes loudly:
- the host seats and unseats by hand, an unseat aborts the activation in
flight, and a say directed at the unseated colleague is refused with the
departure (§5);
- `stop` unseats what the run added and leaves the starting composition
alone (§5);
- the threshold counts an agent that spoke and was unseated (§7).
- `stop` leaves the roster to the next composition row, and the next run
starts from its own (§5);
- the threshold counts an agent that spoke and was unseated (§7);
- every identity reads off the log, in a process that holds no definition
(§7, in [`restart.test.ts`](../packages/ambion/test/restart.test.ts)).

All in-process, in vitest, on a scripted stream.

Expand Down
Loading
Loading