diff --git a/content/docs/ai/actions-as-tools.mdx b/content/docs/ai/actions-as-tools.mdx index f440cd0d98..921b1dd82f 100644 --- a/content/docs/ai/actions-as-tools.mdx +++ b/content/docs/ai/actions-as-tools.mdx @@ -7,10 +7,49 @@ description: Expose declarative Action metadata as AI-callable tools with explic Part of the [AI module](/docs/ai) — how existing Action metadata becomes LLM-callable, and the guardrails around it. -Declarative `Action` metadata can be exposed as AI-callable tools, named -`action_`, but exposure is **explicit opt-in**. Add an `ai:` block -with `exposed: true` and an LLM-facing `description`; otherwise the action stays -human/UI-only. +Any business `Action` you already have — a `script` action or a Flow — can be +reached by an LLM as a callable tool. On the **open edition** this happens +through [`@objectstack/mcp`](/docs/ai): your own AI (Claude, Cursor, any MCP +client, or a local model) connects over the Model Context Protocol, and the +server exposes two business-action tools — `list_actions` and `run_action` — +bound to the caller's principal. The agent operates the app the same way the +Console toolbar does, under the same row-level security and permissions. No +cloud service and no `@objectstack/service-ai` are required. + + +**Cloud / Enterprise** layers an in-product chat *runtime* +(`@objectstack/service-ai`, cloud [ADR-0025](https://github.com/objectstack-ai/cloud/blob/main/docs/adr/0025-service-ai-to-cloud-open-mcp-only.md)) +on top of these same actions: it generates one `action_` tool per action, +gates them behind an `ai.exposed` opt-in, and adds a server-side approval queue. +Those pieces are called out below. The open path — the same Action reachable as +an MCP tool by your own AI — is the default. + + +## The open path: Actions over MCP + +When you run the [MCP server](/docs/ai) over its network (Streamable HTTP) +transport, it self-registers a business-action tool set on top of your objects, +bound to the caller's principal (the API key acts as the user): + +| Tool | What it does | +|:---|:---| +| `list_actions` | Enumerates the invokable business actions the caller is permitted to run — name, target object, description, whether it needs a `recordId`, whether it is destructive, and its declared params. | +| `run_action` | Invokes an action by name with `{ recordId, params }`. Runs the app's registered business logic under the caller's permissions and RLS. | + +`run_action` resolves the action and dispatches it through the framework's own +action mechanism — `IDataEngine.executeAction` for `script` / inline-`body` +actions, or the automation flow runner for `type:'flow'` — exactly the path the +REST `/actions/...` route uses. Because the bridge is bound to the caller's +`ExecutionContext`, a BYO-AI client (Claude Code, Cursor, …) can trigger real +business logic — "complete this task", "convert this lead" — under the same +guardrails as the UI. + +### Describing an action for the LLM + +Add an optional `ai:` block to give the model a precise, LLM-facing description +(and to flag confirmation intent). The block is open metadata on the Action +spec; `list_actions` surfaces `ai.description` to the model, falling back to the +UI `label` when it is absent. ```typescript export const triageCaseAction = { @@ -28,154 +67,140 @@ export const triageCaseAction = { }; ``` -When the LLM picks an exposed action, the AI runtime dispatches to the same -handler the Console row toolbar would invoke, so your business logic stays in -one place. + +**Cloud / Enterprise** — the `ai.exposed` flag is a governance gate for the +in-product chat *runtime*: the cloud `@objectstack/service-ai` bridge registers +an `action_` tool **only** when `ai.exposed === true` (and then +`ai.description` is required, ≥ 40 chars). The open MCP path does **not** use +`ai.exposed` — it filters by permission, returning every action the caller is +allowed to run. Set `ai.description` regardless: both paths read it. + ## What gets exposed -The AI runtime walks every registered object's `actions[]` and registers only -actions with `ai.exposed === true` that also pass safety and wiring checks. -Three action types are supported: +The bridge walks every registered object's `actions[]` and offers only actions +that have a headless dispatch path **and** that the caller is permitted to run. +System objects (`sys_*`) are held back fail-closed. -| `action.type` | Dispatch path | Wiring needed | +| `action.type` | Dispatch path | Available where | |:---|:---|:---| -| `script` | `IDataEngine.executeAction(object, target, ctx)` — the same call Studio makes | none beyond the metadata service | -| `api` | HTTP call to `action.target` via the configured `apiClient` (default: `fetch`) | `apiActionBaseUrl` (or a custom `apiClient`) | -| `flow` | `IAutomationService.execute(target, { triggerData })` | `automation` service registered | - -Console-only types (`url`, `modal`, `form`) are always skipped. Dangerous -actions — those with `confirmText`, `mode: 'delete'`, `variant: 'danger'`, or -`ai.requiresConfirmation: true` — are skipped unless the -[HITL approval queue](#human-in-the-loop-approval) is wired. To assert that a -destructive-looking action is safe for autonomous execution, set +| `script` | `IDataEngine.executeAction(object, target, ctx)` — the same call Studio makes | open (MCP) + cloud | +| `flow` | automation flow runner — `execute(target, { triggerData })` | open (MCP) + cloud; needs the `automation` service registered | +| `api` | HTTP call to `action.target` via a configured `apiClient` | **cloud / Enterprise** runtime only | + +Console-only types (`url`, `modal`, `form`) are always skipped. + +Permission filtering is single-sourced with the REST route: an action's declared +`requiredPermissions` (ADR-0066) are enforced as the caller, so +`list_actions` hides — and `run_action` refuses — anything the user could not +invoke through the API. Destructive actions (`confirmText`, `mode: 'delete'`, +`variant: 'danger'`, or `ai.requiresConfirmation: true`) are reported with +`requiresConfirmation: true` so the client can ask the human before calling. To +assert that a destructive-looking action is safe for autonomous execution, set `ai.requiresConfirmation: false`. ## Wiring it up -```typescript -import { AIServicePlugin } from '@objectstack/service-ai'; - -kernel.use( - new AIServicePlugin({ - // Enables type:'api' action dispatch. Relative `target` paths - // ('/api/v1/...') are resolved against this base URL. - apiActionBaseUrl: process.env.OS_AI_ACTION_API_BASE_URL, - // Forwarded on every api-action HTTP call (auth, environment id, ...). - apiActionHeaders: { Authorization: `Bearer ${process.env.SERVICE_TOKEN}` }, - }), -); -``` - -If `automation` is already registered with the kernel, `type:'flow'` actions are picked up automatically — no extra wiring needed. - -## `type:'api'` body assembly - -For api actions, the request body is built from three sources (last wins): - -1. **User-collected params** (the keys the LLM filled in). -2. **`recordIdParam`** — when row-context, the id is placed flat at `action.recordIdParam`, using `recordIdField` (default `'id'`) to pick the value off the record. -3. **`bodyExtra`** — constant fields that always override. - -`bodyShape: { wrap: 'data' }` nests the user params under `data` while keeping `recordIdParam` flat — matching shapes like better-auth's `organization/update`. - -## Diagnostics - -`registerActionsAsTools()` returns `{ registered, skipped, warnings }`. The -diagnostics include reasons such as `"not AI-exposed"`, -`"no apiClient or apiBaseUrl configured"`, or -`"requires confirmation ... wire HITL approval"` so action authors can see -whether their action will be LLM-callable. - -## Human-In-The-Loop approval - -Destructive actions are too risky to let the LLM execute autonomously, but locking them away entirely defeats agentic UX. The HITL queue strikes the balance: the LLM gets to **propose** the call, a human gets to **approve** it. - -Enable the queue via the plugin option (default: off): +Expose the action tools by running the MCP server over its HTTP transport — no +AI-specific configuration is needed: ```typescript -kernel.use( - new AIServicePlugin({ - enableActionApproval: true, - apiActionBaseUrl: process.env.OS_AI_ACTION_API_BASE_URL, - }), -); -``` - -When the LLM picks an approval-gated tool (e.g. `action_delete_task`), the runtime: - -1. Persists an `ai_pending_actions` row with `status:'pending'`, `tool_input`, `proposed_by`, etc. -2. Returns `{ status: 'pending_approval', pendingActionId }` to the model so it can summarise (e.g. "I've requested approval to delete task #42."). -3. The operator sees the proposal in the **AI Pending Actions** Studio inbox. -4. Clicking **Approve** calls `POST /api/v1/ai/pending-actions/:id/approve` (`ai:approve` permission). The service looks up the pre-registered dispatcher and executes the action with HITL routing disabled, so the same code path runs. The row transitions to `executed` (or `failed` if dispatch threw). -5. Clicking **Reject** calls `POST /api/v1/ai/pending-actions/:id/reject` with an optional reason. - -The queue is exposed via four REST endpoints (`GET`, `GET/:id`, `POST/:id/approve`, `POST/:id/reject`) and via `IAIService.{proposePendingAction,approvePendingAction,rejectPendingAction,listPendingActions}` for programmatic use. - -**Why a dedicated queue (not the multi-step `IApprovalService`)?** AI tool-call HITL is ephemeral: subject is the proposed *call*, not a stable record state; there's no predefined process; operators expect single-click yes/no. The pending-action queue is a thin write log + dispatcher map that delivers that UX without dragging in process-engine overhead. - -### End-to-end example +import { LiteKernel } from '@objectstack/core'; +import { MCPServerPlugin } from '@objectstack/mcp'; -The full lifecycle: a `variant:'danger'` action registered as a tool → -invocation returns `pending_approval` without executing → row persisted → -`approvePendingAction(id, actor)` re-runs the handler → row transitions to -`executed` (the reject path mirrors it). The todo example's -`delete_completed` action (`examples/app-todo/src/actions/task.actions.ts`) -is authored exactly for this: `variant: 'danger'` + `confirmText` route it -through the approval queue. - -A trimmed view of the integration path: +const kernel = new LiteKernel(); +kernel.use(new MCPServerPlugin({ transport: 'http', autoStart: true })); +await kernel.bootstrap(); +``` -```typescript -// 1. Plugin boots with approval gating enabled -new AIServicePlugin({ - adapter: new VercelLLMAdapter({ model }), - enableActionApproval: true, // ← opt-in -}) - -// 2. LLM picks the gated tool — handler short-circuits to pending -const result = await aiService.toolRegistry.execute({ - type: 'tool-call', - toolName: 'action_delete_completed', - input: {}, -} as never); -const envelope = JSON.parse((result.output as { value: string }).value); -// → { ok: true, status: 'pending_approval', pendingActionId: 'pa_...' } - -// 3. Operator approves (REST or programmatic) -const outcome = await ai.approvePendingAction(envelope.pendingActionId, 'alice@example.com'); -// → { status: 'executed', result: } +`type:'flow'` actions are picked up automatically when the `automation` service +is registered. Point your MCP client at the server; the caller's API key acts as +the user, so every `run_action` runs under that principal's RLS. + + +**Cloud / Enterprise** — the in-product runtime wires actions through +`@objectstack/service-ai` instead, e.g. +`new AIServicePlugin({ apiActionBaseUrl, apiActionHeaders })`. `apiActionBaseUrl` +enables `type:'api'` dispatch (relative `/api/v1/...` targets resolve against it) +and `apiActionHeaders` are forwarded on every api-action call. The plugin's +`registerActionsAsTools()` returns `{ registered, skipped, warnings }` with +reasons like `"not AI-exposed"` or `"requires confirmation … wire HITL approval"` +so authors can see whether an action is LLM-callable. + + +### Example + +A BYO-AI client invokes `run_action` the same way it calls any MCP tool: + +```jsonc +// tools/call → run_action +{ + "actionName": "triage_case", + "recordId": "case_42", + "params": { "priority": "high" } +} +// → dispatches case_triage as the caller, under RLS; returns the flow result ``` +## Human-in-the-loop approval + +Destructive actions are too risky to let an LLM execute unattended, but locking +them away entirely defeats agentic UX. On the open MCP path the approval step +lives at the protocol boundary: `run_action` is annotated `destructiveHint: true`, +and each action's per-call risk is surfaced through `requiresConfirmation` in +`list_actions`, so the MCP client (Claude Desktop, Cursor, …) prompts the +operator to approve the call before it runs. The human stays in the loop at the +point of invocation, and the action still executes under the caller's RLS. + + +**Cloud / Enterprise** — the in-product chat runtime adds a *server-side* +approval queue for its own `action_` tools. Enable it with +`new AIServicePlugin({ enableActionApproval: true })`: a gated tool call persists +an `ai_pending_actions` row (`status: 'pending'`) and returns +`{ status: 'pending_approval', pendingActionId }` to the model instead of +executing. An operator resolves it from the **AI Pending Actions** Studio inbox — +`POST /api/v1/ai/pending-actions/:id/approve` (`ai:approve` permission) re-runs +the same dispatcher, or `.../reject` with a reason. The queue is also available +programmatically via +`IAIService.{proposePendingAction, approvePendingAction, rejectPendingAction, listPendingActions}`. +A dedicated queue (rather than the multi-step `IApprovalService`) fits AI +tool-call HITL: the subject is the proposed *call*, there is no predefined +process, and operators expect single-click yes/no. + + ## Permission-aware execution (RLS for agents) -Every AI tool call now runs with the **end-user's** `ExecutionContext`, -so the same row-level-security rules that protect the REST API -automatically scope what an agent can see and do. There is no separate -"agent permission" surface to maintain — if a user can't read account -`acc_42` through ObjectQL, neither can the LLM acting on their behalf. - -**How it works:** - -1. The REST routes for `/api/v1/ai/assistant/chat` and - `/api/v1/ai/agents/:agentName/chat` pull the authenticated principal out of - `req.user` and forward it to `aiService.chatWithTools(...)` as - `toolExecutionContext: { actor, conversationId, environmentId }`. - Both **cookie session** (`better-auth.session_token`) and **Bearer - token** auth are resolved automatically by the dispatcher — your - browser chats and your scripted API calls land in the same RLS - context as a plain ObjectQL request from the same user. -2. The tool registry threads that context into every handler invocation. -3. Built-in data tools (`query_records`, `get_record`, - `aggregate_data`) and auto-generated `action_*` tools convert the - actor into a `{ userId, roles, permissions, isSystem: false }` - engine context on each `IDataEngine` call. RLS engages exactly as - it would for a hand-rolled API endpoint. -4. Action audit logs attribute the dispatch to the real user instead of - a generic "AI Assistant" principal. - -When you invoke `chatWithTools` from a custom server route, opt in by -passing the actor explicitly: +Every action an agent runs executes under the **end-user's** `ExecutionContext`, +so the same row-level-security rules that protect the REST API automatically +scope what the agent can see and do. There is no separate "agent permission" +surface to maintain — if a user cannot read account `acc_42` through ObjectQL, +neither can an LLM acting on their behalf. + +On the open MCP path this is automatic: + +1. The server binds each session to the caller's principal — the API key acts as + the user, resolving to the same `ExecutionContext` a plain ObjectQL request + from that user would get. +2. The action bridge threads that context into every `executeAction` / flow + dispatch and every subject-record load, so RLS engages exactly as it does for + a hand-rolled API endpoint. +3. Declared `requiredPermissions` are enforced against the caller — the same + declaration the REST `/actions/...` route checks — so `list_actions` hides and + `run_action` refuses anything the user cannot invoke. +4. Action audit logs attribute the dispatch to the real user instead of a generic + "AI Assistant" principal. + + +**Cloud / Enterprise** — the in-product chat routes +(`/api/v1/ai/assistant/chat`, `/api/v1/ai/agents/:agentName/chat`) resolve the +authenticated principal from `req.user` — both cookie session +(`better-auth.session_token`) and Bearer token are handled — and forward it to +`aiService.chatWithTools(...)` as +`toolExecutionContext: { actor, conversationId, environmentId }`. That threads the +same RLS context through the runtime's built-in data tools and its +`action_` tools. From a custom server route you opt in by passing the actor +explicitly; omit `toolExecutionContext` to keep system-level behaviour (cron +jobs, internal callers). ```typescript await aiService.chatWithTools(messages, tools, { @@ -191,36 +216,7 @@ await aiService.chatWithTools(messages, tools, { }, }); ``` - -Omit `toolExecutionContext` to keep the previous system-level behaviour -(used by cron jobs, internal callers, and the existing test suite). - -## LLM-generated conversation titles - -Auto-titling is **on by default** for any configured LLM provider (the -memory/echo provider never triggers it, since no real LLM call is made); -toggle `title_generation_enabled` off via the `ai` settings namespace from -Console → Settings → AI if you'd rather leave conversations unnamed. Once a -conversation has at least one user + assistant exchange (≥ 2 messages) and -still has no title, the AI service fires a short, out-of-band LLM call -(default cap **16 characters**, single-line, no quotes) to summarise what the -user is asking about and writes the result back to `ai_conversations.title`. -The summarise call: - -- Reuses the **active chat provider** (so it works with any - `provider/model` you configured via Console → Settings → AI), and - honours the same `${provider}_base_url` you wired for chat — - including custom OpenAI-compatible endpoints (SiliconFlow, DeepSeek, - a local OpenAI proxy, …). -- Runs as a fire-and-forget task with a short timeout — if the title - call fails, the chat reply is unaffected and the conversation simply - keeps its placeholder name. -- Skips re-titling once `title` is set, so manual edits via the - conversation API are never overwritten. - -The Console sidebar groups conversations by recency and displays the -title; if you ever need to force a regenerate, clear the field via the -`ai_conversations` REST API and send another message. + --- @@ -228,3 +224,4 @@ title; if you ever need to force a regenerate, clear the field via the - [AI Overview](/docs/ai) - [AI Agents](/docs/ai/agents) +- [Natural Language Queries](/docs/ai/natural-language-queries) diff --git a/content/docs/ai/agents.mdx b/content/docs/ai/agents.mdx index 3f56f5ccf4..b6c90b6fa0 100644 --- a/content/docs/ai/agents.mdx +++ b/content/docs/ai/agents.mdx @@ -5,26 +5,39 @@ description: The two platform agents (ask and build), how skills extend them, an # AI Agents -Part of the [AI module](/docs/ai) — see the overview for how the cloud / Enterprise in-UI AI runtime differs from the open edition's MCP-only (BYO-AI) approach. +Part of the [AI module](/docs/ai). In the **open edition**, agents, tools, and +skills are **typed metadata**: you author them as source with `defineAgent` / +`defineSkill` / `defineTool` from the open `@objectstack/spec/ai` package, and an +external AI client (Claude, Cursor, a local model — any MCP client) reaches your +objects, queries, and business **Actions** through `@objectstack/mcp` (BYO-AI), +all governed by RLS. This page describes that metadata and the `AgentSchema` it +validates against. + + +**Cloud / Enterprise — the in-product chat runtime.** The authoring format on +this page is open. The *in-product chat runtime* that runs agents for end users — +the two platform agents `ask` and `build`, all in-product chat, and the +`/api/v1/ai/*` chat endpoints — ships only in the cloud / Enterprise distribution +(`@objectstack/service-ai`, cloud ADR-0025). The open edition uses +`@objectstack/mcp` (BYO-AI) instead. The next two sections describe that cloud / +Enterprise runtime. + Per [ADR-0063](https://github.com/objectstack-ai/framework/blob/main/docs/adr/0063-two-kernel-agents-skills-are-the-extension-primitive.md) -the kernel ships **exactly two** platform agents, bound by the *surface* the user -is in — the user never picks from a roster: +the cloud / Enterprise runtime ships **exactly two** platform agents, bound by the +*surface* the user is in — the user never picks from a roster: | Agent | Surface | Does | Edition | |---|---|---|---| | **`ask`** | data console | Read / query / explore records + run the business **actions** the app exposes. RLS-bounded. | cloud · Enterprise | | **`build`** | Studio | Author *metadata* (objects, fields, views, flows) via plan → draft → verify → publish. | cloud · Enterprise | -Both agents are part of the **cloud / Enterprise** in-UI AI runtime (cloud ADR-0025). -The **open edition** ships neither — it uses `@objectstack/mcp` (BYO-AI) for data -query and source-mode authoring instead (see the callout in the -[AI Overview](/docs/ai)). - -Within the cloud / EE runtime there is no per-turn intent classifier and no agent -dropdown: the surface binds the agent (data console → `ask`, Studio → `build`). A -`build`-shaped request that reaches `ask` is declined and redirected to the -Builder, never silently re-routed. +Within this cloud / Enterprise runtime there is no per-turn intent classifier and +no agent dropdown: the surface binds the agent (data console → `ask`, Studio → +`build`). A `build`-shaped request that reaches `ask` is declined and redirected to +the Builder, never silently re-routed. (For data query and source-mode authoring, +the **open edition** ships neither agent and uses `@objectstack/mcp` (BYO-AI) +instead — see the callout in the [AI Overview](/docs/ai).) ## You extend the platform with **skills**, not agents @@ -44,8 +57,9 @@ as Actions / Flows and reach it through `@objectstack/mcp` instead. ## The shape of an agent -An agent is metadata validated by `AgentSchema` (the platform's own `ask` / `build` -records use exactly these fields): +An agent is typed metadata validated by `AgentSchema` — exported, together with the +`defineAgent` factory, from the open `@objectstack/spec/ai` package (the platform's +own `ask` / `build` records use exactly these fields): | Field | Meaning | |------|---------| @@ -59,8 +73,10 @@ records use exactly these fields): There is no `type` field and no fixed agent "type" taxonomy — behaviour comes from persona, instructions, skills, and tools. There are no `triggers` / `schedule` -fields on an agent; drive agents from [Flows/Workflows](/docs/automation) or invoke -them via the chat endpoint. +fields on an agent; drive agents from [Flows/Workflows](/docs/automation), or — on +the **cloud / Enterprise** runtime — via the in-product chat endpoint +(`/api/v1/ai/*`). In the open edition, invoke the underlying Actions/Flows through +`@objectstack/mcp`. Agent tools are **references** to existing Actions, Flows, or queries — you do @@ -70,12 +86,15 @@ LLM-callable. -The agent definitions below illustrate agent **anatomy** — they show how an -`AgentSchema` record is shaped, *not* a tenant-authoring tutorial. On today's -platform you do not ship your own agents (`*.agent.ts` is platform-internal); you -add capability by authoring a **skill** that attaches to the built-in `ask` agent. -Read the examples for structure, then express your own capability as a skill plus -the Actions/Flows its tools reference. +The agent definitions below illustrate agent **anatomy** — how an `AgentSchema` +record is shaped (that schema and the `defineAgent` factory are part of the open +`@objectstack/spec/ai` package). Note that the `agent` metadata type is +`allowRuntimeCreate:false` / `allowOrgOverride:false` (ADR-0063 §2): the **cloud / +Enterprise** in-product runtime honors only the two platform agents `ask` and +`build`, so you do not ship a custom agent *to that runtime* — you extend it by +authoring a **skill** (open metadata) that attaches to `ask`. In the open edition, +express the same capability as Actions / Flows plus the skills/tools its references +point at, and reach it through `@objectstack/mcp` (BYO-AI). ## Sales Assistant Agent diff --git a/content/docs/ai/chatbot-integration.mdx b/content/docs/ai/chatbot-integration.mdx deleted file mode 100644 index 05ee3433a6..0000000000 --- a/content/docs/ai/chatbot-integration.mdx +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Connecting plugin-chatbot to a framework AI backend -description: Wire @object-ui/plugin-chatbot in Console (or any frontend) to the framework's /api/v1/ai/* REST surface — agents picker, SSE chat, models, HITL pending-action inbox. ---- - -# Connecting plugin-chatbot to a framework AI backend - - -**Cloud / Enterprise tier.** The in-UI AI backend this guide wires up — the -`/api/v1/ai/*` routes, the `ask` / `build` assistants, the models picker, and the -HITL inbox (`@objectstack/service-ai`) — ships in the **cloud / Enterprise** -distribution, not the open framework (cloud ADR-0025: `service-ai → cloud; open = -MCP-only`). On the **open edition** there is no in-product chat backend; expose -the app to your own AI through `@objectstack/mcp` (BYO-AI) instead — see the -[AI Capabilities guide](/docs/ai). Follow this guide when you run against -a cloud / EE host (or a dev server with the AI tier mounted). - - -`@object-ui/plugin-chatbot` (a React component shipped from the `objectui` -monorepo) is the canonical chat UI for ObjectStack Console. It speaks the -Vercel AI Data Stream protocol, so it pairs natively with the AI routes -exposed by `@objectstack/service-ai` once the framework dev server is run -with the AI tier enabled. - -This guide maps every chatbot configuration knob to its framework endpoint -so you can drop the plugin into an app without reverse-engineering the -contract. - -## 1. Enable the AI tier on the backend - -On a cloud / EE dev host (where `@objectstack/service-ai` is available — -see the callout above), the `default` and `full` plugin tier presets both -include the `ai` capability, so `os dev` boots the AI services -unless you opt out with `--preset minimal`. Provide a Vercel AI Gateway -model and key via env vars: - -```bash -AI_GATEWAY_API_KEY=vck_*** \ -AI_GATEWAY_MODEL=openai/gpt-4.1-mini \ -OS_CORS_ORIGIN=http://localhost:5173 \ -pnpm dev -``` - -You should see the `AI Service` plugin (`com.objectstack.service-ai`) in the -plugin list and the endpoints below should answer (the streaming one is -verified in step 3): - -```bash -curl http://localhost:3000/api/v1/ai/agents -# → { "agents": [{ "name": "ask", ... }, ...] } - -curl http://localhost:3000/api/v1/ai/models -# → { "models": [...] } ← may be empty until the adapter lists models -``` - -## 2. Endpoint map - -| Chatbot input | Framework route | Notes | -|----------------|---------------------------------------------------|-------| -| `useAgents({ apiBase })` → `${apiBase}/agents` | `GET /api/v1/ai/agents` | Response is `{ agents: [...] }` (also accepts a bare array). | -| `` source | `GET /api/v1/ai/models` | Returns `{ models: [...] }` (a list of model-id strings). | -| `useObjectChat({ api })` / `` | `POST /api/v1/ai/assistant/chat` | Vercel AI data-stream SSE. Body: `{ messages, agent?, conversationId?, ... }`. | -| HITL pending inbox (custom UI) | `GET /api/v1/ai/pending-actions` | Filter by `?status=pending`. | -| Approve a proposed action | `POST /api/v1/ai/pending-actions/:id/approve` | No body required; the actor is the authenticated user. Requires `ai:approve`. | -| Reject a proposed action | `POST /api/v1/ai/pending-actions/:id/reject` | Body: `{ reason?: string }`; actor is the authenticated user. Requires `ai:approve`. | - -All endpoints accept the standard tenancy header `X-Environment-Id`. The -default Hono adapter reflects CORS origins from the `OS_CORS_ORIGIN` env var -and already exposes `X-Environment-Id` in the allow-list, so cross-origin -calls from a Vite dev server (`http://localhost:5173`) work out of the box. - -## 3. Streaming contract - -`POST /api/v1/ai/assistant/chat` returns: - -``` -HTTP/1.1 200 OK -content-type: text/event-stream -x-vercel-ai-ui-message-stream: v1 -transfer-encoding: chunked - -data: {"type":"start"} -data: {"type":"text-delta","id":"0","delta":"Hello"} -data: {"type":"finish","finishReason":"stop"} -data: [DONE] -``` - -This is the wire format `@ai-sdk/react`'s `useChat` consumes natively — no -client-side parsing is required. Wire it up like: - -```ts -import { useObjectChat } from '@object-ui/plugin-chatbot'; - -const chat = useObjectChat({ - api: 'http://localhost:3000/api/v1/ai/assistant/chat', - headers: { 'X-Environment-Id': 'env_local' }, - body: { agent: 'ask' }, -}); -``` - -> **Heads up:** If you ever see the response come back as a JSON envelope -> like `{"type":"stream","events":{},"vercelDataStream":true,...}` your HTTP -> adapter is not encoding the SSE stream. Make sure you are on a current -> `@objectstack/runtime` and Hono adapter, then restart the dev server. - -## 4. HITL (Human-in-the-Loop) flow - -When the agent picks a dangerous action (e.g. `delete_task`) the tool -handler enqueues an `ai_pending_actions` row and the chat returns a -`pending_approval` tool result. Your Console UI then: - -1. Polls `GET /api/v1/ai/pending-actions?status=pending`. -2. Renders an inbox item per row (`tool_name`, `tool_input`, - `conversation_id`, `proposed_at`). -3. On approve: `POST /api/v1/ai/pending-actions/:id/approve` (no body; the - actor is the authenticated user, recorded on `decided_by`) → row - transitions to `executed` with `result` populated. -4. On reject: `POST /api/v1/ai/pending-actions/:id/reject` with - `{ reason }` → row transitions to `rejected`. - -The `ai_pending_actions` object ships built-in `approve`/`reject` actions -targeting the same REST endpoints, so the Console inbox and a custom widget -share one contract — no additional plumbing. - -## Troubleshooting - -- **`/api/v1/ai/*` returns 404** → the AI tier is not loaded. Don't run with - `--preset minimal`; use the `default` (or `full`) tier. -- **CORS blocked** → set `OS_CORS_ORIGIN=http://your-frontend-origin` before - launching the dev server. -- **Chat returns JSON instead of streaming** → the HTTP adapter isn't - encoding the SSE stream; update `@objectstack/runtime` and the Hono - adapter, then restart. -- **`/api/v1/ai/models` is empty** → register models on the - `AIService.modelRegistry` or skip the picker entirely; chat works - without an explicit model when `AI_GATEWAY_MODEL` is set. diff --git a/content/docs/ai/index.mdx b/content/docs/ai/index.mdx index 47ea0b4291..5a335ea43e 100644 --- a/content/docs/ai/index.mdx +++ b/content/docs/ai/index.mdx @@ -7,17 +7,14 @@ description: Complete guide to leveraging AI agents, RAG pipelines, and intellig AI in ObjectStack is a **cross-protocol capability layer**: agents, tools, and knowledge retrieval sit on top of the same objects, actions, permissions, and automation that power the rest of the platform. This module covers the architecture and each of its moving parts. - -**This guide describes the cloud / Enterprise AI tier. The open edition exposes AI only via MCP (BYO-AI).** + +**The open framework does AI bring-your-own-AI** — your keys, your models, zero platform AI cost. Everything on this page is part of the open edition unless a section is explicitly marked **cloud / Enterprise**: -Per **cloud ADR-0025** (`service-ai → cloud; open = MCP-only` — [`cloud/docs/adr/0025`](https://github.com/objectstack-ai/cloud/blob/main/docs/adr/0025-service-ai-to-cloud-open-mcp-only.md)), the in-UI AI runtime — `@objectstack/service-ai`: both the **`ask`** data-query assistant and the **`build`** Studio authoring assistant, plus all in-product chat — ships in the **cloud / Enterprise** distribution. It is **not** in the open framework, and no open distribution (cloud free tier, Docker, desktop, on-prem) has a built-in `ask` / `build` chat. +- **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. -The **open edition** does AI two ways instead — both bring-your-own-AI, zero platform AI cost: - -- **Data query → `@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. -- **Metadata authoring → source mode.** Author typed metadata as source (`*.object.ts`, `*.flow.ts`, …) with your own AI coding agent (Claude Code, Cursor), aided by the ObjectStack [skills](/docs/ai/skills-reference) and MCP introspection. There is no in-product Builder chat in the open edition. - -Everything below (agents, the `ask` / `build` personas, `@objectstack/service-ai` wiring, the `/api/v1/ai/*` routes) therefore describes the **cloud / Enterprise** distribution. +The **cloud / Enterprise** tier 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 (`@objectstack/service-ai`, cloud [ADR-0025](https://github.com/objectstack-ai/cloud/blob/main/docs/adr/0025-service-ai-to-cloud-open-mcp-only.md)). The open edition has no built-in in-product chat. ## What's in this module @@ -28,7 +25,6 @@ Everything below (agents, the `ask` / `build` personas, `@objectstack/service-ai - [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 - [AI Skills Reference](/docs/ai/skills-reference) — the per-skill catalog -- [Chatbot Integration](/docs/ai/chatbot-integration) — wiring `plugin-chatbot` to the framework AI backend - Spec: [Knowledge Protocol](/docs/protocol/knowledge) - Schema reference: [AI](/docs/references/ai) @@ -138,10 +134,19 @@ ObjectStack provides a comprehensive AI platform: ### Complete Sales AI Workflow Agents are metadata, not classes — there are no `.enrich()` / `.predict()` / -`.query()` methods to call. You invoke an agent over HTTP (the REST chat -endpoint) or, server-side, via `aiService.chatWithTools(...)`. Enrichment, -scoring, and email drafting are implemented as **Actions/Flows exposed as -tools**, and the LLM calls them while reasoning over the conversation. +`.query()` methods to call. Enrichment, scoring, and email drafting are +implemented as **Actions/Flows exposed as tools**, and the LLM calls them while +reasoning over the conversation. In the **open edition**, your own AI reaches +those same tools over MCP (`@objectstack/mcp`), and you drive them on a trigger +or schedule from a [Flow or Workflow](/docs/automation). + + +**Cloud / Enterprise runtime.** The in-product chat invocation shown below — the +REST chat endpoint (`/api/v1/ai/agents/:agentName/chat`) and the server-side +`aiService.chatWithTools(...)` — is the `@objectstack/service-ai` chat runtime, +which ships in the **cloud / Enterprise** tier. The agent, tool, and skill +*metadata* it consumes is open; only this in-product chat runtime is not. + ```typescript // Invoke an agent over the REST chat endpoint. diff --git a/content/docs/ai/knowledge-rag.mdx b/content/docs/ai/knowledge-rag.mdx index 0edb580c18..4617cdb546 100644 --- a/content/docs/ai/knowledge-rag.mdx +++ b/content/docs/ai/knowledge-rag.mdx @@ -7,7 +7,11 @@ description: The Knowledge Protocol — permission-aware RAG for agents via plug Part of the [AI module](/docs/ai) — how agents retrieve knowledge through the Knowledge Protocol and its adapter plugins. -ObjectStack ships a Knowledge Protocol that lets agents call `search_knowledge(query, sourceIds?, topK?)` against pluggable backends (RAGFlow, LlamaIndex, Dify, custom pgvector, …). The framework defines the contract and runs permission-aware filtering; the adapter plugin does the actual retrieval. See [the protocol design](/docs/protocol/knowledge) for the rationale. +ObjectStack ships a Knowledge Protocol that lets you retrieve from pluggable backends (RAGFlow, LlamaIndex, Dify, custom pgvector, …) with one call: `KnowledgeService.search(query, { sourceIds?, topK? })`. The framework defines the contract and runs permission-aware filtering; the adapter plugin does the actual retrieval. See [the protocol design](/docs/protocol/knowledge) for the rationale. + + +**This whole stack is open.** The `@objectstack/service-knowledge` service, the adapter plugins (`@objectstack/knowledge-memory`, `@objectstack/knowledge-ragflow`), the `@objectstack/embedder-openai` embedder, and the permission-aware retrieval + event sync below all ship in the **open edition**. The workflow is: declare your knowledge sources, pick an adapter, and call `search` — retrieval respects the same row-level security as any ObjectQL query. Only the in-product chat *runtime* that consumes retrieval (`@objectstack/service-ai`) is cloud / Enterprise; see the callout under [Retrieving knowledge](#retrieving-knowledge). + ## Wiring @@ -40,25 +44,39 @@ kernel.use(new KnowledgeMemoryPlugin()); // kernel.use(new KnowledgeRagflowPlugin({ endpoint, apiKey })); ``` -Then register the AI tool so agents can call it. (This step uses -`@objectstack/service-ai`, i.e. the **cloud / Enterprise** in-UI AI runtime — -see the callout in the [AI Overview](/docs/ai). The Knowledge Protocol itself -and the adapter plugins above ship in the open framework.) +## Retrieving knowledge + +Retrieval is a plain service call — no AI runtime required. Resolve the `knowledge` +service and call `search`. Hits are re-checked against the caller's +`ExecutionContext` (RLS), sorted by score, and capped at `topK` before they come +back. ```ts -import { registerKnowledgeTools } from '@objectstack/service-ai'; +const knowledge = ctx.getService('knowledge'); -ctx.hook('ai:ready', async (ai) => { - const knowledge = ctx.getService('knowledge'); - registerKnowledgeTools(ai.toolRegistry, { knowledgeService: knowledge }); +const hits = await knowledge.search('proposals about ACME', { + sourceIds: ['task_notes', 'product_docs'], // optional — defaults to every source the caller may see + topK: 5, + executionContext, // the caller's context — retrieval drops any hit RLS would hide }); +// hits: KnowledgeHit[] — each { chunkId, documentId, sourceId, sourceRecordId?, score, snippet, title? } ``` + +**In-product chat is cloud / Enterprise.** Exposing this retrieval to a chat UI as +the `search_knowledge` tool — the `ask` / `build` personas, the `/api/v1/ai/*` +endpoints, and `aiService.chatWithTools` — is provided by `@objectstack/service-ai`, +which ships only in the **cloud / Enterprise** tier (cloud [ADR-0025](https://github.com/objectstack-ai/cloud/blob/main/docs/adr/0025-service-ai-to-cloud-open-mcp-only.md)). The +open edition has no built-in in-product chat, but everything else here — the Knowledge +Protocol, adapters, embedder, and `KnowledgeService.search` — is open, so you can call +`search` directly from your own code, agent, or MCP tool. + + ## What you get for free - **Permission-aware retrieval.** Every hit with a `sourceRecordId` is re-checked against the caller's `ExecutionContext` via `IDataEngine` — the same RLS that gates plain ObjectQL queries. A salesperson asking "find proposals about ACME" only sees the proposals they could already read directly. File / HTTP hits pass through (ACL is the adapter's problem). - **Inline event sync.** When records on indexed objects change, the kernel's `IRealtimeService` events drive `KnowledgeService.handleRecordUpsert/Delete` automatically. No cron, no queue (yet — Phase 2). -- **Adapter swap, zero LLM changes.** Move from `memory` to `ragflow` to a custom adapter without touching the `search_knowledge` tool or any agent prompt. +- **Adapter swap, zero caller changes.** Move from `memory` to `ragflow` to a custom adapter without touching your `search` calls, the `search_knowledge` tool, or any agent prompt. ## Why we did not build a vector DB diff --git a/content/docs/ai/meta.json b/content/docs/ai/meta.json index 9da85f00df..3140ed6a8f 100644 --- a/content/docs/ai/meta.json +++ b/content/docs/ai/meta.json @@ -8,7 +8,6 @@ "knowledge-rag", "natural-language-queries", "skills", - "skills-reference", - "chatbot-integration" + "skills-reference" ] } diff --git a/content/docs/ai/natural-language-queries.mdx b/content/docs/ai/natural-language-queries.mdx index cac5f36b84..1f1aca6541 100644 --- a/content/docs/ai/natural-language-queries.mdx +++ b/content/docs/ai/natural-language-queries.mdx @@ -7,18 +7,46 @@ description: How agents query live data through the built-in data tools (query_r Part of the [AI module](/docs/ai) — how natural-language questions become ObjectQL queries at runtime. -The data tools described here ship with `@objectstack/service-ai`, i.e. the -**cloud / Enterprise** in-UI AI runtime (see the callout in the -[AI Overview](/docs/ai)). On the open edition, point your own AI at the same -objects and queries through `@objectstack/mcp` instead. +Natural-language querying is **open**. Point your own AI — Claude, Cursor, any +MCP client, or a local model — at your app through `@objectstack/mcp`, and it +turns questions into ObjectQL against your objects, governed by the same +row-level security as the REST API. The data tools and the ObjectQL engine that +back this are part of the open framework — no `@objectstack/service-ai` and no +cloud studio are required. -Agents query your data through the built-in **data tools** — -`query_records`, `get_record`, and `aggregate_data` — which the LLM calls with +Your AI queries your data through the built-in **data tools** — +`query_records`, `get_record`, and `aggregate_data` — which the model calls with structured arguments. These run as ordinary [ObjectQL](/docs/protocol/objectql) queries over your objects (ObjectStack uses ObjectQL, not SOQL), and they execute under the caller's `ExecutionContext`, so row-level security applies exactly as it does for the REST API. +The open MCP server plugin exposes these tools automatically — it bridges the +kernel's tool registry, metadata, and data engine to any connected MCP client: + +```typescript +import { LiteKernel } from '@objectstack/core'; +import { MCPServerPlugin } from '@objectstack/mcp'; + +const kernel = new LiteKernel(); +kernel.use(new MCPServerPlugin({ autoStart: true })); +await kernel.bootstrap(); +``` + +Point any MCP client at the server and ask questions in natural language: the +model discovers `query_records` / `get_record` / `aggregate_data` and calls them +under RLS as the authenticated caller. + +There is no separate natural-language-to-query metadata type to author — the +model is prompted with the available objects and translates the user's question +into `query_records` / `aggregate_data` calls at runtime. + + +**Cloud / Enterprise — bundled in-product chat.** The cloud tier's in-UI AI +runtime (`@objectstack/service-ai`, the `ask` data-query assistant, and the +`/api/v1/ai/*` chat endpoints) registers these same data tools into its chat +loop via `registerDataTools`: + ```typescript import { registerDataTools } from '@objectstack/service-ai'; @@ -27,9 +55,10 @@ ctx.hook('ai:ready', async (ai) => { }); ``` -There is no separate natural-language-to-query metadata type to author — the -model is prompted with the available objects and translates the user's question -into `query_records` / `aggregate_data` calls at runtime. +This is the cloud runtime, not the open path. On the open edition, use +`@objectstack/mcp` above to get the same natural-language querying with your +own AI. + --- diff --git a/content/docs/deployment/cloud-artifact-api.mdx b/content/docs/deployment/cloud-artifact-api.mdx deleted file mode 100644 index c45d099ff5..0000000000 --- a/content/docs/deployment/cloud-artifact-api.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Cloud Environment Artifact API -description: HTTP contract for publishing and resolving ObjectStack environment artifacts. ---- - -# Cloud Environment Artifact API - -The Cloud Environment Artifact API is the control-plane contract used to publish -compiled metadata and activate immutable environment revisions. The framework -runtime consumes the resulting artifact plus deployment config to boot the -target environment. - -The artifact envelope is defined by `EnvironmentArtifactSchema` in -`packages/spec/src/system/environment-artifact.zod.ts`. - ---- - -## Publish endpoint - -```text -POST /api/v1/cloud/environments/:environment/metadata -``` - -The body is the compiled JSON produced by `os compile` or `objectstack -compile`. The control plane validates the object payload, stores it as a new -revision, computes a checksum, and returns the commit metadata used by runtime -caches. - -```jsonc -{ - "success": true, - "data": { - "environmentId": "env_prod", - "commitId": "9ce1bd48dd7022b8", - "checksum": { "algorithm": "sha256", "value": "..." } - } -} -``` - -This control-plane endpoint is driven by the Cloud / Marketplace install flow. -The framework CLI publishes through the package catalog: - -```bash -os package publish ./dist/objectstack.json --env env_prod --install -``` - -(The legacy direct-to-environment `os publish` CLI was removed — #2237.) - ---- - -## Activate revision endpoint - -```text -POST /api/v1/cloud/environments/:environment/revisions/:commit/activate -``` - -Activating a revision changes the environment's current artifact pointer. -Runtime nodes can then reload or refresh their environment kernel based on the -new commit/checksum. - -Revision activation is a Cloud control-plane operation. The framework `os rollback` -CLI was removed (#2237); to change an environment's active artifact, install the -desired package version into it. - ---- - -## Artifact envelope - -`EnvironmentArtifactSchema` contains the immutable, cacheable portion of a -deployment: - -- `schemaVersion` -- `environmentId` -- `commitId` -- `checksum` -- `metadata` -- `functions` -- `manifest` -- optional `builtAt`, `builtWith`, and `payloadRef` - -Deployment config does not belong in the artifact. Database coordinates, -secrets, runtime credentials, and hostname bindings are mutable operational -inputs provided by the host or Cloud control plane. - ---- - -## Runtime resolution - -Environment-aware runtime hosts resolve the target environment before serving a -data-plane request: - -1. `/api/v1/environments/:environmentId/...` -2. Hostname through the environment registry -3. `X-Environment-Id` -4. `session.activeEnvironmentId` -5. Configured default environment -6. Single unambiguous environment - -This resolution order is implemented by the cloud host distribution's -kernel-resolver (`@objectstack/objectos-runtime`). The open-source dispatcher -(`packages/runtime/src/http-dispatcher.ts`) only provides the seam: when no -resolver is injected it serves every request from a single default kernel. - -Control-plane routes under `/api/v1/cloud/environments/...` are management -calls, not data-plane calls. - ---- - -## Reference implementation - -| File | Purpose | -|:---|:---| -| `packages/cli/src/commands/package/publish.ts` | CLI `os package publish` command and endpoint construction (the legacy `publish.ts` / `rollback.ts` commands were removed — #2237). | -| `packages/runtime/src/http-dispatcher.ts` | Kernel-resolution seam for per-request environment resolution. The concrete id/hostname registry ships in the host distribution `@objectstack/objectos-runtime` (not part of this open-source repo). | -| `packages/cloud-connection/src/runtime-config-plugin.ts` | Console runtime-config for default (hostname-resolved) environment state. | -| `packages/spec/src/system/environment-artifact.zod.ts` | Normative artifact envelope schema. | - ---- - -## Related - -- [Deployment Modes](/docs/deployment) -- [Environment-Scoped Routing](/docs/api/environment-routing) -- [North Star](/docs/concepts/north-star) diff --git a/content/docs/deployment/meta.json b/content/docs/deployment/meta.json index 4cd596d944..b72957ac75 100644 --- a/content/docs/deployment/meta.json +++ b/content/docs/deployment/meta.json @@ -8,7 +8,6 @@ "publish-and-preview", "environment-variables", "single-project-mode", - "cloud-artifact-api", "migration-from-objectql", "troubleshooting" ] diff --git a/content/docs/deployment/publish-and-preview.mdx b/content/docs/deployment/publish-and-preview.mdx index 504d860be9..8d429898ac 100644 --- a/content/docs/deployment/publish-and-preview.mdx +++ b/content/docs/deployment/publish-and-preview.mdx @@ -69,9 +69,7 @@ Common flags: | `--install` | — | Auto-install the new version into `--env` after publishing | In user mode the package is owned by your active organization; in service mode -(bearer key) pass `--org`. See [Packages](/docs/plugins/packages) for the package model and the -[Cloud Environment Artifact API](/docs/deployment/cloud-artifact-api) for the -control-plane endpoints. +(bearer key) pass `--org`. See [Packages](/docs/plugins/packages) for the package model. --- @@ -101,4 +99,3 @@ Use one of these shapes: - [Packages](/docs/plugins/packages) - [Deployment Modes](/docs/deployment) - [Environment-Scoped Routing](/docs/api/environment-routing) -- [Cloud Environment Artifact API](/docs/deployment/cloud-artifact-api) diff --git a/content/docs/permissions/sso.mdx b/content/docs/permissions/sso.mdx index 51e1b32056..047659e2eb 100644 --- a/content/docs/permissions/sso.mdx +++ b/content/docs/permissions/sso.mdx @@ -107,11 +107,14 @@ APPLE_CLIENT_SECRET=your-apple-private-key-jwt --- -## Enterprise SSO (OIDC) Extension Example - -Enterprise packages can pass `oidcProviders` into `@objectstack/plugin-auth` -or contribute them through `auth:configure`. The open-source package does not -ship a generic OIDC settings UI. +## Enterprise SSO (OIDC) + +Admin-managed **OIDC SSO ships in the open framework**: `@objectstack/plugin-auth` +registers external IdPs through `@better-auth/sso`, and admins add them **without +code** from **Setup → SSO Providers** (see the ADR-0069 note below). The +`oidcProviders` config shown here is the in-process path for framework or product +packages that prefer wiring providers in code, or contributing them through +`auth:configure`. > **Admin-managed external IdP (ADR-0069).** Recent releases add a > per-environment external-IdP path built on `@better-auth/sso` and surface an