# Chats: How Lovable's Agents Work Together

> Source: <https://lovable.dev/blog/how-lovable-agents-work-together>
> Published: 2026-09-24 14:22:21+00:00

Building an agentic product gets more interesting when one conversation needs to set work in motion somewhere else.

An agent delegates a task to another agent. That agent has its own history and might already be working. New instructions arrive before the task finishes. Results need to make their way back to the conversation that started it. How do you connect these pieces without losing context, mixing up responsibilities, or making every interaction a special case?

These are some of the engineering questions behind **Chats**, our new way to explore ideas, work through decisions, and get things done with your Lovable projects in view. You can start with a conversation and, when you're ready, confirm a build that turns your idea into changes to an app.

Chats builds on infrastructure we originally developed for agents and subagents to work together. On a typical weekday, our Trajectory System appends roughly half a billion events, covering about 2.6 million user turns.

In this post, we'll unpack the engineering behind that scale, the tradeoffs we made, and how Chats uses this foundation to connect conversations to work on your apps.

### The short version

Our architecture separates three things that are easy to conflate: what happened, what an agent needs to know, and when it should run.

We record what happened in an append-only, forkable history called a **trajectory**. Each agent's model context is built from that history, rather than treating the history itself as a prompt. Agents communicate through durable **inboxes**, while **activations** arrange for them to run and pick up new work.

In Chats, this is what connects talking about an idea to building it. Once you confirm a change, Chats passes the work to the agent building your app and brings its updates back into the conversation.

Here's how we built that connection.

## Trajectories: an event log modeled after Git

Every agent conversation in Lovable is a log of events. We capture everything the LLM produces at the granularity of the Lovable Agent Framework: UserMessage, AgentStart and AgentDone bracketing an agent's response, IterationStart and IterationEnd around each LLM call, the model's thinking, content and tool_call output, and the full tool lifecycle—ToolParametersValidated, ToolExecutionStart, ToolExecutionEnd, plus ToolApprovalRequired when a human has to weigh in. Every tool call is matched to its result. Nothing is thrown away: events are immutable and append-only, and even a revert is recorded as a Revert event rather than by deleting anything.

The design is modeled after Git commits and Git history. Each event carries a single parent pointer. Follow the parent chain backwards and you get the full log. Named *heads* point at the tip of each line of history, like branches in Git.

That single property gives us something powerful for free: *any iteration boundary can fork*. From any IterationEnd or AgentDone in a conversation, we can create a new head positioned at that event. The new thread shares all history up to that point, exactly like branching in Git, and a fork costs almost nothing because no events are copied—the first event on the fork, a ThreadForkConfig, simply names the fork point as its parent.

One deliberate departure from Git: an event has exactly one parent. We never merge one trajectory into another, because merging two agent histories has unclear semantics: what should interleaving two independent streams of thoughts, tool calls and results even mean? When agents need to share results, they don't merge; they send each other messages through the Agent Control Plane, which we'll come back to later in the post. Trajectories only ever branch.

## Context is a projection of the trajectory

The trajectory is the source of truth, but it is not literally what we send to the model. At the start of every iteration, right after IterationStart is written, the prompt builder walks the agent head backwards and renders the prompt *from* the trajectory. The event log captures everything that has happened; how much of it the model sees, and in what shape, is a separate decision made per call.

To be clear, context is always assembled from the trajectory—the win isn't avoiding that work, it's the flexibility to assemble it to fit the situation. Compaction is the clearest example, and it's recorded in the log like everything else. When the agent decides to compact, it writes a PromptCompactionStart onto its trajectory and keeps going. A summarizer runs in the background as a small agent loop of its own, on a *side trajectory* that branches off the agent's history. Its result never lands on the agent trajectory directly. Instead the side trajectory acts much like an inbox: at its next iteration boundary the agent decides to admit the finished summary and records it as a PromptCompactionEnd on its own trajectory. Building the next prompt is then a backward walk: on meeting an End paired with its Start, the builder adopts the summary in place of the older events and renders everything after the compaction point as ordinary history.

This separation between the event log and the LLM context unlocks a number of patterns:

- Asynchronous compaction. The summarizer runs while the agent keeps iterating. The agent never pauses for it; the next prompt build simply finds the completed compaction and uses it.
- Background agents with shared history. A forked agent's prompt builder walks straight through the parent's events as its own history—the fork sees the exact same history as the original, for free, and renders its own prompt from it.
- Use-case-specific prompts. The builder, subagents and other agents all render from the same event types, with different presets: which notices to include, whether to pull in codebase context, how to frame inherited history.

## Streaming: partial events and field deltas

Events are immutable and whole, but an LLM answer arrives as a stream of small chunks, and users want to watch it appear. Waiting for a content event to be complete before showing anything would make the agent feel frozen. So the Trajectory System has a side channel for events that don't exist yet: *partials*.

