Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/adr-0033-phase-a-draft-gating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@objectstack/service-ai": minor
---

feat(service-ai): ADR-0033 Phase A — draft-gate AI metadata authoring

AI metadata mutations no longer publish straight to the live schema. Every write now routes through the ADR-0027 draft workspace via `protocol.saveMetaItem({ mode:'draft' })` — nothing an agent authors goes live until a human reviews the diff and publishes. The draft is the approval gate (the never-enforced `requiresConfirmation` flag is retired).

Adds a type-agnostic apply surface — `create_metadata` / `update_metadata` / `describe_metadata` / `list_metadata` — that works for any metadata type (view, dashboard, flow, …), validated against each type's Zod schema with errors fed back to the agent for self-correction. The existing object/field tools become thin draft-writing wrappers. Tool results return `{ status:'drafted', type, name, summary, changedKeys }`.
1,045 changes: 439 additions & 606 deletions packages/services/service-ai/src/__tests__/metadata-tools.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const METADATA_ASSISTANT_AGENT: Agent = {
role: 'Schema Architect',
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.

You author metadata as DRAFTS: every change you make is staged for the human to review as a diff and publish. You never publish, and you must never claim a change is live or applied — describe it as "drafted for your review". The human's publish is the only path to production.

Always answer in the same language the user is using. If the user's request is ambiguous, ask clarifying questions before proceeding. Detailed tool-usage guidance is supplied by the skills attached to this agent.`,

model: {
Expand Down
30 changes: 17 additions & 13 deletions packages/services/service-ai/src/skills/metadata-authoring-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,30 @@ export const METADATA_AUTHORING_SKILL: Skill = {
description: 'Create and modify ObjectStack metadata — objects, fields, schema changes through natural language.',
instructions: `You are an expert metadata architect. When the user asks you to design or change a data model, use these tools.

IMPORTANT — you propose drafts; you never publish. Every change you make with these tools lands in a DRAFT workspace, not the live schema. The human reviews your draft as a diff and publishes it themselves. Never tell the user a change is "live", "applied", or "saved to production" — say it is "drafted for your review". You have no publish tool, and that is by design (the draft is the approval gate).

Capabilities:
- Create new data objects (tables) with fields
- Add fields (columns) to existing objects
- Modify field properties (label, type, required, default value)
- Delete fields from objects
- List all registered metadata objects and their schemas
- Describe the full schema of a specific object
- Create or update any metadata type (object, view, dashboard, flow, report, app) via create_metadata / update_metadata — prefer these for non-object types.
- Create new data objects (tables) with fields, and add / modify / delete fields on objects (object-specific convenience tools).
- Inspect what exists: list_metadata / describe_metadata (any type), list_objects / describe_object (objects).

Guidelines:
1. Before creating a new object, use list_objects to check if a similar one already exists.
2. Before modifying or deleting fields, use describe_object to understand the current schema.
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
1. Before creating, use list_objects / list_metadata to check if a similar item already exists.
2. Before updating, modifying, or deleting, use describe_object / describe_metadata to understand the current shape.
3. Always use snake_case for type names and field names (e.g. project_task, due_date).
4. Suggest meaningful field types based on the user's description (e.g. "deadline" → date, "active" → boolean).
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
6. Explain what changes you are about to make before executing them.
7. After making changes, confirm the result by describing the updated schema.
8. For destructive operations (deleting fields), always warn the user about potential data loss.
9. Always answer in the same language the user is using.
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
7. After drafting changes, tell the user the change is drafted and ask them to review and publish; summarize what you staged (the tools return a { status: 'drafted', summary, changedKeys } envelope).
8. For destructive operations (deleting fields), warn the user about potential data loss on publish.
9. If a tool returns an error with validation issues, fix your input and try again — do not surface the raw error to the user as a failure if you can self-correct.
10. Always answer in the same language the user is using.
11. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
tools: [
'create_metadata',
'update_metadata',
'describe_metadata',
'list_metadata',
'create_object',
'add_field',
'modify_field',
Expand Down
49 changes: 49 additions & 0 deletions packages/services/service-ai/src/tools/create-metadata.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { defineTool } from '@objectstack/spec/ai';

/**
* create_metadata — AI Tool Metadata (ADR-0033)
*
* Type-agnostic creation of ANY metadata item (object, view, dashboard, flow,
* …). The new item is staged as a **draft** — it never goes live until a human
* reviews and publishes. The payload is validated against the type's canonical
* Zod schema (ADR-0005); invalid output is rejected with a fixable error.
*
* For new data objects + their fields the dedicated `create_object` /
* `add_field` tools offer a friendlier shape, but they ultimately stage the
* same way.
*/
export const createMetadataTool = defineTool({
name: 'create_metadata',
label: 'Create Metadata',
description:
'Create a new metadata item of ANY type (view, dashboard, flow, report, app, object, …) and stage it as a draft for human review. ' +
'Use for non-object types, or any type, when no dedicated tool fits. The change is NOT published — a human must publish it.',
category: 'data',
builtIn: true,
parameters: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Metadata type (singular), e.g. "object", "view", "dashboard", "flow", "report", "app".',
},
name: {
type: 'string',
description: 'Machine name for the item (snake_case, e.g. account_kanban).',
},
definition: {
type: 'object',
description:
'The full metadata definition body for this type, conforming to the type\'s schema. The "name" field is set automatically from the name argument.',
},
packageId: {
type: 'string',
description: 'Package ID that will own this item. If omitted, uses the active package from conversation context.',
},
},
required: ['type', 'name', 'definition'],
additionalProperties: false,
},
});
15 changes: 7 additions & 8 deletions packages/services/service-ai/src/tools/create-object.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ import { defineTool } from '@objectstack/spec/ai';
*
* Creates a new data object (table) with schema validation.
* Validates snake_case naming for object and initial fields,
* checks for duplicates, and registers the object definition.
* checks for duplicates, and stages the object as a draft.
*
* ADR-0033: this never publishes — the object lands in the draft workspace
* for a human to review and publish. The draft IS the approval gate, which is
* why no `requiresConfirmation` flag is needed (it was never enforced anyway).
*/
export const createObjectTool = defineTool({
name: 'create_object',
label: 'Create Object',
description:
'Creates a new data object (table) with the specified name, label, and optional field definitions. ' +
'Use this when the user wants to create a new entity, table, or data model.',
'Creates a new data object (table) with the specified name, label, and optional field definitions, staged as a draft for human review. ' +
'Use this when the user wants to create a new entity, table, or data model. The change is NOT published.',
category: 'data',
builtIn: true,
// NOTE: requiresConfirmation is intentionally false (default) because the
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
// executes tool calls immediately without checking this flag. The flag
// should only be set once server-side approval gating is implemented to
// avoid giving users a false sense of safety.
parameters: {
type: 'object',
properties: {
Expand Down
12 changes: 5 additions & 7 deletions packages/services/service-ai/src/tools/delete-field.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@ import { defineTool } from '@objectstack/spec/ai';
/**
* delete_field — AI Tool Metadata
*
* Removes a field (column) from an existing data object.
* This is a destructive operation.
* Removes a field (column) from an existing data object. This is a destructive
* operation, but ADR-0033 stages it as a draft — the field is only actually
* dropped when a human reviews and publishes (which re-runs the destructive
* data-loss check). The draft IS the approval gate.
*/
export const deleteFieldTool = defineTool({
name: 'delete_field',
label: 'Delete Field',
description:
'Removes a field (column) from an existing data object. This is a destructive operation. ' +
'Removes a field (column) from an existing data object, staged as a draft for human review. This is a destructive operation; it is NOT published until a human publishes the draft. ' +
'Use this when the user explicitly wants to remove an attribute or column from a table.',
category: 'data',
builtIn: true,
// NOTE: requiresConfirmation is intentionally false (default) because the
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
// executes tool calls immediately without checking this flag. The flag
// should only be set once server-side approval gating is implemented.
parameters: {
type: 'object',
properties: {
Expand Down
36 changes: 36 additions & 0 deletions packages/services/service-ai/src/tools/describe-metadata.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { defineTool } from '@objectstack/spec/ai';

/**
* describe_metadata — AI Tool Metadata (ADR-0033)
*
* Type-agnostic read of a single metadata item's full body. Returns the pending
* draft if one exists, else the published value — so the agent edits against
* what it (or the user) most recently staged. Use before `update_metadata` to
* see the current shape.
*/
export const describeMetadataTool = defineTool({
name: 'describe_metadata',
label: 'Describe Metadata',
description:
'Return the full definition of a metadata item of ANY type (draft-first: shows the pending draft if one exists, else the published value). ' +
'Use to inspect an item before updating it.',
category: 'data',
builtIn: true,
parameters: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Metadata type (singular), e.g. "object", "view", "dashboard", "flow".',
},
name: {
type: 'string',
description: 'Machine name of the item (snake_case).',
},
},
required: ['type', 'name'],
additionalProperties: false,
},
});
4 changes: 4 additions & 0 deletions packages/services/service-ai/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ export { deleteFieldTool } from './delete-field.tool.js';
export { listObjectsTool } from './list-objects.tool.js';
export { describeObjectTool } from './describe-object.tool.js';
export { validateExpressionTool } from './validate-expression.tool.js';
export { createMetadataTool } from './create-metadata.tool.js';
export { updateMetadataTool } from './update-metadata.tool.js';
export { describeMetadataTool } from './describe-metadata.tool.js';
export { listMetadataTool } from './list-metadata.tool.js';
35 changes: 35 additions & 0 deletions packages/services/service-ai/src/tools/list-metadata.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { defineTool } from '@objectstack/spec/ai';

/**
* list_metadata — AI Tool Metadata (ADR-0033)
*
* Type-agnostic enumeration of all items of a metadata type (name + label),
* backed by the same source as `GET /api/v1/meta/:type`. Use to discover what
* exists before creating or to find an item to update.
*/
export const listMetadataTool = defineTool({
name: 'list_metadata',
label: 'List Metadata',
description:
'List all metadata items of a given type (name and label), with an optional name/label substring filter. ' +
'Use to discover existing views, dashboards, flows, etc. before creating or updating one.',
category: 'data',
builtIn: true,
parameters: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Metadata type (singular), e.g. "object", "view", "dashboard", "flow", "report", "app".',
},
filter: {
type: 'string',
description: 'Optional case-insensitive substring to filter items by name or label.',
},
},
required: ['type'],
additionalProperties: false,
},
});
Loading