Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions create-agent-app/index.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env node
// create-agent-app — scaffold a new Tangle agent product on @tangle-network/agent-app.
//
// Dependency-light by design: Node built-ins only. The CLI copies the `template/`
// tree verbatim, substitutes a small set of `__TOKEN__` placeholders, and renames
// Dependency-light by design: Node built-ins only. The CLI copies one template
// tree verbatim (`template/` by default, `template-chat/` with `--chat`),
// substitutes a small set of `__TOKEN__` placeholders, and renames
// files whose template name would otherwise interfere with tooling (a template's
// own `package.json` must not be read by the scaffolder's package manager; a
// template `gitignore` must not be applied to the scaffolder repo). The generated
Expand All @@ -17,7 +18,13 @@ import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

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

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

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

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

const files = await walk(TEMPLATE_DIR)
const files = await walk(templateDir)
for (const rel of files) {
const src = join(TEMPLATE_DIR, rel)
const src = join(templateDir, rel)
// Resolve any renamed path segments (only basenames are renamed).
const parts = rel.split(/[\\/]/)
const baseName = parts[parts.length - 1]
Expand Down
5 changes: 3 additions & 2 deletions create-agent-app/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tangle-network/create-agent-app",
"version": "0.1.0",
"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).",
"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).",
"keywords": [
"tangle",
"ai-agent",
Expand All @@ -22,7 +22,8 @@
},
"files": [
"index.mjs",
"template"
"template",
"template-chat"
],
"publishConfig": {
"access": "public"
Expand Down
18 changes: 18 additions & 0 deletions create-agent-app/template-chat/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copy to .dev.vars for local development (never commit .dev.vars).
# Production: `wrangler secret put <NAME>` for each secret.

# better-auth HMAC secret — generate one: openssl rand -base64 32
BETTER_AUTH_SECRET=REPLACE_WITH_RANDOM_SECRET

# Tangle Router key the harness bills model calls against.
TANGLE_API_KEY=
# Optional router override; omit for the platform default.
# TANGLE_ROUTER_URL=

# Sandbox gateway credentials — the agent runs in a Tangle sandbox. Without
# these every turn fails loud (there is no mock agent).
SANDBOX_API_KEY=
SANDBOX_GATEWAY_URL=

# Model override without a redeploy (falls back to config.model.default).
# MODEL_NAME=
73 changes: 73 additions & 0 deletions create-agent-app/template-chat/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AGENTS.md — you are customizing a chat agent-app

You are a coding agent working in a project generated by `create-agent-app --chat`.
This project is a thin customization layer on top of `@tangle-network/agent-app`
(the shell): the whole server chat vertical — auth, thread/message persistence,
streaming turns with buffered replay, multimodal uploads, human-in-the-loop asks —
is ASSEMBLED from shell factories, not written here. Walk this contract before you
touch anything. It is a checklist, not prose — follow it in order.

## 0. Orient

- [ ] Read this file end to end.
- [ ] Read `CUSTOMIZE.md` — the ordered fill-checklist. It is your task list.
- [ ] Run `pnpm install && pnpm typecheck && pnpm test`. Confirm green BEFORE editing.
The test suite includes the end-to-end turn gate (fake sandbox producer →
streamed turn → persisted transcript) — it proves the assembly, not stubs.

## 1. The one rule (layering)

> You change behavior by editing DATA and the COMPOSER — never by editing
> `@tangle-network/agent-app`.

- The shell owns mechanism: turn protocol + hook order (`createChatTurnRoutes`
over agent-runtime's `handleChatTurn`), persistence columns (`createChatTables`),
the upload inline-vs-sandbox split, the interaction answer contract, auth guards.
- You own: `agent.config.ts` + `prompts/system.md` (DATA), `src/` (the COMPOSER +
routes), `migrations/` (must mirror the schema — the e2e test executes it),
`public/` (the dev page). That is the whole surface.

## 2. DATA vs CODE — know which file you're in

- [ ] DATA → `agent.config.ts`: name, system prompt, model default + effort,
harness, renderable ask kinds. Plain values. If you're writing an `if`,
you're in the wrong file.
- [ ] DATA → `prompts/system.md`: the persona. State intents and hard rules,
never implementations — no shell commands, CLI flags, or install scripts.
The executing agent chooses tools at execution time.
- [ ] CODE → `src/chat.ts`: the COMPOSER. Wires config + env into the shell's
factories. Extend seams here (billing hooks, `transformFinalText`,
`onTurnComplete`); never re-implement what a factory already does.
- [ ] CODE → `src/sandbox.ts`: the sandbox lane (box naming, credentials,
profile). All agent intelligence lives IN the sandbox; this file only
reaches it.
- [ ] CODE → `src/worker.ts`: routing only. New endpoint = new handler in
`src/chat.ts`, one `if` here.

## 3. Invariants — fail-closed, never relax

- [ ] TRUSTED CONTEXT: `userId`/`workspaceId` come from the better-auth SESSION,
never from a request body or model output. The `authorize` seam is the only
place identity is established — keep it that way.
- [ ] THREAD ACCESS: an inaccessible thread reads as 404, indistinguishable from
a missing one. A cross-workspace probe must not learn the thread exists.
- [ ] NO MOCK AGENT: without sandbox credentials a turn fails loud with a clear
error. Never add a canned-response fallback — a fake answer is worse than
an honest failure.
- [ ] AGENT-NATIVE: intelligence and tooling live in the sandboxed agent;
durability (rows, turn buffer) and money live here. If you're about to
parse the agent's prose for data, stop — that's a schema-validated tool's
job (see the shell's `/tools`).
- [ ] GROUNDING: the persona's fabrication rule stays. A real record or an
explicit "NOT ON FILE".

## 4. Verify (every change ends here)

- [ ] `pnpm typecheck` — clean. Proves the assembly matches the shell contract.
- [ ] `pnpm test` — green. The e2e gate runs the REAL migration + REAL factories
with one fake (the sandbox event feed). If it fails, the app drifted from
the framework — fix the drift, not the test.
- [ ] For a real deploy: fill `wrangler.toml` + `.dev.vars`, run
`pnpm db:migrate:local`, then `pnpm dev` and exercise the dev page.

If any of these is red, you are not done. Do not weaken a test to pass.
6 changes: 6 additions & 0 deletions create-agent-app/template-chat/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# CLAUDE.md

The behavior contract for this project lives in `AGENTS.md`. Read it first, then
walk `CUSTOMIZE.md` (the fill-checklist).

@AGENTS.md
88 changes: 88 additions & 0 deletions create-agent-app/template-chat/CUSTOMIZE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# CUSTOMIZE.md — fill this project, in order

This is the trail. Walk it top to bottom. Each step is a checklist item paired
with the DISCOVERY QUESTION it answers — answer the question, then make the
edit. The whole job is filling `agent.config.ts` + `prompts/system.md` (DATA)
and wiring credentials; the chat vertical itself is already assembled and
proven by the e2e test.

When every box is checked and `pnpm typecheck && pnpm test` are green and a
message round-trips on `pnpm dev`, the app is customized.

---

## ① Identity — `agent.config.ts` + `prompts/system.md`

Discovery: **Whose job does this agent do, in whose voice, under what hard rules?**

- [ ] Set `name` in `agent.config.ts` to the product/agent name.
- [ ] Rewrite `prompts/system.md` as the real persona: role + voice + remit +
hard rules. State intents, never implementations (no commands, no tool
scripts — the sandboxed agent picks its own tools).
- [ ] Keep the grounding rule (never fabricate; cite a record or NOT ON FILE).

## ② Model + harness — `agent.config.ts` → `model`, `harness`

Discovery: **Which model answers by default, at what effort, on which harness?**

- [ ] Set `model.default` to a model your Tangle Router key can reach.
- [ ] Pick `harness` (`opencode` default; vendor-locked harnesses like
`claude-code` must pair with their own provider's models).
- [ ] Leave per-turn overrides alone — the client can send `model`/`effort`
per request and `MODEL_NAME` overrides without a redeploy.

## ③ Infrastructure — `wrangler.toml` + `.dev.vars` + `migrations/`

Discovery: **Where does this app live and what may it spend?**

- [ ] `wrangler d1 create <name>` → paste `database_id` into `wrangler.toml`.
- [ ] Copy `.dev.vars.example` → `.dev.vars`; fill `BETTER_AUTH_SECRET`,
`TANGLE_API_KEY`, `SANDBOX_API_KEY`, `SANDBOX_GATEWAY_URL`.
- [ ] `pnpm db:migrate:local` — applies `migrations/0001_init.sql` (auth +
chat + turn buffer). The e2e test executes this same file, so it cannot
silently drift from the schema.
- [ ] R2 stays commented out unless the product stores artifacts.

## ④ Prove the loop — `pnpm dev`

Discovery: **Does a real message round-trip through a real box?**

- [ ] `pnpm dev`, open http://localhost:8787, sign up, send a message.
- [ ] Confirm: streamed text renders live, the transcript reloads with parts
and a usage receipt, and a second turn continues the same agent session.
- [ ] Kill the tab mid-turn, reopen the thread — the persisted row is intact
(the turn keeps running server-side and buffers for replay).

## ⑤ The product UI — replace the dev page

Discovery: **What does the real surface look like?**

- [ ] `public/index.html` is the dev harness, not the product. Build the real
surface in React on `@tangle-network/agent-app/web-react`:
`ChatComposer` (`onSendParts` + `onAttach`), `ChatMessages`,
`streamChatTurn` (start + resume against `/api/chat/replay/:turnId`),
`useChatInteractions` + `InteractionQuestionCard`/`InteractionPlanCard`
for the ask channel this server already exposes.
- [ ] Keep the wire contract: `chatTurnRequestInit` from web-react serializes
exactly what `createChatTurnRoutes` parses.

## ⑥ Extend, don't fork

Discovery: **What does this product add beyond the vertical?**

- [ ] Billing/audit/titles → `onTurnComplete` seam in `src/chat.ts`.
- [ ] PII scrubbing before persistence → `transformFinalText` +
`@tangle-network/agent-app/redact`.
- [ ] Structured agent writes (proposals, records) → the shell's `/tools`
side channel, never prose parsing.
- [ ] Multi-user teams → `@tangle-network/agent-app/teams` and pass your
workspace table to `createChatTables({ workspaceTable })` (new migration).

## ⑦ Verify

Discovery: **Does the customized app hold its contract?**

- [ ] `pnpm typecheck` — clean.
- [ ] `pnpm test` — green (the e2e turn gate + fail-closed auth).
- [ ] `pnpm dev` — a real turn round-trips end to end.
- [ ] `pnpm deploy` + `pnpm db:migrate` when it's real.
50 changes: 50 additions & 0 deletions create-agent-app/template-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# __PROJECT_NAME__

A multimodal chat agent product scaffolded with `create-agent-app --chat`, built
on [`@tangle-network/agent-app`](https://github.com/tangle-network/agent-app):
the whole server chat vertical — better-auth sessions, thread/message
persistence with typed parts + usage receipts, streaming turns with buffered
replay, file uploads, human-in-the-loop asks — assembled from shell factories.
The agent itself runs in a Tangle sandbox (a full harness: skills, tools, bash,
MCP); this app coordinates UI, durability, and access around it.

## Layout

| Path | Surface | Edit when |
|---|---|---|
| `agent.config.ts` | DATA — name, model default, harness, ask kinds | defining the agent |
| `prompts/system.md` | DATA — the persona (imported as a Text module) | shaping behavior |
| `src/chat.ts` | CODE — the composer (factories → the chat vertical) | extending seams |
| `src/sandbox.ts` | CODE — the sandbox lane (boxes, credentials, profile) | provisioning changes |
| `src/worker.ts` | CODE — HTTP routing only | adding an endpoint |
| `src/db/schema.ts` | CODE — auth tables + `createChatTables()` | schema changes (+ migration) |
| `migrations/` | SQL the e2e test executes for real | schema changes |
| `public/index.html` | the dev chat page (not the product UI) | never — replace it (CUSTOMIZE ⑤) |
| `tests/` | the e2e turn gate this app ships with | extending coverage |

## Get started

```bash
pnpm install
pnpm typecheck && pnpm test # green before you edit anything
```

Then walk the trail:

1. `AGENTS.md` — the behavior contract (you are customizing a chat agent-app).
2. `CUSTOMIZE.md` — the ordered fill-checklist, from persona to deploy.

## Scripts

- `pnpm dev` — run the worker + dev page locally (fill `wrangler.toml` + `.dev.vars` first).
- `pnpm db:migrate:local` / `pnpm db:migrate` — apply `migrations/` to D1.
- `pnpm typecheck` — `tsc --noEmit`.
- `pnpm test` — vitest, including the end-to-end turn gate (fake sandbox
producer → streamed turn → persisted transcript → replay).
- `pnpm deploy` — `wrangler deploy`.

## Invariants

Identity comes from the session, never a request body. Inaccessible threads
read as 404. No mock agent — missing sandbox credentials fail loud. See
`AGENTS.md` for the full contract.
6 changes: 6 additions & 0 deletions create-agent-app/template-chat/_gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
.wrangler/
.dev.vars
*.local
.DS_Store
38 changes: 38 additions & 0 deletions create-agent-app/template-chat/_package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "__PACKAGE_NAME__",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"db:migrate": "wrangler d1 migrations apply __PACKAGE_NAME__ --remote",
"db:migrate:local": "wrangler d1 migrations apply __PACKAGE_NAME__ --local",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@tangle-network/agent-app": "__AGENT_APP_VERSION__",
"@tangle-network/agent-interface": "^0.15.0",
"@tangle-network/agent-runtime": "^0.79.3",
"@tangle-network/sandbox": "^0.10.5",
"better-auth": "^1.6.16",
"drizzle-orm": "^0.45.2"
},
"peerDependencies": {
"@tangle-network/agent-eval": ">=0.100.0",
"@tangle-network/agent-integrations": ">=0.44.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250620.0",
"@tangle-network/agent-eval": "^0.100.0",
"@tangle-network/agent-integrations": "^0.44.0",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.6.0",
"better-sqlite3": "^12.10.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0",
"wrangler": "^4.0.0"
}
}
16 changes: 16 additions & 0 deletions create-agent-app/template-chat/_tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"types": ["@cloudflare/workers-types/2023-07-01", "node"]
},
"include": ["agent.config.ts", "declarations.d.ts", "src", "tests", "vitest.config.ts"]
}
Loading