Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/vnext-inprocess-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@reaatech/agent-mesh': minor
'@reaatech/agent-mesh-router': minor
'@reaatech/agent-mesh-postgres': minor
'@reaatech/agent-mesh-redis': minor
---

agent-mesh v-next, part 2 — in-process transport and Postgres/Redis persistence:

- **router / core: in-process transport.** Agents can now be dispatched in-process
(`type: 'inprocess'`, no HTTP hop) via `registerInProcessAgent`, alongside the MCP
transport. `dispatchToAgent` routes by `agent.type` and threads an optional
`metadata` passthrough (e.g. a tenant `orgId`) into the `ContextPacket`. Existing
`type: 'mcp'` agents are unchanged.
- **core: consolidate the duplicated `AgentConfig`.** It was defined in both core and
the registry; it now lives once in core (with the SSRF-safe endpoint check) and the
registry re-exports it. `type` is `enum(['mcp','inprocess'])` and `endpoint` is
optional (required for mcp via a refine).
- **new `@reaatech/agent-mesh-postgres`** — Postgres-backed `SessionStore` +
`BreakerStore` adapters (+ `ensureSchema` / `SCHEMA_SQL`), leader election via a
single-row lease with `SELECT … FOR UPDATE`.
- **new `@reaatech/agent-mesh-redis`** — Redis-backed `SessionStore` + `BreakerStore`
adapters, leader election via a `SET NX PX` lease.

Docs, examples, and package READMEs updated for the pluggable classifier/persistence
and the in-process transport.
10 changes: 8 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,16 @@ tsup (dual CJS/ESM build) + Biome (lint/format) + Vitest (testing).

### 1. Stateless by Design
- No in-memory state shared across requests
- All session state persisted to Firestore
- Circuit breaker state persisted with leader election
- All session state persisted via a pluggable `SessionStore` (Firestore default; Postgres/Redis/in-memory adapters)
- Circuit breaker state persisted via a pluggable `BreakerStore` with leader election (Firestore default; Postgres/Redis)
- Enables horizontal scaling on Cloud Run / Kubernetes

### 1b. Pluggable Providers
- **Classifier** — `ClassifierProvider` (Gemini default; inject any model, incl. a host-resolved one). No hard dependency on a specific model vendor.
- **Persistence** — `SessionStore` / `BreakerStore` interfaces; Firestore is the default impl, Postgres/Redis/in-memory are drop-in.
- **Transport** — agents dispatch over MCP (`type: 'mcp'`) or **in-process** (`type: 'inprocess'`) for co-located handlers, with a `metadata` passthrough for host context.
- Defaults are unchanged for existing deployments — every provider is opt-in via injection.

### 2. Zero-Trust Security
- All inputs validated with Zod schemas
- SSRF protection on agent endpoint URLs
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ This monorepo provides the full orchestrator stack for building multi-agent AI s

## Features

- **Confidence-gated routing** — Gemini Flash intent classification with a 5-rule decision tree (route / clarify / fallback) and per-agent confidence thresholds
- **MCP agent dispatch** — StreamableHTTP transport with connection pooling, retries, and Zod-validated responses
- **Per-agent circuit breakers** — Three-state (CLOSED/OPEN/HALF_OPEN) with exponential backoff, Firestore persistence, and cross-instance leader election
- **Session management** — Firestore-backed multi-turn sessions with sliding TTL, turn history, workflow state passthrough, and classification bypass
- **Confidence-gated routing** — a 5-rule decision tree (route / clarify / fallback) with per-agent confidence thresholds. **Pluggable classifier** (`ClassifierProvider`): Gemini Flash by default, or inject any model — including a host-resolved one.
- **Agent dispatch** — StreamableHTTP MCP transport (connection pooling, retries, Zod-validated responses) **or an in-process transport** (`type: 'inprocess'`) for agents co-located with the orchestrator, with a `metadata` passthrough for host context (e.g. a tenant `orgId`).
- **Per-agent circuit breakers** — Three-state (CLOSED/OPEN/HALF_OPEN) with exponential backoff and cross-instance leader election. **Pluggable persistence** (`BreakerStore`): Firestore by default, or Postgres/Redis.
- **Session management** — multi-turn sessions with sliding TTL, turn history, workflow-state passthrough, and classification bypass. **Pluggable persistence** (`SessionStore`): Firestore by default, or Postgres/Redis/in-memory.
- **Hot-reload agent registry** — YAML-based agent configuration with `${ENV_VAR}` expansion, SIGHUP reload, SSRF-safe URL validation, and cross-agent invariant enforcement
- **Express gateway** — API key authentication (Secret Manager-backed), token bucket rate limiting, TLS enforcement with HSTS, and Slack profile resolution
- **MCP server interface** — Exposes the orchestrator as an MCP-compliant agent with JSON-RPC 2.0 routing, tool registration, and SSE transport
Expand Down Expand Up @@ -119,13 +119,15 @@ See the [`examples/orchestrator/`](./examples/orchestrator/) directory for the c
| ------- | ----------- |
| [`@reaatech/agent-mesh`](./packages/core) | Core domain types, Zod schemas, environment config, and shared constants |
| [`@reaatech/agent-mesh-registry`](./packages/registry) | Agent YAML loader, SIGHUP hot-reload, SSRF-safe URL validation |
| [`@reaatech/agent-mesh-session`](./packages/session) | Firestore-backed multi-turn session management |
| [`@reaatech/agent-mesh-classifier`](./packages/classifier) | Gemini Flash intent classification with mock fallback |
| [`@reaatech/agent-mesh-session`](./packages/session) | Multi-turn session management with a pluggable `SessionStore` (Firestore default + in-memory) |
| [`@reaatech/agent-mesh-classifier`](./packages/classifier) | Intent classification with a pluggable `ClassifierProvider` (Gemini Flash default + mock) |
| [`@reaatech/agent-mesh-confidence`](./packages/confidence) | Confidence-gated routing decision tree and clarification cache |
| [`@reaatech/agent-mesh-router`](./packages/router) | MCP-based agent dispatch with connection pooling |
| [`@reaatech/agent-mesh-router`](./packages/router) | Agent dispatch — MCP (pooled) or in-process transport |
| [`@reaatech/agent-mesh-gateway`](./packages/gateway) | Express middleware (auth, rate limiting, TLS) and request handler |
| [`@reaatech/agent-mesh-mcp-server`](./packages/mcp-server) | MCP server exposing orchestrator with JSON-RPC 2.0 and SSE transport |
| [`@reaatech/agent-mesh-utils`](./packages/utils) | Per-agent circuit breaker with Firestore persistence and leader election |
| [`@reaatech/agent-mesh-utils`](./packages/utils) | Per-agent circuit breaker with a pluggable `BreakerStore` (Firestore default) + leader election |
| [`@reaatech/agent-mesh-postgres`](./packages/postgres) | Postgres-backed `SessionStore` + `BreakerStore` adapters |
| [`@reaatech/agent-mesh-redis`](./packages/redis) | Redis-backed `SessionStore` + `BreakerStore` adapters |
| [`@reaatech/agent-mesh-observability`](./packages/observability) | Structured logging, OpenTelemetry tracing/metrics, and audit events |

