Avagenc Chat
2026 — A group chat where every other member is an AI agent
01 Problem
An AI assistant that plays music, reads email, and runs the smart home is usually one agent holding every tool, or several agents hidden behind a router. The router picks one agent, that agent answers, and whatever it saw is thrown away. When a request needs two of them, they pass each other a summary, and the person's own words are lost on the way.
The person never sees any of it. They talk to one voice, cannot tell which agent did what, and cannot ask a specialist directly.
02 Solution
Avagenc Chat is a group chat instead. Each person has exactly one thread, and they, Ava, and every specialist write into it and read from it. Ava listens and hands work on, to Zee for the Tuya smart home, Yori for Spotify, and Rafal for Gmail, Calendar, and Contacts.
So Ava never restates a request when she hands it on. Her whole turn is "@zee", and Zee reads the person's own words a few messages up. The thread is the only state there is, and a run is just the thread plus whatever woke it. That thread is the agents' episodic memory, everything that was said, in order.
That puts the weight on the model, because every turn rereads a long thread the agent mostly did not write. Of the models tried, Gemini kept using that context better than the OpenAI, Qwen, and Kimi models, so it runs every agent.
03 Multi-agent routing
A message with no mention goes to Ava. A message that mentions exactly one agent goes straight to that agent, so "@rafal any email today?" never passes through Ava. Mentioning two or more goes back to Ava, because work across specialists needs someone to order it.
Every turn records who triggered it, whether the human, Ava handing work on, or Postera, the library that keeps Ava's reminders, waking her when one is due. The same specialist answers a human who tagged it, but skips the greeting when Ava only passed the work on.
Silence is a valid answer. When a specialist has already replied in the thread, Ava's right turn is empty, and the server returns it as a success instead of an error.

if event.IsFinalResponse() {
var resp strings.Builder
if event.Content != nil {
for _, p := range event.Content.Parts {
resp.WriteString(p.Text)
}
}
apihttp.WriteJSON(w, http.StatusOK, struct {
Response string `json:"response"`
}{resp.String()})
return
}04 Instruction layers
Routing decides who runs, but each agent ships with its own instruction and knows nothing about this chat. So the app tells it where it is, through one field every agent has, AdditionalInstruction, placed beneath the agent's own instruction.
That text runs from what never changes to what changes on every run. It starts with where the agent is and how the group treats each other, then the channel, then the rules for a lead or a specialist, then what woke it, and last how to read the messages and the time where the person is. The Agent Development Kit, the Google library that runs each agent's loop, fills the parts that change from the session on every run. The diagram below shows the layers for Ava.
Ava's "@zee" comes from these layers. Her message to a specialist is exactly the tag, not "yori?", and not "@zee nyalain lampu kamar ya", which is Indonesian for turn on the bedroom light. Getting there took three fixes on one day, 5 July 2026. She passed "yori?" as the message, copying the person. She wrote "@zee" as a plain reply, believing the text itself would wake Zee, when only the tool call does. She ended a turn with a second "@zee", which would only call Zee again. Each was fixed with a sentence in her instruction, not with code.
The channel layer decides whether she stays silent. In the chat, the person already sees Zee's reply on the screen, so her final reply is empty. She speaks only when she adds something, like putting together what several specialists found, a judgment of her own, or warmth at the right moment.
On a voice call only her voice is heard. Zee's reply still goes into the chat, but the person is not looking at it, so an empty reply would be silence and they would think the line dropped. There she always gives the result herself, in one or two sentences, without lists, markdown, or the brackets that text-to-speech cannot read.
Every reply on a call carries at least one emotion tag at the end of a sentence, chosen by her from a fixed list of twenty like #relieved# or #smug#. The tag is never read aloud, and it adds a short pause and colours her voice. When the person says goodbye, she says goodbye back and ends with #end#, which hangs up.
The silence rule first applied everywhere. On 19 July 2026 it moved into a layer per channel, because on a call it swallowed the specialist's answer. The code below swaps the voice rules in for a call.
runEvents := h.runner.Run(
ctx,
userID,
sessID,
msg,
adkagent.RunConfig{},
runner.WithStateDelta(map[string]any{
agent.ChannelInstructionDeltaKey: voiceChannelInstruction,
agent.RunInstructionDeltaKey: "",
}),
)05 Semantic memory
The second kind of memory is semantic, what the agents have learned about the person rather than what was said. Zep reads the thread and keeps a knowledge graph of the person on its own, made of people, objects, and preferences joined by facts that know when they became true. Nobody writes to the graph by hand.
In testing, a passing remark about a favourite food became a Preference node tied to the human and to Ava, and a message whose run failed still taught the graph a new hobby, because the graph learns from the thread, not from the reply.
The web app draws the whole graph, and resetting it is one action that forgets the chat and the knowledge together.

