Skip to content

feat: add @objectstack/service-ai — unified AI capability service plugin#1000

Merged
hotlong merged 6 commits intomainfrom
copilot/add-service-ai-plugin
Mar 31, 2026
Merged

feat: add @objectstack/service-ai — unified AI capability service plugin#1000
hotlong merged 6 commits intomainfrom
copilot/add-service-ai-plugin

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 31, 2026

AI capabilities lack a standardized kernel service entry point. Contracts exist (IAIService), data schemas exist (agent.zod.ts, conversation.zod.ts, etc.), but no service plugin wires them together — leading to ad-hoc LLM integrations scattered across projects.

Contract expansion (packages/spec/src/contracts/ai-service.ts)

  • Unified AIMessage: The base AIMessage type now supports the tool role and optional toolCalls/toolCallId fields directly, eliminating the need for a separate tool-aware message type. AIMessageWithTools is kept as a deprecated alias for backward compatibility.
  • Unified AIRequestOptions: Tool-related fields (tools, toolChoice) are now on AIRequestOptions directly, enabling tool calling in both chat() and streamChat(). AIRequestOptionsWithTools is kept as a deprecated alias.
  • Streaming: AIStreamEvent + streamChat?() on IAIService returning AsyncIterable<AIStreamEvent>
  • Conversation management: AIConversation (with unified AIMessage[] that carries tool metadata), IAIConversationService (CRUD + message append)
  • Fully backward-compatible — deprecated aliases preserve the old type names

New package: @objectstack/service-ai

Follows service-analytics / service-automation patterns:

  • adapters/LLMAdapter interface + MemoryLLMAdapter (deterministic echo adapter for tests/dev)
  • tools/ToolRegistry with registration, parallel execution, error handling
  • conversation/InMemoryConversationService implementing IAIConversationService
  • routes/ — 8 REST/SSE route definitions (/api/v1/ai/{chat,chat/stream,complete,models,conversations}), depending on IAIService + IAIConversationService contracts (not concrete classes), with full input validation (message role/content, query param parsing)
  • ai-service.ts — Core AIService implementing IAIService, delegates to pluggable adapter. conversationService typed as IAIConversationService for easy injection of custom implementations.
  • plugin.tsAIServicePlugin registers as kernel 'ai' service, emits ai:ready and ai:routes hooks
import { LiteKernel } from '@objectstack/core';
import { AIServicePlugin } from '@objectstack/service-ai';

const kernel = new LiteKernel();
kernel.use(new AIServicePlugin({ adapter: myOpenAIAdapter }));
await kernel.bootstrap();

const ai = kernel.getService<IAIService>('ai');

// Non-streaming tool calling
const result = await ai.chat(
  [{ role: 'user', content: 'What is the weather?' }],
  { tools: [weatherTool], toolChoice: 'auto' },
);

// Streaming
for await (const event of ai.streamChat!(messages, { tools })) {
  if (event.type === 'text-delta') process.stdout.write(event.textDelta!);
}

Housekeeping

  • Added @objectstack/service-ai to changeset fixed group
  • Updated CHANGELOG.md and ROADMAP.md (Phase 7.1 core AI services, service matrix table)
  • 60 service tests + 22 contract tests, all passing

Copilot AI linked an issue Mar 31, 2026 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Mar 31, 2026 3:54am
spec Ready Ready Preview, Comment Mar 31, 2026 3:54am

Request Review

Copilot AI and others added 3 commits March 31, 2026 03:04
…ing, and conversation management

- Add AIToolDefinition, AIToolCall, AIToolResult types for tool calling protocol
- Add AIMessageWithTools extending AIMessage with 'tool' role and tool call fields
- Add AIRequestOptionsWithTools extending AIRequestOptions with tool definitions
- Add AIStreamEvent type for streaming chat completions
- Add streamChat optional method to IAIService interface
- Add AIConversation type and IAIConversationService interface for conversation CRUD
- Add comprehensive tests for all new types and interfaces

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…IToolDefinition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Expand IAIService contract with streamChat, tool calling, conversation management
- Create packages/services/service-ai with LLM adapter layer, ToolRegistry,
  InMemoryConversationService, REST/SSE routes, kernel plugin