## Documentation
Expand Down
51 changes: 51 additions & 0 deletions examples/orchestrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,57 @@ Reference deployment example for the agent-mesh monorepo.
npm run build && node dist/index.js
```

## Extending

agent-mesh's provider seams let you swap backends and models without forking. Wire
these **before** starting the server (e.g. at the top of `main()`).

### Bring your own classifier (instead of Gemini)

```ts
import { setClassifier } from './wherever-you-hold-it'; // see @reaatech/agent-mesh-classifier
import { classifierService, createClassifier, type ClassifierProvider } from '@reaatech/agent-mesh-classifier';

const myClassifier: ClassifierProvider = {
classify: async (userInput, registry) => {
// call your own model; return a ClassifierOutput
},
};
const classifier = createClassifier(myClassifier); // default (no arg) = Gemini + mock fallback
```

### Swap the session + breaker persistence (Postgres/Redis instead of Firestore)

```ts
import { Pool } from 'pg';
import { PostgresSessionStore, PostgresBreakerStore, ensureSchema } from '@reaatech/agent-mesh-postgres';
import { setSessionStore } from '@reaatech/agent-mesh-session';
import { setBreakerStore } from '@reaatech/agent-mesh-utils';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await ensureSchema(pool);
setSessionStore(new PostgresSessionStore(pool));
setBreakerStore(new PostgresBreakerStore(pool));

// …or the in-memory adapters for local/dev:
// import { InMemorySessionStore } from '@reaatech/agent-mesh-session';
// setSessionStore(new InMemorySessionStore());
```

### Register an in-process agent (no HTTP hop)

Give the agent `type: 'inprocess'` in its YAML (no `endpoint` needed), then register
a handler:

```ts
import { registerInProcessAgent } from '@reaatech/agent-mesh-router';

