Skip to main content

add-ai-chat-tool

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.

インストールへ移動

ソース情報

リポジトリ
ryokun6/ryos
ソースの最終更新活動
2026年7月8日 22:33
検出された SKILL.md の言語
英語
スター
1,255
フォーク
182

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
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で見る