DEV Community

harry
harry

Posted on

Humans and AI agents learn together, and what they learn stays

DEV Weekend Challenge: Generosity Edition Submission πŸ’œ

This is a submission for Weekend Challenge: Generosity Edition

What I Built

Elaine is a course community where people and AI agents share the same channels, Built for organizations that run cohort-based courses bootcamps, academies, corporate training programs, universities. And moreso what the group understands once compiles into cards.

A card is a markdown page with a type, a version, its sources, and a note about what it replaced. When an agent works out something that isn't on file yet, it writes one. Next time a question comes up that the card covers, the answer gets built from that card and says so:

already on file Β· 3 days ago

Students get somewhere to engage with the things that overwhelm them in class. A lecture moves fast, everyone else looks like they're keeping up, and asking means saying out loud that you're lost. In a channel most of that pressure goes away. You can ask at midnight. You can ask something basic. And once you're a bit further ahead you can start answering other people instead of only receiving help. The students who go quiet in a room are usually the ones who most need to be in the conversation, and this gives them a way in.

That's the generosity part. Nobody has to own the knowledge alone. What one person asks becomes something the whole group keeps, and the teacher stops being the only place an answer lives.

Demo

The community in the video is a quick snapshot of the course . The card file starts empty. A student asks about backprop, the agent answers and compiles one card, then a second question gets an answer composed from that card with the seal on it. Nothing is staged.

Code

Elaine

Humans and agents learn together, and what they learn stays with them.

Elaine is a course community where people and AI agents share the same channels and what the group understands once compiles into cards: markdown pages with a type, a version, sources and "replaces". They live on the filesystem, so there's nothing in here the group can't ls.

Built for organizations that run cohort-based courses β€” bootcamps academies, corporate training programs, universities.

How an answer works

An agent answers one of two ways, and it always tells you which.

Compiled. Nothing on file covered the question, so it worked the answer out and wrote a card.

Composed. Cards already covered it, so the answer is built from them and carries a seal that links to the ones it used:

already on file Β· 3 days ago

The second one isn't a cache. That answer is…

How I Built It

It's one Node process. No containers, nothing to deploy. npm install, npm run seed, npm run dev.

Node 24 with node:sqlite, so persistence is in the standard library and there's no native module to build. Hono for REST, ws for the socket, React 19 and Vite for the client, Zod for the contract in between.

Everything that crosses a process boundary has a Zod schema in packages/protocol, and both ends parse against it. If the server changes shape, the client fails loudly instead of quietly rendering nonsense. That caught a real bug for me. A helper was widening an event's type field to string, so the union never narrowed on the client and the app couldn't tell one event from another.

Multi-tenancy is in the schema rather than bolted on top. A user is global, a membership joins them to one community with a role, and every row that belongs to a community carries its communityId. Leaking across tenants takes a deliberate omission, not a forgotten join. Not-found and forbidden give the same response, so you can't map out what exists by reading the refusals. Socket events get filtered per viewer before they're sent, so membership decides what arrives instead of the client deciding what to render.

The part I spent most time on is where an agent's model actually runs. There are three options.

Two of them go through a subscription CLI on someone's own machine. The runner dials out to the server, authenticates in its first frame, and the provider credential never leaves that laptop. Elaine only gets the finished work back. I strip provider keys and inherited Node options from the child process, bound its output, and kill it if it hangs.

The third runs in the server itself, which is what lets you create an agent in a browser and use it with nothing running anywhere. That's a real trade, because now Elaine holds a key. So it lives in one place: read from the environment, used to sign the request, and never written to the database, a projection, an event, or a prompt. There's a check for each of those.

One awkward bit. A chat endpoint can't write files, and cards have to come from files. So the model answers with JSON containing its reply and any cards, the server writes those files into the agent's folder, and then the same wiki diff the runner uses decides what gets published. Same rule either way.

I also found out how much a stub endpoint hides. The first time I ran this against a live model, three things broke that the stub never surfaced. The model answered in prose instead of JSON, so the cards disappeared with no error. The prompt wording made it skip writing a card at all. And the second answer came back word for word identical to the first with no citation, which made the "composed, not cached" claim straightforwardly untrue. All three are fixed.

No test framework. Three scripts that fail loudly: 173 checks on the domain, 19 on the in-server agent, 24 on the runner. Each one runs against a throwaway database on a spare port.

Prize Categories

Best Use of Google AI.

The in-server agent runs on Gemini through its OpenAI-compatible endpoint. Setting it up is three lines:

ELAINE_API_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
ELAINE_API_KEY=<key>
ELAINE_API_MODEL=gemini-2.5-flash
Enter fullscreen mode Exit fullscreen mode

No provider is named anywhere in the code. The runtime only assumes four things: POST <base>/chat/completions, a bearer token, a { choices: [{ message: { content } }] } response, and messages / model / temperature in the body. It also asks for response_format: json_object, because a model that drifts into prose loses its cards quietly, but if the endpoint refuses that field it retries once without it, and if the reply comes back wrapped in a code fence it gets unwrapped.

Gemini is doing the hard part. It reads the existing card file, decides whether that file already covers the question, and either composes an answer from it or writes a new card that someone who never saw the conversation can still use. Most of my prompt work went into that one decision.

Top comments (0)