- name
- add-ai-chat-tool
- description
- Add or modify an AI chat tool ("Ask Ryo" capability) in ryOS. Covers the server-side tool definition (Zod schema + description + optional execute) and the client-side handler dispatch, plus the server-vs-client execution split. Use when giving the AI a new capability, adding a tool to the chat agent, or editing chat/tool schemas, descriptions, or handlers.
# Adding an AI Chat Tool
ryOS chat tools follow the Vercel AI SDK tool-loop pattern. A tool is **defined on the server** (name + description + Zod `inputSchema`), and is either:
- **Server-executed** — has an `execute` fn that runs in `api/chat/tools/` (needs Redis, secrets, server fetch). Add the name to `SERVER_EXECUTED_TOOL_NAMES`.
- **Client-executed** — has **no** `execute`. The model emits a tool call, the browser runs a handler in `src/apps/chats/tools/`, and the result is sent back via `addToolOutput` (needs Zustand stores, IndexedDB, media/DOM APIs).
## File Map
| Concern | File |
|---------|------|
| Server: input schemas (Zod) | `api/chat/tools/schemas.ts` |
| Server: descriptions + tool object + profile filtering | `api/chat/tools/index.ts` (`TOOL_DESCRIPTIONS`, `createChatTools`) |
| Server: shared types/constants | `api/chat/tools/types.ts` |
| Server: `execute` logic | `api/chat/tools/executors.ts`, `app-state-executors.ts`, `maps-executor.ts` |
| Server/client: execution split source of truth | `src/shared/tools/serverExecuted.ts` |
| Client: per-tool handlers | `src/apps/chats/tools/<name>Handler.ts` |
| Client: handler types/registry | `src/apps/chats/tools/types.ts`, `index.ts` |
| Client: dispatch (`switch` on tool name) | `src/apps/chats/hooks/useAiChat.ts` |
## Decision: Server or Client?
| Needs… | Execution |
|--------|-----------|
| Redis, secrets, server-only fetch, SSRF-safe fetch | **Server** (`execute`) |
| Zustand store mutation, IndexedDB/VFS, media playback, app windows, DOM | **Client** (handler) |
Some tools run **both** ways (e.g. `stickiesControl`, `calendarControl`, `contactsControl`): client in web chat, server in the Telegram profile. In that case provide both a handler and an executor.
---
## A. Add the Schema (`api/chat/tools/schemas.ts`)
Schemas are Zod. Keep action-style tools as a discriminated/enum `action` plus optional params, and use `.superRefine` for cross-field rules so the model gets a clear error before the handler runs.
```typescript
export const myFeatureControlSchema = z.object({
action: z.enum(["list", "create", "delete"]),
id: z.string().optional(),
content: z.string().optional(),
});
```
Add any shared enums/constants to `types.ts` and import them here (mirrors `CALENDAR_ACTIONS`, `TV_ACTIONS`, etc.).
## B. Define the Tool (`api/chat/tools/index.ts`)
1. Add a clear, behavior-specifying entry to `TOOL_DESCRIPTIONS` (the model relies heavily on this — describe each action, required params, and when NOT to use it).
2. Add the tool to the `allTools` object inside `createChatTools`.
Client-executed (no `execute`):
```typescript
myFeatureControl: {
description: TOOL_DESCRIPTIONS.myFeatureControl,
inputSchema: schemas.myFeatureControlSchema,
// No execute — handled client-side (requires Zustand store access)
},
```
Server-executed:
```typescript
myFeatureControl: {
description: TOOL_DESCRIPTIONS.myFeatureControl,
inputSchema: schemas.myFeatureControlSchema,
execute: async (input: MyFeatureControlInput) => executeMyFeatureControl(input, context),
},
```
If the tool should be available to the Telegram/memory profiles, also add it to the relevant branch in `createChatTools` (the `telegram` profile object or `MEMORY_TOOL_NAMES`). Tools default to the `"all"` profile.
## C. Mark Execution Side (`src/shared/tools/serverExecuted.ts`)
If (and only if) the tool is server-executed, add it to `TOOL_EXECUTION_METADATA` with `execution: "server"`. The client uses `SERVER_EXECUTED_TOOL_NAME_SET` to skip client dispatch for these (it returns early without running a handler).
```typescript
export const TOOL_EXECUTION_METADATA = [
// ...
{ name: "myFeatureControl", execution: "server" },
] as const;
```
Client-executed tools do NOT go here.
---
## D. Client Handler (client-executed tools only)
Create `src/apps/chats/tools/myFeatureHandler.ts`. The handler reads/writes Zustand stores and reports a result through `context.addToolOutput`.
```typescript
import type { ToolContext } from "./types";
import { useMyFeatureStore } from "@/stores/useMyFeatureStore";
import { useAppStore } from "@/stores/useAppStore";
import i18n from "@/lib/i18n";
export interface MyFeatureControlInput {
action: "list" | "create" | "delete";
id?: string;
content?: string;
}
export const handleMyFeatureControl = (
input: MyFeatureControlInput,
toolCallId: string,
context: ToolContext
): void => {
const store = useMyFeatureStore.getState();
try {
switch (input.action) {
case "list": {
context.addToolOutput({
tool: "myFeatureControl",
toolCallId,
output: JSON.stringify(store.items, null, 2),
});
break;
}
// create / delete ...
default:
context.addToolOutput({
tool: "myFeatureControl",
toolCallId,
state: "output-error",
errorText: i18n.t("apps.chats.toolCalls.unknownError"),
});
}
} catch (error) {
context.addToolOutput({
tool: "myFeatureControl",
toolCallId,
state: "output-error",
errorText: error instanceof Error ? error.message : "error",
});
}
};
```
Handler conventions (match existing handlers like `stickiesHandler.ts`):
- Always emit exactly one `addToolOutput` per call — a success `output` string or an `{ state: "output-error", errorText }`.
- Localize user-facing strings via `i18n.t(...)` (`apps.chats.toolCalls.*`).
- Open the relevant app first when a mutation should surface it (`context.launchApp("myfeature")` / guard with `useAppStore.getState().getInstancesByAppId`).
- For list→mutate flows where the AI passes IDs back, use `createShortIdMap` / `resolveId` from `./helpers` to keep token usage low.
`ToolContext` provides `{ launchApp, addToolOutput, detectUserOS }`.
## E. Wire the Client Dispatch
1. In `src/apps/chats/tools/index.ts`, export the handler and its input type.
2. In `src/apps/chats/tools/dispatchToolCall.ts` (shared by the Chats app and the desktop assistant), add a `case` to the `switch (toolCall.toolName)`:
```typescript
case "myFeatureControl": {
handleMyFeatureControl(
toolCall.input as MyFeatureControlInput,
toolCall.toolCallId,
toolContext
);
result = ""; // handler already called addToolOutput
break;
}
```
> Dispatch is an explicit `switch` — there is no handler registry. Add the `case`, otherwise the tool falls through to the `default` branch and reports "Unhandled tool". Set `result = ""` when the handler emits its own output (return a non-empty string only for trivial tools that don't call `addToolOutput`). VFS tools (`list`/`open`/`read`/`write`/`edit`) live in `vfsHandlers.ts` and receive a `VfsToolContext` with `saveFile` + `recordOpenedInstance`.
## F. Server Executor (server-executed / dual tools)
Add `executeMyFeatureControl(input, context)` to `executors.ts` (or `app-state-executors.ts` for app-state tools), export it from `api/chat/tools/index.ts`, and reference it in the tool's `execute`. The executor receives the server `context` (`MemoryToolContext`: logging, env, redis/auth helpers) and must return a JSON-serializable result.
For tools that return images to the model (like `infiniteMacControl`'s `readScreen`), add a `toModelOutput` that converts the result into multimodal content.
---
## Testing
- **Schema** (fast, no server): add a `tests/unit/<domain>/test-<feature>-schema.test.ts` that `safeParse`s valid and invalid inputs (see `tests/unit/media/test-media-control-unified.test.ts`). Unit suites are auto-discovered. See the `write-tests` skill.
- **Server executor**: cover via the AI endpoint suite (`test:ai`) where applicable.
- **Client handler / end-to-end**: exercise in the Chats app by asking Ryo to use the capability and confirming the store/app updates and the tool result bubble.
## Checklist
```
- [ ] Schema in api/chat/tools/schemas.ts (+ shared enums in types.ts)
- [ ] Description in TOOL_DESCRIPTIONS + entry in createChatTools (right profile)
- [ ] If server-executed: add to TOOL_EXECUTION_METADATA (serverExecuted.ts) + write executor
- [ ] If client-executed: handler in src/apps/chats/tools/ + export + switch case in useAiChat.ts
- [ ] Localize tool-call strings (apps.chats.toolCalls.*)
- [ ] Schema unit test in tests/ (registered in test:unit)
```
GitHubで見る