Skip to main content
Let a writer ask a reviewer for help, then pick up the reply without you relaying it. Give each member a name, a role and a brief. Put the writer and reviewer in a room. A saved message that mentions a member requests that member’s next turn. The conversation stays in the run record, so a fresh executor can read the messages and the turns they requested.

Define the conversation

Use the installed runtime package in your own TypeScript file. The complete source below uses local functions for the writer and reviewer, so it needs no model account. It writes a temporary record, reopens it within the program and removes its temporary files before exit.
The reviewer waits for a question because its initialTurn is false:
The writer sends its question in its successful turn result. mentions contains member IDs; the runtime does not look for names in the text:
The reviewer receives the saved question. Its reply mentions writer, which gives the writer another turn with that reply. The example checks that the writer’s final answer contains the reply before it prints success.

Output

The saved messages, dispatch order and final answers come from the run’s record and result. The fresh executor adds no events when it reads the completed conversation. This is an offline demonstration that reopens storage within one program, not a command that leaves a record behind or resumes across separate invocations.

Gotchas

  1. A message sends when the turn ends. It is saved with the member’s successful result, not while an engine is still speaking. A member that needs an answer ends its turn. The reply queues it again. A failed or unfinished turn sends nothing.
  2. A mention waits for active work. It requests a later turn; it never interrupts one already running. The active batch settles before another batch starts. Several pending mentions for the same member share one queued turn and keep their triggering messages.
  3. The conversation has limits. Set a turn cap for each member and a message-tail limit for its input. A requested turn beyond the cap fails with TEAM_TURN_LIMIT, not a successful answer. The host’s ordinary input and output byte limits also apply.
  4. Room membership is not a filesystem sandbox. It controls accepted posts and the input supplied to each member. The operator’s full record contains every room. Keep that record and the room-file directory outside member workspaces and use the host’s access controls. Processes sharing unrestricted filesystem access can read the same files.
  5. Choose the execution path explicitly. team(config) is a callable job with its own workspace and review behavior. The compiled teamGraphType uses the stored graph executor and host bindings. It does not create or merge worktrees or run callable review functions for you. A team result is not proof that a merge or review happened. See the callable team contract.

Team fields

teamGraphType is the workflow shape passed to compileGraph. TeamDefinition contains an id, a definitionVersion, team data, member nodes and an empty edges array. Messages request turns; edges do not connect members.

Team data

All three limits, including tailMessages below, must be positive safe integers. The member count multiplied by maxTurnsPerMember must also be a safe integer.

Members

Each node’s id is the member’s address. Its data is a TeamNodeData value:

Rooms

TeamCommunication.rooms is a list of TeamRoom values. Each room has a unique id and a members list of distinct node IDs. List every member for a run-wide room, one team’s members for a team room, or two members for a direct message. Membership is part of the saved definition; a member cannot change it during the run. TeamCommunication.tailMessages sets how many recent messages from each permitted room enter a member’s next input. A pending message that requested the turn is included even when it falls outside that tail. Messages present in both groups appear once, in saved order. The tail limit is therefore not a limit on all messages in an input; the input-byte budget still applies.

Turn results

Every successful member result is a TeamTurnResult: A TeamPost has roomId, text and mentions. The sender must belong to the room, and every mentioned member must belong to that same room. Every post is checked before any post from that turn is accepted. Invalid result content fails the turn with RESULT_INVALID before a completion is saved. The team returns TEAM_NODE_FAILED after active turns settle. If an invalid completion is already in the record, replay retains that failure and the earlier valid results and messages. It delivers none of the bad turn’s posts, and room files can still be rebuilt from accepted messages. Malformed event records and unsupported event versions are rejected. The example also binds a result contract that reuses the compiled form’s membership check. A saved TeamMessage retains those three fields and adds sender, position and id. The runtime obtains the sender from the completed node. The message ID combines the dispatch position with the post’s zero-based index. Each next-turn input contains task, role, brief, result and messages. result is this member’s preceding result, or null on its first turn. It is not the whole team’s result. A member receives messages only from rooms it belongs to.

Final answers

Successful execution returns a TeamGraphResult in the executor outcome’s output: the task and an agents list in member declaration order. Each entry contains name, role and that member’s last result. A member that never receives a turn has result: null. After active turns settle, TEAM_NODE_FAILED reports any failed member, even when another member is paused. Without a failed member, a paused member keeps the run paused until the host uses the executor’s existing resume contract. TEAM_TURN_LIMIT reports a queued member whose turn cap is exhausted.

Room files

projectTeamRooms reads a saved team run and rebuilds its room files. Supply the run’s storage, its runId and the operator’s output directory. The result contains the last read revision and a list of { roomId, path } entries. Use that list to find each file; do not construct its filename from the room name. Each file contains one readable line per accepted message: sender, message ID, mentions, a colon and the text. An empty room has an empty file. A run without communication returns no files. Newlines and terminal control characters are printed as escapes. People and tools can tail these files like an ordinary text chat. The helper changes no stored events. A rebuild replaces its own files and leaves unrelated files alone. Each file replacement is atomic; the set of room files is not replaced as one operation. The returned revision says what was read, not that the run stopped changing during the write. Deleting a room file does not delete its messages from the record.

Source

Copy this complete file into a TypeScript project with @obversa/runtime installed.