Skip to content

Commit 37c0f8e

Browse files
authored
Merge pull request #1028 from objectstack-ai/copilot/remove-custom-protocol-definitions
2 parents 27d40a4 + bcf879f commit 37c0f8e

28 files changed

Lines changed: 709 additions & 528 deletions

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **AI Chat Protocol Aligned with Vercel AI SDK** — Removed custom AI chat protocol
12+
types and Zod schemas (`AIMessage`, `AIToolCall`, `AIStreamEvent`,
13+
`AiChatRequestSchema`, `AiChatResponseSchema`) from `@objectstack/spec`. The
14+
canonical message, tool-call, and streaming types are now re-exported from the
15+
Vercel AI SDK (`ai` v6):
16+
- `ModelMessage` replaces `AIMessage`
17+
- `ToolCallPart` replaces `AIToolCall`
18+
- `ToolResultPart` replaces `AIToolResult`
19+
- `TextStreamPart<ToolSet>` replaces `AIStreamEvent`
20+
- `IAIService` and `LLMAdapter` method signatures now accept `ModelMessage[]`
21+
and return `TextStreamPart<ToolSet>` for streaming
22+
- Deprecated type aliases preserved for migration convenience
23+
- NLQ, Suggest, and Insights protocols (ObjectStack-specific) are retained
24+
- **`@objectstack/service-ai` migrated to Vercel AI SDK types** — All source files
25+
and tests now use canonical Vercel types (`ModelMessage`, `ToolCallPart`,
26+
`ToolResultPart`, `TextStreamPart<ToolSet>`) instead of deprecated aliases:
27+
- `ToolRegistry.execute()` accepts `ToolCallPart` and returns `ToolExecutionResult`
28+
(extends `ToolResultPart` with `isError?: boolean`)
29+
- Tool call loop in `AIService.chatWithTools()` constructs proper
30+
`AssistantModelMessage` and `ToolModelMessage` with Vercel-format content arrays
31+
- `MemoryLLMAdapter.streamChat()` emits Vercel `TextStreamPart<ToolSet>` events
32+
- Conversation services serialize/deserialize `ModelMessage` union to flat DB columns
33+
- All 158 service-ai tests updated and passing
34+
35+
### Removed
36+
- `AiChatRequestSchema` / `AiChatResponseSchema` Zod schemas from
37+
`@objectstack/spec/api` — the AI chat wire protocol now uses Vercel AI SDK's
38+
data stream format (`toDataStreamResponse()`)
39+
- `aiChat` method from `IObjectStackAPI` and client SDK — consumers should use
40+
`@ai-sdk/react/useChat` directly
41+
- AI `/chat` endpoint from `DEFAULT_AI_ROUTES` plugin REST API definition
42+
43+
### Added
44+
- `ai` v6 as a dependency of `@objectstack/spec` for type re-exports
45+
1046
## [4.0.1] — 2026-03-31
1147

1248
### Fixed

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ObjectStack Protocol — Road Map
22

3-
> **Last Updated:** 2026-03-31
3+
> **Last Updated:** 2026-04-01
44
> **Current Version:** v4.0.1
55
> **Status:** Protocol Specification Complete · Runtime Implementation In Progress
66

content/docs/references/api/protocol.mdx

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,39 +36,13 @@ Architecture Alignment:
3636
## TypeScript Usage
3737

3838
```typescript
39-
import { AiChatRequest, AiChatResponse, AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api';
40-
import type { AiChatRequest, AiChatResponse, AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api';
39+
import { AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api';
40+
import type { AiInsightsRequest, AiInsightsResponse, AiNlqRequest, AiNlqResponse, AiSuggestRequest, AiSuggestResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetViewRequest, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, WorkflowApproveRequest, WorkflowApproveResponse, WorkflowRejectRequest, WorkflowRejectResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api';
4141

4242
// Validate data
43-
const result = AiChatRequest.parse(data);
43+
const result = AiNlqRequest.parse(data);
4444
```
4545