When the agent starts receiving, say, a thinking block from the model, it opens a partial: a PartialOpened carrying the event's initial fields and a unique id. As chunks stream in, it emits PartialDeltas—small edits to one field of that open partial, typically "append this text". Each delta is pushed live to the browser, which applies it to its in-flight copy and renders the text word by word. When the model finishes the block, the agent appends the real, complete thinking event to the trajectory, carrying the same unique id. That append *closes* the partial: anyone who was following the deltas now knows which persisted event they add up to, and swaps their in-flight copy for the authoritative one.

Partials are not events. They have no position in the trajectory, no parent, and are never persisted; only the final events are. That keeps the write path simple—there is nothing to discard or compact when a stream ends—and it keeps live reads honest. A browser that connects halfway through a response gets the persisted events so far plus each open partial materialized once, with its deltas already folded in, and then follows the live stream from there. Reconnects, refreshes and multiple tabs all converge on the same trajectory, because the trajectory is the only thing that was ever written.

## Two logs per agent: the inbox and the agent trajectory

Here's a detail that turned out to matter a lot: each agent actually has *two* event logs.

1. The inbox. Everything sent *to* the agent lands here: the user's UserMessages, and ExternalAgentNotifications from other agents or from the control plane—a subagent finishing, a message from another agent, a scheduled wake-up.
2. The agent trajectory. The agent's actual running history: what it thought, said, and did.

Whenever the agent runs, it looks at the inbox and copies whatever hasn't been handled yet onto its own trajectory. This happens at the start of a run and again at every iteration boundary, so a message that arrives mid-response is picked up as an interjection instead of waiting for the whole response to finish.

The split means external arrival is decoupled from execution. Messages can pile up in the inbox at any time, from anywhere—other actors only ever write to the inbox, never to the agent trajectory—but the agent admits them into its own history on its own schedule, at well-defined points. The trajectory stays a clean, ordered record of what the agent actually processed.

## The Agent Control Plane

The Trajectory System gives every agent a durable, forkable history. It says nothing about *when* an agent runs, *where* it runs, or how one agent's work reaches another. Once you have more than one agent, those questions arrive all at once. A builder spawns explore subagents and needs their results back. A background fork should run when a session goes quiet. One agent wants to hand work to several others and hear how they're getting on. A scheduled task needs to wake an agent that nobody has talked to for days. Every one of these is a slightly different orchestration pattern—parent and child, fan-out and fan-in, fire-and-forget, timers—and none of them belong inside the agent loop itself.

That's the job of the Agent Control Plane, or ACP: the orchestration layer that sits on top of the Trajectory System. It knows which agents exist and which trajectory each one runs on. It creates new agents—either from scratch on a fresh trajectory, or as a fork of an existing one. It delivers messages between agents. And it decides when an agent should be running, by issuing *activations*: wake-up signals that any node in our fleet can pick up to boot the agent with its trajectory and run it.

The reason it exists as a separate layer is that it collapses all of those patterns into one primitive. Every interaction between agents—a spawn, a result, a progress report, a hand-off from one agent to another, a scheduled wake-up—is the same two steps: append a message to the recipient's inbox, then send an activation. Agents don't call each other, hold connections to each other, or know which machine the other is on. They write to an inbox and let ACP handle the rest. Its surface is correspondingly small: SpawnAgent, ForkAndSendMessage, SendMessage, NotifyParents, StopAgent, and a few reads.

Here is one of those patterns end to end—a subagent reporting back to the builder that spawned it:

1. The subagent reaches AgentDone.
2. Its completion envelope calls NotifyParents, which builds an ExternalAgentNotification with the result and a terminal status (Completed, Failed or Cancelled).
3. SendMessage appends it to the parent's inbox. This write is the durable part: the message is now safe whatever happens next.
4. ACP sends an activation. If the parent is asleep, a node picks the activation up and starts a run that finds the notification in the inbox. If the parent is already running, its next iteration boundary picks the notification up itself, and the activation doesn't start a second run: a run claims its activation, and each trajectory has a single exclusive writer, so a duplicate is simply acknowledged and dropped—the inbox already holds the message.

The point is the decoupling. The sender appends to an inbox and is done; the receiver reads its inbox whenever it next runs. The wake-up is a signal, not the payload, and the inbox is the source of truth—so a message to a busy agent and a message to an idle one are the same write, and neither side ever has to coordinate with the other.

## Suspend and resume: agents that survive deploys

One of the neatest consequences of this architecture is how easily we can suspend and resume agents.

At any *iteration boundary*—the moment after an IterationEnd is written—the event history on the trajectory is everything the next iteration needs. So at that boundary the loop can simply stop: the run exits as suspended, no AgentDone is written, and the still-open agent block is itself the signal that there is work to continue. ACP then sends a fresh activation with a resume reason. Any node in the fleet can pick it up, check the inbox, and run the next iteration as if nothing had happened.

