Skip to content

Commit a7e5009

Browse files
committed
fix(spec): fold agent knowledge.topics into sources; mark unenforced AI config experimental (#1891, #1893)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CMaDBhnZEUu1fcw8Rvo6Yq
1 parent af5a224 commit a7e5009

6 files changed

Lines changed: 80 additions & 26 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): fold agent `knowledge.topics` into `sources` at parse; mark unenforced AI config experimental (#1891, #1893)
6+
7+
Two liveness-audit closeouts (umbrella #1878):
8+
9+
- **`AIKnowledgeSchema`** now folds the deprecated `topics` alias into the
10+
canonical `sources` at parse time (canonical wins; alias dropped from the
11+
output — mirrors the `visibleWhen` normalization, ADR-0089 D2). Authoring
12+
`topics` was a silent no-op: the renderer only reads `sources`. The schema's
13+
JSDoc example now shows `sources`.
14+
- **Author-facing experimental markers** added to config that is parsed but has
15+
no runtime consumer, matching the liveness ledger (ADR-0078): agent
16+
`memory` / `guardrails` / `structuredOutput` / `lifecycle`, and tool
17+
`outputSchema` (keys folded into the LLM-facing description only — no output
18+
validation).
19+
20+
Reference docs regenerated. No parse-acceptance change; `Agent`'s inferred
21+
output type no longer carries `knowledge.topics` (input still accepts it).

content/docs/references/ai/agent.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const result = AIKnowledge.parse(data);
3030
| Property | Type | Required | Description |
3131
| :--- | :--- | :--- | :--- |
3232
| **sources** | `string[]` | optional | Knowledge sources/tags to recruit RAG context from. Canonical key consumed by the agent renderer (objectui AgentPreview KnowledgeSummary). |
33-
| **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`. |
33+
| **topics** | `string[]` | optional | Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Folded into `sources` at parse time; prefer `sources`. |
3434
| **indexes** | `string[]` || Vector Store Indexes |
3535

3636

@@ -75,19 +75,19 @@ const result = AIKnowledge.parse(data);
7575
| **avatar** | `string` | optional | |
7676
| **role** | `string` || The persona/role (e.g. "Senior Support Engineer") |
7777
| **instructions** | `string` || System Prompt / Prime Directives |
78-
| **model** | `{ provider: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature: number; maxTokens?: number; … }` | optional | |
79-
| **lifecycle** | `{ id: string; description?: string; contextSchema?: Record<string, any>; initial: string; … }` | optional | State machine defining the agent conversation follow and constraints |
80-
| **surface** | `Enum<'ask' \| 'build'>` | | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
78+
| **model** | `{ provider?: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature?: number; maxTokens?: number; … }` | optional | |
79+
| **lifecycle** | `{ id: string; description?: string; contextSchema?: Record<string, any>; initial: string; … }` | optional | [EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893). |
80+
| **surface** | `Enum<'ask' \| 'build'>` | optional | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
8181
| **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) |
8282
| **tools** | `{ type: Enum<'action' \| 'flow' \| 'query' \| 'vector_search'>; name: string; description?: string }[]` | optional | Direct tool references (legacy fallback) |
8383
| **knowledge** | `{ sources?: string[]; topics?: string[]; indexes: string[] }` | optional | RAG access |
84-
| **active** | `boolean` | | |
84+
| **active** | `boolean` | optional | |
8585
| **access** | `string[]` | optional | Who can chat with this agent |
8686
| **permissions** | `string[]` | optional | Required permission-set capabilities |
87-
| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration |
88-
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory management |
89-
| **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | Safety guardrails for the agent |
90-
| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record<string, any>; strict: boolean; retryOnValidationFailure: boolean; … }` | optional | Structured output format and validation configuration |
87+
| **planning** | `{ maxIterations?: integer }` | optional | Autonomous reasoning and planning configuration |
88+
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | [EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet (liveness #1878/#1893). |
89+
| **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). |
90+
| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record<string, any>; strict?: boolean; retryOnValidationFailure?: boolean; … }` | optional | [EXPERIMENTAL — not enforced] Structured output format and validation configuration. Parsed but no runtime consumer yet (liveness #1878/#1893). |
9191
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this agent. |
9292
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
9393
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

content/docs/references/ai/tool.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ AI tool definition. [READ-ONLY PROJECTION — not an execution entry point] Auth
3838
| **description** | `string` || Tool description for LLM function calling |
3939
| **category** | `Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>` | optional | Tool category for grouping and filtering |
4040
| **parameters** | `Record<string, any>` || JSON Schema for tool parameters |
41-
| **outputSchema** | `Record<string, any>` | optional | JSON Schema for tool output |
41+
| **outputSchema** | `Record<string, any>` | optional | [EXPERIMENTAL — not enforced] JSON Schema for tool output. Keys are folded into the tool description only; outputs are not validated (liveness #1878/#1893). |
4242
| **objectName** | `string` | optional | Target object name (snake_case) |
4343
| **requiresConfirmation** | `boolean` || Require user confirmation before execution |
4444
| **permissions** | `string[]` | optional | Required permission-set capabilities |

packages/spec/src/ai/agent.test.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,37 @@ describe('AIToolSchema', () => {
9393
});
9494

9595
describe('AIKnowledgeSchema', () => {
96-
it('should accept knowledge config', () => {
96+
it('should accept knowledge config (canonical sources)', () => {
9797
const knowledge = {
98-
topics: ['product_docs', 'faq', 'troubleshooting'],
98+
sources: ['product_docs', 'faq', 'troubleshooting'],
9999
indexes: ['vector_store_main', 'vector_store_archive'],
100100
};
101101

102102
expect(() => AIKnowledgeSchema.parse(knowledge)).not.toThrow();
103103
});
104104

105+
it('folds the deprecated topics alias into sources at parse time (#1891)', () => {
106+
const parsed = AIKnowledgeSchema.parse({
107+
topics: ['product_docs', 'faq'],
108+
indexes: ['vector_store_main'],
109+
});
110+
expect(parsed.sources).toEqual(['product_docs', 'faq']);
111+
expect('topics' in parsed).toBe(false);
112+
});
113+
114+
it('lets canonical sources win when both keys are present', () => {
115+
const parsed = AIKnowledgeSchema.parse({
116+
sources: ['canonical'],
117+
topics: ['legacy'],
118+
indexes: [],
119+
});
120+
expect(parsed.sources).toEqual(['canonical']);
121+
expect('topics' in parsed).toBe(false);
122+
});
123+
105124
it('should accept empty arrays', () => {
106125
const knowledge = {
107-
topics: [],
126+
sources: [],
108127
indexes: [],
109128
};
110129

@@ -214,7 +233,7 @@ describe('AgentSchema', () => {
214233
role: 'Documentation Assistant',
215234
instructions: 'Answer questions using the knowledge base.',
216235
knowledge: {
217-
topics: ['api_docs', 'user_guide', 'faq'],
236+
sources: ['api_docs', 'user_guide', 'faq'],
218237
indexes: ['main_index', 'legacy_index'],
219238
},
220239
};
@@ -233,7 +252,7 @@ describe('AgentSchema', () => {
233252
{ type: 'flow', name: 'process_data' },
234253
],
235254
knowledge: {
236-
topics: ['everything'],
255+
sources: ['everything'],
237256
indexes: ['master_index'],
238257
},
239258
};
@@ -385,7 +404,7 @@ Always be polite, empathetic, and solution-oriented.`,
385404
},
386405
],
387406
knowledge: {
388-
topics: ['product_docs', 'faq', 'troubleshooting', 'api_reference'],
407+
sources: ['product_docs', 'faq', 'troubleshooting', 'api_reference'],
389408
indexes: ['support_kb_v2'],
390409
},
391410
access: ['support_team', 'customers'],
@@ -439,7 +458,7 @@ Be persuasive but honest. Focus on value creation.`,
439458
},
440459
],
441460
knowledge: {
442-
topics: ['sales_playbooks', 'product_features', 'case_studies', 'competitor_analysis'],
461+
sources: ['sales_playbooks', 'product_features', 'case_studies', 'competitor_analysis'],
443462
indexes: ['sales_intelligence'],
444463
},
445464
access: ['sales_team'],
@@ -482,7 +501,7 @@ Be precise, data-driven, and clear in your explanations.`,
482501
},
483502
],
484503
knowledge: {
485-
topics: ['sql_guides', 'metrics_definitions'],
504+
sources: ['sql_guides', 'metrics_definitions'],
486505
indexes: ['analytics_kb'],
487506
},
488507
access: ['analysts', 'executives'],