06 Prospective memory
The third kind is prospective memory, remembering to do something later, which agent frameworks usually skip. Through Postera, my open source Go library, Ava wakes herself up at the moment she chooses and acts on her own. She writes a message to her future self, a posterum, and Cloud Tasks delivers it back into the same thread at that moment.
Ava then speaks first. In a real thread, a reminder asked for at 11:39 came back on its own at 11:44.
She can also keep going. Told about picking up Nay at five, she sets a reminder for 4:30. When nobody answers it, she sets the next one for 4:45, then one for 5:00, then a last one for 5:10, and she cancels that last one once the human says they are on the way.
Nothing waits in memory meanwhile. The server scales to zero, so the process that scheduled a reminder is usually gone when it fires. The reminder lives in Postgres and the queue.
When a note wakes Ava, she is told it comes from her past self and not from the person. A relative time in it, like in five minutes, counts from when she wrote it. She acts at once when it is clearly wanted and low in risk, and otherwise asks briefly. Her reply arrives as a notification, so its first line has to make sense in the preview.
Ava, 09:15
Writes the posterum
"Remind the human to pick up Nay at 17:00"
Postarius
Enqueues it on Cloud Tasks
Saves it to Postgres
Cloud Tasks
Waits until 16:30
16:30
POST /ava/awaken
Hands the posterum back to Ava
Ava, 16:30
"it's 4:30, half an hour until you pick up Nay"
type Store interface {
Save(ctx context.Context, p Posterum) error
Get(ctx context.Context, id string) (Posterum, error)
Remove(ctx context.Context, id string) error
List(ctx context.Context, q Query) ([]Posterum, error)
}
type Queue interface {
Enqueue(ctx context.Context, p Posterum) error
Cancel(ctx context.Context, id string) error
}07 Paying per token
There is no subscription. Every run is charged to the person's rupiah wallet by the tokens it used, after it ends, because nobody knows the cost before.
The wallet is a double-entry ledger. Every charge is a set of postings that sum to zero, amounts are whole micro-rupiah, and a balance is never stored, only summed. A run that fails halfway is still charged for what it burned.

if _, err := b.ledger.Transact(context.WithoutCancel(ctx), wallet.Spec{
Type: TxAgentRun,
Metadata: metadata,
Postings: []wallet.Posting{
{AccountID: wallet.UserAccountID(userID, chargeCurrency), Amount: -micros},
{AccountID: wallet.RevenueAccountID(chargeCurrency), Amount: micros},
},
}); err != nil {
return fmt.Errorf("charge %s run: %w", run.Agent, err)
}08 Web and iOS
The web app is a SvelteKit single-page app and the iOS app is native SwiftUI, both signing in with Google through Firebase. The server derives the thread from the signed-in person, so the same conversation follows them from one device to the other. There is also an Android app, which I did not work on.
Neither streams. A message waits for the whole run to finish while the client polls the thread, so a specialist's answer appears the moment it is written, before Ava's own turn returns.
09 System design
Every client calls one backend, the chat API, a single Go program on Cloud Run that scales to zero when nobody is talking. The web app, the iOS app, and the Android app sign in with Google through Firebase Authentication. A voice chat speaker from a partner and Cloud Tasks waking Ava each prove who they are with their own API key instead, so the agents are never tied to one app.
Inside, the API is split by domain rather than by layer. Identity knows who is calling. Agent runs the agent modules on Gemini and bills every run to Wallet, whose guard also refuses a run when the balance is empty. Each agent is its own module at go.avagenc.com and reaches only its own integration. Zee controls the smart home on Tuya Cloud, Yori plays music on Spotify, and Rafal works in Gmail, Calendar, and Contacts, while Ava sets the reminders. Integration Linking keeps the tokens each person grants to those integrations in Firestore. Session, Knowledge, and Postera hold the three kinds of memory, the thread and the knowledge graph in Zep and the reminders in their own Neon Postgres, queued on Cloud Tasks.
The diagram draws which domain depends on which service, never the path of a request. An agent decides on its own whether to hand work on, to whom, and how often, so any single path drawn would be one sample that most runs never take.
A push to dev or main deploys to its own environment. GitHub Actions builds the image, runs the Goose migrations of the wallet, and rolls out the new revision.
10 Module design
The API went from microservices by function, to microservices by domain, to one monolith, before it settled as a modular monolith. Each domain keeps its own boundary as a microservice would, yet everything ships as one program with one pipeline, and the domains call each other in process instead of over the network.
The code is split by who it belongs to. The chat and its domains belong to this product. The agents live at go.avagenc.com, one module each, so Zee can join another Avagenc product, an assistant for a hotel for instance, without being written again. Everything that has nothing to do with Avagenc lives at go.trueardian.com as my open source libraries.
agentkit turns each service into tools for Google's Agent Development Kit and lets many agents share one Zep thread, aware of the time where the person is rather than where the server is. tuya, spotify, and gworkspace are clients any Go program can use, postera lets an AI agent wake itself at any moment in the future, and api reads who is calling and their time zone from every request.
11 Why it paused
Every integration works, and every one stops at the platform's gate before the public can use it. Spotify grants extended access only to organizations with 250,000 monthly users and plays music only for Premium accounts. Gmail's restricted scope needs a paid security assessment every year, and until then Google expires the sign-in after seven days. Tuya links a person's home only through a cloud partnership.
None of that is a code problem, so the app is paused with no plan to continue. In the development environment, Zee now tells the human that the Tuya IoT Core subscription has expired.
Avagenc Chat
Four agents share one thread on web and iOS, paused by what Spotify, Google, and Tuya require