@objectstack/service-ai 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +20 -0
  3. package/dist/index.cjs +1245 -54
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +344 -77
  6. package/dist/index.d.ts +344 -77
  7. package/dist/index.js +1230 -51
  8. package/dist/index.js.map +1 -1
  9. package/package.json +26 -4
  10. package/src/__tests__/ai-service.test.ts +248 -27
  11. package/src/__tests__/auth-and-toolcalling.test.ts +627 -0
  12. package/src/__tests__/chatbot-features.test.ts +229 -82
  13. package/src/__tests__/metadata-tools.test.ts +964 -0
  14. package/src/__tests__/objectql-conversation-service.test.ts +34 -16
  15. package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
  16. package/src/adapters/index.ts +2 -0
  17. package/src/adapters/memory-adapter.ts +17 -9
  18. package/src/adapters/vercel-adapter.ts +148 -0
  19. package/src/agent-runtime.ts +27 -3
  20. package/src/agents/index.ts +1 -0
  21. package/src/agents/metadata-assistant-agent.ts +87 -0
  22. package/src/ai-service.ts +174 -22
  23. package/src/conversation/in-memory-conversation-service.ts +2 -2
  24. package/src/conversation/objectql-conversation-service.ts +67 -18
  25. package/src/index.ts +22 -3
  26. package/src/plugin.ts +166 -9
  27. package/src/routes/agent-routes.ts +28 -3
  28. package/src/routes/ai-routes.ts +231 -14
  29. package/src/routes/index.ts +1 -1
  30. package/src/stream/index.ts +3 -0
  31. package/src/stream/vercel-stream-encoder.ts +129 -0
  32. package/src/tools/add-field.tool.ts +70 -0
  33. package/src/tools/create-object.tool.ts +66 -0
  34. package/src/tools/delete-field.tool.ts +38 -0
  35. package/src/tools/describe-metadata-object.tool.ts +32 -0
  36. package/src/tools/index.ts +12 -1
  37. package/src/tools/list-metadata-objects.tool.ts +34 -0
  38. package/src/tools/metadata-tools.ts +430 -0
  39. package/src/tools/modify-field.tool.ts +44 -0
  40. package/src/tools/tool-registry.ts +32 -9
