diff --git a/.claude/workflows/docs-accuracy-audit.js b/.claude/workflows/docs-accuracy-audit.js index 849c878e9c..c4bd28069d 100644 --- a/.claude/workflows/docs-accuracy-audit.js +++ b/.claude/workflows/docs-accuracy-audit.js @@ -41,6 +41,7 @@ const ALL_HANDWRITTEN = [ "content/docs/ai/natural-language-queries.mdx", "content/docs/ai/skills-reference.mdx", "content/docs/ai/skills.mdx", + "content/docs/ai/tools.mdx", "content/docs/api/client-sdk.mdx", "content/docs/api/data-api.mdx", "content/docs/api/data-flow.mdx", diff --git a/content/docs/ai/index.mdx b/content/docs/ai/index.mdx index 3d6971fabf..477f4740b4 100644 --- a/content/docs/ai/index.mdx +++ b/content/docs/ai/index.mdx @@ -18,7 +18,7 @@ AI in ObjectStack is a **cross-protocol capability layer**: agents, tools, and k - **Data & actions → `@objectstack/mcp`** (BYO-AI). Point your own AI — Claude, Cursor, any MCP client, or a local model — at the app's objects, queries, and business **actions**, governed by the same RLS. With a local model, data *and* inference stay inside your boundary. - **Knowledge & RAG → the Knowledge Protocol + adapter plugins** (`knowledge-memory`, `knowledge-ragflow`, `embedder-openai`) — permission-aware retrieval over your own objects. -- **Agents, tools, skills → typed metadata** (`defineAgent` / `defineTool` / `defineSkill`) plus the Model Registry. Author them as source (`*.agent.ts`, `*.tool.ts`, …) with your own AI coding agent (Claude Code, Cursor), aided by the ObjectStack [skills](/docs/ai/skills-reference) and MCP introspection. +- **Skills → typed metadata** (`defineSkill`) plus the Model Registry. A skill is the third-party extension primitive; agents are platform-owned, and a `defineTool` record is an *optional refinement layer* you will rarely need — see [Tool Records](/docs/ai/tools) for which of the three paths your case wants. Author skills as source (`*.skill.ts`) with your own AI coding agent (Claude Code, Cursor), aided by the ObjectStack [skills](/docs/ai/skills-reference) and MCP introspection. **ObjectOS** adds an in-product chat *runtime* on top of these same primitives — the `ask` data-query assistant, the `build` Studio authoring assistant, and the `/api/v1/ai/*` chat endpoints. The open-source framework has no built-in in-product chat — that runtime is documented in the [ObjectOS AI & Agents docs](https://docs.objectos.app/docs/ai). @@ -27,6 +27,7 @@ AI in ObjectStack is a **cross-protocol capability layer**: agents, tools, and k - [AI Agents](/docs/ai/agents) — the two platform agents (`ask` / `build`), skills as the extension primitive, and agent anatomy - [Actions as Tools](/docs/ai/actions-as-tools) — explicit opt-in exposure of Actions to the LLM, HITL approval, permission-aware execution +- [Tool Records](/docs/ai/tools) — the three ways a capability reaches an agent, and the narrow case where authoring a `tool` record is the right answer - [Knowledge & RAG](/docs/ai/knowledge-rag) — the Knowledge Protocol and its adapter plugins - [Natural Language Queries](/docs/ai/natural-language-queries) — the built-in data tools that turn questions into ObjectQL - [AI Skills System](/docs/ai/skills) — structured knowledge modules for AI coding assistants diff --git a/content/docs/ai/meta.json b/content/docs/ai/meta.json index 6d8375bfb3..649174c45c 100644 --- a/content/docs/ai/meta.json +++ b/content/docs/ai/meta.json @@ -5,6 +5,7 @@ "connect-mcp", "agents", "actions-as-tools", + "tools", "knowledge-rag", "natural-language-queries", "skills", diff --git a/content/docs/ai/tools.mdx b/content/docs/ai/tools.mdx new file mode 100644 index 0000000000..eeb58b35f1 --- /dev/null +++ b/content/docs/ai/tools.mdx @@ -0,0 +1,218 @@ +--- +title: Tool Records +description: The three ways a capability reaches an agent, and the narrow case where authoring a tool record is the right answer +--- + +# Tool Records + +Part of the [AI module](/docs/ai). `tool` is an authorable metadata kind — +`ToolSchema`, declared as `defineStack({ tools })` — and it is the **least +likely** answer to "how do I give my agent a new capability". + +So this page opens with the decision instead of the shape. If you read only the +next section and leave, you will be on the right path. The declaration shape is +further down, for the reader who has already established that they need it. + + +**The default third-party path declares no tool records at all.** That is +[ADR-0109](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0109-ai-tool-authoring-model.md), +and the `stack.tools` field says so in its own description: + +```text +AI Tool metadata records — optional refinement layer, never required: the +default path is skills referencing platform tools or materialised action_ +tools (ADR-0109) +``` + + +## Three ways a capability reaches an agent + +An agent's capability set is the union of its surface-compatible **skills'** +tools ([ADR-0064](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0064-tool-scoping-to-agent.md)), +so every one of these is ultimately a name in some skill's `tools[]`. What +differs is where that name comes from: + +| What you want the agent to do | How you get there | Tool record? | +|:---|:---|:---| +| Read, query, aggregate, or search — what the platform already does for every app | Name the **platform tool** in your skill: `query_records`, `get_record`, `aggregate_data`, `search_knowledge`, `describe_object`, … | **No** | +| Run something your app already does — a `script` / `api` / `flow` Action | Opt the Action in with `ai.exposed` + `ai.description`; the runtime materialises one `action_` tool per exposed Action and your skill names that. See [Actions as Tools](/docs/ai/actions-as-tools). | **No** | +| Reach a system outside your app | Connect it over MCP — see [Connect an MCP Client](/docs/ai/connect-mcp) | **No** | +| Present an executable to the model *differently* from the way your app runs it | A `tool` record — the optional refinement layer described below | **Yes**, and rarely | + +There is a fourth thing authors reach for that is not on this list, because it +is not a tool at all: *reasoning*. "Analyse the pipeline", "draft this email", +"score this lead" are things the model does with data it already has. Writing +them as tool names is the most common authoring mistake on this surface, and it +produces an assistant that claims abilities it does not have. + +### The default path, end to end + +Two declarations, no tool record — the Action you already ship for your UI, and +a skill that names its materialised tool: + +{/* os:check */} +```typescript +import { defineAction } from '@objectstack/spec/ui'; +import { defineSkill } from '@objectstack/spec/ai'; + +// 1. An Action the app already has — opted in to AI. +export const EscalateCaseAction = defineAction({ + name: 'escalate_case', + label: 'Escalate Case', + objectName: 'support_case', + type: 'flow', + target: 'case_escalation_flow', + ai: { + exposed: true, + description: 'Escalates a support case to the on-call queue and notifies the account owner.', + }, +}); + +// 2. The skill names the materialised tool. No defineTool anywhere. +export const CaseTriageSkill = defineSkill({ + name: 'case_triage', + label: 'Case Triage', + surface: 'ask', + instructions: 'Read the case and its recent activity, then escalate when the customer is blocked.', + tools: ['get_record', 'query_records', 'action_escalate_case'], +}); +``` + +The full walkthrough — including the three conditions that decide whether +`action_` exists at all — is in +[AI Agents](/docs/ai/agents#a-skill-needs-no-tool-records--name-the-action). + +### Why that is the default + +- **AI capability ≡ application capability.** The executable, its permission + checks and its audit trail are the Action your UI button already runs. There + is no second security surface to review, and no way for the agent to do + something the app cannot. +- **One less namespace to get wrong.** A tool record is a second place a name + has to stay consistent — and a second place an AI author can invent one. +- **Unresolved names surface at authoring time.** `os validate` reports a + `skill.tools[]` entry that resolves to nothing (`ai-skill-tool-unresolved`, + advisory). The rule exists because an app once shipped ten fictional tool + names across six skills, every one of them passing validation. + +## When a tool record is the right answer + +Reach for one only when the **AI-facing surface must differ from the raw +executable**. ADR-0109 names the qualifying cases: + +- **A different LLM-facing description** — the model needs a contract written + for it, and the Action's own `ai.description` cannot serve both audiences. +- **Parameter narrowing** — the Action takes twenty parameters and the agent + should see three, or an enum should be tighter for the model than for the API. +- **Exposing a Flow** — there is no materialised family for flows, only for + Actions. +- **A stable AI-facing name** — decoupled from the Action's name, so renaming + the Action does not rename the tool the model was trained against. +- **Execution policy** — a confirm-before-run flag. Note that + `requiresConfirmation` was *removed* from `ToolSchema` as unenforced + ([ADR-0049](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0049-no-unenforced-security-properties.md)); + it returns only together with its enforcement. + +If none of those describe your situation, you do not need a tool record. If one +of them does, read the next callout before you write it. + + +**What a tool record does today.** `ToolSchema` has no `implementation` or +`handler` field, and no framework executor loads a metadata-authored tool — +authoring one does **not** make anything runnable. The refinement layer above is +ADR-0109 *Phase 2*, which is gated on a real refinement need and has not landed: +`stack.tools` has no runtime reader yet. + +What a record does do today is narrower and worth knowing: it survives stack +composition, it is mirrored into the metadata store for Studio and discovery, +and its name joins the resolution universe for `skill.tools[]` — so a skill +naming it validates clean. + +When Phase 2 lands, a third-party tool record must carry a `binding` +(`{ type: 'action' | 'flow', name }`) to the executable it refines. Handlers +never live on the tool. Write the record as a *view* of something your app +already executes, and it will still be one. + + +## The declaration shape + +`ToolSchema` and the `defineTool` factory are exported from +`@objectstack/spec/ai`. The generated field reference is +[Tool](/docs/references/ai/tool); the authoring-relevant fields are: + +| Field | Required | Meaning | +|:---|:---|:---| +| `name` | ✅ | Machine name, `snake_case`, globally unique — this is what a skill's `tools[]` names | +| `label` | ✅ | Human-readable display name | +| `description` | ✅ | The text the **model** reads to decide when to call the tool | +| `parameters` | ✅ | JSON Schema for the tool input — the model generates arguments conforming to it | +| `outputSchema` | optional | ⚠️ **Experimental, not enforced.** Its top-level keys are folded into the description shown to the model; outputs are never validated against it | +| `objectName` | optional | The object this tool operates on, when there is exactly one | +| `protection` | optional | Package-author lock policy ([ADR-0010](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0010-metadata-protection-model.md)) | + +The shape is **strict**: an undeclared key is rejected at parse time, not +stripped. Five keys that were once authorable — `permissions`, `active`, +`category`, `builtIn` and `requiresConfirmation` — were removed because nothing +read them, and each rejects today with the prescription for what to write +instead. If you are porting an older record, the parse error is the instruction. + +{/* os:check */} +```typescript +import { defineTool, defineSkill } from '@objectstack/spec/ai'; + +// A refinement: the underlying Action takes the full case payload, but the +// agent only ever needs the record and a length hint. +export const SummariseCaseTool = defineTool({ + name: 'summarise_case', + label: 'Summarise Case', + description: + 'Summarise a support case and its recent activity for a human reader. ' + + 'Use it before escalating, so the summary can be pasted into the handover note.', + objectName: 'support_case', + parameters: { + type: 'object', + properties: { + caseId: { type: 'string', description: 'Record id of the case to summarise' }, + length: { type: 'string', enum: ['short', 'detailed'] }, + }, + required: ['caseId'], + }, +}); + +// The skill names it exactly the way it names a platform or materialised tool. +export const CaseHandoverSkill = defineSkill({ + name: 'case_handover', + label: 'Case Handover', + surface: 'ask', + instructions: 'Summarise before you escalate, and put the summary in the handover note.', + tools: ['summarise_case', 'action_escalate_case', 'get_record'], +}); +``` + +## How the name is resolved + +A `skill.tools[]` entry resolves against three sources, in order: + +1. the stack's own `tools[]` names — the refinement records on this page; +2. the curated registry of tools the platform runtime registers at boot; +3. the materialised `action_` family, one per AI-exposed Action declared + on the stack or on any object. + +A trailing wildcard matches every member of that universe sharing the prefix, so +`action_*` subscribes a skill to all of the app's exposed Actions at once. + +**Agents do not name tools.** `agent.tools` was removed in protocol 17: it was +the one seam that let an agent reach a tool no skill of its surface declared. +An agent reaches exactly the tools its surface-compatible skills declare +(ADR-0064), so a tool record becomes reachable by attaching the skill that names +it, never by listing it on the agent. + +--- + +## See also + +- [Actions as Tools](/docs/ai/actions-as-tools) — the default path: an Action materialised as an `action_` tool +- [AI Agents](/docs/ai/agents) — the two platform agents, and skills as the extension primitive +- [Connect an MCP Client](/docs/ai/connect-mcp) — reaching tools outside your app +- [AI Skills System](/docs/ai/skills) — a different layer: the `SKILL.md` knowledge modules that teach a coding assistant to *write* your metadata +- [Tool reference](/docs/references/ai/tool) — every field, generated from `ToolSchema`