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
21 changes: 21 additions & 0 deletions .changeset/agent-knowledge-alias-and-experimental-markers.md
Original file line number Diff line number Diff line change
@@ -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).
18 changes: 9 additions & 9 deletions content/docs/references/ai/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |


Expand Down Expand Up @@ -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<string, any>; 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<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). |
| **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<string, any>; 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<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). |
| **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. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ai/tool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>` | ✅ | JSON Schema for tool parameters |
| **outputSchema** | `Record<string, any>` | optional | JSON Schema for tool output |
| **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). |
| **objectName** | `string` | optional | Target object name (snake_case) |
| **requiresConfirmation** | `boolean` | ✅ | Require user confirmation before execution |
| **permissions** | `string[]` | optional | Required permission-set capabilities |
Expand Down
35 changes: 27 additions & 8 deletions packages/spec/src/ai/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};

Expand Down Expand Up @@ -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'],
},
};
Expand All @@ -233,7 +252,7 @@ describe('AgentSchema', () => {
{ type: 'flow', name: 'process_data' },
],
knowledge: {
topics: ['everything'],
sources: ['everything'],
indexes: ['master_index'],
},
};
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down
22 changes: 16 additions & 6 deletions packages/spec/src/ai/agent.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}));

/**
Expand Down Expand Up @@ -110,7 +120,7 @@ export type StructuredOutputConfig = z.infer<typeof StructuredOutputConfigSchema
* role: 'Help Desk Assistant',
* instructions: 'You are a helpful assistant. Always verify user identity first.',
* skills: ['case_management', 'knowledge_search'],
* knowledge: { topics: ['faq', 'policies'], indexes: ['support_docs'] },
* knowledge: { sources: ['faq', 'policies'], indexes: ['support_docs'] },
* });
* ```
*
Expand Down Expand Up @@ -138,7 +148,7 @@ export const AgentSchema = lazySchema(() => 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
Expand Down Expand Up @@ -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({
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/spec/src/ai/tool.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down