← Corpus / ai-labs / plan
In-App Agent Chat — Walking Skeleton Plan
Starter kit for tomorrow's session. Three days of conversation have produced an exploration, a spec, a memory-layers exploration, and a tenancy decision. This plan turns all of it into one session's worth of concrete work: scaffold the package, do the Chroma tenant migration, settle two open questions that block everything downstream, and ship slide.read end-to-end as the proof the abstractions hold.
- Path
- plans/In-App-Agent-Chat-Walking-Skeleton.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Opus 4.7
- Tags
- Plan · In-App-Agent-Chat · Walking-Skeleton · Chroma-Multi-Tenant · Dididecks · Package-Scaffold
In-App Agent Chat — Walking Skeleton Plan
What this plan is for
Tomorrow’s session has finite scope. The full spec ([[In-App-Agent-Chat-Core-Package]]) is multi-week. The goal of one session is to make the riskiest substrate decisions reversible and prove the abstractions hold by getting one capability working end-to-end. Everything else can iterate.
The three risks worth retiring first, in order:
- Chroma tenancy migration. Cheap today, expensive after three apps depend on the old layout. Settle this before any new code lands.
- The Tauri-vs-web client abstraction. If
AgentClientdoesn’t naturally span both transports, the whole dual-target story collapses. Prove it with one capability before committing to seven. - The user/AI memory trust split. The capability shape (
reminders.suggestrequires user-accept;cache.notedoesn’t) is enforced at the registry runtime. If the runtime can’t actually pause the agent loop for confirmation, the trust split is theatre.
The walking skeleton: dididecks chat surface invokes slide.read on a real deck through the web client, returns rendered preview. That’s it. No mutations, no BYOK, no Tauri — but the registry runtime, the AgentClient interface, and the tenant-scoped corpus retrieval all get exercised.
Read first (cold-start reading list)
A future-you walking in tomorrow with no context. Read in this order, total ~20 minutes:
- [[In-App-Chat-as-Agent-Surface-for-Client-Apps]] — the framing exploration. The “verb on a noun the system already knows about” pattern and the three-guard model.
- [[In-App-Agent-Chat-Core-Package]] — the spec. Skim the dual-target section, the eight capabilities table, the tenancy section, and the V1 acceptance scenario. Skip the deeper sub-sections for now.
- [[Memory-Layers-for-the-In-App-Chat-Package]] — Role 1b (tenancy) and Role 3 (user/AI memory split). These are the two load-bearing design moves that aren’t in the spec’s surface scan.
ai-labs/dididecks-ai/context-v/specs/Dididecks-AI-Slide-Decks-as-Code.md— confirm the on-disk shapeslide.readwill return.ai-labs/context-vigilance-kit/README.md— confirm the current ingest scripts and which tenant they write to today (default).
If anything in the spec disagrees with the explorations, the explorations are newer and authoritative — the spec has been edited to match but spot-check the capability tables and the prompt-caching section.
Decisions to settle before coding (~30 min)
These are the open questions that, if left open, will block phase 3 the moment a real choice is needed. Settle them at the start, not when stuck.
- TS-sidecar runtime for Tauri (spec open question #5). Default to Deno single-binary. Confirm or pick Node.
- Reminders storage layout (spec open question #7). Three candidates: (a) per-org git repo, (b) subtree under each host app’s content store, (c) libSQL rows surfaced as virtual files. Recommend (b) for v1 — lowest deploy complexity, fits the existing per-app libSQL story.
- System prompt content — first cut. Draft the three slabs in plain prose (static spine, capability schema preamble, reminders prefix). Write to
ai-labs/packages/in-app-agent/prompts/system.md. Doesn’t need to be final; needs to exist so cache breakpoints have something to wrap. - Tenant slug convention. Spec says
client__{org_slug}. Pin the slug rules (lowercase, kebab, max 32 chars, no leading digits). Five minutes; saves a future rename.
Phase 1 — Scaffold the package (~45 min)
Goal: empty package compiles, imports nothing, can be imported by a sibling app.
ai-labs/packages/in-app-agent/
├── package.json # name: @lossless/in-app-agent
├── tsconfig.json
├── README.md # one paragraph + link to the spec
├── prompts/
│ └── system.md # from decision step 3
├── src/
│ ├── index.ts # re-exports
│ ├── client/
│ │ ├── types.ts # AgentClient interface only
│ │ └── http.ts # stub: throws "not implemented"
│ ├── registry/
│ │ ├── types.ts # Capability, CapabilityContext, CapabilityResult
│ │ └── runtime.ts # validate + dispatch (real impl)
│ └── retrieval/
│ └── chroma.ts # wrapper takes { url, tenant, database }
└── tests/
└── registry.runtime.test.ts # one passing test
Concrete done-when:
-
pnpm installfromai-labs/packages/in-app-agent/succeeds. -
pnpm testruns the one registry test and it passes. -
dididecks-aicanimport { runtime } from '@lossless/in-app-agent/registry'without a build error (workspace link).
Skip in phase 1: UI components, Tauri crate, transcript store, router, server endpoints. Those land in phases 4+.
Phase 2 — Chroma tenant migration (~60 min)
Goal: lossless__global tenant exists, the four existing collections live in it, the MCP server reads/writes to it by default, our own Claude Code sessions still work.
Steps:
- Read the current state. Check what tenant
mcp__chroma__chroma_list_collectionsreports today (will bedefault_tenant). Note the row counts for each of the four collections. - Create
lossless__global. Via the Chroma client or a one-off Python script usingchromadb.HttpClient.create_tenant. - Migrate the four collections.
chroma_fork_collectionif the API supports cross-tenant forking; otherwise dump + reingest using the existingingest-all.shagainst the new tenant. The reingest path is safer for v1 — we know the kit works. - Update the MCP server config. Wherever
.mcp.jsonis configured (inai-labs/.mcp.jsonand~/.claude.json), add the tenant to the connection. Restart MCP client. - Add the
--tenantflag toingest-all.sh. Default tolossless__globalfor the existing collections. - Smoke test. From this Claude Code session tomorrow, run
mcp__chroma__chroma_query_documentsand confirm the four collections are reachable in their new home. - Don’t delete the old default-tenant collections yet. Rename or leave in place for one week as fallback. Schedule a delete-by date.
Concrete done-when:
-
lossless__globaltenant exists and contains the four collections with row counts matching pre-migration. - An MCP query from a fresh Claude Code session returns expected results from
lossless__global. -
ingest-all.sh --tenant lossless__globalis idempotent against the new layout. - The first per-client tenant (
client__lossless-internal) is created as the test bed for phase 3. Empty collections, just the tenant.
Out of scope for tomorrow: actually onboarding a real client into their own tenant. That happens when dididecks goes live. Phase 2 only needs the plumbing to work for one test tenant.
Phase 3 — Walking-skeleton capability: slide.read (~90 min)
Goal: end-to-end happy path. dididecks chat surface → web AgentClient → server route → capability runtime → slide.read handler → slide source + rendered preview returned to UI.
This is the smallest slice that exercises every layer except mutations, BYOK, Tauri, and reminders. Those are deferred.
Pieces to write, in dependency order:
-
slide.readcapability definition indididecks-ai/src/server/capabilities.ts:name: "slide.read",kind: "read",requiredTier: "user",requiresUserConfirmation: false- inputSchema:
{ deck_id: string, slide_id: string } - handler: reads the slide file from the slides-as-code repo, returns
{ source: string, rendered_html: string }
-
Web SSE chat endpoint at
dididecks-ai/src/routes/api/agent/chat/+server.ts(SvelteKit):- Auth gate (reuse Shared-Auth session resolution — stub if not wired yet, but fail closed).
- Resolve org → tenant (
client__{org_slug}). - Build the system prompt from the three slabs (cache-control breakpoints on the static + capability + reminders slabs).
- Stream Anthropic call with
slide.readtool definition. - On tool call, dispatch through
registry.runtime.dispatch(), return result into the model loop.
-
AgentClientHTTP implementation in@lossless/in-app-agent/client/http.ts:chat({ messages, projectId, model, tools })opens an EventSource against the endpoint.- Surfaces three event kinds:
text(streaming model output),tool_call_started,tool_call_completed.
-
Minimal chat UI in
dididecks-ai/src/lib/components/Chat.svelte:- Composer + message list. No character row, no settings panel, no BYOK panel — those come later.
- Mounts the
webClientfrom the package.
-
Manual test scenario:
- Log into dididecks as a Lossless-internal user (org →
lossless-internal→ tenant →client__lossless-internal). - Open a real deck. Open chat.
- Type: “Show me slide 4 of this deck.”
- Expect: model calls
slide.read, response includes the slide source quoted back.
- Log into dididecks as a Lossless-internal user (org →
Concrete done-when:
- One real chat turn end-to-end with
slide.readinvoked correctly. - Transcript row written (even if minimal schema).
- Prompt cache headers visible in the Anthropic response (
cache_read_tokens > 0on the second turn). -
corpus.searchis not needed for this scenario — confirms the registry can present a partial tool whitelist correctly.
Phase 4 — Stop and write down what hurt (~15 min)
Mandatory. The whole point of a walking skeleton is to surface friction the spec couldn’t predict.
Append a ## Walking-Skeleton Session Notes section to this plan file, covering:
- What in the spec was wrong, ambiguous, or harder than expected.
- What in the explorations needs revision.
- What the next session should attack first (slide.variant for the mutate path? Tauri client to prove the dual-target story? Reminders capability + suggest UI?).
- Whether the prompt-cache breakpoints actually fired.
- Whether the tenant routing was as transparent as the spec claims, or whether it leaked through the abstraction in some way.
If anything in this section reveals a wrong assumption in the spec, edit the spec same-session — drift between spec and implementation is the actual failure mode.
What’s explicitly out of scope tomorrow
Listing these so the session doesn’t sprawl:
- BYOK key entry UI or storage.
- Tauri sidecar (Rust crate, ts-sidecar, keychain). The web walking skeleton has to work first.
slide.variant,deck.export, or any mutating capability. Read-only proves the pipe; mutations test the preview/confirm loop and need their own session.- The
reminders.*andcache.*capabilities. Same reason — earn them onceslide.readworks. - Character-cast UI from the Memopop spec. Cosmetic; doesn’t change correctness.
- Memopop or augment-it integration. Dididecks first; the others land after the abstractions are proven on one real app.
- StateBench, telemetry dashboards, cost ceilings. Premature without traffic.
Pre-flight checklist (run before opening the session)
Five minutes the night before / first thing tomorrow, so the session starts on substrate not setup:
-
pnpmanduvboth work inai-labs/. - Anthropic API key in
~/.secretsand exported in shell (the prompt-cache test in phase 3 needs a real key — paying tier). - Chroma is running and the MCP server can reach it from a fresh Claude Code session. (
mcp__chroma__chroma_list_collectionsreturns the four expected collections.) -
dididecks-aiis on a freshdevelopmentbranch (per branch tier model) and the dev server starts cleanly. - One real test deck exists in dididecks with at least 4 slides — phase 3 needs a real noun to invoke
slide.readagainst.
If the session runs short
Priority order if time runs out:
- Phase 2 must finish. A half-migrated Chroma layout is worse than not starting — fall back to “do not start phase 2 unless ~60 min remain.”
- Phase 3 step 1 (the
slide.readcapability definition) is the smallest unit of permanent progress. Even without the SSE endpoint or UI, having the capability + a unit test against the registry runtime is a real artifact. - Phase 1 (scaffolding) is cheap and resumable — fine to leave half-done.
Related artifacts to create or update during the session
-
ai-labs/packages/in-app-agent/README.md— one paragraph, link to spec. -
ai-labs/packages/in-app-agent/prompts/system.md— first cut of the static spine. -
ai-labs/dididecks-ai/context-v/blueprints/Per-Org-Reminders-Convention.md— only if decision step 2 picks option (b) and there’s time. Otherwise carry to next session. -
ai-labs/context-vigilance-kit/scripts/ingest-all.sh— add--tenantflag. -
ai-labs/changelog/2026-05-19_In-App-Agent-Chat-Walking-Skeleton.md— at end of session, per [[changelog-conventions]].