di-framework latest Help

AI

Spring AI–aligned chat, tools, RAG, MCP, and agents for TypeScript. Portable model abstractions sit on top of OpenAI-compatible and Anthropic HTTP adapters (no vendor SDKs). Domain services stay on @di-framework/core; this package is the AI layer — wire it with annotations (@AiService, @Agent, @Tool, …) or configureAi.

Features

  • Annotation DX: @AiService/@Agent assistants, @Tool/@ToolSet beans, @WithMemory/@WithRag/@WithTools, workflows (@Chain, @Route, …).

  • ChatClient: fluent prompt / call / stream API with an advisor chain (memory, tools, RAG, logging, observation).

  • Prototype builder: inject AiTokens.CHAT_CLIENT_BUILDER (fresh per resolve), like Spring’s ChatClient.Builder.

  • Providers: OpenAiChatModel and AnthropicChatModel over fetch — no official SDKs. createChatModel() selects API or CLI subscription access (OpenAI, Anthropic, xAI, and subscription-only CLIs).

  • Tools: functionToolCallback, method-level @Tool on DI beans, automatic tool-calling loops.

  • Structured output: JSON Schema converters and call().entity(...).

  • Memory / RAG / MCP / agents: same runtime as the imperative APIs, annotation-friendly.

  • Agent Skills / plugins: not in this package — see Agents (@di-framework/ai-utils).

Installation

bun add @di-framework/ai @di-framework/core
npm install @di-framework/ai @di-framework/core

Peer: @di-framework/core. Runtime dependency: @modelcontextprotocol/sdk (MCP helpers).

Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or XAI_API_KEY when using the HTTP API route.

Decorators need TypeScript 5 and experimentalDecorators. emitDecoratorMetadata is not required. Parameter decorators are factories — call them with parentheses: @UserMessageAnn(), @MemoryId(), @ToolParam().

Quick Start (annotations)

import { Container } from '@di-framework/core/decorators'; import { Agent, AiService, configureAi, OpenAiChatModel, resolveAiService, resolveAnnotatedAgent, SystemMessageAnn, Tool, ToolParam, ToolSet, UserMessageAnn, WithMemory, MemoryId, } from '@di-framework/ai'; @ToolSet() @Container() class WeatherTools { @Tool({ description: 'Get weather for a city', inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], }, }) getWeather(@ToolParam('City name') input: { city: string }) { return { temp: 68, city: input.city }; } } @AiService({ tools: [WeatherTools] }) @WithMemory() class WeatherBot { @SystemMessageAnn('You help with weather questions.') ask(@UserMessageAnn() question: string, @MemoryId() sessionId: string): Promise<string> { throw new Error('handled by AiService proxy'); } } @Agent({ system: 'You help with weather.', tools: [WeatherTools], }) class WeatherAgent {} configureAi({ chatModel: new OpenAiChatModel({ model: 'gpt-4o-mini' }), toolBeans: [WeatherTools], }); const bot = resolveAiService(WeatherBot); await bot.ask('Weather in Yorktown?', 'session-1'); const agent = resolveAnnotatedAgent(WeatherAgent); await agent.chat('Weather in Yorktown?');

configureAi registers the chat model, a singleton ChatClient, a prototype ChatClient.Builder, optional memory/tools/advisors, and (by default) scans annotations so @AiService/@Agent classes become resolvable factories.

Inject a prototype ChatClient.Builder

import { Component, Container } from '@di-framework/core/decorators'; import { AiTokens, ChatAgent, type ChatClientBuilder, configureAi, OpenAiChatModel, } from '@di-framework/ai'; configureAi({ chatModel: new OpenAiChatModel() }); @Container() class SupportAgentService { private readonly agent: ChatAgent; constructor(@Component(AiTokens.CHAT_CLIENT_BUILDER) builder: ChatClientBuilder) { this.agent = ChatAgent.fromBuilder(builder) .system('You are an e-commerce support assistant.') .build(); } chat(prompt: string, sessionId: string) { return this.agent.chat(prompt, { conversationId: sessionId }); } }

Each resolve of AiTokens.CHAT_CLIENT_BUILDER yields a fresh builder (Spring @Scope("prototype") style). Customize per service without mutating a shared client.

Imperative ChatClient

import { ChatClient, OpenAiChatModel } from '@di-framework/ai'; const model = new OpenAiChatModel({ model: 'gpt-4o-mini' }); const client = ChatClient.create(model); const answer = await client .prompt() .system('You are concise.') .user('What is Yorktown known for?') .call() .content();

Advisors, default tools, and default system text can be attached on the builder before build(), or per prompt.

DI with configureAi

