Add AI conversation memory and cost tracking protocols#88
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
There was a problem hiding this comment.
Pull request overview
Adds new AI protocol schemas for (1) multi-turn conversation memory with token budgeting and (2) detailed cost tracking with budget/alert/reporting structures, and wires them into the AI module exports with accompanying Vitest coverage.
Changes:
- Introduces
Conversation*Zod schemas/types for message/session state, token budgeting, pruning events, and analytics. - Introduces
Cost*Zod schemas/types for cost entries, budgets/status, alerts, analytics, and query filters. - Adds Vitest test suites for both protocols and exports them from
packages/spec/src/ai/index.ts(plus new npm lockfiles).
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/ai/index.ts | Exposes the new conversation and cost protocols from the AI namespace. |
| packages/spec/src/ai/conversation.zod.ts | Defines conversation message/session/token budget schemas and derived types. |
| packages/spec/src/ai/conversation.test.ts | Adds Vitest coverage validating conversation protocol parsing and defaults. |
| packages/spec/src/ai/cost.zod.ts | Defines cost tracking/budget/alert/report schemas and derived types. |
| packages/spec/src/ai/cost.test.ts | Adds Vitest coverage validating cost protocol parsing and defaults. |
| packages/spec/package-lock.json | Adds an npm lockfile for @objectstack/spec. |
| package-lock.json | Adds an npm lockfile at monorepo root. |
Files not reviewed (1)
- packages/spec/package-lock.json: Language not supported
| { | ||
| "name": "@objectstack/spec", | ||
| "version": "0.3.0", | ||
| "lockfileVersion": 3, | ||
| "requires": true, | ||
| "packages": { | ||
| "": { | ||
| "name": "@objectstack/spec", | ||
| "version": "0.3.0", | ||
| "license": "Apache-2.0", | ||
| "dependencies": { | ||
| "zod": "^3.22.4" |
There was a problem hiding this comment.
Since the monorepo uses pnpm (root package.json packageManager + pnpm-lock.yaml), committing an additional packages/spec/package-lock.json adds a second lockfile for the same package set and can lead to inconsistent installs. Consider removing this lockfile and relying on pnpm’s lockfile only.
| */ | ||
| export const MessageContentSchema = z.object({ | ||
| type: MessageContentTypeSchema.default('text'), | ||
| text: z.string().optional().describe('Text content'), | ||
| imageUrl: z.string().url().optional().describe('Image URL for vision models'), | ||
| fileUrl: z.string().url().optional().describe('File attachment URL'), | ||
| mimeType: z.string().optional().describe('MIME type for files'), | ||
| metadata: z.record(z.any()).optional().describe('Additional metadata'), | ||
| }); | ||
|
|
There was a problem hiding this comment.
MessageContentSchema is too permissive: it allows invalid combinations like { type: "image" } without imageUrl, or imageUrl/fileUrl present while type defaults to text. Consider modeling content as a discriminated union on type (e.g., text requires text, image requires imageUrl, file requires fileUrl, etc.) so invalid message payloads are rejected at validation time.
| */ | |
| export const MessageContentSchema = z.object({ | |
| type: MessageContentTypeSchema.default('text'), | |
| text: z.string().optional().describe('Text content'), | |
| imageUrl: z.string().url().optional().describe('Image URL for vision models'), | |
| fileUrl: z.string().url().optional().describe('File attachment URL'), | |
| mimeType: z.string().optional().describe('MIME type for files'), | |
| metadata: z.record(z.any()).optional().describe('Additional metadata'), | |
| }); | |
| * | |
| * Discriminated union on `type` to ensure valid combinations: | |
| * - `text` → requires `text` | |
| * - `image` → requires `imageUrl` | |
| * - `file` → requires `fileUrl` | |
| * - `code` → requires `text` (code content) | |
| * - `structured`→ primarily uses `metadata` / optional `text` | |
| */ | |
| const BaseMessageContentMetadataSchema = z.object({ | |
| metadata: z | |
| .record(z.any()) | |
| .optional() | |
| .describe('Additional metadata'), | |
| }); | |
| export const MessageContentSchema = z.discriminatedUnion('type', [ | |
| // Plain text content | |
| BaseMessageContentMetadataSchema.extend({ | |
| type: z.literal('text'), | |
| text: z.string().describe('Text content'), | |
| }), | |
| // Image content (for vision models), with optional caption/alt text | |
| BaseMessageContentMetadataSchema.extend({ | |
| type: z.literal('image'), | |
| imageUrl: z.string().url().describe('Image URL for vision models'), | |
| text: z.string().optional().describe('Alt text or caption for the image'), | |
| }), | |
| // File attachment content | |
| BaseMessageContentMetadataSchema.extend({ | |
| type: z.literal('file'), | |
| fileUrl: z.string().url().describe('File attachment URL'), | |
| mimeType: z.string().optional().describe('MIME type for files'), | |
| text: z.string().optional().describe('Optional description of the file'), | |
| }), | |
| // Code content (source code snippet) | |
| BaseMessageContentMetadataSchema.extend({ | |
| type: z.literal('code'), | |
| text: z.string().describe('Code content'), | |
| mimeType: z | |
| .string() | |
| .optional() | |
| .describe('MIME type or language identifier for the code'), | |
| }), | |
| // Structured content (JSON-like payloads, tables, etc.) | |
| BaseMessageContentMetadataSchema.extend({ | |
| type: z.literal('structured'), | |
| text: z.string().optional().describe('Optional human-readable summary of the structured content'), | |
| }), | |
| ]); |
| strategy: TokenBudgetStrategySchema.default('sliding_window'), | ||
|
|
||
| /** Strategy-Specific Options */ | ||
| slidingWindowSize: z.number().int().positive().optional().describe('Number of recent messages to keep'), |
There was a problem hiding this comment.
TokenBudgetConfigSchema defaults strategy to sliding_window, but slidingWindowSize is optional, which makes the default configuration ambiguous (it’s not clear what window size applies). Consider either (a) providing a sensible default for slidingWindowSize, or (b) making the config a discriminated union so slidingWindowSize is required when strategy === "sliding_window" (and similarly require minImportanceScore/semanticThreshold for their strategies).
| slidingWindowSize: z.number().int().positive().optional().describe('Number of recent messages to keep'), | |
| slidingWindowSize: z.number().int().positive().default(50).describe('Number of recent messages to keep'), |
| */ | ||
| export const ConversationContextSchema = z.object({ | ||
| /** Identity */ | ||
| sessionId: z.string().describe('Conversation session ID'), |
There was a problem hiding this comment.
ConversationSessionSchema contains both context.sessionId and a top-level id, but there’s no guarantee they match. This can lead to inconsistent session identifiers across the protocol. Consider removing sessionId from ConversationContextSchema (derive it from ConversationSession.id), or add validation to enforce equality.
| sessionId: z.string().describe('Conversation session ID'), |
| /** Period */ | ||
| period: BillingPeriodSchema, | ||
| customPeriodDays: z.number().int().positive().optional().describe('Custom period in days'), | ||
|
|
There was a problem hiding this comment.
BudgetLimitSchema allows period: "custom" without customPeriodDays, even though the schema describes customPeriodDays as the custom period definition. Consider enforcing that customPeriodDays is required when period === "custom" (and ideally disallow it for non-custom periods) so invalid budget configurations don’t validate.
Implements two core AI protocols: conversation state management with token budgeting, and comprehensive cost tracking with multi-level budget enforcement.
Conversation Protocol (
conversation.zod.ts)Multi-turn conversation state with token-aware context management:
Cost Protocol (
cost.zod.ts)Granular cost tracking with hierarchical budget enforcement:
Both protocols export TypeScript types derived from Zod schemas for runtime validation and type safety.
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.