- Add 52 comprehensive tests covering all components
- Update changeset config, CHANGELOG.md, ROADMAP.md

Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/c48a190a-19bb-45fd-8caa-0b6b9e1f7c49

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@hotlong hotlong marked this pull request as ready for review March 31, 2026 03:20
Copilot AI review requested due to automatic review settings March 31, 2026 03:20
Copilot AI changed the title [WIP] Add service-ai: Unified AI capability service plugin feat: add @objectstack/service-ai — unified AI capability service plugin Mar 31, 2026
Copilot AI requested a review from hotlong March 31, 2026 03:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new @objectstack/service-ai package and expands the IAIService contract to support streaming and tool-calling concepts, wiring the service into the kernel as an 'ai' service with framework-agnostic REST/SSE route definitions.

Changes:

  • Expanded packages/spec AI contracts with tool-calling + streaming event types and added a conversation service contract.
  • Introduced packages/services/service-ai implementing an adapter-based AI service, tool registry, in-memory conversation store, kernel plugin, and REST/SSE route builders.
  • Updated project metadata/docs (ROADMAP, root CHANGELOG, changesets fixed group, lockfile) to include the new service.

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
ROADMAP.md Marks service-ai as implemented and updates AI roadmap items/status.
pnpm-lock.yaml Adds lock entries for the new packages/services/service-ai workspace package.
packages/spec/src/contracts/ai-service.ts Extends AI contracts with tool calling, streaming events, and conversation management interfaces.
packages/spec/src/contracts/ai-service.test.ts Adds contract-level type tests for tool calling, streaming, and conversations.
packages/services/service-ai/tsconfig.json TypeScript config for the new service package.
packages/services/service-ai/src/tools/tool-registry.ts Implements a basic tool registry with execution helpers.
packages/services/service-ai/src/tools/index.ts Exports tool registry public API.
packages/services/service-ai/src/routes/index.ts Exports route builder public API.
packages/services/service-ai/src/routes/ai-routes.ts Defines framework-agnostic REST/SSE route handlers for AI endpoints.
packages/services/service-ai/src/plugin.ts Registers AIService as the kernel 'ai' service and triggers hooks for tools/routes.
packages/services/service-ai/src/index.ts Package entrypoint exporting service, plugin, adapters, tools, conversations, and routes.
packages/services/service-ai/src/conversation/index.ts Exports in-memory conversation service.
packages/services/service-ai/src/conversation/in-memory-conversation-service.ts Implements IAIConversationService with an in-memory Map store.
packages/services/service-ai/src/ai-service.ts Implements IAIService orchestrator delegating to an adapter and exposing tool/conversation subcomponents.
packages/services/service-ai/src/adapters/types.ts Defines the LLMAdapter interface.
packages/services/service-ai/src/adapters/memory-adapter.ts Provides a deterministic in-memory adapter for dev/test.
packages/services/service-ai/src/adapters/index.ts Exports adapter types and memory adapter.
packages/services/service-ai/src/tests/ai-service.test.ts Adds unit/integration tests for adapters, registry, conversations, routes, and plugin behavior.
packages/services/service-ai/package.json Declares the new package, exports, scripts, and dependencies.
packages/services/service-ai/CHANGELOG.md Adds initial package changelog entry.
CHANGELOG.md Adds unreleased notes for the AI service plugin and contract expansions.
.changeset/config.json Adds @objectstack/service-ai to the fixed versioning group.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

.changeset/config.json:45

  • The repo uses Changesets for versioning (package.json scripts run changeset version/publish), but this PR doesn’t add a .changeset/*.md entry for the new @objectstack/service-ai package / contract changes. Without a changeset, automated versioning/changelog generation may miss this release. Consider adding an appropriate changeset file describing the addition.
      "@objectstack/service-job",
      "@objectstack/service-queue",
      "@objectstack/service-realtime",
      "@objectstack/service-ai",
      "@objectstack/service-storage",
      "@objectstack/docs",
      "create-objectstack",
      "objectstack-vscode"
    ]

Comment on lines 150 to +188
@@ -83,4 +177,90 @@ export interface IAIService {
* @returns Array of model identifiers
*/
listModels?(): Promise<string[]>;

