Skip to content

Feat/server decoupling#290

Closed
alasano wants to merge 22 commits intoVoltAgent:mainfrom
alasano:feat/server-decoupling
Closed

Feat/server decoupling#290
alasano wants to merge 22 commits intoVoltAgent:mainfrom
alasano:feat/server-decoupling

Conversation

@alasano
Copy link
Contributor

@alasano alasano commented Jun 21, 2025

RFC: Server Decoupling Architecture - Introducing createVoltServer API

TL;DR

Separates agent logic from server dependencies. If you only use agents directly, your bundle is now lighter.

If you need HTTP endpoints, install @voltagent/server-hono and use createVoltServer({ agents: { agent } }) instead of new VoltAgent({ agents: { agent } }).

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

Understanding VoltAgent's Role

VoltAgent has two distinct use cases that are often confused:

1. Using Agents Directly (No VoltAgent needed)

import { Agent } from "@voltagent/core";

const agent = new Agent({
  name: "Chat Assistant",
  instructions: "A helpful assistant that can check the weather.",
  llm: new VercelAIProvider(),
  model: openai("gpt-4o"),
  tools: [weatherTool],
});

const result = await agent.generateText("What's the weather in NYC?");
// Works perfectly! No server started, no HTTP endpoints created

2. Exposing Agents via HTTP/WebSocket (VoltAgent's purpose)

import { VoltAgent } from "@voltagent/core";

new VoltAgent({ agents: { agent } });
// Creates HTTP endpoints: POST /agents/assistant/text
// Creates WebSocket: ws://localhost:3456/ws/agents/assistant
// Enables console.voltagent.dev integration

Current Architecture Problems

Problem 1: Unnecessary Dependencies

  • Even if you ONLY want to use agents directly (case 1), installing @voltagent/core gives you:
    • All agent logic ✓
    • Plus server dependencies you don't need ✗
    • Including: hono, ws, @hono/* packages

Problem 2: What Actually Happens

// Scenario A: Just using an agent
const agent = new Agent({
  name: "Support Bot",
  instructions: "You are a customer support assistant. Be helpful and polite.",
  llm: new VercelAIProvider(),
  model: openai("gpt-4o-mini"),
});
// Result: No server started ✓
// But: You have server dependencies installed that you don't use ✗

// Scenario B: Using VoltAgent (auto-start)
new VoltAgent({ agents: { agent } });
// Result: Automatically starts HTTP server on port 3456 (by default)
// Creates routes like /agents, /agents/:id/text, /agents/:id/stream

// Scenario C: Using VoltAgent (manual start)
const voltAgent = new VoltAgent({ agents: { agent }, autoStart: false });
// Result: No server started yet
await voltAgent.startServer(); // Start manually when ready

Problem 3: Technical Debt

  • AgentRegistry.getInstance() - global singleton pattern
  • Server code (server/ directory) mixed into core package
  • Tight coupling to Hono framework
  • Cannot integrate with existing Express/Fastify servers

What is the new behavior?

Clear Separation of Concerns

1. Agent Usage Remains Unchanged

import { Agent } from "@voltagent/core";

const agent = new Agent({
  name: "Data Analyst",
  instructions: "You are a data analysis expert. Help users understand their data.",
  llm: new VercelAIProvider(),
  model: openai("gpt-4o"),
  tools: [sqlQueryTool, chartTool],
});

const result = await agent.generateText("Analyze sales trends for Q4");
// Still works exactly the same!
// But now @voltagent/core has only the dependencies you actually need

2. Server Functionality Moved to Separate Package

import { Agent } from "@voltagent/core";
import { createVoltServer } from "@voltagent/server-hono";

// Create your agent instance
const apiAgent = new Agent({
  name: "API Assistant",
  instructions: "You are an API documentation expert. Help users understand and use APIs.",
  llm: new VercelAIProvider(),
  model: openai("gpt-4o-mini"),
  tools: [apiTesterTool],
});

// When you WANT HTTP/WebSocket endpoints:
const server = createVoltServer({
  agents: { apiAgent },
  port: 3141
});

// Explicit control - no automatic startup
await server.start();

Key Differences Explained

BEFORE: Mixed Concerns

// @voltagent/core included everything:
// - Agent logic
// - Hono server
// - WebSocket server  
// - OpenAPI/Swagger
// - HTTP route handlers

// Using VoltAgent was confusing:
new VoltAgent({ agents: { myAgent } });
// What does this do? Start a server? Register agents? Both?

AFTER: Clear Separation

// @voltagent/core - Just agent logic
// @voltagent/server-hono - Server functionality (installed separately)

// Clear intent:
const agent = new Agent(...);  // I want to use an agent
const server = createVoltServer({ agents: { agent } });  // I want HTTP endpoints for agents

Technical Architecture Changes

  1. Package Structure

    • @voltagent/core: Pure agent logic, zero server dependencies
    • @voltagent/server-adapter: Framework-agnostic HTTP abstraction
    • @voltagent/server-hono: Hono implementation of server
  2. Dependency Changes

    // @voltagent/core/package.json
    dependencies: {
    -  "@hono/node-server": "^1.14.0",
    -  "@hono/node-ws": "^1.1.1",
    -  "@hono/swagger-ui": "^0.5.1",
    -  "@hono/zod-openapi": "^0.19.6",
    -  "hono": "^4.7.7",
    -  "ws": "^8.18.1",
      // ... only agent-related deps remain
    }
  3. Registry Pattern

    • OLD: AgentRegistry.getInstance() - global singleton
    • NEW: LocalAgentRegistry - instance-based, no global state
  4. Registry Management

    // How createVoltServer manages agent registries:
    
    // Agents can be created independently:
    const agent1 = new Agent({ name: "agent1", llm, model });
    const agent2 = new Agent({ name: "agent2", llm, model });
    
    // Server injects shared registry for unified management:
    createVoltServer({
      agents: { agent1, agent2 }  // Server injects shared LocalAgentRegistry
    });
    
    // All agents now share the same registry for server management
    // While preserving their individual configurations and capabilities

Migration Guide

If you only use agents (no HTTP endpoints)

No changes needed! Your code works exactly the same, just with a lighter bundle.

If you use VoltAgent for HTTP endpoints

Option 1: Keep existing code (deprecated)

new VoltAgent({ agents: { myAgent } });
// Still works, shows deprecation warning
// Will be removed in v2.0

Option 2: Migrate to new API

// Before
import { VoltAgent, Agent } from "@voltagent/core";
const agent = new Agent({
  name: "Support Bot",
  instructions: "You are a customer support assistant.",
  llm: new VercelAIProvider(),
  model: openai("gpt-4o-mini"),
});
new VoltAgent({ agents: { agent } });

// After  
import { Agent } from "@voltagent/core";
import { createVoltServer } from "@voltagent/server-hono";

const agent = new Agent({
  name: "Support Bot",
  instructions: "You are a customer support assistant.",
  llm: new VercelAIProvider(),
  model: openai("gpt-4o-mini"),
});

const server = createVoltServer({
  agents: { agent }
});
await server.start();

Benefits Summary

  1. For Agent-Only Users: Cleaner dependency tree, only install what you need
  2. For Server Users: Explicit control, framework flexibility
  3. For Everyone: Cleaner architecture, better testing, clearer intent

Implementation Notes

  • The deprecated VoltAgent uses dynamic imports to maintain backward compatibility
  • @voltagent/server-adapter is automatically installed with @voltagent/server-hono
  • It was suggested to rename @voltagent/server-hono to just @voltagent/server TBD
  • All existing HTTP routes and WebSocket protocols remain unchanged
  • Full compatibility with console.voltagent.dev maintained

Future Work / Possibilities

  • Express adapter implementation
  • Fastify adapter implementation
  • v2.0: Remove deprecated VoltAgent class

Fixes #281

alasano added 21 commits June 9, 2025 10:50
  - Add registerCustomEndpoints export to core for legacy API
  - Update legacy server to merge global and instance endpoints
  - Clean up duplicate custom-endpoints code between packages
  - Fix tests to use new RouteDefinition object format
  - Add missing error handling to HonoServerAdapter routes
  - Replace mock-socket with custom ws mocks for server-side testing
  - Remove _testOnlyAgentConnections export from production code
  - Add comprehensive WebSocket server behavior tests
  - Test connection handling, broadcasting, events, and error scenarios
  - Increase overall coverage from 39.35% to 93.54%
  - All 116 tests now passing including 24 WebSocket tests
  - Change createVoltServer agents parameter from AgentDefinition[] to Record<string,
  Agent<any>>
  - Remove AgentDefinition type and agent creation logic
  - Inject shared registry into pre-instantiated agents
  - Update telemetry handling for agent instances
  - Add comprehensive test for subagent support
  - Update all tests to use Agent instances instead of definitions
@changeset-bot
Copy link

changeset-bot bot commented Jun 21, 2025

⚠️ No Changeset found

Latest commit: 38fb7fc

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@omeraplak
Copy link
Member

Hey @alasano , thanks a lot for the PR!
Would you have a chance to take care of the conflicts, or should I take a look?

Copy link
Contributor

@zrosenbauer zrosenbauer left a comment

Choose a reason for hiding this comment

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

@alasano this is a huge win when we get this out and a ton to love in this PR. I tagged some general feedback and things we probably need to chat with @omeraplak on as they are huge changes to the underlying design/implemenation.

Some easier wins are:

  • The default and IMO the default we will go forward with is NEVER use classes unless you need to maintain state (i.e. the Queue implmentation in #287)... we aren't making major changes to this YET but will in future: #192
  • rename server-hono (unless @omeraplak disagrees) to server as the default VoltAgent server will be hono but the adapter allows custom integrations into any server

Overall 🙏 TY for your work on this as this is huge for the project (I'm anxiously waiting to finish my fastify PR!).


/**
* Main VoltAgent class for managing agents and server
* @deprecated The VoltAgent class is deprecated and will be removed in v2.0. Please use the new `createVoltServer` function from `@voltagent/server-hono` or your chosen server adapter.
Copy link
Contributor

Choose a reason for hiding this comment

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

@omeraplak @alasano do we want to remove this? This is the primary interface I use for setting up telemetry and also this assumes that every use case involves a server. Its 100% possibly to execute VoltAgent in things like:

  • scripts
  • cli
  • browser (with some work we could)

Another note we are not at a v1 yet so I don't think we'd want to wait for major changes until v2

Copy link
Member

Choose a reason for hiding this comment

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

Maybe not now, but possibly in the future. Making too many changes at once could be a bit risky.

this.retriever = options.retriever;
this.voice = options.voice;
this.markdown = options.markdown ?? false;
this.registry = options.registry || new LocalAgentRegistry();
Copy link
Contributor

Choose a reason for hiding this comment

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

I love this concept but @omeraplak I have an idea (unless I'm way off on the intention).

@omeraplak is the intention of the registry primarily around the LLMOps / Observability and console? If so we could lean towards giving the dev more control, with solid defaults.

Instead of requiring the registry make it 100% optional and completely decouple from the Agent. So for example if you want to add a team of Agents you'd do the following:

const subagent1 = new Agent();
const subagent2 = new Agent();
const supervisor = new Agent({
   subagents: [subagent1, subagent2]
});

const registry = new VoltAgent({
   agents: [subagent1, subagent2]
});

// Adding on the fly while decoupled:

const subagent3 = new Agent();

registry.registerAgent(subagent3); // first you add the agent to the registry
supervisor.addSubagent(subagent3); // second you add the subAgent where you want it

This would allow use to create agents without having to inject and pass around the registry which would IMO make it really hard to trace bugs etc. (state is a pain).

In addition if we wanted to keep the default functionality for a more zero-config set up we can provide this:

class AgentLite {} // no registry

class Agent extends AgentLite {} // has registry via instance and/or injection

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I’m not sure. I think it’s valuable for users to be able to use VoltOps effortlessly, without needing any configuration. That’s why I’d prefer not to separate it out too much. I’m not sure if what you’re suggesting goes against that idea or not.

* Formats agent data for console consumption
* Ensures compatibility with console.voltagent.dev
*/
export class ConsoleDataFormatter {
Copy link
Contributor

Choose a reason for hiding this comment

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

Love this but I don't think it needs to be a class as there is no state management, I'd just create as a set of utils.

@@ -0,0 +1,42 @@
// Base adapter
Copy link
Contributor

Choose a reason for hiding this comment

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

👏 I love this set up as it makes it 100% clear what should be exposed in the package, great addition.

/**
* Factory to create VoltAgent routes for any server framework
*/
export class VoltRouteFactory {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this could be a set of functions wrapped in a function as the class isn't really used outside of being init in the function.

I'd be fine for keeping as a class if we only export the type something like:

export type { VoltRouteFactory };

Or export the type only in the index.ts

// Export all schemas
export * from "./types";

// Import route definitions
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Import route definitions

@@ -0,0 +1,56 @@
// Export all schemas
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Export all schemas

@alasano
Copy link
Contributor Author

alasano commented Jun 24, 2025

Hey @alasano , thanks a lot for the PR! Would you have a chance to take care of the conflicts, or should I take a look?

Hey @omeraplak , yeah I can do that. I think it's a bit more involved given how much everything changes, so i have to cherry pick changes across newer commits.

I think it's worth deciding on the things brought up by @zrosenbauer first, that way i can rework the current version and then proceed to resolve conflicts and get it up to date with the latest commits.

@alasano
Copy link
Contributor Author

alasano commented Jun 24, 2025

@alasano this is a huge win when we get this out and a ton to love in this PR. I tagged some general feedback and things we probably need to chat with @omeraplak on as they are huge changes to the underlying design/implemenation.

Some easier wins are:

* The default and IMO the default we will go forward with is NEVER use classes unless you need to maintain state (i.e. the Queue implmentation in [fix: optimize stream/generate performance #287](https://github.com/VoltAgent/voltagent/pull/287))... we aren't making major changes to this YET but will in future: [chore(core): move to `createAgent` instead of class based `new Agent()` #192](https://github.com/VoltAgent/voltagent/pull/192)

* rename `server-hono` (unless @omeraplak disagrees) to `server` as the default VoltAgent server will be `hono` but the adapter allows custom integrations into any server

Overall 🙏 TY for your work on this as this is huge for the project (I'm anxiously waiting to finish my fastify PR!).

Thanks for taking the time to look through it @zrosenbauer 🚀

The default and IMO the default we will go forward with is NEVER use classes unless you need to maintain state (i.e. the Queue implmentation in #287)... we aren't making major changes to this YET but will in future: #192

Can you explain your thought process for this, was it discussed anywhere or just the PR you linked? It aligns with

rename server-hono (unless @omeraplak disagrees) to server as the default VoltAgent server will be hono but the adapter allows custom integrations into any server

I agree with this, it's the default it makes sense.

@myrrakh
Copy link

myrrakh commented Jun 26, 2025

I'm not sure how accurate is this since since is a large codebase, I will give some tries later, but it would be nice if you guys could also review it: https://github.com/myrrakh/voltagent

one thing inside core package.json I needed to set, it should have a script to change between workspace and latest for development
"@voltagent/internal": "workspace:*"

The current errors on test:
@voltagent/core: × VoltAgent serverMode functionality > deprecation warnings > should show deprecation warning for default autoStart behavior 27ms
@voltagent/core: → expected "warn" to be called with arguments: [ StringContaining "DEPRECATION" ]

and

@voltagent/google-ai: Failed to parse JSON response or validate against schema: SyntaxError: Expected property name or '}' in JSON at position 2
@voltagent/google-ai: at JSON.parse ()
@voltagent/google-ai: at GoogleGenAIProvider.generateObject (/Users/robertveloso/2025/agents/voltagent2/packages/google-ai/src/index.ts:797:50)
@voltagent/google-ai: at processTicksAndRejections (node:internal/process/task_queues:95:5)
@voltagent/google-ai: stderr | src/index.spec.ts > GoogleGenAIProvider > generateObject > should throw error if response does not match schema
@voltagent/google-ai: Failed to parse JSON response or validate against schema: ZodError: [
@voltagent/google-ai: {
@voltagent/google-ai: "code": "invalid_type",
@voltagent/google-ai: "expected": "number",
@voltagent/google-ai: "received": "string",
@voltagent/google-ai: "path": [
@voltagent/google-ai: "age"
@voltagent/google-ai: ],
@voltagent/google-ai: "message": "Expected number, received string"
@voltagent/google-ai: }
@voltagent/google-ai: ]

@zrosenbauer
Copy link
Contributor

zrosenbauer commented Jun 26, 2025

@alasano no matter what this is a huge change and you'll probably have a ton of conflicts if anyone edits the server anything...

On class

Caution

DISCLAIMER: No idea how deep you are into TS/JS world so if I share stuff you are aware of feel free to ignore it 😄

Classes in TypeScript have multiple issues, I've broken down by facts/observations and opinions:

Facts & Observations

  • JS Classes are not the same as other OOP as they're built on top of prototypes which can provide a lot of footguns for devs if they don't know how to use them & i expect a lot of folks new to TS to use Volt
  • TypeScript classes add an additional abstraction with their public and private as JS has its own concept of private using # prefix aka private in TS is purely for types

Opinions

  • Classes also encourage inheritance which for most part is a pain to maintain, test, and introduces a host of issues (much of JS world if not the vast majority leans towards a more functional/stateless vs OOP approach)
  • You also just don't need them (you can actually mirror REAL private methods with a function vs in TS where they are still technically accessible).

FYI I'm also not militant in my non-use of classes as there are plenty of use cases for them, primarily something that is super stateful such as a in-memory queue (even though as I show below you still don't need them)

Helpful Examples

Example of how to mirror stateful w/o classes:

class StatefulThing {
  public counter = 0;
  
  private notRealPrivate() {
    console.log('nuke me');
  }

  public up() {
     notRealPrivate();
     this.counter = this.counter + 1;
  }

  public down() {
    notRealPrivate();
    this.counter = this.counter - 1;
  }
}

function createStatefulThing() {
   let counter = 0;
  
   function realPrivate() {
       console.log("you can't do anything to me!");
   }
  
  return {
     up() {
         realPrivate();
         counter = counter + 1;
     },
     down() {
       realPrivate();
        counter = counter - 1
     }
  }
}

const stateClass = new StatefulThing();
const stateFunc = createStatefulThing();

// overwrites
stateClass.notRealPrivate = () => {
   throw new Error('Uh oh');
}

// does nothing 
stateFunc.realPrivate = () => {
   throw new Error('throw away i dont care');
}

Examples of how to create agents without classes:

import { createAgent } from '@voltagent/core';
import { wrapSpecialAgent, createSpecialAgent } from '../mycode';

const agent1 = createAgent({
   ...opts,
});

const agent2 = createAgent({
   ...opts
});

// we wrap the existing agent and make changes to it under the hood and since each invocation is a copy it doesn't matter
// i.e. we could inject logging, default settings
const specialAgent1 = wrapSpecialAgent(agent2);

// another example of composition using pure funcs no "instance"
const specialAgent2 = createSpecialAgent(
  createAgent({
     ...opts
  })
);

const superVisor = createAgent({
  ...opts
  subagents: [agent1, agent2]
});

I have more thoughts about the design but just haven't had time to put pen to paper yet!

@zrosenbauer
Copy link
Contributor

@alasano let me know if you need / want support on this. I might be able to take a stab at this over the weekend.

@myrrakh
Copy link

myrrakh commented Jul 2, 2025

I'm also available if you guys want an additional help

- Rename packages/server-hono directory to packages/server
- Update package name from @voltagent/server-hono to @voltagent/server
- Update all imports and references across the codebase
- Update deprecation messages to reference the new package name
- Update test files and documentation references
@alasano
Copy link
Contributor Author

alasano commented Jul 7, 2025

I've started the pretty complicated process of getting this up to date with the main branch, however there's no real way to have this be free of merge conflicts at the end.

Main is ~180 commits ahead of my branch, I've started a commit by commit process of cherry picking each commit ahead of my fork (a lot of them easily integrate since they don't touch the core package).

For commits that made changes to core or the coupled server parts of core, the changes are being applied to the corresponding parts of core and decoupled server/server-adapter packages in my fork.

I'll keep pushing progress in the branch here https://github.com/alasano/voltagent/tree/migration-integrator.

I've got it set up so that it goes pretty fast, once I'm up to date with main I'll update my changes with feedback you guys have given.

Even though it's fully backward compatible as discussed, it's still a big step to merge it after because you'll need to plan for it @omeraplak

@zrosenbauer
Copy link
Contributor

TY @alasano love it and yea Volt is moving lightening fast atm :)

@zrosenbauer
Copy link
Contributor

@alasano are you still working on this? so much has changed I think this pull would be hard to resolve/merge.

@omeraplak if you're going to take this on possibly (or myself) we can probably close and just make sure we have a solid GitHub issue and put on roadmap

@omeraplak
Copy link
Member

Hey,
Thanks for this great effort. We’ll be releasing v1 soon, and I’ll handle server decoupling as part of that. I’m closing this PR with regret :(

@omeraplak omeraplak closed this Aug 14, 2025
@alasano
Copy link
Contributor Author

alasano commented Aug 15, 2025

No worries @omeraplak , I wasn't able to put enough time in to catch up with all the changes (voltagent moving too fast ;)). Looking forward to the official version of it.

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.

[FEAT] Expose Hono Server Instance for Custom Integrations

4 participants