This agent takes a URL or text snippet from the user, asks the LLM to extract a title, summary, and tags, saves the result to the database, and navigates to the bookmark list.
**What this covers:** prompts, MCP tools, Create Hook, Next Hook, `ctx.memory.context`, `ctx.Send action: navigate`.
---
## Directory Structure
```
assistants/bookmark/
├── package.yao
├── prompts.yml
├── prompts/
│ └── edit.yml
├── mcps/
│ └── store.mcp.yao
└── src/
└── index.ts
```
---
## Step 1: `package.yao`
```json
{
"name": "Bookmark Assistant",
"connector": "$ENV.DEFAULT_CONNECTOR",
"options": { "temperature": 0.3 },
"mcp": {
"servers": [
{
"server_id": "agents.bookmark.store",
"tools": ["save_bookmark", "get_bookmark"]
}
]
}
}
```
`server_id` format: `agents.<assistant-path-dots>.<mcp-filename>`. For an assistant at `assistants/bookmark/`, the ID is `bookmark`, so the server_id is `agents.bookmark.store`.
Low temperature for consistent JSON output.
> **MCP vs Process in Hooks:** MCP tools are exposed to the LLM for automatic invocation. In this agent, the LLM doesn't need to call tools — it just returns JSON text. The `Next` Hook then calls `Process()` directly to save. We register the MCP tools anyway so the LLM can optionally query existing bookmarks via `get_bookmark` if needed.
---
## Step 2: Default Prompt (`prompts.yml`)
```yaml
- role: system
content: |
You are a bookmark assistant. Extract structured information from URLs or text.
When given input, respond with ONLY valid JSON in this exact format:
{
"title": "concise title",
"description": "2-3 sentence summary",
"tags": ["tag1", "tag2", "tag3"],
"source_url": "https://... or null"
}
Rules:
- title: specific, not generic (max 80 chars)
- description: what this is about, why it matters
- tags: 3-5 keywords, lowercase, relevant
- source_url: the URL if provided, null if text input
```
---
## Step 3: Edit Prompt (`prompts/edit.yml`)
```yaml
- role: system
content: |
You are a bookmark assistant in edit mode.
The user wants to update an existing bookmark. Return ONLY valid JSON in
the same format as the create mode, incorporating the user's changes
while preserving information that wasn't changed.
Existing bookmark data will be injected as context.
```
---
## Step 4: MCP Tool (`mcps/store.mcp.yao`)
```json
{
"label": "Bookmark Store",
"description": "Save and retrieve bookmarks",
"transport": "process",
"tools": {
"save_bookmark": "models.bookmark.Save",
"get_bookmark": "models.bookmark.Find"
}
}
```
This uses a `bookmark` model. For the model definition, see the Advanced chapter.
---
## Step 5: Hooks (`src/index.ts`)
```typescript
import { agent, log, Process } from "@yao/runtime";
// ── Create Hook ────────────────────────────────────────────────
export function Create(
ctx: agent.Context,
messages: agent.Message[]
): agent.Create {
// Store timing and chat ID for Next Hook
ctx.memory.context.Set("start_time", Date.now());
ctx.memory.context.Set("chat_id", ctx.chat_id);
// Save user input
const lastMsg = messages[messages.length - 1];
if (lastMsg?.content) {
ctx.memory.context.Set("user_input", lastMsg.content);
}
// Detect edit mode: URL contains ?id=xxx
const idMatch = ctx.route?.match(/[?&]id=([^&]+)/);
if (idMatch) {
const id = idMatch[1];
ctx.memory.context.Set("mode", "edit");
ctx.memory.context.Set("record_id", id);
// Inject existing record as context so LLM can update it
const existing = Process("models.bookmark.Find", Number(id), {});
if (existing) {
const enriched = [
{
role: "system" as const,
content: `Existing bookmark:\n${JSON.stringify(existing, null, 2)}`,
},
...messages,
];
return { messages: enriched, prompt_preset: "edit" };
}
}
ctx.memory.context.Set("mode", "create");
return { messages };
}
// ── Next Hook ──────────────────────────────────────────────────
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
log.Error("Bookmark agent error: " + payload.error);
ctx.Send({ type: "error", props: { message: payload.error } });
return { data: { status: "error" } };
}
const raw = payload.completion?.content;
const content = typeof raw === "string" ? raw : "";
// Parse JSON from LLM response
let bookmark: any;
try {
const match = content.match(/```json\n([\s\S]*?)\n```/);
bookmark = JSON.parse(match ? match[1] : content);
} catch (e) {
ctx.Send("Could not parse response. Please try again.");
return { data: { status: "parse_error" } };
}
const mode = ctx.memory.context.Get("mode") as string || "create";
const chatId = ctx.memory.context.Get("chat_id") as string || ctx.chat_id;
let id: number;
if (mode === "edit") {
const recordId = ctx.memory.context.Get("record_id") as string;
id = Number(recordId);
Process("models.bookmark.Save", { id, ...bookmark }); // update existing
} else {
id = Process("models.bookmark.Save", { ...bookmark, chat_id: chatId }) as number;
}
// Navigate to bookmark list
ctx.Send({
type: "action",
props: {
name: "navigate",
payload: { route: `/agents/bookmark/list?highlight=${id}` },
},
});
// Success message with timing
const duration = Date.now() - (ctx.memory.context.Get("start_time") as number);
const verb = mode === "edit" ? "Updated" : "Saved";
ctx.Send(`${verb}: **${bookmark.title}** (${duration}ms)`);
return { data: { status: "success", id, mode } };
}
```
---
## Test It
```bash
yao agent test -n bookmark -i "https://go.dev"
yao agent test -n bookmark -i "Clean Architecture is a software design philosophy..."
```
Expected output: the LLM returns JSON, the bookmark saves, and you see a navigate action.
---
## Summary
You've built a complete end-to-end agent:
| File | What it does |
|------|-------------|
| `package.yao` | Wires connector + MCP tools |
| `prompts.yml` | Defines LLM role and output format |
| `prompts/edit.yml` | Override prompt for edit mode |
| `mcps/store.mcp.yao` | Maps DB operations to MCP tools |
| `src/index.ts` | Create: detect mode, inject context — Next: parse, save, navigate |
This pattern — prompts + MCP + Create/Next hooks + memory — is the foundation of every Yao Agent. CLI Agent uses the same structure with one change: the LLM is replaced by an external CLI Agent.
→ **[Chapter 3: CLI Agent](../cli-agent)**