registerInProcessAgent('local-agent', async (ctx) => {
// ctx.metadata carries host context (e.g. a tenant orgId)
return { content: `handled: ${ctx.raw_input}`, workflow_complete: true };
});
```

## License

MIT
3 changes: 3 additions & 0 deletions packages/classifier/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# @reaatech/agent-mesh-classifier

> **Pluggable** via `ClassifierProvider` — inject any intent classifier with
> `createClassifier(provider)`. Default (no arg) = Gemini Flash + mock fallback.

[![npm version](https://img.shields.io/npm/v/@reaatech/agent-mesh-classifier.svg)](https://www.npmjs.com/package/@reaatech/agent-mesh-classifier)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/reaatech/agent-mesh/blob/main/LICENSE)
[![CI](https://github.com/reaatech/agent-mesh/actions/workflows/ci.yml/badge.svg)](https://github.com/reaatech/agent-mesh/actions/workflows/ci.yml)
Expand Down
66 changes: 54 additions & 12 deletions packages/core/src/domain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
import { z } from 'zod';
import { PRIVATE_IP_RANGES } from './constants.js';

function isSsrfSafeUrl(url: string): boolean {
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
const hostname = parsed.hostname.toLowerCase();
for (const pattern of PRIVATE_IP_RANGES) {
if (pattern.test(hostname)) {
return false;
}
}
return true;
} catch {
return false;
}
}

const ssrfSafeUrl = z.string().refine((url) => isSsrfSafeUrl(url), {
message:
'Endpoint URL is not allowed: localhost and private IP ranges are rejected for SSRF protection.',
});

export const IncomingRequestSchema = z.object({
input: z.string().min(1, 'input is required').max(10000, 'input too long'),
Expand Down Expand Up @@ -38,18 +62,36 @@ export const ClassifierOutputSchema = z.object({

export type ClassifierOutput = z.infer<typeof ClassifierOutputSchema>;

export const AgentConfigSchema = z.object({
agent_id: z.string().regex(/^[a-z0-9-]+$/, 'agent_id must be lowercase with hyphens'),
display_name: z.string().min(1),
description: z.string().min(1),
endpoint: z.string().url(),
type: z.literal('mcp'),
is_default: z.boolean().default(false),
confidence_threshold: z.number().min(0).max(1).default(0),
clarification_required: z.boolean().default(false),
clarification_context: z.string().optional(),
examples: z.array(z.string()).min(1),
});
// Canonical agent config — the single source of truth. The registry package
// re-exports this (it owns only the AgentRegistry array schema + invariants).
export const AgentConfigSchema = z
.object({
agent_id: z
.string()
.min(1, 'agent_id is required')
.regex(/^[a-z0-9-]+$/, 'agent_id must be lowercase alphanumeric with hyphens'),
display_name: z.string().min(1, 'display_name is required').max(200),
description: z
.string()
.min(1, 'description is required')
.max(5000, 'description too long (max 5000 chars)'),
// Required for `type: 'mcp'` (remote agent); omitted for `type: 'inprocess'`
// (a locally-registered handler). Enforced by the refine below.
endpoint: ssrfSafeUrl.optional(),
type: z.enum(['mcp', 'inprocess']).default('mcp'),
is_default: z.boolean().default(false),
confidence_threshold: z.number().min(0).max(1).default(0),
clarification_required: z.boolean().default(false),
clarification_context: z.string().max(500).optional(),
examples: z
.array(z.string().min(1).max(500))
.min(1, 'at least one example is required')
.max(20, 'maximum 20 examples allowed'),
})
.refine((cfg) => cfg.type !== 'mcp' || Boolean(cfg.endpoint), {
message: 'endpoint is required for mcp agents',
path: ['endpoint'],
});

export type AgentConfig = z.infer<typeof AgentConfigSchema>;

Expand Down
37 changes: 37 additions & 0 deletions packages/postgres/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# @reaatech/agent-mesh-postgres

Postgres-backed `SessionStore` and `BreakerStore` adapters for
[agent-mesh](https://github.com/reaatech/agent-mesh) — a drop-in alternative to
the default Firestore backend.

```bash
pnpm add @reaatech/agent-mesh-postgres pg
```

## Usage

```ts
import { Pool } from 'pg';
import {
PostgresSessionStore,
PostgresBreakerStore,
ensureSchema,
} from '@reaatech/agent-mesh-postgres';
import { setSessionStore } from '@reaatech/agent-mesh-session';
import { setBreakerStore } from '@reaatech/agent-mesh-utils';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

await ensureSchema(pool); // idempotent DDL — run once on boot (or use SCHEMA_SQL in a migration)

setSessionStore(new PostgresSessionStore(pool));
setBreakerStore(new PostgresBreakerStore(pool));
```

Tables (created by `ensureSchema` / `SCHEMA_SQL`): `agent_mesh_sessions`,
`agent_mesh_circuit_breakers`, and `agent_mesh_leader` (a single-row lease used for
leader election via `SELECT … FOR UPDATE`). `pg` is a peer dependency.

> **Note:** unit-tested against a mock pool (SQL issuance + row mapping). Run a smoke
> test against a live Postgres (jsonb, `FOR UPDATE`, timestamptz, multi-instance
> leader election) before production use.
52 changes: 52 additions & 0 deletions packages/postgres/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@reaatech/agent-mesh-postgres",
"version": "0.1.0",
"type": "module",
"description": "Postgres-backed SessionStore and BreakerStore adapters for agent-mesh",
"license": "MIT",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"pg": ">=8"
},
"dependencies": {
"@reaatech/agent-mesh": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@types/pg": "^8.11.10",
"pg": "^8.13.1",
"tsup": "^8.4.0"
},
"repository": {
"type": "git",
"url": "https://github.com/reaatech/agent-mesh.git",
"directory": "packages/postgres"
},
"author": "Rick Somers <rick@reaatech.com> (https://reaatech.com)",
"homepage": "https://github.com/reaatech/agent-mesh/tree/main/packages/postgres#readme",
"bugs": {
"url": "https://github.com/reaatech/agent-mesh/issues"
}
}
Loading
Loading