@@ -1,4 +1,4 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  export { buildAIRoutes } from './ai-routes.js';
4
- export type { RouteDefinition, RouteRequest, RouteResponse } from './ai-routes.js';
4
+ export type { RouteDefinition, RouteRequest, RouteResponse, RouteUserContext } from './ai-routes.js';
@@ -0,0 +1,3 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ export { encodeStreamPart, encodeVercelDataStream } from './vercel-stream-encoder.js';
@@ -0,0 +1,129 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * Vercel AI SDK v6 — UI Message Stream Encoder
5
+ *
6
+ * Converts `AsyncIterable<TextStreamPart<ToolSet>>` (the internal ObjectStack
7
+ * streaming format) into the Vercel AI SDK v6 **UI Message Stream Protocol**.
8
+ *
9
+ * Wire format: Server-Sent Events (SSE) with JSON payloads.
10
+ * `data: {"type":"text-delta","id":"0","delta":"Hello"}\n\n`
11
+ *
12
+ * The client-side `DefaultChatTransport` from `ai` v6 uses
13
+ * `parseJsonEventStream` to parse these SSE events.
14
+ *
15
+ * @see https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol
16
+ */
17
+
18
+ import type { TextStreamPart, ToolSet } from 'ai';
19
+
20
+ // ── SSE helpers ──────────────────────────────────────────────────────
21
+
22
+ function sse(data: object): string {
23
+ return `data: ${JSON.stringify(data)}\n\n`;
24
+ }
25
+
26
+ // ── Public API ──────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Encode a single `TextStreamPart` event into SSE-formatted UI Message
30
+ * Stream chunk(s).
31
+ *
32
+ * Returns an empty string for event types that have no wire-format mapping.
33
+ */
34
+ export function encodeStreamPart(part: TextStreamPart<ToolSet>): string {
35
+ switch (part.type) {
36
+ case 'text-delta':
37
+ return sse({ type: 'text-delta', id: '0', delta: part.text });
38
+
39
+ case 'tool-input-start':
40
+ return sse({
41
+ type: 'tool-input-start',
42
+ toolCallId: part.id,
43
+ toolName: part.toolName,
44
+ });
45
+
46
+ case 'tool-input-delta':
47
+ return sse({
48
+ type: 'tool-input-delta',
49
+ toolCallId: part.id,
50
+ inputTextDelta: part.delta,
51
+ });
52
+
53
+ case 'tool-call':
54
+ return sse({
55
+ type: 'tool-input-available',
56
+ toolCallId: part.toolCallId,
57
+ toolName: part.toolName,
58
+ input: part.input,
59
+ });
60
+
61
+ case 'tool-result':
62
+ return sse({
63
+ type: 'tool-output-available',
64
+ toolCallId: part.toolCallId,
65
+ output: part.output,
66
+ });
67
+
68
+ case 'error':
69
+ return sse({
70
+ type: 'error',
71
+ errorText: String(part.error),
72
+ });
73
+
74
+ // finish-step and finish are handled by the generator, not here
75
+ default:
76
+ return '';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Transform an `AsyncIterable<TextStreamPart>` into an `AsyncIterable<string>`
82
+ * where each yielded string is an SSE-formatted UI Message Stream chunk.
83
+ *
84
+ * Lifecycle order required by the client:
85
+ * start → start-step → text-start → text-delta* → text-end → finish-step → finish → [DONE]
86
+ */
87
+ export async function* encodeVercelDataStream(
88
+ events: AsyncIterable<TextStreamPart<ToolSet>>,
89
+ ): AsyncIterable<string> {
90
+ // Preamble
91
+ yield sse({ type: 'start' });
92
+ yield sse({ type: 'start-step' });
93
+ yield sse({ type: 'text-start', id: '0' });
94
+
95
+ let textOpen = true;
96
+ let finishReason = 'stop';
97
+
98
+ for await (const part of events) {
99
+ // Capture finish reason
100
+ if (part.type === 'finish') {
101
+ finishReason = part.finishReason ?? 'stop';
102
+ }
103
+
104
+ // Before finish-step/finish, close the text part first
105
+ if (part.type === 'finish-step' || part.type === 'finish') {
106
+ if (textOpen) {
107
+ yield sse({ type: 'text-end', id: '0' });
108
+ textOpen = false;
109
+ }
110
+ // Don't emit these via encodeStreamPart — we handle them in postamble
111
+ continue;
112
+ }
113
+
114
+ const frame = encodeStreamPart(part);
115
+ if (frame) {
116
+ yield frame;
117
+ }
118
+ }
119
+
120
+ // Close text if still open (safety)
121
+ if (textOpen) {
122
+ yield sse({ type: 'text-end', id: '0' });
123
+ }
124
+
125
+ // Postamble
126
+ yield sse({ type: 'finish-step' });
127
+ yield sse({ type: 'finish', finishReason });
128
+ yield 'data: [DONE]\n\n';
129
+ }
@@ -0,0 +1,70 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineTool } from '@objectstack/spec/ai';
4
+
5
+ /**
6
+ * add_field — AI Tool Metadata
7
+ *
8
+ * Adds a new field (column) to an existing data object.
9
+ * Validates snake_case for objectName, field name, reference,
10
+ * and select option values before merging into the definition.
11
+ */
12
+ export const addFieldTool = defineTool({
13
+ name: 'add_field',
14
+ label: 'Add Field',
15
+ description:
16
+ 'Adds a new field (column) to an existing data object. ' +
17
+ 'Use this when the user wants to add a property, column, or attribute to a table.',
18
+ category: 'data',
19
+ builtIn: true,
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ objectName: {
24
+ type: 'string',
25
+ description: 'Target object machine name (snake_case)',
26
+ },
27
+ name: {
28
+ type: 'string',
29
+ description: 'Field machine name (snake_case, e.g. due_date)',
30
+ },
31
+ label: {
32
+ type: 'string',
33
+ description: 'Human-readable field label (e.g. Due Date)',
34
+ },
35
+ type: {
36
+ type: 'string',
37
+ description: 'Field data type',
38
+ enum: ['text', 'textarea', 'number', 'boolean', 'date', 'datetime', 'select', 'lookup', 'formula', 'autonumber'],
39
+ },
40
+ required: {
41
+ type: 'boolean',
42
+ description: 'Whether the field is required',
43
+ },
44
+ defaultValue: {
45
+ description: 'Default value for the field',
46
+ },
47
+ options: {
48
+ type: 'array',
49
+ description: 'Options for select/picklist fields',
50
+ items: {
51
+ type: 'object',
52
+ properties: {
53
+ label: { type: 'string' },
54
+ value: {
55
+ type: 'string',
56
+ description: 'Option machine identifier (lowercase snake_case, e.g. high_priority)',
57
+ pattern: '^[a-z_][a-z0-9_]*$',
58
+ },
59
+ },
60
+ },
61
+ },
62
+ reference: {
63
+ type: 'string',
64
+ description: 'Referenced object name for lookup fields (snake_case, e.g. account)',
65
+ },
66
+ },
67
+ required: ['objectName', 'name', 'type'],
68
+ additionalProperties: false,
69
+ },
70
+ });
@@ -0,0 +1,66 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineTool } from '@objectstack/spec/ai';
4
+
5
+ /**
6
+ * create_object — AI Tool Metadata
7
+ *
8
+ * Creates a new data object (table) with schema validation.
9
+ * Validates snake_case naming for object and initial fields,
10
+ * checks for duplicates, and registers the object definition.
11
+ */
12
+ export const createObjectTool = defineTool({
13
+ name: 'create_object',
14
+ label: 'Create Object',
15
+ description:
16
+ 'Creates a new data object (table) with the specified name, label, and optional field definitions. ' +
17
+ 'Use this when the user wants to create a new entity, table, or data model.',
18
+ category: 'data',
19
+ builtIn: true,
20
+ // NOTE: requiresConfirmation is intentionally false (default) because the
21
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
22
+ // executes tool calls immediately without checking this flag. The flag
23
+ // should only be set once server-side approval gating is implemented to
24
+ // avoid giving users a false sense of safety.
25
+ parameters: {
26
+ type: 'object',
27
+ properties: {
28
+ name: {
29
+ type: 'string',
30
+ description: 'Machine name for the object (snake_case, e.g. project_task)',
31
+ },
32
+ label: {
33
+ type: 'string',
34
+ description: 'Human-readable display name (e.g. Project Task)',
35
+ },
36
+ fields: {
37
+ type: 'array',
38
+ description: 'Initial fields to create with the object',
39
+ items: {
40
+ type: 'object',
41
+ properties: {
42
+ name: { type: 'string', description: 'Field machine name (snake_case)' },
43
+ label: { type: 'string', description: 'Field display name' },
44
+ type: {
45
+ type: 'string',
46
+ description: 'Field data type',
47
+ enum: ['text', 'textarea', 'number', 'boolean', 'date', 'datetime', 'select', 'lookup', 'formula', 'autonumber'],
48
+ },
49
+ required: { type: 'boolean', description: 'Whether the field is required' },
50
+ },
51
+ required: ['name', 'type'],
52
+ },
53
+ },
54
+ enableFeatures: {
55
+ type: 'object',
56
+ description: 'Object capability flags',
57
+ properties: {
58
+ trackHistory: { type: 'boolean' },
59
+ apiEnabled: { type: 'boolean' },
60
+ },
61
+ },
62
+ },
63
+ required: ['name', 'label'],
64
+ additionalProperties: false,
65
+ },
66
+ });
@@ -0,0 +1,38 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineTool } from '@objectstack/spec/ai';
4
+
5
+ /**
6
+ * delete_field — AI Tool Metadata
7
+ *
8
+ * Removes a field (column) from an existing data object.
9
+ * This is a destructive operation.
10
+ */
11
+ export const deleteFieldTool = defineTool({
12
+ name: 'delete_field',
13
+ label: 'Delete Field',
14
+ description:
15
+ 'Removes a field (column) from an existing data object. This is a destructive operation. ' +
16
+ 'Use this when the user explicitly wants to remove an attribute or column from a table.',
17
+ category: 'data',
18
+ builtIn: true,
19
+ // NOTE: requiresConfirmation is intentionally false (default) because the
20
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
21
+ // executes tool calls immediately without checking this flag. The flag
22
+ // should only be set once server-side approval gating is implemented.
23
+ parameters: {
24
+ type: 'object',
25
+ properties: {
26
+ objectName: {
27
+ type: 'string',
28
+ description: 'Target object machine name (snake_case)',
29
+ },
30
+ fieldName: {
31
+ type: 'string',
32
+ description: 'Field machine name to delete (snake_case)',
33
+ },
34
+ },
35
+ required: ['objectName', 'fieldName'],
36
+ additionalProperties: false,
37
+ },
38
+ });
@@ -0,0 +1,32 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineTool } from '@objectstack/spec/ai';
4
+
5
+ /**
6
+ * describe_metadata_object — AI Tool Metadata
7
+ *
8
+ * Returns the full metadata schema of a data object including all
9
+ * fields, types, relationships, and configuration. Uses a unique name
10
+ * (`describe_metadata_object`) to avoid collision with the data-tools
11
+ * `describe_object` tool.
12
+ */
13
+ export const describeMetadataObjectTool = defineTool({
14
+ name: 'describe_metadata_object',
15
+ label: 'Describe Metadata Object',
16
+ description:
17
+ 'Returns the full metadata schema details of a data object, including all fields, types, relationships, and configuration. ' +
18
+ 'Use this when the user wants to inspect or understand the metadata structure of a specific table or entity.',
19
+ category: 'data',
20
+ builtIn: true,
21
+ parameters: {
22
+ type: 'object',
23
+ properties: {
24
+ objectName: {
25
+ type: 'string',
26
+ description: 'Object machine name to describe (snake_case)',
27
+ },
28
+ },
29
+ required: ['objectName'],
30
+ additionalProperties: false,
31
+ },
32
+ });
@@ -1,7 +1,18 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  export { ToolRegistry } from './tool-registry.js';
4
- export type { ToolHandler } from './tool-registry.js';
4
+ export type { ToolHandler, ToolExecutionResult } from './tool-registry.js';
5
5
 