packages/spec/src/ai/agent.zod.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,18 @@ export const AIToolSchema = lazySchema(() => z.object({
3333
*/
3434
export const AIKnowledgeSchema = lazySchema(() => z.object({
3535
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).'),
36-
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`.'),
36+
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`.'),
3737
indexes: z.array(z.string()).describe('Vector Store Indexes'),
38+
}).transform((input) => {
39+
// #1891: fold the deprecated `topics` alias into the canonical `sources` and
40+
// drop it from the output (mirrors `normalizeVisibleWhen`, ADR-0089 D2) so
41+
// authoring `topics` is no longer a silent no-op — the renderer reads
42+
// `sources`. The canonical key wins when both are present.
43+
const { topics, ...rest } = input;
44+
if (rest.sources === undefined && topics !== undefined) {
45+
return { ...rest, sources: topics };
46+
}
47+
return rest;
3848
}));
3949

4050
/**
@@ -110,7 +120,7 @@ export type StructuredOutputConfig = z.infer<typeof StructuredOutputConfigSchema
110120
* role: 'Help Desk Assistant',
111121
* instructions: 'You are a helpful assistant. Always verify user identity first.',
112122
* skills: ['case_management', 'knowledge_search'],
113-
* knowledge: { topics: ['faq', 'policies'], indexes: ['support_docs'] },
123+
* knowledge: { sources: ['faq', 'policies'], indexes: ['support_docs'] },
114124
* });
115125
* ```
116126
*
@@ -138,7 +148,7 @@ export const AgentSchema = lazySchema(() => z.object({
138148
/** Cognition */
139149
instructions: z.string().describe('System Prompt / Prime Directives'),
140150
model: AIModelConfigSchema.optional(),
141-
lifecycle: StateMachineSchema.optional().describe('State machine defining the agent conversation follow and constraints'),
151+
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).'),
142152

143153
/**
144154
* ADR-0063 §1 / ADR-0064 — the product surface this agent IS. The kernel
@@ -213,7 +223,7 @@ export const AgentSchema = lazySchema(() => z.object({
213223

214224
/** Reflection interval — how often the agent reflects on past actions */
215225
reflectionInterval: z.number().int().min(1).optional().describe('Reflect every N interactions to improve behavior'),
216-
}).optional().describe('Agent memory management'),
226+
}).optional().describe('[EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet (liveness #1878/#1893).'),
217227

