create-curb.js 0.0.0 → 1.15.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,30 @@
1
1
  {
2
2
  "name": "create-curb.js",
3
- "version": "0.0.0",
4
- "description": "Placeholder until the first Curb release.",
3
+ "version": "1.15.0-preview.1",
4
+ "description": "Create a Curb voice agent: npm create curb.js my-agent",
5
5
  "license": "Apache-2.0",
6
- "repository": { "type": "git", "url": "git+https://github.com/Kugelaudio/curb.js.git" }
6
+ "homepage": "https://curbjs.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Kugelaudio/curb.js.git"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=22.12.0"
14
+ },
15
+ "bin": {
16
+ "create-curb.js": "./bin/create-curb.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "templates",
21
+ "NOTICE",
22
+ "RELEASE_NOTES.md"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test \"test/**/*.test.mjs\""
26
+ },
27
+ "dependencies": {
28
+ "picocolors": "^1.1.1"
29
+ }
7
30
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "mcpServers": {
3
+ "kugelaudio": { "type": "http", "url": "https://www.kugelaudio.com/mcp" },
4
+ "kugelaudio-docs": { "type": "http", "url": "https://docs.kugelaudio.com/mcp" }
5
+ }
6
+ }
@@ -0,0 +1,160 @@
1
+ # AGENTS.md
2
+
3
+ This is a **curb.js** voice agent. React renders the agent's context (its prompt, knowledge
4
+ and tools) instead of HTML, and the framework runs the call: speech, turn-taking,
5
+ interruptions. You edit `src/agent/` and `context/`; components come from `"curb"`, the
6
+ providers from `"curb/providers"`. The project starts as Juno, a demo assistant: replace
7
+ her prompt, skills and tools with the user's agent and delete what it does not need.
8
+
9
+ The agent runs on KugelAudio: its EU-hosted LLM (`kugelaudio.llm()`), Kugel Ear and Kugel TTS.
10
+ Sign in with `npx curb login` in this folder: it opens the browser once to sign in, and
11
+ every curb command here then runs on that login (`--no-open` only prints the link, for a
12
+ machine without a browser). There is no key to copy; never ask for a key in the chat. A `KUGELAUDIO_API_KEY` the user sets in `.env` or the environment (an
13
+ API key from the dashboard, for CI or a server) also works and wins over the login. Another
14
+ LLM (`anthropic()`, `openai()`, `openaiCompatible()`) is one `llm` prop away and brings its
15
+ own key. A turn answered 403 `LLM_NOT_ENABLED` means the user's organization does not have
16
+ KugelAudio's LLM yet (it comes with hosted agents, in closed alpha): they ask
17
+ hello@kugelaudio.com, or the agent uses another LLM meanwhile.
18
+
19
+ **Without a shell** (claude.ai, the Claude desktop chat), or when the person asks for it, build
20
+ in the agent's **browser project**: the platform keeps the project, the person's own browser
21
+ tab runs it, and you work through the `kugelaudio` MCP server's tools. Run no command and ask
22
+ the person to run none.
23
+
24
+ 1. `agent_list` finds the agent, or `agent_create` makes one named for what it does. `agent_open`
25
+ answers its session link: the person opens it beside the chat and keeps that tab open.
26
+ Every tool below but `agent_publish` runs in the tab, and a closed tab is refused with the
27
+ link to send.
28
+ 2. `agent_overview` shows the project. Read with `agent_read_file`, `agent_list_files` and
29
+ `agent_search`; change it with `agent_edit_file`, `agent_write_file` and `agent_delete_file`.
30
+ Read `reload` in each change's answer and fix `failed` diagnostics first.
31
+ 3. `agent_add_dependency` and `agent_remove_dependency` change the packages. One that needs
32
+ native addons, a shell or child processes does not run in the browser.
33
+ 4. `agent_request_secret` makes the tab ask the person for a key. It stays in that tab, and
34
+ `agent_publish` does not deploy it; say so when the live agent needs the key too.
35
+ 5. After every change run `agent_check`, then `agent_say` with what a caller says
36
+ (`new_conversation` after a code change). `agent_get_preview_log` shows what went wrong,
37
+ `agent_list_preview_conversations` and `agent_get_preview_conversation` what the person
38
+ said in the tab, and `agent_restart_preview` unsticks a preview.
39
+ 6. `agent_publish` puts it live. Run it only when the person asks.
40
+
41
+ With a shell, the rest of this file applies as it stands.
42
+
43
+ **Say what you do.** Before each step (creating the project, installing, checking, signing
44
+ in, deploying), tell the user in one plain sentence what it does and what, if anything, they
45
+ have to do, such as open a sign-in link. They may never have used a terminal.
46
+
47
+ A new project is created with `npm create curb.js my-agent`, and you then work in
48
+ `my-agent` (`npm create curb.js .` fills the current empty folder instead).
49
+
50
+ The full guide ships in `node_modules/curb/docs/agent-guide/`: read `README.md` first, then
51
+ `what-the-model-receives.md`, `agents-and-skills.md`, `tools.md`, `state.md` and `testing.md`.
52
+ There is one page per task, such as `human-handoff.md` and `confirmations.md`;
53
+ `api-reference.md` leads to every export, and `improve-from-calls.md` is the way to fix an
54
+ agent from a call that went wrong.
55
+ A fault in Curb itself is reported as `reporting-a-bug.md` says, with the person's yes first.
56
+
57
+ ## Verify every change
58
+
59
+ 1. `npx curb check`: type-checks `src/`, renders every skill and prints warnings. Fix every
60
+ error and every warning; each one names a file, a line and the fix.
61
+ 2. `npx curb say "<what the caller says>"`: one spoken turn, headless. It prints the reply,
62
+ each tool call and the context it changed. The next `curb say` continues the
63
+ conversation; `npx curb say --new "<first line>"` starts over (do that after a code change).
64
+ 3. `npx curb test`: runs every `src/**/*.scenario.ts` conversation headlessly, with scripted
65
+ caller lines or a simulated caller pursuing a goal, and fails on turn errors, repeated
66
+ sentences or a goal not reached. Keep one scenario per flow that matters (`testing.md`).
67
+
68
+ 4. `npm run dev`: the browser preview with chat and voice on http://127.0.0.1:4000. Open it in the person's own browser yourself (`open http://127.0.0.1:4000` on macOS, `xdg-open` on Linux, `start` on Windows): a browser built into an app, like the Claude app's Browser pane, blocks the microphone, so a call there fails.
69
+
70
+ **To improve the agent, read its recent conversations first.** `npx curb conversations`
71
+ prints the last ones the preview and `curb say` ran, with every tool call, its
72
+ input and its result (`--last <n>`, `--id <id>`, `--json`). Calls to the hosted agent are
73
+ read with the `kugelaudio` MCP server's `agent_list_calls`, `agent_get_call` and
74
+ `agent_get_call_timeline` where it is signed in. Find where the agent went wrong, fix that,
75
+ and turn the conversation into a scenario so it stays fixed.
76
+
77
+ ## The preview
78
+
79
+ It shows what the agent sees: its prompt, the skills it has loaded and the tools it can
80
+ call. You change the agent by editing code, never in the preview; a tool's form there only
81
+ tries the tool, and `<Tool tryWith={{ ... }}>` fills it with values your demo data knows.
82
+ **Copy for coding agent** copies a conversation with the skill map, so you can work from
83
+ what really happened.
84
+
85
+ ## Map
86
+
87
+ ```text
88
+ src/agent/agent.tsx the agent, where every conversation starts: <Agent llm stt tts>,
89
+ system prompt, skills index, tools every skill has, store
90
+ src/agent/<name>/skill.tsx one folder per skill, opened when needed; nested folders are sub-skills
91
+ context/*.md knowledge a skill reads with <Text src="file.md" />
92
+ .env other providers' keys, an optional KUGELAUDIO_API_KEY; never commit it
93
+ .data/conversations.jsonl every local conversation: tool calls, inputs, results
94
+ ```
95
+
96
+ ## Rules
97
+
98
+ - **Prose is one template literal**: ``<Instruction>{`...`}</Instruction>``, the same in
99
+ `<SystemPrompt>` and `<Text>`. Paragraphs and `- ` lists reach the model; multi-line JSX
100
+ text is joined into one line.
101
+ - **A skill's description is written once**, as `meta.description` in its `skill.tsx`,
102
+ phrased as a routing rule ("Use for ...", "NOT for ..."). Its `<Skill>` link repeats none
103
+ (a `description` on the link works too, but keep one).
104
+ - **No per-call state at module scope**: one runner serves several calls. Keep a call's
105
+ state in a `createAgentStore` store, never in a module-level variable.
106
+ - **Secrets** come from `.env` locally and `npx curb secrets push` when hosted. Read them
107
+ in tool handlers (`process.env.NAME`); never render them into a prompt.
108
+ - **A key never goes into a tool call.** Never paste one into a command line, a chat message
109
+ or an MCP tool's arguments (`agent_set_secrets` included). The user puts it in `.env`
110
+ themselves, and `npx curb secrets push` sends it to the hosted agent. A hosted agent gets
111
+ `KUGELAUDIO_API_KEY` from KugelAudio, so the default agent has nothing to push.
112
+ - **No hard-coded names or dates.** The framework tells the model today's date on every
113
+ turn; set `defineAgent({ timeZone })` to the callers' zone (default UTC). Examples use
114
+ placeholders such as "Max Mustermann" or "the caller's name".
115
+ - **The opening says it is an AI**: the agent's first line tells the caller they are talking
116
+ to an AI assistant. For exact words, use `defineAgent({ firstTurn: { say } })`.
117
+ - **A tool's `schema` is what the model sees**: lists (`z.array`), nested objects,
118
+ `.nullable()`, enums, ranges and `.regex()` all reach it, so pass a list as a list, never
119
+ as one string to split. Give every field a `.describe()`.
120
+ - **No prompt asks for "one moment" lines around tools**: Curb holds back the newest sentence
121
+ before a tool call, so a lone "One moment." is never spoken, but of two sentences the first is.
122
+ - **Only an action with consequences** (it books, sends, pays or cancels) uses
123
+ `<Tool confirm={(input) => "<read-back question>"}>`: the framework runs it only after the
124
+ caller's next turn says yes. To repeat a detail back (a name, a number), say it in the
125
+ reply; a confirm tool turns the caller's next words into the answer to its question.
126
+ - **Gate with state, not prompt text**: a tool that is not rendered does not exist for the
127
+ model. Render it from store state, and give a locked branch text that says how to unlock it.
128
+ - **What counts as a successful call is code too**: `defineAgent({ goals, experiments })`
129
+ names each goal (a tool that must succeed, or a sentence a judge checks) and each A/B test
130
+ with its weighted variants, and `useVariant("<test>")` reads the call's variant. The
131
+ dashboard only shows the results; `authoring.md` ("Goals and A/B experiments") has the rules.
132
+ - Tool names are unique snake_case verbs. Zod `.describe()` texts and error messages are
133
+ read by the model, so phrase them as the next step.
134
+
135
+ ## Deploy to KugelAudio
136
+
137
+ `.mcp.json` registers two MCP servers for Claude Code started in this folder (it asks once
138
+ to use them): `kugelaudio`, your KugelAudio account (sign in from `/mcp`), and
139
+ `kugelaudio-docs`, the docs (no sign-in). Another client adds
140
+ `https://www.kugelaudio.com/mcp` as a remote MCP server. Look KugelAudio facts up with
141
+ `search_docs` and `read_docs_page`; for hosted agents use the `kugelaudio` server's,
142
+ since `kugelaudio-docs` serves public pages only. Deploy and read build and call logs
143
+ with the `kugelaudio` tools: `agent_create` (once), `agent_create_upload`, `agent_deploy`,
144
+ `agent_get_logs`; keys reach it only through `npx curb secrets push`. Without them:
145
+
146
+ ```bash
147
+ npx curb login --no-open # once; prints a sign-in link and waits up to 5 minutes
148
+ npx curb secrets push # the .env keys onto the hosted agent; prints names, never values
149
+ npx curb deploy # build and go live; prints the playground URL
150
+ ```
151
+
152
+ Not signed in? Do not stop there: run `npx curb login --no-open` in the background, give
153
+ the user the link it prints to open in their browser on this machine, and go on once it
154
+ prints `Signed in as`. Hosted agents are in a
155
+ closed alpha, switched on per organization: on `CURB_HOSTED_AGENTS_NOT_ENABLED` the user
156
+ asks hello@kugelaudio.com, and `curb dev` and `curb say` keep working meanwhile. A dry run
157
+ is `npx curb deploy --no-activate` (or `npx curb build`, which uploads nothing).
158
+
159
+ Run `npx curb check` before deploying. `.env*`, `node_modules/`, `.git/`, `.curb/` and
160
+ `.data/` are never uploaded. Details: `node_modules/curb/docs/agent-guide/deploying.md`.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,60 @@
1
+ # {{APP_NAME}}
2
+
3
+ A voice agent built with **curb.js**: React renders the agent's prompt, knowledge and
4
+ tools instead of HTML. It starts as **Juno**, a demo personal assistant, so you can try
5
+ the framework before you change anything.
6
+
7
+ ## Run it
8
+
9
+ Sign in with `npx curb login`; there is no key to copy. Every curb command in this folder
10
+ then runs on your login: KugelAudio's EU-hosted LLM thinks, Kugel Ear listens and Kugel TTS
11
+ speaks. A deployed agent gets its access from KugelAudio. Prefer an API key (CI, a server)?
12
+ Set `KUGELAUDIO_API_KEY` in `.env`; it wins over the login. Another provider's key goes in
13
+ `.env` too, which `npm create curb.js` already wrote.
14
+
15
+ ```bash
16
+ npx curb login # sign in in the browser; Curb uses your login
17
+ npm run dev # preview, chat and voice on http://127.0.0.1:4000
18
+ npx curb check # types, render and warnings
19
+ npx curb say "Hallo" # one caller turn in the terminal, no browser
20
+ ```
21
+
22
+ ## Meet Juno
23
+
24
+ Type or talk in the preview:
25
+
26
+ - **"Put buy milk on my list."** The `add_task` tool updates shared state, and the task
27
+ list in Juno's context changes with it.
28
+ - **"What do you know about me?"** Juno reads the about-you skill, whose profile is
29
+ `context/about-you.md`. Edit the file and ask again.
30
+ - **"Wrap up my day."** The `wrap_up_day` tool exists only once a task is done, so with
31
+ nothing done she cannot.
32
+ - **"How do you work?"** Juno explains how she is built.
33
+
34
+ ## Where things live
35
+
36
+ - `src/agent/agent.tsx`: the agent, where every conversation starts: its providers
37
+ (`<Agent llm stt tts>`), system prompt, skills index, and the task list with its tools.
38
+ - `src/agent/<name>/skill.tsx`: one folder per skill, opened only when the conversation
39
+ needs it.
40
+ - `context/`: markdown the skills read.
41
+
42
+ ## Deploy to KugelAudio
43
+
44
+ ```bash
45
+ npx curb login
46
+ npx curb secrets push
47
+ npx curb deploy
48
+ ```
49
+
50
+ `deploy` prints the playground URL where you can call the live agent. Your `.env` is
51
+ never uploaded; `secrets push` sends its values and prints only their names. Details:
52
+ `node_modules/curb/docs/agent-guide/deploying.md`. To run the voice worker on your own LiveKit instead,
53
+ see `node_modules/curb/docs/agent-guide/hosting.md`.
54
+
55
+ ## Learn more
56
+
57
+ `AGENTS.md` briefs coding agents (and you) on the rules; `CLAUDE.md` imports it for
58
+ Claude Code. `.mcp.json` connects Claude Code to your KugelAudio account (sign in from
59
+ `/mcp`) and to the KugelAudio docs. The full guide is in
60
+ `node_modules/curb/docs/agent-guide/`.
@@ -0,0 +1,13 @@
1
+ # About you
2
+
3
+ Edit this file with your real details: the agent's knowledge hot-reloads, no restart needed.
4
+
5
+ - **Name:** Your name here
6
+ - **City:** Where you live
7
+ - **What I do:** Your job, studies, or main project
8
+
9
+ ## Preferences
10
+
11
+ - I prefer short answers
12
+ - Tea, not coffee
13
+ - Replace these bullets with your own
@@ -0,0 +1,16 @@
1
+ # Keys only. Which providers run is chosen in src/agent/agent.tsx (<Agent llm stt tts>).
2
+ # A chosen provider whose key is missing fails with an error naming its variable; the
3
+ # table in node_modules/curb/docs/agent-guide/configuration.md lists every key. A new
4
+ # provider's key also goes into package.json curb.requiredRuntimeSecrets.
5
+ # Every curb command loads .env, and `npx curb secrets push` copies it to the hosted agent.
6
+
7
+ # KugelAudio runs the whole agent: its EU-hosted LLM thinks (kugelaudio.llm()), Kugel Ear
8
+ # listens (kugelaudio.stt()) and Kugel TTS speaks (kugelaudio.tts()). A hosted agent gets
9
+ # access from the platform.
10
+ # Locally, KugelAudio access comes from `npx curb login`: there is no key to copy.
11
+ # KUGELAUDIO_API_KEY= # optional: an API key instead of npx curb login (CI, servers)
12
+
13
+ # Your own LiveKit, only for `npm start` (the self-hosted voice worker):
14
+ # LIVEKIT_URL=wss://your-project.livekit.cloud
15
+ # LIVEKIT_API_KEY=
16
+ # LIVEKIT_API_SECRET=
@@ -0,0 +1,15 @@
1
+ node_modules/
2
+ dist/
3
+
4
+ # secrets — never commit these
5
+ .env
6
+ .env.local
7
+ .env*.local
8
+ .mcp-auth/
9
+
10
+ # generated / runtime state
11
+ .curb/
12
+ .data/
13
+ context/context.index.json
14
+
15
+ .DS_Store
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "curb": {
6
+ "requiredRuntimeSecrets": [
7
+ "KUGELAUDIO_API_KEY"
8
+ ]
9
+ },
10
+ "scripts": {
11
+ "dev": "curb dev",
12
+ "check": "curb check",
13
+ "start": "curb start",
14
+ "server": "curb server",
15
+ "upgrade": "curb upgrade"
16
+ },
17
+ "allowScripts": {
18
+ "@livekit/blingfire": false,
19
+ "@livekit/local-inference": false,
20
+ "esbuild": false,
21
+ "fsevents": false,
22
+ "onnxruntime-node": false
23
+ },
24
+ "dependencies": {
25
+ "curb": "{{FRAMEWORK_SPEC}}",
26
+ "react": "^19.2.7",
27
+ "react-dom": "^19.2.7"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^19.2.17",
31
+ "typescript": "^6.0.3"
32
+ }
33
+ }
@@ -0,0 +1,52 @@
1
+ import { Instruction, Text, Tool, z, type SkillMeta } from "curb";
2
+ import { useAssistant } from "../agent.js";
3
+
4
+ export const meta: SkillMeta = {
5
+ title: "About you",
6
+ description: "Use for anything about the user themselves: their name, preferences, facts to remember.",
7
+ };
8
+
9
+ /**
10
+ * A file-backed skill: the profile lives in context/about-you.md and reaches the model's
11
+ * context through <Text src>. Edit the markdown and Juno's knowledge changes live. The
12
+ * remember tool writes to the same shared store her day planner in agent.tsx reads.
13
+ */
14
+
15
+ const rememberSchema = z.object({
16
+ note: z
17
+ .string()
18
+ .min(1)
19
+ .describe("One short fact or preference in third person, e.g. 'prefers tea over coffee'."),
20
+ });
21
+
22
+ export default function AboutYou() {
23
+ const notes = useAssistant((s) => s.notes);
24
+ const remember = useAssistant((s) => s.remember);
25
+
26
+ return (
27
+ <>
28
+ <Instruction>{`
29
+ Answer questions about the user from the profile below and the remembered notes.
30
+ While the profile still holds placeholder text, invite the user once to put their own
31
+ details into context/about-you.md: your knowledge updates live, no restart needed.
32
+ `}</Instruction>
33
+
34
+ {notes.length === 0
35
+ ? "No notes remembered yet this session."
36
+ : `Remembered this session: ${notes.join("; ")}.`}
37
+
38
+ <Tool
39
+ schema={rememberSchema}
40
+ name="remember"
41
+ description="Save a lasting fact or preference the user just told you about themselves."
42
+ success="Noted. It is now part of the context in every skill that shows it."
43
+ onSubmit={(input) => {
44
+ remember(input.note);
45
+ return { ok: true };
46
+ }}
47
+ />
48
+
49
+ <Text src="about-you.md" />
50
+ </>
51
+ );
52
+ }
@@ -0,0 +1,223 @@
1
+ import {
2
+ Agent,
3
+ GlobalTools,
4
+ Skill,
5
+ Skills,
6
+ SystemPrompt,
7
+ Text,
8
+ Tool,
9
+ createAgentStore,
10
+ defineAgent,
11
+ z,
12
+ } from "curb";
13
+ import { kugelaudio } from "curb/providers";
14
+
15
+ /**
16
+ * Juno, the demo assistant of a new project. This file is the agent level, where every
17
+ * conversation starts: her prompt, her day planner (the task list and its tools) and the
18
+ * skills she opens only when needed. Her profile of the user is a markdown file, and wrapping
19
+ * up the day exists as a tool only once a task is done. Replace her with your own agent; the
20
+ * shape (system prompt, skills index, tools, store) stays.
21
+ */
22
+
23
+ export type Task = { title: string; done: boolean };
24
+
25
+ export const useAssistant = createAgentStore<{
26
+ tasks: Task[];
27
+ notes: string[];
28
+ addTask: (title: string) => void;
29
+ completeTask: (title: string) => void;
30
+ removeTask: (title: string) => void;
31
+ clearCompleted: () => void;
32
+ remember: (note: string) => void;
33
+ }>("juno", (set) => ({
34
+ tasks: [],
35
+ notes: [],
36
+ addTask: (title) =>
37
+ set((state) => {
38
+ const exists = state.tasks.some(
39
+ (task) => task.title.toLowerCase() === title.toLowerCase(),
40
+ );
41
+ if (exists) return state;
42
+ return { tasks: [...state.tasks, { title, done: false }] };
43
+ }),
44
+ completeTask: (title) =>
45
+ set((state) => ({
46
+ tasks: state.tasks.map((task) =>
47
+ task.title.toLowerCase() === title.toLowerCase()
48
+ ? { ...task, done: true }
49
+ : task,
50
+ ),
51
+ })),
52
+ removeTask: (title) =>
53
+ set((state) => ({
54
+ tasks: state.tasks.filter(
55
+ (task) => task.title.toLowerCase() !== title.toLowerCase(),
56
+ ),
57
+ })),
58
+ clearCompleted: () =>
59
+ set((state) => ({ tasks: state.tasks.filter((task) => !task.done) })),
60
+ remember: (note) => set((state) => ({ notes: [...state.notes, note] })),
61
+ }));
62
+
63
+ const addSchema = z.object({
64
+ title: z
65
+ .string()
66
+ .min(1)
67
+ .describe("Short task title, e.g. 'buy milk'. Keep it to a few words."),
68
+ });
69
+
70
+ const completeSchema = z.object({
71
+ title: z
72
+ .string()
73
+ .min(1)
74
+ .describe("Title of the task to mark done. Match a task from the open list."),
75
+ });
76
+
77
+ const removeSchema = z.object({
78
+ title: z.string().min(1).describe("Title of the task to remove from the list."),
79
+ });
80
+
81
+ /**
82
+ * Juno's day planner, in her context from the first turn. The task list is live state: a tool
83
+ * call updates the store, the component re-renders, and the model's context changes. Watch it
84
+ * in the preview's context inspector while you talk.
85
+ */
86
+ function DayPlanner() {
87
+ const tasks = useAssistant((s) => s.tasks);
88
+ const notes = useAssistant((s) => s.notes);
89
+ const addTask = useAssistant((s) => s.addTask);
90
+ const completeTask = useAssistant((s) => s.completeTask);
91
+ const removeTask = useAssistant((s) => s.removeTask);
92
+ const clearCompleted = useAssistant((s) => s.clearCompleted);
93
+
94
+ const open = tasks.filter((task) => !task.done);
95
+ const done = tasks.filter((task) => task.done);
96
+
97
+ return (
98
+ <>
99
+ <Text>
100
+ {tasks.length === 0
101
+ ? "No tasks yet."
102
+ : [
103
+ open.length > 0 ? `Open tasks: ${open.map((t) => t.title).join(", ")}.` : null,
104
+ done.length > 0 ? `Done: ${done.map((t) => t.title).join(", ")}.` : null,
105
+ ]
106
+ .filter(Boolean)
107
+ .join(" ")}
108
+ </Text>
109
+
110
+ {/* Shared state across skills: the remember tool lives in /about-you,
111
+ but its effect is visible right here. */}
112
+ {notes.length > 0 ? (
113
+ <Text>{`Things I know about the user (remembered this session): ${notes.join("; ")}.`}</Text>
114
+ ) : null}
115
+
116
+ <GlobalTools>
117
+ <Tool
118
+ schema={addSchema}
119
+ name="add_task"
120
+ description="Add a task to the user's list for today."
121
+ success="Added to the list."
122
+ onSubmit={(input) => {
123
+ addTask(input.title.toLowerCase());
124
+ return { ok: true };
125
+ }}
126
+ />
127
+ <Tool
128
+ schema={completeSchema}
129
+ name="complete_task"
130
+ description="Mark a task on the list as done."
131
+ onSubmit={(input) => {
132
+ const match = open.find(
133
+ (task) => task.title.toLowerCase() === input.title.toLowerCase(),
134
+ );
135
+ if (!match) {
136
+ return {
137
+ ok: false,
138
+ message: `No open task titled "${input.title}". Retry with a title from the open task list.`,
139
+ };
140
+ }
141
+ completeTask(input.title);
142
+ return { ok: true, message: `Marked "${match.title}" done.` };
143
+ }}
144
+ />
145
+ <Tool
146
+ schema={removeSchema}
147
+ name="remove_task"
148
+ description="Remove a task from the list entirely, done or not."
149
+ onSubmit={(input) => {
150
+ removeTask(input.title);
151
+ return { ok: true, message: "Removed from the list." };
152
+ }}
153
+ />
154
+
155
+ {/* Structural gating: wrapping up the day does not exist for the model
156
+ until at least one task is done. No prompt text can route around it. */}
157
+ {done.length > 0 ? (
158
+ <Tool
159
+ name="wrap_up_day"
160
+ description="Wrap up the day: clear the completed tasks and recap what got done."
161
+ onSubmit={() => {
162
+ const count = done.length;
163
+ clearCompleted();
164
+ return {
165
+ ok: true,
166
+ message: `Day wrapped up: ${count} task${count === 1 ? "" : "s"} done and cleared, ${open.length} still open.`,
167
+ };
168
+ }}
169
+ />
170
+ ) : (
171
+ <Text>Wrap-up is locked: the wrap_up_day tool appears once at least one task is marked done.</Text>
172
+ )}
173
+ </GlobalTools>
174
+ </>
175
+ );
176
+ }
177
+
178
+ export function App() {
179
+ // The providers Juno talks through; their keys live in .env. She listens and speaks in
180
+ // English: another language sets `language` on both stt and tts, with a voice that speaks it.
181
+ return (
182
+ <Agent llm={kugelaudio.llm()} stt={kugelaudio.stt({ language: "en" })} tts={kugelaudio.tts()}>
183
+ <SystemPrompt>{`
184
+ You are Juno, a personal assistant for daily planning and the demo agent of this project.
185
+
186
+ You can:
187
+ - keep the user's task list,
188
+ - remember what they tell you about themselves,
189
+ - explain how you are built.
190
+
191
+ The task list changes only through its tools: call add_task, complete_task or remove_task first, and say a task is added or done only after the tool answered.
192
+
193
+ Greet the user as Juno and tell them that you are an AI assistant.
194
+ Then help them plan their day: offer to keep their task list.
195
+ Mention once that they can ask "how does this work?" to hear how this demo is built.
196
+ Be warm and brief: one or two sentences per reply.
197
+ `}</SystemPrompt>
198
+ {/* Each link's routing description is the target skill's meta.description. */}
199
+ <Skills>
200
+ <Skill path="/about-you">About you</Skill>
201
+ <Skill path="/how-this-works">How this works</Skill>
202
+ </Skills>
203
+ <DayPlanner />
204
+ </Agent>
205
+ );
206
+ }
207
+
208
+ export const agent = defineAgent({
209
+ app: App,
210
+ // Voice calls open with Juno's greeting; "user" waits for the caller instead.
211
+ firstTurn: "agent",
212
+ // Every turn tells the model today's date and time in this zone (default "UTC").
213
+ // timeZone: "Europe/Berlin",
214
+ preview: {
215
+ suggestedPrompts: [
216
+ "Put buy milk on my list",
217
+ "What do you know about me?",
218
+ "Remember that I prefer tea",
219
+ "How does this demo work?",
220
+ ],
221
+ chatPlaceholder: "What's on for today?",
222
+ },
223
+ });
@@ -0,0 +1,36 @@
1
+ import { Instruction, Text, type SkillMeta } from "curb";
2
+ import { useAssistant } from "../agent.js";
3
+
4
+ export const meta: SkillMeta = {
5
+ title: "How this works",
6
+ description: "Use FIRST when the user asks how this demo, this agent, or the framework works.",
7
+ };
8
+
9
+ /**
10
+ * The demo explains itself. Loading this skill IS the demo of skill routing: the model
11
+ * read these instructions on demand because the user asked how things work.
12
+ */
13
+ export default function HowThisWorks() {
14
+ const tasks = useAssistant((s) => s.tasks);
15
+ const notes = useAssistant((s) => s.notes);
16
+
17
+ return (
18
+ <>
19
+ <Instruction>{`
20
+ Explain how this agent is built, in plain spoken language, a couple of sentences at a time.
21
+ Invite the user to watch the preview's context inspector change while they talk.
22
+ `}</Instruction>
23
+
24
+ <Text>{`
25
+ How this project works:
26
+ - The agent is a React tree that renders context and tools for a language model instead of HTML.
27
+ - src/agent/agent.tsx is where every conversation starts: Juno's prompt, her task list and its tools are in her context from the first turn.
28
+ - Each folder under src/agent/ is a skill: instructions and tools the model reads on demand. Reading this skill is how this answer came about.
29
+ - The user's profile is the markdown file context/about-you.md, read by a Text component.
30
+ - Tasks and remembered notes are shared React state: a tool call updates the store, the tree re-renders, and the model's context changes. Tasks on the list right now: ${tasks.length}. Remembered notes: ${notes.length}.
31
+ - Wrapping up the day is gated structurally: the wrap_up_day tool does not exist in the model's context until a task is done.
32
+ - Everything editable lives in src/agent/ and context/, and changes hot-reload into the running conversation.
33
+ `}</Text>
34
+ </>
35
+ );
36
+ }