6
6
  export { registerDataTools, DATA_TOOL_DEFINITIONS } from './data-tools.js';
7
7
  export type { DataToolContext } from './data-tools.js';
8
+
9
+ export { registerMetadataTools, METADATA_TOOL_DEFINITIONS } from './metadata-tools.js';
10
+ export type { MetadataToolContext } from './metadata-tools.js';
11
+
12
+ // Individual tool metadata exports
13
+ export { createObjectTool } from './create-object.tool.js';
14
+ export { addFieldTool } from './add-field.tool.js';
15
+ export { modifyFieldTool } from './modify-field.tool.js';
16
+ export { deleteFieldTool } from './delete-field.tool.js';
17
+ export { listMetadataObjectsTool } from './list-metadata-objects.tool.js';
18
+ export { describeMetadataObjectTool } from './describe-metadata-object.tool.js';
@@ -0,0 +1,34 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineTool } from '@objectstack/spec/ai';
4
+
5
+ /**
6
+ * list_metadata_objects — AI Tool Metadata
7
+ *
8
+ * Lists all registered metadata objects (tables) with optional filtering.
9
+ * Uses a unique name (`list_metadata_objects`) to avoid collision with
10
+ * the data-tools `list_objects` tool.
11
+ */
12
+ export const listMetadataObjectsTool = defineTool({
13
+ name: 'list_metadata_objects',
14
+ label: 'List Metadata Objects',
15
+ description:
16
+ 'Lists all registered metadata objects (tables) in the current environment. ' +
17
+ 'Use this when the user wants to see what tables, entities, or data models are defined in metadata.',
18
+ category: 'data',
19
+ builtIn: true,
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ filter: {
24
+ type: 'string',
25
+ description: 'Optional name or label substring to filter objects',
26
+ },
27
+ includeFields: {
28
+ type: 'boolean',
29
+ description: 'Whether to include field summaries for each object (default: false)',
30
+ },
31
+ },
32
+ additionalProperties: false,
33
+ },
34
+ });