218228
/** Guardrails */
219229
guardrails: z.object({
@@ -225,10 +235,10 @@ export const AgentSchema = lazySchema(() => z.object({
225235

226236
/** Topics or actions the agent must avoid */
227237
blockedTopics: z.array(z.string()).optional().describe('Forbidden topics or action names'),
228-
}).optional().describe('Safety guardrails for the agent'),
238+
}).optional().describe('[EXPERIMENTAL — not enforced] Safety guardrails for the agent. Parsed but not enforced — real limits come from the quota service (liveness #1878/#1893).'),
229239

230240
/** Structured Output */
231-
structuredOutput: StructuredOutputConfigSchema.optional().describe('Structured output format and validation configuration'),
241+
structuredOutput: StructuredOutputConfigSchema.optional().describe('[EXPERIMENTAL — not enforced] Structured output format and validation configuration. Parsed but no runtime consumer yet (liveness #1878/#1893).'),
232242
/**
233243
* ADR-0010 §3.7 — Package-level protection envelope. Package
234244
* authors declare lock policy here; the loader translates it

packages/spec/src/ai/tool.zod.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@ export const ToolSchema = lazySchema(() => z.object({
8282

8383
/**
8484
* Optional JSON Schema for the tool output.
85-
* Used for structured output validation and downstream tool chaining.
85+
*
86+
* ⚠️ EXPERIMENTAL — NOT ENFORCED (liveness #1878/#1893). The runtime folds
87+
* the top-level keys into the tool description shown to the LLM
88+
* (service-ai action-tools) but performs NO output validation against this
89+
* schema, and downstream tool chaining does not consume it either.
8690
*/
87-
outputSchema: z.record(z.string(), z.unknown()).optional().describe('JSON Schema for tool output'),
91+
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).'),
8892

8993
/**
9094
* Associated object name (when the tool operates on a specific data object).

0 commit comments

Comments
 (0)