From a7e500902a233e32236312af2b27c0db3e279ca1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 14:23:09 +0000 Subject: [PATCH] fix(spec): fold agent knowledge.topics into sources; mark unenforced AI config experimental (#1891, #1893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Liveness-audit closeouts (umbrella #1878): - AIKnowledgeSchema folds the deprecated `topics` alias into canonical `sources` at parse time (canonical wins; alias dropped from output, mirroring the visibleWhen normalization). Authoring `topics` was a silent no-op — the renderer reads `sources`. JSDoc example updated; fold covered by new tests. - Author-facing `[EXPERIMENTAL — not enforced]` markers on parsed-but- unconsumed AI config, matching the liveness ledger (ADR-0078): agent memory/guardrails/structuredOutput/lifecycle, tool outputSchema. - Reference docs (agent.mdx / tool.mdx) regenerated. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CMaDBhnZEUu1fcw8Rvo6Yq --- ...nowledge-alias-and-experimental-markers.md | 21 +++++++++++ content/docs/references/ai/agent.mdx | 18 +++++----- content/docs/references/ai/tool.mdx | 2 +- packages/spec/src/ai/agent.test.ts | 35 ++++++++++++++----- packages/spec/src/ai/agent.zod.ts | 22 ++++++++---- packages/spec/src/ai/tool.zod.ts | 8 +++-- 6 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 .changeset/agent-knowledge-alias-and-experimental-markers.md diff --git a/.changeset/agent-knowledge-alias-and-experimental-markers.md b/.changeset/agent-knowledge-alias-and-experimental-markers.md new file mode 100644 index 0000000000..da11b849d4 --- /dev/null +++ b/.changeset/agent-knowledge-alias-and-experimental-markers.md @@ -0,0 +1,21 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): fold agent `knowledge.topics` into `sources` at parse; mark unenforced AI config experimental (#1891, #1893) + +Two liveness-audit closeouts (umbrella #1878): + +- **`AIKnowledgeSchema`** now folds the deprecated `topics` alias into the + canonical `sources` at parse time (canonical wins; alias dropped from the + output — mirrors the `visibleWhen` normalization, ADR-0089 D2). Authoring + `topics` was a silent no-op: the renderer only reads `sources`. The schema's + JSDoc example now shows `sources`. +- **Author-facing experimental markers** added to config that is parsed but has + no runtime consumer, matching the liveness ledger (ADR-0078): agent + `memory` / `guardrails` / `structuredOutput` / `lifecycle`, and tool + `outputSchema` (keys folded into the LLM-facing description only — no output + validation). + +Reference docs regenerated. No parse-acceptance change; `Agent`'s inferred +output type no longer carries `knowledge.topics` (input still accepts it). diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index 13f06435fa..8611e59f17 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -30,7 +30,7 @@ const result = AIKnowledge.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **sources** | `string[]` | optional | Knowledge sources/tags to recruit RAG context from. Canonical key consumed by the agent renderer (objectui AgentPreview KnowledgeSummary). | -| **topics** | `string[]` | optional | Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Prefer `sources`; `topics` is retained for back-compat but the renderer reads `sources`. | +| **topics** | `string[]` | optional | Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Folded into `sources` at parse time; prefer `sources`. | | **indexes** | `string[]` | ✅ | Vector Store Indexes | @@ -75,19 +75,19 @@ const result = AIKnowledge.parse(data); | **avatar** | `string` | optional | | | **role** | `string` | ✅ | The persona/role (e.g. "Senior Support Engineer") | | **instructions** | `string` | ✅ | System Prompt / Prime Directives | -| **model** | `{ provider: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature: number; maxTokens?: number; … }` | optional | | -| **lifecycle** | `{ id: string; description?: string; contextSchema?: Record; initial: string; … }` | optional | State machine defining the agent conversation follow and constraints | -| **surface** | `Enum<'ask' \| 'build'>` | ✅ | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 | +| **model** | `{ provider?: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature?: number; maxTokens?: number; … }` | optional | | +| **lifecycle** | `{ id: string; description?: string; contextSchema?: Record; initial: string; … }` | optional | [EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893). | +| **surface** | `Enum<'ask' \| 'build'>` | optional | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 | | **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) | | **tools** | `{ type: Enum<'action' \| 'flow' \| 'query' \| 'vector_search'>; name: string; description?: string }[]` | optional | Direct tool references (legacy fallback) | | **knowledge** | `{ sources?: string[]; topics?: string[]; indexes: string[] }` | optional | RAG access | -| **active** | `boolean` | ✅ | | +| **active** | `boolean` | optional | | | **access** | `string[]` | optional | Who can chat with this agent | | **permissions** | `string[]` | optional | Required permission-set capabilities | -| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration | -| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory management | -| **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | Safety guardrails for the agent | -| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record; strict: boolean; retryOnValidationFailure: boolean; … }` | optional | Structured output format and validation configuration | +| **planning** | `{ maxIterations?: integer }` | optional | Autonomous reasoning and planning configuration | +| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | [EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet (liveness #1878/#1893). | +| **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | [EXPERIMENTAL — not enforced] Safety guardrails for the agent. Parsed but not enforced — real limits come from the quota service (liveness #1878/#1893). | +| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record; strict?: boolean; retryOnValidationFailure?: boolean; … }` | optional | [EXPERIMENTAL — not enforced] Structured output format and validation configuration. Parsed but no runtime consumer yet (liveness #1878/#1893). | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this agent. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | diff --git a/content/docs/references/ai/tool.mdx b/content/docs/references/ai/tool.mdx index c7ca0e36c4..252f84433f 100644 --- a/content/docs/references/ai/tool.mdx +++ b/content/docs/references/ai/tool.mdx @@ -38,7 +38,7 @@ AI tool definition. [READ-ONLY PROJECTION — not an execution entry point] Auth | **description** | `string` | ✅ | Tool description for LLM function calling | | **category** | `Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>` | optional | Tool category for grouping and filtering | | **parameters** | `Record` | ✅ | JSON Schema for tool parameters | -| **outputSchema** | `Record` | optional | JSON Schema for tool output | +| **outputSchema** | `Record` | optional | [EXPERIMENTAL — not enforced] JSON Schema for tool output. Keys are folded into the tool description only; outputs are not validated (liveness #1878/#1893). | | **objectName** | `string` | optional | Target object name (snake_case) | | **requiresConfirmation** | `boolean` | ✅ | Require user confirmation before execution | | **permissions** | `string[]` | optional | Required permission-set capabilities | diff --git a/packages/spec/src/ai/agent.test.ts b/packages/spec/src/ai/agent.test.ts index 7754a04bc3..7ba8426b72 100644 --- a/packages/spec/src/ai/agent.test.ts +++ b/packages/spec/src/ai/agent.test.ts @@ -93,18 +93,37 @@ describe('AIToolSchema', () => { }); describe('AIKnowledgeSchema', () => { - it('should accept knowledge config', () => { + it('should accept knowledge config (canonical sources)', () => { const knowledge = { - topics: ['product_docs', 'faq', 'troubleshooting'], + sources: ['product_docs', 'faq', 'troubleshooting'], indexes: ['vector_store_main', 'vector_store_archive'], }; expect(() => AIKnowledgeSchema.parse(knowledge)).not.toThrow(); }); + it('folds the deprecated topics alias into sources at parse time (#1891)', () => { + const parsed = AIKnowledgeSchema.parse({ + topics: ['product_docs', 'faq'], + indexes: ['vector_store_main'], + }); + expect(parsed.sources).toEqual(['product_docs', 'faq']); + expect('topics' in parsed).toBe(false); + }); + + it('lets canonical sources win when both keys are present', () => { + const parsed = AIKnowledgeSchema.parse({ + sources: ['canonical'], + topics: ['legacy'], + indexes: [], + }); + expect(parsed.sources).toEqual(['canonical']); + expect('topics' in parsed).toBe(false); + }); + it('should accept empty arrays', () => { const knowledge = { - topics: [], + sources: [], indexes: [], }; @@ -214,7 +233,7 @@ describe('AgentSchema', () => { role: 'Documentation Assistant', instructions: 'Answer questions using the knowledge base.', knowledge: { - topics: ['api_docs', 'user_guide', 'faq'], + sources: ['api_docs', 'user_guide', 'faq'], indexes: ['main_index', 'legacy_index'], }, }; @@ -233,7 +252,7 @@ describe('AgentSchema', () => { { type: 'flow', name: 'process_data' }, ], knowledge: { - topics: ['everything'], + sources: ['everything'], indexes: ['master_index'], }, }; @@ -385,7 +404,7 @@ Always be polite, empathetic, and solution-oriented.`, }, ], knowledge: { - topics: ['product_docs', 'faq', 'troubleshooting', 'api_reference'], + sources: ['product_docs', 'faq', 'troubleshooting', 'api_reference'], indexes: ['support_kb_v2'], }, access: ['support_team', 'customers'], @@ -439,7 +458,7 @@ Be persuasive but honest. Focus on value creation.`, }, ], knowledge: { - topics: ['sales_playbooks', 'product_features', 'case_studies', 'competitor_analysis'], + sources: ['sales_playbooks', 'product_features', 'case_studies', 'competitor_analysis'], indexes: ['sales_intelligence'], }, access: ['sales_team'], @@ -482,7 +501,7 @@ Be precise, data-driven, and clear in your explanations.`, }, ], knowledge: { - topics: ['sql_guides', 'metrics_definitions'], + sources: ['sql_guides', 'metrics_definitions'], indexes: ['analytics_kb'], }, access: ['analysts', 'executives'], diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 0e3ec8e8ed..b9a0bbfd20 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -33,8 +33,18 @@ export const AIToolSchema = lazySchema(() => z.object({ */ export const AIKnowledgeSchema = lazySchema(() => z.object({ sources: z.array(z.string()).optional().describe('Knowledge sources/tags to recruit RAG context from. Canonical key consumed by the agent renderer (objectui AgentPreview KnowledgeSummary).'), - topics: z.array(z.string()).optional().describe('Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Prefer `sources`; `topics` is retained for back-compat but the renderer reads `sources`.'), + topics: z.array(z.string()).optional().describe('Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Folded into `sources` at parse time; prefer `sources`.'), indexes: z.array(z.string()).describe('Vector Store Indexes'), +}).transform((input) => { + // #1891: fold the deprecated `topics` alias into the canonical `sources` and + // drop it from the output (mirrors `normalizeVisibleWhen`, ADR-0089 D2) so + // authoring `topics` is no longer a silent no-op — the renderer reads + // `sources`. The canonical key wins when both are present. + const { topics, ...rest } = input; + if (rest.sources === undefined && topics !== undefined) { + return { ...rest, sources: topics }; + } + return rest; })); /** @@ -110,7 +120,7 @@ export type StructuredOutputConfig = z.infer z.object({ /** Cognition */ instructions: z.string().describe('System Prompt / Prime Directives'), model: AIModelConfigSchema.optional(), - lifecycle: StateMachineSchema.optional().describe('State machine defining the agent conversation follow and constraints'), + lifecycle: StateMachineSchema.optional().describe('[EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893).'), /** * ADR-0063 §1 / ADR-0064 — the product surface this agent IS. The kernel @@ -213,7 +223,7 @@ export const AgentSchema = lazySchema(() => z.object({ /** Reflection interval — how often the agent reflects on past actions */ reflectionInterval: z.number().int().min(1).optional().describe('Reflect every N interactions to improve behavior'), - }).optional().describe('Agent memory management'), + }).optional().describe('[EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet (liveness #1878/#1893).'), /** Guardrails */ guardrails: z.object({ @@ -225,10 +235,10 @@ export const AgentSchema = lazySchema(() => z.object({ /** Topics or actions the agent must avoid */ blockedTopics: z.array(z.string()).optional().describe('Forbidden topics or action names'), - }).optional().describe('Safety guardrails for the agent'), + }).optional().describe('[EXPERIMENTAL — not enforced] Safety guardrails for the agent. Parsed but not enforced — real limits come from the quota service (liveness #1878/#1893).'), /** Structured Output */ - structuredOutput: StructuredOutputConfigSchema.optional().describe('Structured output format and validation configuration'), + structuredOutput: StructuredOutputConfigSchema.optional().describe('[EXPERIMENTAL — not enforced] Structured output format and validation configuration. Parsed but no runtime consumer yet (liveness #1878/#1893).'), /** * ADR-0010 §3.7 — Package-level protection envelope. Package * authors declare lock policy here; the loader translates it diff --git a/packages/spec/src/ai/tool.zod.ts b/packages/spec/src/ai/tool.zod.ts index 72c8494d9d..758c64a42c 100644 --- a/packages/spec/src/ai/tool.zod.ts +++ b/packages/spec/src/ai/tool.zod.ts @@ -82,9 +82,13 @@ export const ToolSchema = lazySchema(() => z.object({ /** * Optional JSON Schema for the tool output. - * Used for structured output validation and downstream tool chaining. + * + * ⚠️ EXPERIMENTAL — NOT ENFORCED (liveness #1878/#1893). The runtime folds + * the top-level keys into the tool description shown to the LLM + * (service-ai action-tools) but performs NO output validation against this + * schema, and downstream tool chaining does not consume it either. */ - outputSchema: z.record(z.string(), z.unknown()).optional().describe('JSON Schema for tool output'), + outputSchema: z.record(z.string(), z.unknown()).optional().describe('[EXPERIMENTAL — not enforced] JSON Schema for tool output. Keys are folded into the tool description only; outputs are not validated (liveness #1878/#1893).'), /** * Associated object name (when the tool operates on a specific data object).