configureAi({ chatModel: new OpenAiChatModel(), defaultSystem: 'You help with weather questions.', toolBeans: [WeatherTools], observation: true, agent: true, // optional AiTokens.CHAT_AGENT scanAnnotations: true, // default — processes @AiService / @Agent / … });

Useful options:

Option

Default

Meaning

chatModel

Instance or factory; required unless already registered

registerChatClient

true

Register singleton ChatClient

registerChatClientBuilder

true

Register prototype builder

toolBeans

Beans scanned for @Tool methods

memory

ChatMemory instance or factory

advisors

Extra advisors on the default client

observation

Emit redacted ai.chat.* container events

scanAnnotations

true

Register annotated assistants / agents / workflows

agent

Register a default ChatAgent under AiTokens.CHAT_AGENT

Fine-grained helpers: registerChatModel, registerChatClient, registerChatClientBuilder, registerChatMemory, registerToolCallbacks, registerChatAgent, plus matching resolve* functions.

Tools

Two equivalent paths:

  1. Bean methods@ToolSet() + @Tool({ description, inputSchema }) on a @Container() class; pass the class to toolBeans or @AiService({ tools: [...] }).

  2. CallbacksfunctionToolCallback({ name, description, inputSchema }, handler) for imperative wiring.

Tool-calling loops run through the advisor chain (ToolCallingAdvisor). Duplicate tool names are deduped (last registration wins) when combining toolBeans and per-assistant tools.

Memory and advisors

import { MessageWindowChatMemory, configureAi, WithMemory, MemoryId } from '@di-framework/ai'; configureAi({ chatModel: /* … */, memory: new MessageWindowChatMemory({ maxMessages: 20 }), }); @AiService() @WithMemory() class RememberingBot { talk(@UserMessageAnn() message: string, @MemoryId() sessionId: string): Promise<string> { throw new Error('proxy'); } }

@MemoryId()/@ConversationId() bind a method parameter to the chat-memory conversation id. Other advisor stereotypes: @WithRag/@RetrievalAugmented, @WithTools, @AiObserved/@Observed, plus @AiAdvisor/@AdvisorOrder for custom advisor beans.

RAG

Imperative pieces: SimpleVectorStore, FakeEmbeddingModel/ embedding models, VectorStoreDocumentRetriever, RetrievalAugmentationAdvisor. Annotation markers (@VectorStoreAnn, @EmbeddingModelAnn, @Retriever, @IndexedDocument, @WithRag) declare intent for scanning; wire stores and embeddings through configureAi({ embeddingModel, vectorStore }) or manual registration under AiTokens.

Agents and workflows

  • ChatAgent — multi-turn chat with tools/memory; build via ChatAgent.create(...), chatAgent(...), or ChatAgent.fromBuilder(builder).

  • @Agent/@ChatAgentBean — declarative bean resolved with resolveAnnotatedAgent.

  • Workflows (imperative + stereotypes): ChainWorkflow, RoutingWorkflow, ParallelizationWorkflow, OrchestratorWorkersWorkflow, EvaluatorOptimizerWorkflow, with matching @Chain, @Route/@Router, @Parallel, @Orchestrator/@Worker, @Evaluate/@Optimize.

  • GraphWorkflow — typed graph runtime for arbitrary control flow (linear, branch, loop, nested subgraphs). Fluent builder with async edge predicates/transforms, AbortSignal, maxSteps, build-time validation, and lifecycle hooks. Helpers: chatToolLoopGraph, simpleAgentGraph. Graph stereotypes are deferred until the imperative API is stable.

  • PlannerExecutorWorkflow — plan → act → replan loop on ChatClient + tools, with step limits and cycle protection.

  • A2ABus — thin in-process agent-to-agent message bus with optional human-in-the-loop hooks (local only; not a network protocol).

import { GRAPH_FINISH, GRAPH_START, GraphWorkflow, PlannerExecutorWorkflow } from '@di-framework/ai'; const graph = GraphWorkflow.builder<number, string>('example') .node('double', (n) => n * 2) .node('label', (n) => `value=${n}`) .edge(GRAPH_START, 'double') .edge('double', 'label') .edge('label', GRAPH_FINISH) .build(); const { output, path } = await graph.run(21, { maxSteps: 50 }); const { answer } = await PlannerExecutorWorkflow.of(chatClient).run(goal, { tools: [tool], maxSteps: 6, });

MCP

Uses @modelcontextprotocol/sdk. Adapt an SDK client and expose remote tools as ToolCallbacks, or mark beans with @McpClient/@McpTool. Token: AiTokens.MCP_CLIENT.

Network A2A 1.0 (Agent-to-Agent Protocol over HTTP)

@di-framework/ai implements the standard AAIF Agent-to-Agent (A2A) 1.0 Protocol over HTTP JSON-RPC.

  • Agent Cards: Discover skills and capabilities at GET {url}/.well-known/agent-card.json.

  • JSON-RPC Operations: Standard 1.0 methods (SendMessage, GetTask, ListTasks, CancelTask) over HTTP POST.

  • Task Lifecycle: Strict states (submittedworkingcompleted | failed | canceled | rejected | input-required | auth-required).

  • Discovery: A2ADirectory fetches cards from registered origins and returns connected A2AClient instances.

  • Process Boundary Opacity: Wire representation carries only messages, tasks, and artifacts. Internal prompts, tool definitions (MCP), and memory keys remain completely private inside the serving process.

  • MCP vs A2A: MCP equips agents with internal tools and resources; A2A dispatches tasks and work across independent agent services over the network.

Exposing an Agent over A2A

import { Agent, EnableAi } from '@di-framework/ai'; @Agent({ name: 'ReviewAgent', description: 'Automated code review agent', skills: [{ id: 'dev.review', description: 'Review pull request diffs' }], a2a: { url: 'https://agents.example.com/review' }, system: 'You are an expert code reviewer.', tools: [/* internal MCP tools */], }) export class ReviewAgent {} @EnableAi({ a2a: true, // opts into serving Agent Card and JSON-RPC HTTP handlers }) export class AppModule {}

Discovering and Dispatching Work via A2ADirectory and A2AClient

import { A2ADirectory, A2AClient } from '@di-framework/ai'; // Initialize directory with remote origins const directory = A2ADirectory.create({ origins: [ 'https://agents.example.com/aria', 'https://agents.example.com/ravi', ], }); // Discover a peer advertising the required skill const reviewer: A2AClient = await directory.find({ skill: 'dev.review' }); // Dispatch task and await completion const task = await reviewer.sendAndWait({ skill: 'dev.review', message: 'git diff main...feature', metadata: { workId: 'ticket-456' }, }); console.log(task.status.state); // 'completed' console.log(task.artifacts); // array of A2AArtifact

In-process Local Bus (Non-network)

import { A2ABus } from '@di-framework/ai'; const bus = A2ABus.create(); bus.register('researcher', async (msg) => `notes:${msg.content}`); const reply = await bus.request('user', 'researcher', 'topic');

Agent Skills

Reusable SKILL.md folders and .agents/plugins discovery live in @di-framework/ai-utils, not this package. Prefer SkillsAgent.builder()/SkillsToolbox.builder() for skills, and validatePluginCatalog/loadPluginDirectory for plugins. For network agent capability advertising, use A2A Agent Card skills on @Agent({ skills, a2a }).

See Agents.

Providers

new OpenAiChatModel({ apiKey: process.env.OPENAI_API_KEY, baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini', }); new AnthropicChatModel({ apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-sonnet-4-20250514', });

Both speak HTTP via fetch. Point baseUrl at any OpenAI-compatible gateway when needed.

Select API or subscription access

createChatModel() synchronously selects a model. There is no fallback from subscription to API on missing CLIs, login errors, or provider failures.

import { createChatModel, configureAi } from '@di-framework/ai'; configureAi({ chatModel: () => createChatModel() });
PROVIDER=openai AUTH=subscription bun my-program.ts

Explicit provider, auth, and model override PROVIDER, AUTH, and MODEL. A provider is required. api.model also precedes MODEL; the top-level model wins over both.

Provider

API route

Subscription route

openai

OpenAI; existing default model

Codex CLI

anthropic

Anthropic; existing default model

Claude CLI

xai

xAI OpenAI-compatible endpoint; model required

Grok CLI

agy

Unsupported

AGY CLI

junie

Unsupported

Junie CLI

hermes

Unsupported

Local Nous proxy; model required

Vendor names default to API access. Aliases codex, claude, and grok, and subscription-only providers, default to subscription access. Explicit auth always wins. API keys come from api.apiKey or OPENAI_API_KEY/ANTHROPIC_API_KEY/XAI_API_KEY; xAI never borrows an OpenAI key. Existing HTTP constructors keep their original behavior.

CLI inference requires Bun and the selected CLI on PATH (codex login, Claude login, grok login, AGY Google sign-in, or Junie including JUNIE_API_KEY). Hermes uses http://127.0.0.1:8645/v1; start hermes proxy start --provider nous separately.

api accepts the existing HTTP provider options (including a custom fetch). subscription accepts timeoutMs (default 120000), maxCalls (default 32), executable, toolCallingManager, and onEvent. Irrelevant route options are rejected. Native CLIs own their model/tool loop; framework callbacks go through a private MCP child. Streaming, token usage, sampling controls, media, and provider output schemas are unsupported on the subscription route.

Testing

import { ChatClient, ScriptedChatModel, toolCall, toolCallResponse } from '@di-framework/ai'; const model = new ScriptedChatModel([ { respond: toolCallResponse([toolCall('c1', 'getWeather', { city: 'Yorktown' })]) }, { respond: '68F in Yorktown' }, ]); const client = ChatClient.create(model);

Also available: FakeChatModel for fixed-content replies. Prefer scripted models in unit tests so tool loops stay deterministic.

Name collisions

Chat message types (SystemMessage, UserMessage, AssistantMessage) and other runtime types keep their names. Where a decorator would collide, the package exports an *Ann (or renamed) decorator:

Decorator export

Collides with

SystemMessageAnn/UserMessageAnn/AssistantMessageAnn

Message types

ChatModelAnn/ChatClientAnn/ChatMemoryAnn

Model / client / memory types

VectorStoreAnn/DocumentAnn/EmbeddingModelAnn

Store / document / embedding types

AiAdvisor

Advisor type

PromptTemplate

Prompt class

ChatAgentBean

ChatAgent class

Tokens

Prefer AiTokens over ad-hoc strings:

Token

Default string

Role

CHAT_MODEL

chatModel

Primary chat model

CHAT_MODEL_DEFAULT

chat.default

Alias

CHAT_CLIENT

chatClient

Singleton client

CHAT_CLIENT_BUILDER

chatClientBuilder

Prototype builder

CHAT_AGENT

chatAgent

Default agent

CHAT_MEMORY

chatMemory

Memory bean

EMBEDDING_MODEL/VECTOR_STORE/DOCUMENT_RETRIEVER

RAG

TOOL_CALLBACKS

ai.tools

Aggregated tools

ADVISORS

ai.advisors

Aggregated advisors

MCP_CLIENT

mcpClient

MCP session

A2A_TASK_STORE

a2a.taskStore

A2A server task store

A2A_HTTP_HANDLER

a2a.httpHandler

A2A Fetch HTTP handler

Decorator catalog (selection)

Decorator

Purpose

@AiService/@Assistant

Declarative chat assistant (class → proxy)

@Agent/@ChatAgentBean

Declarative ChatAgent bean and A2A Agent Card endpoint

@SystemMessageAnn/@UserMessageAnn()/@MemoryId()

Prompt + session wiring

@Tool/@ToolSet/@ToolParam()

Tool methods on beans

@WithMemory/@WithRag/@WithTools/@AiObserved

Attach advisors

@EnableAi

Bootstrap scanning + configureAi options on an app class

@Chain/@Route/@Parallel/ …

Workflow stereotypes

API Reference

Export

Description

configureAi/enableAi

Bootstrap model, client, builder, annotations, A2A

ChatClient/ChatClientBuilder

Fluent chat API

OpenAiChatModel/AnthropicChatModel

HTTP providers

AiService/Assistant/resolveAiService

Annotated assistants

Agent/ChatAgentBean/resolveAnnotatedAgent

Annotated agents

ChatAgent/ChatAgent.fromBuilder

Imperative / builder agents

Tool/ToolSet/ToolParam/functionToolCallback

Tools

MessageWindowChatMemory/MessageChatMemoryAdvisor

Memory

RetrievalAugmentationAdvisor/ vector store helpers

RAG

ChainWorkflow/RoutingWorkflow/ …

Fixed-pattern workflows

GraphWorkflow/chatToolLoopGraph

Graph agent runtime

PlannerExecutorWorkflow

Plan → act → replan

A2ADirectory

Registry and remote A2A agent discovery

A2AClient

HTTP JSON-RPC client for A2A operations

A2ATaskStore

In-memory task state lifecycle store

A2AJsonRpcHandler

JSON-RPC 2.0 A2A request dispatcher

createA2AHttpHandler

Standard Web Fetch (Request) => Promise<Response> handler

createA2AAuthHandler

Bearer token auth handler via @di-framework/auth

A2ABus

In-process multi-agent messages (local bus)

ScriptedChatModel/FakeChatModel

Tests

AiTokens

Well-known DI tokens

AiError/isAiError

Typed errors

Non-goals (v1)

  1. Official vendor SDKs — HTTP adapters only; bring your own SDK behind a custom ChatModel if needed.

  2. Hosting / orchestration platforms — no LangSmith, Bedrock Agents console, or cloud agent runtimes.

  3. Full prompt IDE / playground — library APIs and annotations only.

  4. Authorization of tool calls — tools run as wired; gate them in your handlers.

  5. Persistent vector DBs as first-party drivers — in-memory / simple store plus interfaces; plug your own VectorStore.

Example

Package tests under packages/di-framework-ai/tests cover ChatClient, tools, memory, RAG, MCP, agents, A2A 1.0, and the annotation DX.

Last modified: 15 September 2026