Skip to content

Commit fac17e9

Browse files
authored
feat(create-agent-app): --chat template variant — the assembled multimodal chat app (#199)
Scaffolds the examples/chat-app.md assembly as a runnable Cloudflare Workers product: createAppAuth over drizzle/D1, createChatTables + createChatStore, createChatTurnRoutes with the config-driven sandbox producer lane, createUploadRoute (inline vs sandbox path split), the /interactions answer endpoints, a D1 migration the generated app's own e2e test executes for real, and a dependency-free dev chat page. The generated app ships tests/chat-turn.e2e.test.ts: fake sandbox producer -> POST turn -> NDJSON stream consumed -> user+assistant rows persisted with typed parts + usage receipt -> buffered replay. The repo-side gate (tests/create-agent-app-chat.test.ts) scaffolds into tmp, links the real dist offline, typechecks the generated app, and runs its suite — CI reds if the template ever drifts from the framework (#188 wave 3).
1 parent c44a60a commit fac17e9

28 files changed

Lines changed: 1836 additions & 39 deletions

create-agent-app/index.mjs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#!/usr/bin/env node
22
// create-agent-app — scaffold a new Tangle agent product on @tangle-network/agent-app.
33
//
4-
// Dependency-light by design: Node built-ins only. The CLI copies the `template/`
5-
// tree verbatim, substitutes a small set of `__TOKEN__` placeholders, and renames
4+
// Dependency-light by design: Node built-ins only. The CLI copies one template
5+
// tree verbatim (`template/` by default, `template-chat/` with `--chat`),
6+
// substitutes a small set of `__TOKEN__` placeholders, and renames
67
// files whose template name would otherwise interfere with tooling (a template's
78
// own `package.json` must not be read by the scaffolder's package manager; a
89
// template `gitignore` must not be applied to the scaffolder repo). The generated
@@ -17,7 +18,13 @@ import { dirname, join, resolve } from 'node:path'
1718
import { fileURLToPath } from 'node:url'
1819

1920
const HERE = dirname(fileURLToPath(import.meta.url))
20-
const TEMPLATE_DIR = join(HERE, 'template')
21+
// Template variants: the default tool-loop skeleton, and `--chat` — the
22+
// assembled multimodal chat vertical (auth + chat-store + chat-routes +
23+
// sandbox producer + uploads + replay) from `examples/chat-app.md`.
24+
const TEMPLATES = {
25+
default: join(HERE, 'template'),
26+
chat: join(HERE, 'template-chat'),
27+
}
2128

2229
// The agent-app version range the generated project depends on. Kept as a single
2330
// constant so a release bump touches one line.
@@ -39,6 +46,7 @@ function parseArgs(argv) {
3946
const a = argv[i]
4047
if (a === '--name') args.name = argv[++i]
4148
else if (a === '--agent-app-version') args.agentAppVersion = argv[++i]
49+
else if (a === '--chat') args.template = 'chat'
4250
else if (a === '--force') args.force = true
4351
else if (a === '-h' || a === '--help') args.help = true
4452
else if (!a.startsWith('-')) args._.push(a)
@@ -54,6 +62,10 @@ function usage() {
5462
'Scaffolds a new Tangle agent product on @tangle-network/agent-app.',
5563
'',
5664
'Options:',
65+
' --chat Scaffold the multimodal chat variant instead: the',
66+
' assembled chat vertical (auth, thread/message store,',
67+
' streaming turns + replay, uploads, agent asks) with',
68+
' its own end-to-end test. Default: the tool-loop skeleton.',
5769
' --name <name> Project name (default: the target dir basename).',
5870
' --agent-app-version <range> @tangle-network/agent-app version (default: ' + AGENT_APP_RANGE + ').',
5971
' --force Write into a non-empty directory.',
@@ -62,7 +74,7 @@ function usage() {
6274
'After scaffolding:',
6375
' cd <target-dir> && pnpm install',
6476
' pnpm typecheck && pnpm test',
65-
' # then follow CUSTOMIZE.md: fill agent.config.ts, seed knowledge/, run pnpm knowledge:ingest',
77+
' # then follow CUSTOMIZE.md (each template ships its own fill-checklist)',
6678
].join('\n')
6779
}
6880

@@ -110,6 +122,7 @@ async function main() {
110122
const projectName = args.name ?? targetDir.split(/[\\/]/).pop()
111123
const packageName = toPackageName(projectName)
112124
const agentAppVersion = args.agentAppVersion ?? AGENT_APP_RANGE
125+
const templateDir = TEMPLATES[args.template ?? 'default']
113126

114127
if (existsSync(targetDir)) {
115128
const entries = await readdir(targetDir).catch(() => [])
@@ -129,9 +142,9 @@ async function main() {
129142
AGENT_APP_VERSION: agentAppVersion,
130143
}
131144

132-
const files = await walk(TEMPLATE_DIR)
145+
const files = await walk(templateDir)
133146
for (const rel of files) {
134-
const src = join(TEMPLATE_DIR, rel)
147+
const src = join(templateDir, rel)
135148
// Resolve any renamed path segments (only basenames are renamed).
136149
const parts = rel.split(/[\\/]/)
137150
const baseName = parts[parts.length - 1]

create-agent-app/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@tangle-network/create-agent-app",
33
"version": "0.1.0",
4-
"description": "Scaffold a new Tangle agent product on @tangle-network/agent-app: a filled agent.config skeleton, a wired chat route over the Cloudflare preset, an empty knowledge/ dir, and the agent-followable breadcrumb docs (AGENTS.md / CUSTOMIZE.md / KNOWLEDGE.md).",
4+
"description": "Scaffold a new Tangle agent product on @tangle-network/agent-app: the tool-loop skeleton (default) or, with --chat, the assembled multimodal chat vertical (auth, chat store, streaming turns with replay, uploads, agent asks) with its own end-to-end test — plus the agent-followable breadcrumb docs (AGENTS.md / CUSTOMIZE.md).",
55
"keywords": [
66
"tangle",
77
"ai-agent",
@@ -22,7 +22,8 @@
2222
},
2323
"files": [
2424
"index.mjs",
25-
"template"
25+
"template",
26+
"template-chat"
2627
],
2728
"publishConfig": {
2829
"access": "public"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copy to .dev.vars for local development (never commit .dev.vars).
2+
# Production: `wrangler secret put <NAME>` for each secret.
3+
4+
# better-auth HMAC secret — generate one: openssl rand -base64 32
5+
BETTER_AUTH_SECRET=REPLACE_WITH_RANDOM_SECRET
6+
7+
# Tangle Router key the harness bills model calls against.
8+
TANGLE_API_KEY=
9+
# Optional router override; omit for the platform default.
10+
# TANGLE_ROUTER_URL=
11+
12+
# Sandbox gateway credentials — the agent runs in a Tangle sandbox. Without
13+
# these every turn fails loud (there is no mock agent).
14+
SANDBOX_API_KEY=
15+
SANDBOX_GATEWAY_URL=
16+
17+
# Model override without a redeploy (falls back to config.model.default).
18+
# MODEL_NAME=
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# AGENTS.md — you are customizing a chat agent-app
2+
3+
You are a coding agent working in a project generated by `create-agent-app --chat`.
4+
This project is a thin customization layer on top of `@tangle-network/agent-app`
5+
(the shell): the whole server chat vertical — auth, thread/message persistence,
6+
streaming turns with buffered replay, multimodal uploads, human-in-the-loop asks —
7+
is ASSEMBLED from shell factories, not written here. Walk this contract before you
8+
touch anything. It is a checklist, not prose — follow it in order.
9+
10+
## 0. Orient
11+
12+
- [ ] Read this file end to end.
13+
- [ ] Read `CUSTOMIZE.md` — the ordered fill-checklist. It is your task list.
14+
- [ ] Run `pnpm install && pnpm typecheck && pnpm test`. Confirm green BEFORE editing.
15+
The test suite includes the end-to-end turn gate (fake sandbox producer →
16+
streamed turn → persisted transcript) — it proves the assembly, not stubs.
17+
18+
## 1. The one rule (layering)
19+
20+
> You change behavior by editing DATA and the COMPOSER — never by editing
21+
> `@tangle-network/agent-app`.
22+
23+
- The shell owns mechanism: turn protocol + hook order (`createChatTurnRoutes`
24+
over agent-runtime's `handleChatTurn`), persistence columns (`createChatTables`),
25+
the upload inline-vs-sandbox split, the interaction answer contract, auth guards.
26+
- You own: `agent.config.ts` + `prompts/system.md` (DATA), `src/` (the COMPOSER +
27+
routes), `migrations/` (must mirror the schema — the e2e test executes it),
28+
`public/` (the dev page). That is the whole surface.
29+
30+
## 2. DATA vs CODE — know which file you're in
31+
32+
- [ ] DATA → `agent.config.ts`: name, system prompt, model default + effort,
33+
harness, renderable ask kinds. Plain values. If you're writing an `if`,
34+
you're in the wrong file.
35+
- [ ] DATA → `prompts/system.md`: the persona. State intents and hard rules,
36+
never implementations — no shell commands, CLI flags, or install scripts.
37+
The executing agent chooses tools at execution time.
38+
- [ ] CODE → `src/chat.ts`: the COMPOSER. Wires config + env into the shell's
39+
factories. Extend seams here (billing hooks, `transformFinalText`,
40+
`onTurnComplete`); never re-implement what a factory already does.
41+
- [ ] CODE → `src/sandbox.ts`: the sandbox lane (box naming, credentials,
42+
profile). All agent intelligence lives IN the sandbox; this file only
43+
reaches it.
44+
- [ ] CODE → `src/worker.ts`: routing only. New endpoint = new handler in
45+
`src/chat.ts`, one `if` here.
46+
47+
## 3. Invariants — fail-closed, never relax
48+
49+
- [ ] TRUSTED CONTEXT: `userId`/`workspaceId` come from the better-auth SESSION,
50+
never from a request body or model output. The `authorize` seam is the only
51+
place identity is established — keep it that way.
52+
- [ ] THREAD ACCESS: an inaccessible thread reads as 404, indistinguishable from
53+
a missing one. A cross-workspace probe must not learn the thread exists.
54+
- [ ] NO MOCK AGENT: without sandbox credentials a turn fails loud with a clear
55+
error. Never add a canned-response fallback — a fake answer is worse than
56+
an honest failure.
57+
- [ ] AGENT-NATIVE: intelligence and tooling live in the sandboxed agent;
58+
durability (rows, turn buffer) and money live here. If you're about to
59+
parse the agent's prose for data, stop — that's a schema-validated tool's
60+
job (see the shell's `/tools`).
61+
- [ ] GROUNDING: the persona's fabrication rule stays. A real record or an
62+
explicit "NOT ON FILE".
63+
64+
## 4. Verify (every change ends here)
65+
66+
- [ ] `pnpm typecheck` — clean. Proves the assembly matches the shell contract.
67+
- [ ] `pnpm test` — green. The e2e gate runs the REAL migration + REAL factories
68+
with one fake (the sandbox event feed). If it fails, the app drifted from
69+
the framework — fix the drift, not the test.
70+
- [ ] For a real deploy: fill `wrangler.toml` + `.dev.vars`, run
71+
`pnpm db:migrate:local`, then `pnpm dev` and exercise the dev page.
72+
73+
If any of these is red, you are not done. Do not weaken a test to pass.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# CLAUDE.md
2+
3+
The behavior contract for this project lives in `AGENTS.md`. Read it first, then
4+
walk `CUSTOMIZE.md` (the fill-checklist).
5+
6+
@AGENTS.md
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# CUSTOMIZE.md — fill this project, in order
2+
3+
This is the trail. Walk it top to bottom. Each step is a checklist item paired
4+
with the DISCOVERY QUESTION it answers — answer the question, then make the
5+
edit. The whole job is filling `agent.config.ts` + `prompts/system.md` (DATA)
6+
and wiring credentials; the chat vertical itself is already assembled and
7+
proven by the e2e test.
8+
9+
When every box is checked and `pnpm typecheck && pnpm test` are green and a
10+
message round-trips on `pnpm dev`, the app is customized.
11+
12+
---
13+
14+
## ① Identity — `agent.config.ts` + `prompts/system.md`
15+
16+
Discovery: **Whose job does this agent do, in whose voice, under what hard rules?**
17+
18+
- [ ] Set `name` in `agent.config.ts` to the product/agent name.
19+
- [ ] Rewrite `prompts/system.md` as the real persona: role + voice + remit +
20+
hard rules. State intents, never implementations (no commands, no tool
21+
scripts — the sandboxed agent picks its own tools).
22+
- [ ] Keep the grounding rule (never fabricate; cite a record or NOT ON FILE).
23+
24+
## ② Model + harness — `agent.config.ts``model`, `harness`
25+
26+
Discovery: **Which model answers by default, at what effort, on which harness?**
27+
28+
- [ ] Set `model.default` to a model your Tangle Router key can reach.
29+
- [ ] Pick `harness` (`opencode` default; vendor-locked harnesses like
30+
`claude-code` must pair with their own provider's models).
31+
- [ ] Leave per-turn overrides alone — the client can send `model`/`effort`
32+
per request and `MODEL_NAME` overrides without a redeploy.
33+
34+
## ③ Infrastructure — `wrangler.toml` + `.dev.vars` + `migrations/`
35+
36+
Discovery: **Where does this app live and what may it spend?**
37+
38+
- [ ] `wrangler d1 create <name>` → paste `database_id` into `wrangler.toml`.
39+
- [ ] Copy `.dev.vars.example``.dev.vars`; fill `BETTER_AUTH_SECRET`,
40+
`TANGLE_API_KEY`, `SANDBOX_API_KEY`, `SANDBOX_GATEWAY_URL`.
41+
- [ ] `pnpm db:migrate:local` — applies `migrations/0001_init.sql` (auth +
42+
chat + turn buffer). The e2e test executes this same file, so it cannot
43+
silently drift from the schema.
44+
- [ ] R2 stays commented out unless the product stores artifacts.
45+
46+
## ④ Prove the loop — `pnpm dev`
47+
48+
Discovery: **Does a real message round-trip through a real box?**
49+
50+
- [ ] `pnpm dev`, open http://localhost:8787, sign up, send a message.
51+
- [ ] Confirm: streamed text renders live, the transcript reloads with parts
52+
and a usage receipt, and a second turn continues the same agent session.
53+
- [ ] Kill the tab mid-turn, reopen the thread — the persisted row is intact
54+
(the turn keeps running server-side and buffers for replay).
55+
56+
## ⑤ The product UI — replace the dev page
57+
58+
Discovery: **What does the real surface look like?**
59+
60+
- [ ] `public/index.html` is the dev harness, not the product. Build the real
61+
surface in React on `@tangle-network/agent-app/web-react`:
62+
`ChatComposer` (`onSendParts` + `onAttach`), `ChatMessages`,
63+
`streamChatTurn` (start + resume against `/api/chat/replay/:turnId`),
64+
`useChatInteractions` + `InteractionQuestionCard`/`InteractionPlanCard`
65+
for the ask channel this server already exposes.
66+
- [ ] Keep the wire contract: `chatTurnRequestInit` from web-react serializes
67+
exactly what `createChatTurnRoutes` parses.
68+
69+
## ⑥ Extend, don't fork
70+
71+
Discovery: **What does this product add beyond the vertical?**
72+
73+
- [ ] Billing/audit/titles → `onTurnComplete` seam in `src/chat.ts`.
74+
- [ ] PII scrubbing before persistence → `transformFinalText` +
75+
`@tangle-network/agent-app/redact`.
76+
- [ ] Structured agent writes (proposals, records) → the shell's `/tools`
77+
side channel, never prose parsing.
78+
- [ ] Multi-user teams → `@tangle-network/agent-app/teams` and pass your
79+
workspace table to `createChatTables({ workspaceTable })` (new migration).
80+
81+
## ⑦ Verify
82+
83+
Discovery: **Does the customized app hold its contract?**
84+
85+
- [ ] `pnpm typecheck` — clean.
86+
- [ ] `pnpm test` — green (the e2e turn gate + fail-closed auth).
87+
- [ ] `pnpm dev` — a real turn round-trips end to end.
88+
- [ ] `pnpm deploy` + `pnpm db:migrate` when it's real.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# __PROJECT_NAME__
2+
3+
A multimodal chat agent product scaffolded with `create-agent-app --chat`, built
4+
on [`@tangle-network/agent-app`](https://github.com/tangle-network/agent-app):
5+
the whole server chat vertical — better-auth sessions, thread/message
6+
persistence with typed parts + usage receipts, streaming turns with buffered
7+
replay, file uploads, human-in-the-loop asks — assembled from shell factories.
8+
The agent itself runs in a Tangle sandbox (a full harness: skills, tools, bash,
9+
MCP); this app coordinates UI, durability, and access around it.
10+
11+
## Layout
12+
13+
| Path | Surface | Edit when |
14+
|---|---|---|
15+
| `agent.config.ts` | DATA — name, model default, harness, ask kinds | defining the agent |
16+
| `prompts/system.md` | DATA — the persona (imported as a Text module) | shaping behavior |
17+
| `src/chat.ts` | CODE — the composer (factories → the chat vertical) | extending seams |
18+
| `src/sandbox.ts` | CODE — the sandbox lane (boxes, credentials, profile) | provisioning changes |
19+
| `src/worker.ts` | CODE — HTTP routing only | adding an endpoint |
20+
| `src/db/schema.ts` | CODE — auth tables + `createChatTables()` | schema changes (+ migration) |
21+
| `migrations/` | SQL the e2e test executes for real | schema changes |
22+
| `public/index.html` | the dev chat page (not the product UI) | never — replace it (CUSTOMIZE ⑤) |
23+
| `tests/` | the e2e turn gate this app ships with | extending coverage |
24+
25+
## Get started
26+
27+
```bash
28+
pnpm install
29+
pnpm typecheck && pnpm test # green before you edit anything
30+
```
31+
32+
Then walk the trail:
33+
34+
1. `AGENTS.md` — the behavior contract (you are customizing a chat agent-app).
35+
2. `CUSTOMIZE.md` — the ordered fill-checklist, from persona to deploy.
36+
37+
## Scripts
38+
39+
- `pnpm dev` — run the worker + dev page locally (fill `wrangler.toml` + `.dev.vars` first).
40+
- `pnpm db:migrate:local` / `pnpm db:migrate` — apply `migrations/` to D1.
41+
- `pnpm typecheck``tsc --noEmit`.
42+
- `pnpm test` — vitest, including the end-to-end turn gate (fake sandbox
43+
producer → streamed turn → persisted transcript → replay).
44+
- `pnpm deploy``wrangler deploy`.
45+
46+
## Invariants
47+
48+
Identity comes from the session, never a request body. Inaccessible threads
49+
read as 404. No mock agent — missing sandbox credentials fail loud. See
50+
`AGENTS.md` for the full contract.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
dist/
3+
.wrangler/
4+
.dev.vars
5+
*.local
6+
.DS_Store
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "__PACKAGE_NAME__",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "wrangler dev",
8+
"deploy": "wrangler deploy",
9+
"db:migrate": "wrangler d1 migrations apply __PACKAGE_NAME__ --remote",
10+
"db:migrate:local": "wrangler d1 migrations apply __PACKAGE_NAME__ --local",
11+
"typecheck": "tsc --noEmit",
12+
"test": "vitest run",
13+
"test:watch": "vitest"
14+
},
15+
"dependencies": {
16+
"@tangle-network/agent-app": "__AGENT_APP_VERSION__",
17+
"@tangle-network/agent-interface": "^0.15.0",
18+
"@tangle-network/agent-runtime": "^0.79.3",
19+
"@tangle-network/sandbox": "^0.10.5",
20+
"better-auth": "^1.6.16",
21+
"drizzle-orm": "^0.45.2"
22+
},
23+
"peerDependencies": {
24+
"@tangle-network/agent-eval": ">=0.100.0",
25+
"@tangle-network/agent-integrations": ">=0.44.0"
26+
},
27+
"devDependencies": {
28+
"@cloudflare/workers-types": "^4.20250620.0",
29+
"@tangle-network/agent-eval": "^0.100.0",
30+
"@tangle-network/agent-integrations": "^0.44.0",
31+
"@types/better-sqlite3": "^7.6.13",
32+
"@types/node": "^25.6.0",
33+
"better-sqlite3": "^12.10.0",
34+
"typescript": "^5.7.0",
35+
"vitest": "^3.0.0",
36+
"wrangler": "^4.0.0"
37+
}
38+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ESNext",
5+
"moduleResolution": "Bundler",
6+
"lib": ["ES2022"],
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"noUncheckedIndexedAccess": true,
12+
"noEmit": true,
13+
"types": ["@cloudflare/workers-types/2023-07-01", "node"]
14+
},
15+
"include": ["agent.config.ts", "declarations.d.ts", "src", "tests", "vitest.config.ts"]
16+
}

0 commit comments

Comments
 (0)