This decouples long-running agents from our deployment infrastructure. When a fleet is restarting, each node stops accepting new activations and lets its in-flight runs drain: most turns simply finish. A turn that is still going suspends at its next boundary instead, and resumes on a freshly deployed node. The same path kicks in when a sandbox dies mid-turn: the failing tool call ends the iteration, and the run suspends at that boundary rather than retrying blind. We keep shipping updates all day without ever having to kill a long-running run.

Our sandbox infrastructure makes this cleaner. Sandboxes run separately from the agent fleet, and the durable state of a project is its repository, not the box. A resumed agent reattaches to its sandbox, or rebuilds one from the repo. The agent process is genuinely disposable; the work lives in the trajectory and in git.

## Putting it all together: Chats

Lovable has always been a place where you go into a project and talk to the builder agent in that project. With Chats, we're launching something different: an agent you can chat with freely, outside any single project.

The chat agent behind Chats runs at the workspace level, on its own trajectory scoped to the workspace rather than to a project. It uses a different model setup, tuned for fast, open-ended conversation rather than for building, and it has access to all of your projects. You can ask it to jump into a project and build something there, or to update several projects in parallel.

This is where Chats leverages the primitives we've described: it is built almost entirely out of trajectories, inboxes and activations. When the chat agent decides a project should do some work, it calls a send_message_to_project tool. Under the hood that is just SendMessage: an ExternalAgentNotification carrying the instructions and any attachments is appended to that project's inbox, and an activation wakes the builder, which picks up its own trajectory exactly where it left off—or, if it's already busy, folds the message in at its next iteration boundary. The chat agent is responsible for writing a self-contained message—it can read a project's chat and state to decide what to say, but the builder only ever sees the message it was sent. Because both trajectories live in the same system, the reverse is also possible: a project agent could read the chat thread for extra context. Today we keep that responsibility on the chat side.

Progress flows back over the same primitive. When a builder posts a progress update mid-turn, ACP notifies its parent: the update is appended to the chat agent's inbox as an ExternalAgentNotification. When the builder's turn closes, one more notification carries the terminal status and a result summary—credits used, files changed, build status. It's the same NotifyParents primitive a subagent uses to report to the agent that spawned it, pointed back upstream. Progress reporting isn't a separate channel—it's one more message landing in an inbox.

## Why we built it this way

### Why an event log instead of snapshotting state?

A snapshot commits you to one representation of "the conversation". The log lets every consumer derive its own: the builder's prompt, the chat agent's prompt, the browser's rendering, a replay for evals—all from the same events. Compaction, reverts and interjections become appends rather than rewrites, so you can always answer "what did the agent actually see when it made that call". Debugging and evaluation need that raw record; once you snapshot, the details that explain a bad turn are gone. The cost is that reads are a walk, not a lookup. We pay that back with storage layout, caching, and compaction as a projection.

### Why two logs—inbox and agent trajectory—instead of one?

Writers and the agent have different needs. Other actors need a place they can append to at any time without contending with the running agent; the agent needs a history only it writes, in an order it controls. At each iteration boundary, the agent copies pending inbox messages into its trajectory before rebuilding the prompt. It also keeps the agent trajectory replayable: every prompt the model saw can be rebuilt from that one log, because external input only enters it at well-defined points. And it decouples the wake-up from the payload. Losing or duplicating an activation is harmless because the inbox is the source of truth, which is what makes crash recovery and re-drives safe.

### Why model it on Git rather than a plain linear log?

A parent pointer per event makes forking free: a fork is a new head pointing at an existing event, no copying, and the forked agent's prompt builder walks straight through the parent's history. Named heads give each line of work an identity—agent history, inbox, subagents and forks are all just heads in the same store, so one storage layer and one read path serve every kind of agent. And the mental model was worth borrowing. Engineers already reason about commits, branches and heads; we didn't have to invent vocabulary.

## Under the hood

A couple of implementation details for the curious.

Storage. Trajectories are stored in Bigtable as a sequence of events. Row keys are designed so that one trajectory's events sit next to each other, which makes reading history—exactly what constructing context is—a fast scan rather than a hop per event. Large payloads such as knowledge files and file changes are offloaded to a content-addressed blob store so the events themselves stay small.

Activations. Every wake-up is recorded in an activation log and published over Pub/Sub, ordered per agent, so any node in the fleet can pick it up, boot the agent with its trajectory, and run it. A periodic reconciler re-drives activations that were published but never resolved, so a lost wake-up is a delay, not a lost message.

For the agent loop itself and how the underlying LLM calls are routed across providers, see our earlier post [Routing Billions of Tokens per Minute](https://lovable.dev/blog/routing-billions-of-tokens-per-minute).

## Wrapping up

Next time you use Chats to turn an idea into a change to your app, you'll have a clearer picture of what's happening underneath: how agents pass work to each other, keep track of context, and bring results back into your conversation.

If you're building an agentic product yourself, these are design choices you can borrow. Your agents will have different jobs, but you'll face many of the same questions about what they need to know and how they work together. We've shared our approach so you have something concrete to build on.

[Try Chats](https://lovable.dev/chats/new) and see it in action.
