.agents/skills/builtin-tool/SKILL.md
A builtin tool is a package the agent runtime can call. It ships five faces:
| Face | Lives in | Audience |
|---|---|---|
| Manifest + types | src/{manifest,types,systemRole}.ts | The LLM (tool spec + system prompt) |
| ExecutionRuntime | src/ExecutionRuntime/ | Server / desktop / any runtime caller |
| Executor | src/client/executor/ | Frontend (wraps stores/services) |
| Client UI | src/client/{Inspector,Render,…}/ | Chat UI |
| Registry wiring | packages/builtin-tools/src/*.ts + src/store/tool/slices/builtin/executors/index.ts | Framework |
| Question | Doc |
|---|---|
| Where do files live? What does each face do? Wiring? | architecture.md |
| How do I name the tool, design APIs, write the manifest, executor, ExecutionRuntime? | tool-design.md |
| How do I build Inspector / Render / Placeholder / Streaming / Intervention / Portal? | ui.md |
packages/builtin-tool-<name>/ packagelobe-<domain> identifier is permanent. It's stored in message history. Renames need @deprecated aliases (see packages/builtin-tools/src/inspectors.ts:88-89). Get it right the first time.as const object, not a TS enum. It doubles as the runtime list BaseExecutor iterates over.content: string → the LLM reads itstate: Record<…> → the UI's pluginState; result-domain only, never echo all params backerror: { type, message, body? } → both LLM and UI; type is a stable codesrc/ExecutionRuntime/ — pure runtime, no React, no Zustand, accepts services via constructor. The default place for new logic.src/client/executor/ — BaseExecutor subclass that calls ExecutionRuntime (or stores/services directly when frontend-only).createStaticStyles + cssVar.* (zero-runtime). Fall back to createStyles + token only when you genuinely need runtime values. Use @lobehub/ui components, not raw antd.src/locales/default/plugin.ts. Inspector titles must come from t('builtins.<identifier>.apiName.<api>') so something renders while args stream.packages/builtin-tool-<name>/
├── package.json
└── src/
├── index.ts # exports manifest + types + systemRole + Identifier (no React, no stores)
├── manifest.ts # BuiltinToolManifest with JSON Schema for every API
├── types.ts # ApiName const + Params/State interfaces per API
├── systemRole.ts # System prompt teaching the model when/how to use the APIs
├── ExecutionRuntime/ # ✅ Default home for runtime logic (server- or anywhere-callable)
│ └── index.ts
└── client/
├── index.ts # Re-exports for the registries
├── executor/ # ✅ Frontend executor — extends BaseExecutor, often delegates to ExecutionRuntime
│ └── index.ts
├── Inspector/ # required — header chip per API
├── Render/ # optional — rich result card
├── Placeholder/ # optional — skeleton during streaming/execution
├── Streaming/ # optional — live output renderer (e.g. RunCommand, WriteFile)
├── Intervention/ # optional — approval / edit-before-run UI
├── Portal/ # optional — full-screen detail view
└── components/ # shared subcomponents used by the surfaces above
Older packages (builtin-tool-task, builtin-tool-calculator, etc.) still have src/executor/ as a sibling of src/client/. That's grandfathered; don't relocate without a deliberate refactor. New packages and new APIs added to existing packages should follow the layout above.
package.json exports map:
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/client/executor/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
}
Before opening the PR:
lobe-<domain> and is stable (lives in message history).<Name>ApiName value has: a manifest api[] entry, an executor method, an Inspector, an i18n apiName.* key.Params interfaces match the JSON Schema; State interfaces match what the executor returns and what the UI surfaces read.ExecutionRuntime/; the client/executor/ only wires stores/services and delegates.{ success, content, state, error? } via a single toResult() funnel — content always non-empty (default to error.message).isArgumentsStreaming, isLoading, partialArgs, missing pluginState.null until it has data; only created for APIs with rich results.humanIntervention is set in the manifest.src/locales/default/plugin.ts plus dev seeds in en-US/zh-CN.bunx vitest run --silent='passed-only' 'packages/builtin-tool-<name>' passes.bun run type-check passes.Pick the closest neighbor and copy:
| If your tool is… | Read first |
|---|---|
| Pure-compute, no UI state | packages/builtin-tool-calculator/ — ExecutionRuntime reuses executor (mathjs/nerdamer work everywhere) |
| CRUD over a domain entity | packages/builtin-tool-task/ — full Inspector + Render set, batch variants |
| Heavy UI (Inspector/Render/Placeholder/Portal) | packages/builtin-tool-web-browsing/ — search-style result UI, Portal for detail view |
| Desktop / filesystem with all surfaces (incl. Streaming + Intervention) | packages/builtin-tool-local-system/ — ExecutionRuntime injects an ILocalSystemService, executor calls it |
| Server-side pure (no client executor) | packages/builtin-tool-web-browsing/ — only ExecutionRuntime is exported; the chat client doesn't run it |
| Needs human approval before running | packages/builtin-tool-local-system/src/client/Intervention/ — per-API approval components |