/**
* Stream a chat completion as an async iterable of events
* @param messages - Array of conversation messages
* @param options - Optional request configuration (supports tool definitions)
* @returns Async iterable of stream events
*/
streamChat?(messages: AIMessage[], options?: AIRequestOptionsWithTools): AsyncIterable<AIStreamEvent>;
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tool definitions/choice are only plumbed through streamChat via AIRequestOptionsWithTools, while chat/complete still accept AIRequestOptions. This makes non-streaming tool calling impossible through the contract even though the PR advertises “tool calling support”. Consider widening chat options (and potentially messages) to the tool-aware types as well, or clarifying that tool calling is streaming-only.

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +214
export interface AIConversation {
/** Conversation ID */
id: string;
/** Title / summary */
title?: string;
/** Associated agent ID */
agentId?: string;
/** User who owns the conversation */
userId?: string;
/** Messages in the conversation */
messages: AIMessage[];
/** Creation timestamp (ISO 8601) */
createdAt: string;
/** Last update timestamp (ISO 8601) */
updatedAt: string;
/** Conversation metadata */
metadata?: Record<string, unknown>;
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AIConversation.messages is typed as AIMessage[], which cannot carry tool-call metadata (toolCalls, toolCallId) or the 'tool' role. If tool calls/results are part of persisted conversations, this should likely use the tool-aware message type (or a union) to avoid losing data / forcing any casts downstream.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +113
/**
* A chat message that may carry tool-related metadata.
* Widens the `role` union to include `tool` for tool result messages.
*/
export interface AIMessageWithTools {
/** Message role – adds `tool` for tool result messages */
role: 'system' | 'user' | 'assistant' | 'tool';
/** Message content */
content: string;
/** Tool calls requested by the assistant */
toolCalls?: AIToolCall[];
/** ID of the tool call this message responds to (for role='tool') */
toolCallId?: string;
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AIMessageWithTools introduces role 'tool' and tool-call metadata, but the core APIs (IAIService.chat, IAIService.streamChat) and AIConversation.messages still use AIMessage[] (which cannot represent tool-result messages). Consider unifying the message type used across the contract (e.g., extend AIMessage with tool fields / 'tool' role, or switch the service + conversation APIs to the tool-aware message type/union) so tool calls/results don’t force any casts or data loss.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +30
export interface AIServiceConfig {
/** LLM adapter to delegate calls to (defaults to MemoryLLMAdapter). */
adapter?: LLMAdapter;
/** Logger instance. */
logger?: Logger;
/** Pre-registered tools. */
toolRegistry?: ToolRegistry;
/** Conversation service (defaults to InMemoryConversationService). */
conversationService?: InMemoryConversationService;
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AIServiceConfig.conversationService is typed as InMemoryConversationService (and the class property is the same), which prevents injecting any other IAIConversationService implementation (e.g., persistent storage) without type casts. Consider typing this as IAIConversationService in the config and on the AIService instance, while still defaulting to new InMemoryConversationService().

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +67
export function buildAIRoutes(service: AIService, logger: Logger): RouteDefinition[] {
return [
// ── Chat ────────────────────────────────────────────────────
{
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildAIRoutes takes a concrete AIService and reaches into service.conversationService, which couples the route layer to this specific implementation and makes it hard to expose the same routes for any other IAIService implementation. Consider depending on contracts instead (e.g., accept IAIService + IAIConversationService as parameters, or define a minimal interface for route wiring).

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +87
const { messages, options } = (req.body ?? {}) as {
messages?: unknown[];
options?: Record<string, unknown>;
};

if (!Array.isArray(messages) || messages.length === 0) {
return { status: 400, body: { error: 'messages array is required' } };
}

try {
const result = await service.chat(messages as any, options as any);
return { status: 200, body: result };
} catch (err) {
logger.error('[AI Route] /chat error', err instanceof Error ? err : undefined);
return { status: 500, body: { error: 'Internal AI service error' } };
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Route input is only shallowly validated and then cast to any (e.g., /api/v1/ai/chat accepts any array and forwards it to the adapter). This can lead to runtime errors and 500s for malformed payloads (missing role, non-string content, etc.). Consider validating each message shape (role enum + string content) before calling service.chat/streamChat and returning a 400 with a clear error when invalid.

Copilot uses AI. Check for mistakes.
description: 'List conversations',
handler: async (req) => {
try {
const conversations = await service.conversationService.list(req.query as any);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET /api/v1/ai/conversations forwards req.query directly into conversationService.list(), but query values are strings; limit (and any future numeric fields) should be parsed/coerced to numbers (and validated) to match the IAIConversationService.list contract. Consider normalizing limit (e.g., Number.parseInt) and rejecting invalid values with a 400.

Suggested change
const conversations = await service.conversationService.list(req.query as any);
const rawQuery = req.query ?? {};
const options: Record<string, unknown> = { ...rawQuery };
if (typeof rawQuery.limit === 'string') {
const parsedLimit = Number.parseInt(rawQuery.limit, 10);
if (!Number.isFinite(parsedLimit) || Number.isNaN(parsedLimit) || parsedLimit <= 0) {
return { status: 400, body: { error: 'Invalid limit parameter' } };
}
options.limit = parsedLimit;
}
const conversations = await service.conversationService.list(options as any);

Copilot uses AI. Check for mistakes.
if (!message || typeof message.content !== 'string') {
return { status: 400, body: { error: 'message with content string is required' } };
}

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST /api/v1/ai/conversations/:id/messages only validates content is a string, but does not validate role (required by the AIMessage contract). This can create stored messages with invalid roles and later break adapters expecting 'system' | 'user' | 'assistant'. Consider validating role and rejecting invalid roles with a 400.

Suggested change
const role = (message as any).role;
if (role !== 'system' && role !== 'user' && role !== 'assistant') {
return {
status: 400,
body: { error: 'message.role must be one of "system", "user", or "assistant"' },
};
}

Copilot uses AI. Check for mistakes.
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Mar 31, 2026

@copilot 仔细评估是否已经完成了任务中要求的所有开发并修正测试出的问题

…t, use contracts, add input validation

- Unify AIMessage type: 'tool' role + toolCalls/toolCallId now on base type
- AIRequestOptions now includes tools/toolChoice for non-streaming tool calling
- AIMessageWithTools/AIRequestOptionsWithTools kept as deprecated aliases
- AIServiceConfig.conversationService typed as IAIConversationService
- buildAIRoutes depends on IAIService + IAIConversationService contracts
- Routes validate message role (system/user/assistant/tool) and content
- GET /conversations parses and validates limit from query string
- POST /conversations/:id/messages validates role before persisting
- 60 service tests + 22 contract tests = 82 total passing

Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/be512058-7717-4f5d-b4ba-9861ca48a4e9

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot 仔细评估是否已经完成了任务中要求的所有开发并修正测试出的问题

All 8 review issues have been addressed in commit 2436e95:

  1. Unified AIMessagetool role, toolCalls, toolCallId now on the base type (deprecated aliases kept for backward compat)
  2. Non-streaming tool callingAIRequestOptions includes tools/toolChoice so chat() supports tools
  3. AIConversation.messages — uses unified AIMessage that carries tool metadata
  4. conversationService typed as IAIConversationService — not concrete class
  5. buildAIRoutes depends on contracts — accepts IAIService + IAIConversationService
  6. Message validation — validates role (system/user/assistant/tool) and content (string), returns 400
  7. limit parsingGET /conversations parses and validates limit from query string
  8. role validation on add-message — uses shared validator

82 tests passing (60 service + 22 contract), both packages build successfully.

@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Mar 31, 2026

@copilot 合并最新代码并解决冲突

…HANGELOG conflict

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot 合并最新代码并解决冲突

Merged latest main (including plugin-setup and Vercel serverless fix) into this branch and resolved the CHANGELOG.md conflict in d11a880. All tests pass: 22 contract + 60 service-ai + 20 plugin-setup = 102 total.

@hotlong hotlong merged commit 2a392b9 into main Mar 31, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

新增 service-ai:统一 AI 能力服务插件

3 participants