46-
---
47-
48-
## AiChatRequest
49-
50-
### Properties
51-
52-
| Property | Type | Required | Description |
53-
| :--- | :--- | :--- | :--- |
54-
| **message** | `string` || User message |
55-
| **conversationId** | `string` | optional | Conversation ID for context |
56-
| **context** | `Record<string, any>` | optional | Additional context data |
57-
58-
59-
---
60-
61-
## AiChatResponse
62-
63-
### Properties
64-
65-
| Property | Type | Required | Description |
66-
| :--- | :--- | :--- | :--- |
67-
| **message** | `string` || Assistant response message |
68-
| **conversationId** | `string` || Conversation ID |
69-
| **actions** | `Object[]` | optional | Suggested actions |
70-
71-
7246
---
7347

7448
## AiInsightsRequest

packages/client/src/index.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ import {
6262
MarkAllNotificationsReadResponse,
6363
AiNlqRequest,
6464
AiNlqResponse,
65-
AiChatRequest,
66-
AiChatResponse,
6765
AiSuggestRequest,
6866
AiSuggestResponse,
6967
AiInsightsRequest,
@@ -1209,17 +1207,7 @@ export class ObjectStackClient {
12091207
return this.unwrapResponse<AiNlqResponse>(res);
12101208
},
12111209

1212-
/**
1213-
* Multi-turn AI chat
1214-
*/
1215-
chat: async (request: AiChatRequest): Promise<AiChatResponse> => {
1216-
const route = this.getRoute('ai');
1217-
const res = await this.fetch(`${this.baseUrl}${route}/chat`, {
1218-
method: 'POST',
1219-
body: JSON.stringify(request)
1220-
});
1221-
return this.unwrapResponse<AiChatResponse>(res);
1222-
},
1210+
// AI chat method removed — use Vercel AI SDK `useChat()` / `@ai-sdk/react` directly.
12231211

12241212
/**
12251213
* AI-powered field value suggestions
@@ -1826,8 +1814,6 @@ export type {
18261814
ListNotificationsResponse,
18271815
AiNlqRequest,
18281816
AiNlqResponse,
1829-
AiChatRequest,
1830-
AiChatResponse,
18311817
AiSuggestRequest,
18321818
AiSuggestResponse,
18331819
AiInsightsRequest,

packages/services/service-ai/src/__tests__/ai-service.test.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { describe, it, expect, vi, beforeEach } from 'vitest';
4-
import type { AIMessage, IAIService, AIStreamEvent } from '@objectstack/spec/contracts';
4+
import type { ModelMessage, IAIService, TextStreamPart, ToolSet } from '@objectstack/spec/contracts';
55
import { AIService } from '../ai-service.js';
66
import { MemoryLLMAdapter } from '../adapters/memory-adapter.js';
77
import { ToolRegistry } from '../tools/tool-registry.js';
@@ -35,7 +35,7 @@ describe('MemoryLLMAdapter', () => {
3535
});
3636

3737
it('should echo the last user message in chat()', async () => {
38-
const messages: AIMessage[] = [
38+
const messages: ModelMessage[] = [
3939
{ role: 'system', content: 'You are helpful.' },
4040
{ role: 'user', content: 'Hello AI' },
4141
];
@@ -46,7 +46,7 @@ describe('MemoryLLMAdapter', () => {
4646
});
4747

4848
it('should handle no user message in chat()', async () => {
49-
const messages: AIMessage[] = [{ role: 'system', content: 'System only' }];
49+
const messages: ModelMessage[] = [{ role: 'system', content: 'System only' }];
5050
const result = await adapter.chat(messages);
5151
expect(result.content).toBe('[memory] (no user message)');
5252
});
@@ -57,8 +57,8 @@ describe('MemoryLLMAdapter', () => {
5757
});
5858

5959
it('should stream word-by-word in streamChat()', async () => {
60-
const messages: AIMessage[] = [{ role: 'user', content: 'Hi there' }];
61-
const events: AIStreamEvent[] = [];
60+
const messages: ModelMessage[] = [{ role: 'user', content: 'Hi there' }];
61+
const events: TextStreamPart<ToolSet>[] = [];
6262
for await (const event of adapter.streamChat(messages)) {
6363
events.push(event);
6464
}
@@ -113,24 +113,26 @@ describe('ToolRegistry', () => {
113113
);
114114

115115
const result = await registry.execute({
116-
id: 'call_1',
117-
name: 'add',
118-
arguments: JSON.stringify({ a: 3, b: 4 }),
116+
type: 'tool-call',
117+
toolCallId: 'call_1',
118+
toolName: 'add',
119+
input: { a: 3, b: 4 },
119120
});
120121

121122
expect(result.toolCallId).toBe('call_1');
122-
expect(result.content).toBe('7');
123+
expect(result.output).toEqual({ type: 'text', value: '7' });
123124
expect(result.isError).toBeUndefined();
124125
});
125126

126127
it('should return error for unknown tool', async () => {
127128
const result = await registry.execute({
128-
id: 'call_x',
129-
name: 'unknown',
130-
arguments: '{}',
129+
type: 'tool-call',
130+
toolCallId: 'call_x',
131+
toolName: 'unknown',
132+
input: {},
131133
});
132134
expect(result.isError).toBe(true);
133-
expect(result.content).toContain('not registered');
135+
expect(result.output).toEqual(expect.objectContaining({ type: 'text', value: expect.stringContaining('not registered') }));
134136
});
135137

136138
it('should return error on handler failure', async () => {
@@ -140,12 +142,13 @@ describe('ToolRegistry', () => {
140142
);
141143

142144
const result = await registry.execute({
143-
id: 'call_f',
144-
name: 'fail_tool',
145-
arguments: '{}',
145+
type: 'tool-call',
146+
toolCallId: 'call_f',
147+
toolName: 'fail_tool',
148+
input: {},
146149
});
147150
expect(result.isError).toBe(true);
148-
expect(result.content).toBe('boom');
151+
expect(result.output).toEqual({ type: 'text', value: 'boom' });
149152
});
150153

151154
it('should execute multiple tool calls in parallel', async () => {
@@ -155,13 +158,13 @@ describe('ToolRegistry', () => {
155158
);
156159

157160
const results = await registry.executeAll([
158-
{ id: 'c1', name: 'echo', arguments: '{"msg":"a"}' },
159-
{ id: 'c2', name: 'echo', arguments: '{"msg":"b"}' },
161+
{ type: 'tool-call', toolCallId: 'c1', toolName: 'echo', input: { msg: 'a' } },
162+
{ type: 'tool-call', toolCallId: 'c2', toolName: 'echo', input: { msg: 'b' } },
160163
]);
161164

162165
expect(results).toHaveLength(2);
163-
expect(results[0].content).toBe('a');
164-
expect(results[1].content).toBe('b');
166+
expect(results[0].output).toEqual({ type: 'text', value: 'a' });
167+
expect(results[1].output).toEqual({ type: 'text', value: 'b' });
165168
});
166169

167170
it('should return all definitions', () => {
@@ -272,7 +275,7 @@ describe('AIService', () => {
272275

273276
it('should stream via adapter.streamChat()', async () => {
274277
const service = new AIService({ logger: silentLogger });
275-
const events: AIStreamEvent[] = [];
278+
const events: TextStreamPart<ToolSet>[] = [];
276279
for await (const event of service.streamChat([{ role: 'user', content: 'Hi' }])) {
277280
events.push(event);
278281
}
@@ -289,14 +292,14 @@ describe('AIService', () => {
289292
};
290293
const service = new AIService({ adapter, logger: silentLogger });
291294

292-
const events: AIStreamEvent[] = [];
295+
const events: TextStreamPart<ToolSet>[] = [];
293296
for await (const event of service.streamChat([{ role: 'user', content: 'Hi' }])) {
294297
events.push(event);
295298
}
296299

297300
expect(events).toHaveLength(2);
298301
expect(events[0].type).toBe('text-delta');
299-
expect(events[0].textDelta).toBe('response');
302+
expect(events[0].type === 'text-delta' && events[0].text).toBe('response');
300303
expect(events[1].type).toBe('finish');
301304
});
302305

0 commit comments

Comments
 (0)