diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx index ca5368fc22..6c379974fd 100644 --- a/content/docs/ai/skills-reference.mdx +++ b/content/docs/ai/skills-reference.mdx @@ -48,7 +48,7 @@ ObjectStack ships **11 skills** — one per authoring domain, plus process skill | 3 | [Query](#query) | `query` | `skills/objectstack-query/` | Construct ObjectQL queries — filters, sorting, pagination, aggregation, relation expansion, and full-text search. | | 4 | [UI](#ui) | `ui` | `skills/objectstack-ui/` | Author ObjectStack UI metadata — Views (list/form/kanban/calendar/gantt), Apps (navigation), Pages (structured plus the HTML and React source-authoring tiers, ADR-0080/0081), Dashboards, Reports, Charts, Actions, and package Docs (`src/docs/*.md`). | | 5 | [Automation](#automation) | `automation` | `skills/objectstack-automation/` | Design ObjectStack automation — Flows (visual logic), Triggers, Approvals, state machines, and the `jobs` (`defineJob`) / `webhooks` (`defineWebhook`) stack collections. | -| 6 | [AI](#ai) | `ai` | `skills/objectstack-ai/` | Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. | +| 6 | [AI](#ai) | `ai` | `skills/objectstack-ai/` | Design ObjectStack AI skills, tools, knowledge sources, and the open-edition MCP server surface. | | 7 | [API](#api) | `api` | `skills/objectstack-api/` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch contracts. | | 8 | [i18n](#i18n) | `i18n` | `skills/objectstack-i18n/` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | | 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | @@ -131,13 +131,13 @@ Do not use for data lifecycle hooks at the object layer (see objectstack-data) o **Domain** `ai` · **Path** `skills/objectstack-ai/` -Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. +Design ObjectStack AI skills, tools, knowledge sources, and the open-edition MCP server surface. -Use when the user is adding `*.skill.ts` / `*.tool.ts`, configuring an LLM provider, wiring agent tools, or indexing ObjectStack data as a knowledge source for RAG. Agents themselves are platform-internal (`ask` / `build`) — third parties extend them via skills and tools, not by authoring `*.agent.ts`. +Use when the user is adding `*.skill.ts` / `*.tool.ts`, configuring an LLM provider, wiring the tools a skill grants, or indexing ObjectStack data as a knowledge source for RAG. Agents themselves are platform-internal (`ask` / `build`) — third parties extend them via skills, not by authoring `*.agent.ts`. Do not use for general LLM prompting questions unrelated to ObjectStack metadata. -**Tags:** `agent`, `tool`, `skill`, `conversation`, `llm`, `embedding`, `mcp` +**Tags:** `agent`, `tool`, `skill`, `knowledge`, `llm`, `embedding`, `mcp` --- diff --git a/scripts/role-word-baseline.json b/scripts/role-word-baseline.json index e68a0e0573..1b1e040421 100644 --- a/scripts/role-word-baseline.json +++ b/scripts/role-word-baseline.json @@ -33,7 +33,7 @@ "content/docs/releases/v16.mdx": 5, "content/docs/releases/v17.mdx": 2, "content/docs/ui/forms.mdx": 3, - "skills/objectstack-ai/SKILL.md": 5, + "skills/objectstack-ai/SKILL.md": 1, "skills/objectstack-automation/SKILL.md": 1, "skills/objectstack-data/SKILL.md": 2, "skills/objectstack-data/rules/relationships.md": 1, diff --git a/skills/README.md b/skills/README.md index 8111a0462e..71f04a8461 100644 --- a/skills/README.md +++ b/skills/README.md @@ -34,7 +34,7 @@ apps too). | [Query](./objectstack-query/SKILL.md) | `query` | Construct ObjectQL queries — filters, sorting, pagination, aggregation, relation expansion, and full-text search. | | [UI](./objectstack-ui/SKILL.md) | `ui` | Author ObjectStack UI metadata — Views (list/form/kanban/calendar/gantt), Apps (navigation), Pages (structured plus the HTML and React source-authoring tiers, ADR-0080/0081), Dashboards, Reports, Charts, Actions, and package Docs (`src/docs/*.md`). | | [Automation](./objectstack-automation/SKILL.md) | `automation` | Design ObjectStack automation — Flows (visual logic), Triggers, Approvals, state machines, and the `jobs` (`defineJob`) / `webhooks` (`defineWebhook`) stack collections. | -| [AI](./objectstack-ai/SKILL.md) | `ai` | Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. | +| [AI](./objectstack-ai/SKILL.md) | `ai` | Design ObjectStack AI skills, tools, knowledge sources, and the open-edition MCP server surface. | | [API](./objectstack-api/SKILL.md) | `api` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch contracts. | | [i18n](./objectstack-i18n/SKILL.md) | `i18n` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | | [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index b33bf9b6c9..ecb3ce78f0 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -1,56 +1,32 @@ --- name: objectstack-ai description: > - Design ObjectStack AI skills, tools, knowledge sources, conversations, - model registry entries, and MCP integrations. Use when the user is adding - `*.skill.ts` / `*.tool.ts`, configuring an LLM provider, wiring agent - tools, or indexing ObjectStack data as a knowledge source for RAG. Agents - themselves are platform-internal (`ask` / `build`) — third parties extend - them via skills and tools, not by authoring `*.agent.ts`. Do not use for - general LLM prompting questions unrelated to ObjectStack metadata. + Design ObjectStack AI skills, tools, knowledge sources, and the open-edition + MCP server surface. Use when the user is adding `*.skill.ts` / `*.tool.ts`, + configuring an LLM provider, wiring the tools a skill grants, or indexing + ObjectStack data as a knowledge source for RAG. Agents themselves are + platform-internal (`ask` / `build`) — third parties extend them via skills, + not by authoring `*.agent.ts`. Do not use for general LLM prompting questions + unrelated to ObjectStack metadata. license: Apache-2.0 compatibility: Requires @objectstack/spec 17.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "1.3" + version: "1.4" domain: ai - tags: agent, tool, skill, conversation, llm, embedding, mcp + tags: agent, tool, skill, knowledge, llm, embedding, mcp --- # AI Agent Design — ObjectStack AI Protocol -Expert instructions for designing AI skills, tools, and knowledge sources — -and the platform agents they plug into — using the ObjectStack specification. -This skill covers the Agent → Skill → Tool three-tier architecture aligned with -Salesforce Agentforce, Microsoft Copilot Studio, and ServiceNow Now Assist -patterns. - -> **Edition boundary (`service-ai` → cloud; open = MCP-only).** -> The in-UI AI **runtime** — the `ask` / `build` agents, in-product chat, and the -> `/api/v1/ai/*` routes (`@objectstack/service-ai`) — ships in the **cloud / -> Enterprise** distribution, not the open framework. The agent / skill / tool -> **schemas** in `@objectstack/spec/ai` stay open, so you author `*.skill.ts` / -> `*.tool.ts` as source either way (`*.agent.ts` is platform-internal) — but -> they only execute in a cloud / EE host. On the **open edition** there is no in-product agent: expose the -> app to your own AI via `@objectstack/mcp` (BYO-AI) for data query, and author -> metadata in **source mode** with an AI coding agent (Claude Code, Cursor). - ---- - -## When to Use This Skill - -- You need to define **skills** — bundles of related tools bound to the - `ask` / `build` surfaces. -- You are configuring **tools** for data queries, actions, or integrations. -- You want to index ObjectStack data as a **knowledge source** for RAG - retrieval. -- You are choosing and configuring **LLM models** (model registry). -- You need to read or review **agent** configuration — platform-internal; - third parties extend agents via skills, not by authoring them. - ---- - -## Three-Tier Architecture +> **Edition boundary. ☁️ marks a cloud / Enterprise surface, and is not restated +> below.** The in-UI AI **runtime** — the `ask` / `build` agents, in-product +> chat, `/api/v1/ai/*` (`@objectstack/service-ai`) — ships cloud / EE, not in the +> open framework. The **schemas** in `@objectstack/spec/ai` stay open, so you +> author `*.skill.ts` / `*.tool.ts` as source either way, but they only execute +> in a cloud / EE host. On the **open edition** there is no in-product agent: +> expose the app to your own AI over **MCP** (below), and author metadata in +> **source mode** with an AI coding agent (Claude Code, Cursor). ``` Agent → Skill → Tool @@ -60,190 +36,57 @@ Agent → Skill → Tool └─ Autonomous actor with role, instructions, and guardrails ``` -### Why Three Tiers? - -| Tier | Analogy | Reuse Level | -|:-----|:--------|:------------| -| **Agent** | Job role (e.g., "Help Desk Agent") | Per use-case | -| **Skill** | Competency (e.g., "Case Management") | Across agents | -| **Tool** | Specific operation (e.g., "create_record") | Across skills | - -> **Best practice:** Always model via Skills first. Direct tool assignment to -> agents is supported but considered legacy. Skills provide better -> discoverability, instruction scoping, and reuse. - -### Built-in agents: `ask` & `build` (ADR-0063 / ADR-0064) - -The runtime ships **exactly two** platform agents, bound by *surface* — the user -never picks from a roster; the surface they are in selects the agent: - -- **`ask`** — the **data product** (≈ Claude Chat). Conversational read / query / - explore over records, plus running the business **actions** the app already - exposes. End-user audience, RLS-bounded. Canonical id `ask` (`ASK_AGENT_NAME`). - **Cloud / Enterprise** — the `ask` runtime ships in the closed cloud AI runtime - (`@objectstack/service-ai`); it is the implicit copilot for any cloud / EE app - that does not pin `app.defaultAgent`. (Open editions have no in-product `ask`; - use MCP.) -- **`build`** — the **authoring product** (≈ Claude Code). Agentic authoring of - *metadata* (objects, fields, views, flows) through plan → draft → verify → - publish. Builder audience, governance-gated. Canonical id `build`. Cloud-only · - paid — ships in the cloud AI Studio plugin; Studio pins it via `app.defaultAgent`. - -There is **no per-turn intent classifier**: a `build`-shaped request arriving at -`ask` is declined and redirected to the Builder, never silently re-routed into -authoring (ADR-0063 §1/§5). - -> **Legacy names are aliases only.** `data_chat`→`ask` and -> `metadata_assistant`→`build` resolve through the alias table for old bookmarks -> and persisted `agent_id`s; they are **not** vocabulary — always write `ask` / -> `build`. `*.agent.ts` is closed to third parties (`agent` type is -> `allowRuntimeCreate:false, allowOrgOverride:false`): you extend the platform -> with **skills**, never by authoring an agent (ADR-0063 §2). - -#### Skill → agent affinity: the `surface` field (ADR-0063 §3) - -Every skill declares which surface it binds to via -`surface: 'ask' | 'build' | 'both'` (defaults to `'ask'`). A skill may bind only -to an agent whose surface it matches; `'both'` binds to either. The runtime -enforces this in `resolveActiveSkills` at load time — an incompatible binding is a -**fast load error**, not a silent mis-scope. An agent's tool set is the **union of -its surface-compatible skills' tools** — there is no global fall-through -(ADR-0064), so `ask` cannot author by construction. - -The built-in skills and their affinities: - -| Skill | `surface` | Owns | -|---|---|---| -| `schema_reader` | `both` | `list_objects`, `describe_object`, `query_data` | -| `data_explorer` | `ask` | `query_records`, `get_record`, `aggregate_data`, `visualize_data` | -| `actions_executor` | `ask` | `action_*` (the business actions an object exposes) | -| `metadata_authoring` + `solution_design` | `build` | metadata draft / verify / publish + blueprint propose / apply | - -To grant data exploration to your own (platform-internal) agent, add -`data_explorer` / `schema_reader` to its `skills[]`; deactivating a skill -(`active: false`) revokes that capability for every agent that references it. - -> **All four are cloud-runtime skills — none resolves on OSS.** The open -> framework ships *no* in-product agent (there is no `service-ai` package), so -> `surface` / `tools` / `triggerConditions` are authored for cloud and inert. -> What a skill DOES serve on the open edition is its `instructions`, projected -> onto the MCP `prompts` primitive by `@objectstack/mcp`: a skill with blank -> `instructions`, or `active: false`, is not listed as a prompt at all. - -> **`visualize_data`:** the only built-in tool that draws a chart — -> it aggregates an object and emits an inline `data-chart` part. Auto-registered -> **only** when an analytics service (`IAnalyticsService`) is wired; `query_data` / -> `aggregate_data` return numbers, not charts. - -> **Ops:** set `AI_DAILY_USER_MESSAGES=` to cap user turns per user per day -> (backed by the `ai_usage_daily` object; no-op if unset). Adapter health is -> observable at `GET /api/v1/ai/status`; invalid `ai` settings are rejected at -> save time. - ---- - -## Agent Configuration - -> **Reference only — third parties do not author agents.** The `agent` type is -> closed (`allowRuntimeCreate:false`; ADR-0063 §2): the platform ships exactly -> `ask` and `build`, maintained by platform / cloud plugin authors. You extend -> the platform with **skills + tools** (and knowledge sources) — never by -> adding an agent. This section documents `AgentSchema` for reading existing -> agents and for platform-internal work. - -### Required Properties - -| Property | Type | Description | -|:---------|:-----|:------------| -| `name` | `snake_case` | Unique agent identifier | -| `label` | string | Human-readable name | -| `role` | string | Agent's persona/role description | -| `instructions` | string | System prompt — detailed behavioural guidance | - -### Important Optional Properties - -| Property | Purpose | -|:---------|:--------| -| `skills` | Array of skill names — **the** capability model (ADR-0064) | -| ~~`tools`~~ | REMOVED in protocol 17 — the inline slot resolved names against the *full* registry with no surface check. A parse error now; move each reference into a skill | -| `surface` | `'ask' \| 'build'` — the product surface this agent is (default `'ask'`) | -| `model` | LLM model configuration — `provider`, `model`, `temperature`, `maxTokens`, `topP` | -| ~~`knowledge`~~ | REMOVED in protocol 17 — declaring sources/indexes on an agent never scoped retrieval (`search_knowledge` takes `sourceIds` from the LLM's tool-call arguments). Restrict at the knowledge-service/source level; describe intended grounding in `instructions` | -| `guardrails` | `maxTokensPerInvocation`, `maxExecutionTimeSec`, `blockedTopics` — declared only | -| `structuredOutput` | Output format (JSON schema, regex, etc.) — declared only | -| `planning` | Autonomous reasoning — `maxIterations` (default 10) | -| `memory` | `longTerm` persistence + `reflectionInterval` — declared only | -| `permissions` | Permission-set capabilities required to use the agent | -| `active` | Enable/disable the agent | - -There is **no top-level `temperature` / `maxTokens`** on an agent — sampling -parameters live under `model` (`AIModelConfigSchema`). - -### Agent Example - - -```typescript -import { defineAgent } from '@objectstack/spec'; - -export default defineAgent({ - name: 'support_tier_1', - label: 'First Line Support', - role: 'Help Desk Assistant for customer support cases', - instructions: ` - You are a friendly and professional help desk assistant. - - RULES: - - Always greet the customer by name if available. - - Search the knowledge base before creating a new case. - - Escalate to a human agent if the issue is critical or security-related. - - Never share internal system details with customers. - - Respond in the customer's preferred language. - `, - skills: ['case_management', 'knowledge_search'], - model: { - provider: 'openai', - model: 'gpt-4o', - temperature: 0.3, - }, - guardrails: { // declared only — nothing reads these yet - blockedTopics: ['internal_pricing', 'employee_data'], - maxTokensPerInvocation: 8000, - maxExecutionTimeSec: 60, - }, -}); -``` +Agents are platform-internal (☁️ `ask` / `build`; reference section at the +bottom). **Skills are your entry point** — you extend the platform by authoring +skills, and a skill reaches tools by naming them. --- ## Skill Configuration -A **Skill** is a named bundle of tools with dedicated instructions and -trigger conditions. - -### Required Properties +A **Skill** is a named bundle of tools with dedicated instructions and trigger +conditions. -| Property | Type | Description | +| Required | Type | Description | |:---------|:-----|:------------| -| `name` | `snake_case` | Unique skill identifier (`/^[a-z_][a-z0-9_]*$/`) | -| `label` | string | Human-readable name | -| `tools` | `string[]` | Tool names this skill grants access to (trailing wildcard allowed, e.g. `action_*`) | - -### Important Optional Properties +| `name` | `snake_case` | Unique skill id (`/^[a-z_][a-z0-9_]*$/`) | +| `label` | string | Display name | +| `tools` | `string[]` | Tool names this skill grants (trailing wildcard allowed, e.g. `action_*`) — every name must resolve; see the ladder below | -| Property | Purpose | +| Optional | Purpose | |:---------|:--------| -| `surface` | `'ask' \| 'build' \| 'both'` — agent surface affinity (default `'ask'`; see above) | -| `description` | What the skill does — helps the agent decide when to use it | -| `instructions` | LLM prompt guidance specific to this skill's context | -| `triggerConditions` | Programmatic activation rules | -| `active` | Is the skill enabled (default: `true`) | - -> A skill has **no `permissions` key** — removed in 17.0.0. Skill invocation -> was never gated by it (the registry reads only `active` / `triggerConditions` / -> `tools`), and a security-shaped field that enforces nothing is worse than no -> field at all. Gate access at the **agent** instead — `access` / `permissions` on -> `defineAgent` are enforced at the chat route — or on the underlying actions the -> skill's tools call (permission sets, ADR-0066). +| `surface` | `'ask' \| 'build' \| 'both'` — which platform agent may bind this skill (default `'ask'`). Enforced in `resolveActiveSkills` at load: an incompatible binding is a **fast load error** (ADR-0063 §3) | +| `description` | What the skill does — the agent reads it to decide when to use the skill | +| `instructions` | Prompt guidance for this skill's context. **The one field the open edition reads**: `@objectstack/mcp` projects it onto the MCP `prompts` primitive, so a skill with blank `instructions` — or `active: false` — is not listed as a prompt at all | +| `triggerConditions` | Programmatic activation rules (below) | +| `active` | Enabled (default `true`) | + +> A skill has **no `permissions` key** — removed in 17.0.0 because it gated +> nothing. Gate at the **agent** (`access` / `permissions` on `defineAgent`, +> enforced at the chat route) or on the actions the tools call (ADR-0066). + +### Naming the tools: the resolution ladder + +A `skill.tools[]` entry resolves against the union of three sources +(`packages/lint/src/validate-ai-tool-references.ts:148-171`): + +1. **`stack.tools[].name`** — records your own stack declares. ADR-0109: the + default third-party path declares **none** (see *Tool metadata* below). +2. **`PLATFORM_PROVIDED_TOOL_NAMES`** — the 30 statically-registered platform + tools, grouped by owning package in + `packages/spec/src/system/constants/platform-tool-names.ts:38-82` — 6 data / + knowledge tools from `service-ai` (`query_records`, `get_record`, + `query_data`, `aggregate_data`, `search_knowledge`, `visualize_data`) and 24 + schema / metadata / package tools from `service-ai-studio`. Read that file for + the exact set: it is the registry the lint rule checks against. +3. **`action_`** — one tool per AI-exposed Action of your own stack + (`PLATFORM_TOOL_FAMILY_PREFIXES`). **The default path** for anything your app + does. + +A name in none of the three raises **`ai-skill-tool-unresolved`** (warning, +ADR-0078 advisory-first). The rule exists because one audited app shipped 10 +fictional tools across 6 skills through `validate` / `lint` clean, and became a +copilot claiming abilities it did not have. ### Skill Example @@ -254,19 +97,16 @@ import { defineSkill } from '@objectstack/spec'; export default defineSkill({ name: 'case_management', label: 'Case Management', - description: 'Create, update, query, and escalate support cases.', + description: 'Triage, query, and escalate support cases.', instructions: ` When managing cases: - Always check for duplicate cases before creating a new one. - - Set priority based on customer tier: Enterprise → High, Pro → Medium, Free → Low. - - Escalated cases must include a summary of actions already taken. + - Set priority by customer tier: Enterprise → High, Pro → Medium, Free → Low. + - Escalated cases must summarise the actions already taken. `, - tools: [ - 'query_support_case', - 'create_support_case', - 'update_support_case', - 'escalate_case', - ], + // Every name resolves: two platform tools (rung 2) plus one action tool + // materialised from this stack's own `escalate_case` Action (rung 3). + tools: ['query_records', 'get_record', 'action_escalate_case'], triggerConditions: [ { field: 'objectName', operator: 'eq', value: 'support_case' }, ], @@ -276,121 +116,109 @@ export default defineSkill({ ### Trigger Conditions -| Operator | Meaning | -|:---------|:--------| -| `eq` | Equals | -| `neq` | Not equals | -| `in` | Value is in array | -| `not_in` | Value is not in array | -| `contains` | String contains substring | - ---- +Each condition is `{ field, operator, value }`, ANDed with its siblings. The +shape of `value` is **coupled to `operator`** — a mismatch is a parse error, not +a coercion (`packages/spec/src/ai/skill.zod.ts:37-51, 139-202`): -## Tool Configuration +| Operator | Meaning | `value` must be | +|:---------|:--------|:----------------| +| `eq` | Equals | a **string** — an array is reference identity, so it never matches | +| `neq` | Not equals | a **string** — an array would always fire | +| `in` | In list | an **array** (empty `[]` is allowed and is a real predicate) | +| `not_in` | Not in list | an **array** | +| `contains` | Substring, or array subset | **either** — both spellings execute | -Tools are the atomic operations that skills expose to agents. +Nothing matches phrases: write routing as `triggerConditions` and +natural-language intent in `description` / `instructions`, where the model reads it. -### First-Class Tool Metadata (`defineTool`) - -A tool authored as metadata (`type: 'tool'`, `*.tool.ts`) is validated by -`ToolSchema`: required `name` / `label` / `description`, a **JSON Schema** -`parameters` object, plus optional `objectName` and `outputSchema`. `ToolSchema` -is **strict** — an unknown key (a typo, or a retired key) is a parse error, not -a silent strip. Retired in protocol 17: `category`, `permissions`, -`active` and `builtIn` (all were authorable and inert; `permissions` gated -nothing and `active: false` withdrew nothing — the rejection message carries -each key's replacement), joining `requiresConfirmation`. - - -```typescript -import { defineTool } from '@objectstack/spec'; - -export default defineTool({ - name: 'create_case', - label: 'Create Support Case', - description: 'Creates a new support case record', - parameters: { - type: 'object', - properties: { - subject: { type: 'string', description: 'Case subject' }, - priority: { type: 'string', enum: ['low', 'medium', 'high'] }, - }, - required: ['subject'], - }, - objectName: 'support_case', -}); -``` - -To gate what a tool can do, gate the underlying action -(`action.requiredPermissions`, ADR-0066) or the objects it touches; to withdraw -a tool, remove it from the skills/agents that reference it. Categorization, if -you need it, belongs on the action side (`action.ai.category` — live, but -listing-only: carried onto the tool, never sent to the model). - -> **Tool metadata is a read-only projection — not an execution entry point.** -> `ToolSchema` has no `handler` / `implementation` field, and no framework -> executor loads a metadata-authored tool. The runtime executes a -> separately-registered `AIToolDefinition` (cloud `@objectstack/service-ai`); -> tool metadata is a one-way projection for Studio / discovery. Do not expect a -> hand-authored tool to run in the open edition. - -### Actions as AI Tools — opt-in +--- -> **Cloud / EE runtime.** `registerActionsAsTools()`, `AIServicePlugin`, and -> the HITL approval queue below ship in `@objectstack/service-ai` — the closed -> cloud / Enterprise runtime, not an open package. On the open edition, expose -> actions to your own AI via `@objectstack/mcp` instead. +## Actions as AI Tools — the default path ☁️ -You usually **don't author tool definitions by hand** for action invocation. An `Action` you attach to an object via `defineObject({ actions: [...] })` becomes a tool named `action_` **only when it opts in** — `ai.exposed: true` (default `false`) plus an `ai.description` of ≥ 40 chars, refused by the parse without it (ADR-0011). `registerActionsAsTools()` (from `AIServicePlugin`) walks the opted-in ones; exposure is never automatic, and there is no opt-out key. +You usually **don't author tool definitions by hand** for action invocation. An +`Action` attached to an object via `defineObject({ actions: [...] })` becomes a +tool named `action_` **only when it opts in** — `ai.exposed: true` +(default `false`) plus an `ai.description` of ≥ 40 chars, refused by the parse +without it (ADR-0011). `registerActionsAsTools()` walks the opted-in ones; +exposure is never automatic, and there is no opt-out key. Three action types dispatch headlessly: | `action.type` | Dispatch | Wiring | |:---|:---|:---| | `script` | `IDataEngine.executeAction(object, target, ctx)` — same as Studio's row toolbar | none | -| `api` | HTTP call to `action.target` (`fetch`-based by default) | `AIServicePlugin({ apiActionBaseUrl, apiActionHeaders })` or custom `apiClient` | +| `api` | HTTP call to `action.target` (`fetch`-based by default) | `AIServicePlugin({ apiActionBaseUrl, apiActionHeaders })` or a custom `apiClient` | | `flow` | `IAutomationService.execute(target, { triggerData })` | `automation` service registered with the kernel | -**Skipped even when opted in:** -- UI-only types (`url`, `modal`, `form`). -- Dangerous variants — the declared `mode: 'delete'` / `variant: 'danger'` only (`confirmText` is dialog copy, *not* a destructive signal; `ai.requiresConfirmation` overrides either way) — **unless** the plugin is started with `enableActionApproval: true`, in which case they route through the HITL approval queue (see below). - -**`type:'api'` body assembly** (last wins): user params → `recordIdParam` (using `recordIdField`, default `'id'`) → `bodyExtra`. `bodyShape: { wrap: 'data' }` nests user params under `data` while keeping `recordIdParam` flat. +**Skipped even when opted in:** UI-only types (`url`, `modal`, `form`); and +dangerous variants — the declared `mode: 'delete'` / `variant: 'danger'` only +(`confirmText` is dialog copy, *not* a destructive signal; `ai.requiresConfirmation` +overrides either way) — **unless** the plugin runs with `enableActionApproval: true`, +which routes them through the approval queue instead. + +**`type:'api'` body assembly** (last wins): user params → `recordIdParam` (using +`recordIdField`, default `'id'`) → `bodyExtra`. `bodyShape: { wrap: 'data' }` +nests user params under `data` while keeping `recordIdParam` flat. + +**Human-in-the-loop.** `AIServicePlugin({ enableActionApproval: true })` (default +`false`) persists an `ai_pending_actions` row instead of running a dangerous +action and returns `{ status: 'pending_approval', pendingActionId }`; an operator +approves or rejects in Studio's **AI Pending Actions** inbox (or via +`GET/POST /api/v1/ai/pending-actions/...`). Whether *your* action is held is +decided entirely by the skip rule above — `actionSkipReason(action, ctx)` and +`actionRequiresApproval(action)` (from `@objectstack/service-ai`) answer both +questions, and Studio's "AI exposure" diagnostics use the same predicates. -Use `actionSkipReason(action, ctx)` (exported from `@objectstack/service-ai` — cloud-only, not importable on the open edition) when authoring an action and you want to know *why* it isn't surfacing in chat. Studio's "AI exposure" diagnostics use the same predicate. Pair with `actionRequiresApproval(action)` to know whether a registered action will be routed through HITL. +--- -### Human-In-The-Loop approval +## MCP — the open-edition AI path -> **Cloud / EE runtime.** The HITL approval queue is part of -> `@objectstack/service-ai` and is not available in the open framework. +`@objectstack/mcp` publishes your app over the Model Context Protocol, so an +external agent (Claude, Cursor, Codex, …) can read it and act on it with **no +cloud licence**. (Consuming *external* MCP servers is the inbound sibling, +`@objectstack/connector-mcp`.) ```ts -kernel.use(new AIServicePlugin({ - enableActionApproval: true, // opt in; default is false - apiActionBaseUrl: process.env.OS_AI_ACTION_API_BASE_URL, -})); -``` +import { MCPServerPlugin } from '@objectstack/mcp'; -Flow: -1. LLM picks `action_delete_task` → runtime persists an `ai_pending_actions` row and returns `{ status: 'pending_approval', pendingActionId }`. -2. Operator triages via Studio's **AI Pending Actions** inbox (or the REST endpoints: `GET/POST /api/v1/ai/pending-actions/...`). -3. Approve → service re-runs the action via the pre-registered bypass-approval dispatcher; row transitions to `executed` / `failed`. -4. Reject → row transitions to `rejected` with an optional reason. +kernel.use(new MCPServerPlugin()); +``` -Programmatic API on `IAIService`: `proposePendingAction`, `approvePendingAction`, `rejectPendingAction`, `listPendingActions`. All are optional (returns clear error when no `IDataEngine` is wired). +The HTTP surface is **default-on**, served per-request by the runtime dispatcher +at **`POST /api/v1/mcp`** — nothing to start (`OS_MCP_SERVER_ENABLED=false` opts +out). The long-lived **stdio** transport has its own switch and defaults off: +`autoStart: true`, or `OS_MCP_STDIO_ENABLED=true`. (`OS_MCP_SERVER_ENABLED=true` +still starts it — deprecated, and it warns at boot.) + +The tools a client then sees — from `registerObjectTools` / `registerActionTools`, +each family gated by the caller's OAuth scopes, every call under the caller's +permissions and row-level security: + +| Tools | What they do | +|:--|:--| +| `list_objects`, `describe_object` | schema introspection (system objects filtered out by default) | +| `query_records`, `get_record` | reads — filter, field selection, sorting, pagination | +| `aggregate_records` | GROUP BY totals; registered only when the data bridge implements `aggregate` | +| `validate_expression` | build-accurate CEL check before a formula is authored into metadata | +| `create_record`, `update_record`, `delete_record` | writes; `delete_record` is annotated destructive | +| `list_actions`, `run_action` | your AI-exposed business Actions — the same `ai.exposed` opt-in as above | --- ## Knowledge Sources (RAG) -The platform's RAG primitive is the **KnowledgeSource** -(`KnowledgeSourceSchema` in `@objectstack/spec/ai`): declarative metadata -pairing *what to index* with the id of an `IKnowledgeAdapter` that does the -work. Sources are registered at runtime via -`IKnowledgeService.registerSource()` (there is no `defineStack` collection for -them), and the `search_knowledge` tool exposes registered sources to agents. +The RAG primitive is the **KnowledgeSource** (`KnowledgeSourceSchema` in +`@objectstack/spec/ai`): declarative metadata pairing *what to index* with the id +of an `IKnowledgeAdapter` that does the work. There is no `defineStack` +collection for them — they are registered at runtime, and the ☁️ +`search_knowledge` tool exposes registered sources to agents. -### KnowledgeSource Structure +**Wiring (open packages).** `KnowledgeServicePlugin({ sources })` from +`@objectstack/service-knowledge` registers `IKnowledgeService` with the kernel and +each source at boot (`service.registerSource` still works later). The `adapter` +id resolves against an adapter plugin — `@objectstack/knowledge-memory` +(`'memory'`, dev/test) or `@objectstack/knowledge-ragflow` (`'ragflow'`); +without one the id is unresolvable. | Property | Purpose | |:---------|:--------| @@ -399,20 +227,16 @@ them), and the `search_knowledge` tool exposes registered sources to agents. | `adapter` | Adapter id (e.g. `'ragflow'`, `'memory'`), resolved via `IKnowledgeService.registerAdapter` | | `adapterConfig` | Adapter-specific configuration (opaque to the service) | | `source` | What gets indexed — discriminated on `kind`: `'object'` \| `'file'` \| `'http'` | -| `embedding` | Optional `EmbeddingModelSchema` ref (`provider`, `model`, `dimensions`) — adapters that manage embeddings internally (RAGFlow, Dify, Vectara) may ignore it | +| `embedding` | Optional `EmbeddingModelSchema` ref (`provider`, `model`, `dimensions`) — adapters managing embeddings internally (RAGFlow, Dify, Vectara) may ignore it | | `vectorStore` | Optional `VectorStoreSchema` ref (`provider`, `collection`) — same caveat | -| `refresh` | `onRecordChange` (default `true` for object sources) + optional `cron` (surfaced for an external scheduler, not self-scheduled) | -| `aiExposed` | Whether `search_knowledge` may expose this source to agents (default `true`) | +| `refresh` | `onRecordChange` (default `true` for object sources) + optional `cron` (for an external scheduler; not self-scheduled) | +| `aiExposed` | May `search_knowledge` expose this source to agents (default `true`) — `false` indexes it without making it agent-searchable | -Source kinds: - -| `source.kind` | Fields | -|:--------------|:-------| -| `object` | `object`, `contentFields[]` (min 1; `*` = every readable text field), `metadataFields?`, `where?` (ObjectQL `where` syntax) | -| `file` | `prefix` (storage prefix, e.g. `kb/handbooks/`), `mimeTypes?` | -| `http` | `urls[]`, `userAgent?` | - -### Knowledge Source Example +| `source.kind` | Fields | Hygiene | +|:--------------|:-------|:--------| +| `object` | `object`, `contentFields[]` (min 1; `*` = every readable text field), `metadataFields?`, `where?` (ObjectQL `where` syntax) | `where` keeps drafts and archived rows out of the index; `contentFields` takes meaningful text only (never system fields or ids, `*` sparingly); project what you will filter on at query time (`status`, `owner_id`, `tags`) into `metadataFields` | +| `file` | `prefix` (storage prefix, e.g. `kb/handbooks/`), `mimeTypes?` | narrow the prefix instead of indexing a whole bucket | +| `http` | `urls[]`, `userAgent?` | enumerate URLs explicitly — there is no crawler | ```typescript @@ -433,153 +257,155 @@ export const supportKb: KnowledgeSource = KnowledgeSourceSchema.parse({ }); ``` -> **Chunking, top-K, score thresholds, and rerankers are NOT platform -> metadata.** The spec deliberately scopes them out (`embedding.zod.ts`): -> chunking strategies, retrieval pipelines, and RAG orchestration belong to -> the adapter (`adapterConfig`) or application code. The platform only carries -> the embed + vector primitives so any RAG strategy can be built on top. - -### Knowledge Source Best Practices - -1. **Filter with `where`.** Index only published/active records - (`where: { published: true }`) so draft or archived content never enters - the index. -2. **Index only meaningful text via `contentFields`.** Do not include system - fields or IDs; use `*` (all readable text fields) sparingly. -3. **Project filter fields into `metadataFields`** (e.g. `status`, `owner_id`, - `tags`) so searches can be narrowed at query time. -4. **Hide with `aiExposed: false`** when a source should be indexed but not - agent-searchable. -5. **Tune relevance in the adapter, not the metadata.** Top-K, thresholds, and - reranking are configured in your RAG backend (via `adapterConfig`), not in - ObjectStack metadata. +> **Chunking, top-K, score thresholds and rerankers are NOT platform metadata.** +> The spec scopes them out deliberately (`embedding.zod.ts`): they belong to the +> adapter (`adapterConfig`) or application code. The platform carries only the +> embed + vector primitives, so any RAG strategy can be built on top. --- -## Model Configuration - -### Supported Providers - -| Provider | Models | Use Case | -|:---------|:-------|:---------| -| `openai` | GPT-4o, GPT-4o-mini, o1, o3-mini | General purpose, reasoning | -| `anthropic` | Claude Sonnet 4, Claude Haiku | Long context, safety | -| `azure_openai` | Same as OpenAI, enterprise managed | Compliance, data residency | -| `local` | Ollama, vLLM, llama.cpp | On-premise, air-gapped | - -> The inline agent `model.provider` enum is the narrow set above -> (`openai` / `azure_openai` / `anthropic` / `local`). **Model-registry** entries -> (`ModelProviderSchema`) accept a wider set: also `google`, `cohere`, -> `huggingface`, `custom`. - -### Model Selection Guidelines - -| Scenario | Recommended | -|:---------|:------------| -| Complex reasoning, multi-step planning | GPT-4o / Claude Sonnet 4 | -| High-volume, low-latency | GPT-4o-mini / Claude Haiku | -| Sensitive data, on-premise | Local models via Ollama | -| Structured data extraction | Any model + `structuredOutput` config | - -### Temperature Guidelines - -| Value | Use Case | -|:------|:---------| -| `0.0–0.3` | Factual Q&A, data extraction, code generation | -| `0.3–0.7` | Conversational agents, customer support | -| `0.7–1.0` | Creative writing, brainstorming | -| `> 1.0` | Experimental / highly creative (use with caution) | +## Tool metadata (`defineTool`) — not the default path + +**Default: declare no tool records.** A skill names a platform tool or an +`action_` (ladder above). ADR-0109: a `stack.tools` record "exists only for +AI-presentation refinement (Phase 2: LLM description, parameter narrowing, flow +exposure) and has no runtime reader until that lands" +(`packages/spec/src/stack.zod.ts:595-602`). Author one only to refine how an +existing capability is *presented* — never to add one, and never to make +something executable: `ToolSchema` has no `handler` / `implementation` field, and +no framework executor loads a metadata-authored tool. + +`ToolSchema` is **strict** — an unknown key (a typo, or a retired one) is a parse +error, not a silent strip. Required `name` / `label` / `description` and a JSON +Schema `parameters` object; optional `objectName` and `outputSchema` (⚠️ +experimental — its top-level keys are folded into the tool description shown to +the model, and outputs are **not** validated). Retired in protocol 17: +`category`, `permissions`, `active`, `builtIn` — all authorable and inert — +joining `requiresConfirmation`; each rejection carries its replacement. + +Gate what a tool can do on the underlying action (`action.requiredPermissions`, +ADR-0066) or the objects it touches; to withdraw one, remove it from the skills +referencing it. Categorization belongs on the action (`action.ai.category` — +live, but listing-only: carried onto the tool, never sent to the model). --- -## Structured Output +## Model Configuration -Force the agent to respond in a specific format: +Sampling parameters live under an agent's `model` (`AIModelConfigSchema`) — +there is **no top-level `temperature` / `maxTokens`** on an agent +(`packages/spec/src/ai/agent.zod.ts:36-40`): -```typescript -structuredOutput: { - format: 'json_schema', - schema: { - type: 'object', - properties: { - summary: { type: 'string' }, - priority: { type: 'string', enum: ['low', 'medium', 'high'] }, - action_items: { type: 'array', items: { type: 'string' } }, - }, - required: ['summary', 'priority'], - }, - strict: true, // enforce exact schema compliance (default: false) - maxRetries: 3, // max retries on validation failure (default: 3) -} -``` - -On validation failure the runtime retries by default -(`retryOnValidationFailure: true`). Optional extras: `fallbackFormat` and a -`transformPipeline` of post-processing steps (`trim`, `parse_json`, -`validate`, `coerce_types`). There is no `retry` object — the knobs are -`retryOnValidationFailure` + `maxRetries`. +| Key | Contract | +|:--|:--| +| `provider` | Inline enum `openai` \| `azure_openai` \| `anthropic` \| `local` (default `openai`). **Model-registry** entries (`ModelProviderSchema`) accept a wider set: also `google`, `cohere`, `huggingface`, `custom` | +| `model` | Free string — the provider's own model name | +| `temperature` | `min(0).max(2)`, default `0.7`. **Outside 0–2 is a parse error** | +| `maxTokens` / `topP` | Optional numbers, unbounded by the schema | --- ## Common Pitfalls -1. **Overly broad instructions.** Agents with vague instructions hallucinate - more. Be specific about what the agent should and should not do. -2. **Too many tools per skill.** Keep skills focused (3–8 tools). If a skill - has 15+ tools, split it. -3. **Mistaking `guardrails` for a gate.** `guardrails` / `memory` / - `structuredOutput` are declared only — no runtime reads them, and real - limits come from the quota service. For a gate that is **enforced**, use - `enableActionApproval: true` (HITL queue, cloud) for AI-exposed actions, - `ai.requiresConfirmation` on the **action**, or `approval: 'always'` on an - MCP tool binding. AI metadata edits are already gated: they land as drafts a - human must publish (ADR-0033). - ⚠️ `requiresConfirmation` on the **tool** was REMOVED (ADR-0033 §2) — - it was read by no execution path, so it produced no pause. `ToolSchema` is - strict, so authoring it now fails the parse with the migration attached. - There is no `requireApprovalFor` field. -4. **Ignoring tool descriptions.** The LLM uses tool `description` to decide - when to call it. Poor descriptions = wrong tool selection. -5. **Expecting a phrase to activate a skill.** Nothing matches phrases; write - routing as `triggerConditions` and intent in `description`/`instructions`. -6. **Indexing everything.** A knowledge source without a `where` filter and +1. **Mistaking `guardrails` for a gate.** `guardrails` / `memory` / + `structuredOutput` are declared only — no runtime reads them, and real limits + come from the quota service. For a gate that is **enforced**, use + `enableActionApproval: true` (approval queue ☁️), `ai.requiresConfirmation` on + the **action**, or `approval: 'always'` on an MCP tool binding. AI metadata + edits are already gated: they land as drafts a human must publish (ADR-0033). + ⚠️ `requiresConfirmation` on the **tool** was REMOVED (ADR-0033 §2) — no + execution path read it, so it produced no pause; `ToolSchema` is strict, so + authoring it now fails the parse. There is no `requireApprovalFor` field. +2. **Indexing everything.** A knowledge source without a `where` filter and curated `contentFields` fills the index with drafts and boilerplate that pollute retrieval. Source hygiene is the metadata's job; relevance tuning - (top-K, thresholds, reranking) belongs to the adapter. + belongs to the adapter. --- -## App AI Blueprint (Skills + Tools + Knowledge) - -Reference layout for a scaffolded app: +## App AI Blueprint | Layer | File | Pattern | |:--|:--|:--| -| Reusable skill | `src/skills/lead-qualification.skill.ts` | `defineSkill` — `instructions` + trigger conditions + bounded toolset; pick a `surface` | -| Tool metadata | `src/tools/query-leads.tool.ts` | `defineTool` — JSON-Schema `parameters`; a discovery projection, not an executor (see caveat above) | -| Knowledge source | `src/knowledge/sales-kb.ts` | `KnowledgeSourceSchema` metadata, registered at runtime via `IKnowledgeService.registerSource()` | -| Central registration | `defineStack({ skills: [...], tools: [...] })` | `agents` / `tools` / `skills` are the only AI stack collections — knowledge sources have none; agents are platform-supplied | +| Reusable skill | `src/skills/lead-qualification.skill.ts` | `defineSkill` — `instructions` + trigger conditions + a toolset where every name resolves; pick a `surface` | +| Knowledge source | `src/knowledge/sales-kb.ts` | `KnowledgeSourceSchema` metadata, registered at boot via `KnowledgeServicePlugin({ sources })` | +| Central registration | `defineStack({ skills: [...] })` | `agents` / `tools` / `skills` are the only AI stack collections — knowledge sources have none, agents are platform-supplied, and `tools` is not the default path | + +Default for metadata apps: push business capability into **skills**, expose +business logic as **AI-exposed Actions**, wire domain knowledge through +**knowledge sources**. + +--- -Default for metadata apps: push business capability logic into **skills**, keep -tools atomic, and wire domain knowledge through **knowledge sources**. +## Platform agents — reference only ☁️ + +The `agent` type is **closed** to third parties (`allowRuntimeCreate:false`, +`allowOrgOverride:false`; ADR-0063 §2) and `os g agent` is retired: a +stack-authored agent is filtered out of `listAgents()`, refused outright by +`loadAgent()`, and 404s on chat. You extend the platform with **skills**, never +by adding an agent. `AgentSchema`'s full field list is in +[references/_index.md](./references/_index.md). + +The runtime ships **exactly two** agents, bound by *surface* — the user never +picks from a roster, the surface they are in selects the agent. **`ask`** is the +data product (≈ Claude Chat): conversational read / query / explore over records +plus running the business actions the app exposes, end-user audience, +RLS-bounded, canonical id `ask` (`ASK_AGENT_NAME`), and the implicit copilot for +any app that does not pin `app.defaultAgent`. **`build`** is the authoring +product (≈ Claude Code): agentic authoring of *metadata* through plan → draft → +verify → publish, builder audience, governance-gated, pinned by Studio via +`app.defaultAgent`. There is **no per-turn intent classifier** — a `build`-shaped +request arriving at `ask` is declined and redirected, never silently re-routed +into authoring (ADR-0063 §1/§5). `data_chat`→`ask` and +`metadata_assistant`→`build` resolve through the alias table for old bookmarks +and persisted `agent_id`s; they are **not** vocabulary — always write `ask` / +`build`. + +An agent's tool set is the **union of its surface-compatible skills' tools** — +there is no global fall-through (ADR-0064), so `ask` cannot author by +construction; deactivating a skill (`active: false`) revokes that capability for +every agent referencing it. The built-in skills: + +| Skill | `surface` | Owns | +|---|---|---| +| `schema_reader` | `both` | `list_objects`, `describe_object`, `query_data` | +| `data_explorer` | `ask` | `query_records`, `get_record`, `aggregate_data`, `visualize_data` | +| `actions_executor` | `ask` | `action_*` (the business actions an object exposes) | +| `metadata_authoring` + `solution_design` | `build` | metadata draft / verify / publish + blueprint propose / apply | + +All four are cloud-runtime skills — none resolves on the open edition, so +`surface` / `tools` / `triggerConditions` on your own skills are authored for +cloud and inert there; their `instructions` are what MCP serves. +`visualize_data` is the only built-in that draws a chart (an inline `data-chart` +part), auto-registered **only** when an analytics service (`IAnalyticsService`) +is wired — `query_data` / `aggregate_data` return numbers, not charts. + +Two `AgentSchema` keys are **tombstones**, not legacy options — authoring either +is a parse error carrying its migration +(`packages/spec/src/ai/agent.zod.ts:234, :251`): + +| Retired key | REMOVED in protocol 17 — why, and what replaces it | +|:--|:--| +| `agent.tools` | The inline slot resolved names against the *full* registry with no surface check. Not a rename: the migration deletes the key and names each tool that was listed; re-declare each in a skill (ADR-0064) | +| `agent.knowledge` | Declaring sources/indexes on an agent never scoped retrieval — `search_knowledge` takes `sourceIds` from the model's tool-call arguments. Restrict at the knowledge-service / source level; describe intended grounding in `instructions` | --- ## Verify your work -After authoring a `*.skill.ts` / `*.tool.ts` (or platform-internal -`*.agent.ts`) or a model-registry entry, run the author-time gate before -reporting done: +After authoring a `*.skill.ts` / `*.tool.ts` or a model-registry entry, run the +author-time gate before reporting done: ```bash os validate # Zod schema + CEL predicate validation + bindings (no artifact) # or: os build # the same gates, plus emits dist/ ``` -It confirms the agent/tool/model metadata conforms to the protocol and that any -CEL predicate (e.g. a tool's availability condition) parses and resolves. In a -scaffolded project the gate is `npm run validate`. See objectstack-platform → -**Verify your work**. +It confirms the skill / tool / model metadata conforms to the protocol and that +any CEL predicate parses and resolves — in this domain that is a model-registry +`promptTemplate.system` / `.user`; `ToolSchema` carries no expression field of +any kind. In a scaffolded project the gate is `npm run validate`. See +objectstack-platform → **Verify your work**. --- @@ -589,4 +415,3 @@ See [references/_index.md](./references/_index.md) for the full list of Zod schemas (with one-line descriptions) — pointers into `node_modules/@objectstack/spec/src/`. Always `Read` the source for exact field shapes; do not rely on memory of property names. -