Tip: How Do Entrypoints (also known as Gateways) Work in Solace Agent Mesh?

Solace Agent Mesh (SAM) is an event-driven framework for building distributed ecosystems of collaborative AI agents. Agents do the thinking — they have skills, tools, and LLM access. But agents don’t talk to users directly. That’s where entrypoints come in.

An entrypoint connects an external platform (a web UI, a Slack workspace, a terminal, a REST API) to the agent ecosystem. It translates platform-specific input into the A2A protocol, delivers it to agents via the Solace Event Broker, and translates agent responses back to the platform. Entrypoints make zero LLM calls. All intelligence lives on the agent side.

This post covers how entrypoints work architecturally, what the framework provides, and what you need to build when creating a custom one.

Where Entrypoints Fit

Every entrypoint is a peer. They all connect to the same Solace Event Broker and talk to the same agents. A user chatting in the WebUI and a user interacting through a custom entrypoint are indistinguishable from the agent’s perspective — both arrive as A2A tasks.

The A2A Message Flow

When a user sends a message through any entrypoint, here’s what happens:

  ┌──────────┐     ┌──────────────┐     ┌─────────────┐     ┌──────────────┐
  │ External │────▶│  Entrypoint  │────▶│   Solace    │────▶│ Orchestrator │
  │ Platform │     │   Adapter    │     │   Broker    │     │    Agent     │
  └──────────┘     └──────────────┘     └─────────────┘     └──────┬───────┘
                                                                   │
  ┌──────────┐     ┌──────────────┐     ┌─────────────┐            │
  │ External │◀────│  Entrypoint  │◀────│   Solace    │◀───────────┘
  │ Platform │     │   Adapter    │     │   Broker    │   Streamed response
  └──────────┘     └──────────────┘     └─────────────┘
  1. User input arrives on the external platform (HTTP request, Slack message, etc.)
  2. The entrypoint’s prepare_task() converts it into a SamTask with a session_id
  3. SAM framework publishes a JSON-RPC 2.0 request to the agent’s request topic
  4. The orchestrator processes the request, potentially delegating to specialist agents
  5. Streamed status updates arrive on the entrypoint’s status topic
  6. The final response arrives on the entrypoint’s response topic
  7. The entrypoint translates the response back to the external platform

The entrypoint only handles steps 1, 2, and 7. The framework handles everything in between.

Topic Structure

All communication flows through hierarchical Solace topics:

Direction Topic Pattern
Entrypoint → Agent {namespace}/a2a/v1/agent/request/{agent_name}
Agent → Entrypoint (status) {namespace}/a2a/v1/gateway/status/{gateway_id}/>
Agent → Entrypoint (response) {namespace}/a2a/v1/gateway/response/{gateway_id}/>
Agent discovery {namespace}/a2a/v1/discovery/>

Entrypoints don’t manage these topics directly. The framework subscribes, publishes, and routes automatically based on your config.yaml.

What the Framework Handles vs What You Build

This is the key insight: the framework handles the hard distributed systems problems. You only build the platform-specific translation layer.

Responsibility Owner
Broker connection, reconnection, and health Framework
A2A protocol (JSON-RPC 2.0 over Solace) Framework
Topic routing and subscriptions Framework
Agent discovery and registry Framework
Artifact storage and retrieval Framework
Embed resolution in responses Framework
Conversation history per session Framework
Translate platform input → SamTask Your adapter
Translate agent response → platform output Your adapter
User authentication / identity Your adapter
Session ID management Your adapter
Platform-specific UX (rendering, streaming) Your adapter

The Adapter Pattern

Every entrypoint implements the same adapter interface. Here are the key methods:

Required

async def prepare_task(self, external_input, endpoint_context) -> SamTask:
    """Convert platform-specific input into a SamTask.

    This is the only truly required method. It takes whatever your platform
    provides and produces a SamTask with:
    - parts: List of SamTextPart, SamFilePart, SamDataPart
    - session_id: Scopes conversation history and artifacts
    - target_agent: Which agent to route to (usually OrchestratorAgent)
    - is_streaming: Whether to receive streamed responses
    """

Lifecycle

async def init(self, context: GatewayContext) -> None:
    """Called once on startup. Start your listener (HTTP server, WebSocket, etc).

    The GatewayContext gives you:
    - context.handle_external_input(event) — submit a task to SAM
    - context.list_agents() — discover available agents
    - context.list_artifacts(response_context) — list session artifacts
    - context.load_artifact_content(response_context, filename) — fetch artifact
    - context.submit_feedback(feedback) — submit user ratings
    - context.gateway_id — your unique entrypoint identifier
    - context.namespace — the SAM namespace
    """

async def cleanup(self) -> None:
    """Called on shutdown. Stop your listener, release resources."""

Response Handlers (all optional)

async def handle_text_chunk(self, text: str, context: ResponseContext) -> None:
    """A streaming text chunk arrived. Accumulate or forward in real-time."""

async def handle_status_update(self, status_text: str, context: ResponseContext) -> None:
    """Agent is reporting progress (e.g., 'Searching database...')."""

async def handle_file(self, file_part: SamFilePart, context: ResponseContext) -> None:
    """Agent created a file artifact."""

async def handle_task_complete(self, context: ResponseContext) -> None:
    """Task is done. Render/send the final response."""

async def handle_error(self, error: SamError, context: ResponseContext) -> None:
    """Something went wrong. Show the error to the user."""

Authentication

async def extract_auth_claims(self, external_input, endpoint_context) -> AuthClaims:
    """Identify the user. Returns AuthClaims(id=..., source=...)."""

Sessions: The session_id Concept

Every task includes a session_id. This single string scopes two things on the SAM side:

  1. Conversation history — the agent sees all prior messages in that session
  2. Artifacts — files created by the agent are stored under that session

When a user sends a message with session_id="abc123", the agent automatically receives the full conversation history for abc123. When the user switches to session_id="xyz789", the agent picks up a completely different conversation — no code needed on the entrypoint side to manage history replay or context loading.

How you map your platform’s concept of “conversation” to a session_id is entirely up to you. It could be a browser session cookie, a chat thread, a user-chosen label mapped to an ID, or a deterministic string that survives restarts. The entrypoint decides the mapping. SAM handles the rest.

Session Persistence

SAM maintains conversation history server-side. If your entrypoint restarts, you just pass the same session_id and the agent resumes with full context. There’s no history replay, no context window management, no summarization needed on the entrypoint side.

However, SAM currently has no entrypoint-facing API to delete or expire sessions. Sessions persist indefinitely on the server side. Keep this in mind when designing your session strategy.

Artifacts: How Files Flow Through Entrypoints

Agents can create file artifacts (CSVs, images, code files, etc.) during task execution. These are stored in the artifact service, scoped by session_id.

Inbound (user uploads a file):

  1. Your adapter receives a file from the platform
  2. Create a SamFilePart(name=filename, content_bytes=data, mime_type=type)
  3. Include it in the SamTask.parts list
  4. SAM delivers it to the agent

Outbound (agent creates a file):

  1. Your handle_file() callback receives a SamFilePart
  2. It may contain content_bytes (inline) or a uri (artifact reference)
  3. Display or store it as appropriate for your platform

Listing and downloading artifacts:

# List all artifacts in the current session
artifacts = await self.context.list_artifacts(response_context)

# Download a specific artifact
content = await self.context.load_artifact_content(response_context, "report.csv")

Artifacts are scoped by session. Switch sessions, and you see a different set of artifacts.

Configuration: Wiring an Entrypoint

Every entrypoint is configured in a config.yaml file:

apps:
  - name: my_entrypoint_app
    app_module: solace_agent_mesh.gateway.generic.app
    broker:
      broker_url: ${SOLACE_BROKER_URL, ws://localhost:8080}
      broker_username: ${SOLACE_BROKER_USERNAME, default}
      broker_password: ${SOLACE_BROKER_PASSWORD, default}
      broker_vpn: ${SOLACE_BROKER_VPN, default}

    app_config:
      namespace: "${NAMESPACE}"

      # Point to your adapter class
      gateway_adapter: my_package.adapter.MyEntrypointAdapter

      # Your adapter-specific configuration
      adapter_config:
        host: "0.0.0.0"
        port: 8080

      # Entrypoint ID (must be unique per instance)
      gateway_id: ${MY_ENTRYPOINT_ID, my-ep-01}

      # Artifact storage
      artifact_service:
        type: "filesystem"
        base_path: "/tmp/samv2"
        artifact_scope: namespace

      # Agent to route to by default
      default_agent_name: "OrchestratorAgent"

      # Embed resolution (artifact references in text)
      enable_embed_resolution: true

Key configuration points:

  • gateway_adapter — the Python import path to your adapter class
  • adapter_config — passed to your adapter as context.adapter_config (validated against your ConfigModel)
  • gateway_id — must be unique across all entrypoint instances
  • artifact_service — shared artifact backend (filesystem, S3, etc.)

Packaging as a Plugin

SAM discovers entrypoints as Python packages. Your pyproject.toml needs:

[tool.my_entrypoint_adapter.metadata]
type = "gateway"

[project]
name = "my_entrypoint_adapter"
version = "0.1.0"
dependencies = []

[tool.hatch.build.targets.wheel]
packages = ["src/my_entrypoint_adapter"]

Install with pip install -e ., then run with sam run config.yaml.

Example: A Minimal Entrypoint

Here’s a stripped-down entrypoint that reads from stdin and prints to stdout — the simplest possible implementation:

import asyncio
import sys
from solace_agent_mesh.gateway.adapter.base import GatewayAdapter
from solace_agent_mesh.gateway.adapter.types import (
    AuthClaims, GatewayContext, ResponseContext,
    SamError, SamTask, SamTextPart,
)

class MinimalAdapter(GatewayAdapter):

    async def init(self, context: GatewayContext) -> None:
        self.context = context
        self._response_event = asyncio.Event()
        self._response_text = ""
        asyncio.create_task(self._loop())

    async def cleanup(self) -> None:
        pass

    async def extract_auth_claims(self, external_input, endpoint_context=None):
        return AuthClaims(id="user", source="minimal")

    async def prepare_task(self, external_input, endpoint_context=None) -> SamTask:
        return SamTask(
            parts=[SamTextPart(text=external_input["text"])],
            session_id=external_input.get("session_id", "default"),
            target_agent="OrchestratorAgent",
            is_streaming=True,
        )

    async def handle_text_chunk(self, text, context: ResponseContext):
        self._response_text += text

    async def handle_task_complete(self, context: ResponseContext):
        print(self._response_text)
        self._response_text = ""
        self._response_event.set()

    async def handle_error(self, error: SamError, context: ResponseContext):
        print(f"Error: {error.message}", file=sys.stderr)
        self._response_event.set()

    async def _loop(self):
        loop = asyncio.get_running_loop()
        while True:
            line = await loop.run_in_executor(None, input)
            self._response_event.clear()
            await self.context.handle_external_input({
                "text": line,
                "session_id": "default",
            })
            await self._response_event.wait()

That’s ~45 lines. The framework handles broker connections, A2A protocol, agent discovery, topic routing, artifact storage, and session history. Your adapter just translates between your platform and SamTask.

Existing Entrypoints

SAM ships with and supports several entrypoints:

WebUI (HTTP/SSE)

The built-in entrypoint that powers the SAM web interface. Runs an embedded FastAPI server with:

  • Server-Sent Events for real-time streaming of agent responses
  • Cookie-based sessions via Starlette SessionMiddleware
  • File upload/download through REST endpoints
  • Agent discovery endpoint for the UI to list available agents
  • Artifact management with full CRUD operations
  • Visualization flow for tracing A2A messages in real-time

Slack, MCP, REST, Event Mesh

Documented integration patterns for connecting SAM to additional platforms. Each follows the same adapter pattern — implement prepare_task() and the response handlers, and the framework handles the rest.

Summary

Entrypoints are SAM’s connection layer between external platforms and the agent ecosystem. They follow a simple pattern:

  • Inbound: Translate platform input → SamTask via prepare_task()
  • Outbound: Translate agent responses → platform output via handle_*() callbacks
  • Sessions: A session_id string scopes conversation history and artifacts automatically
  • Everything else: The framework handles broker connections, A2A protocol, topic routing, agent discovery, artifact storage, and embed resolution

Building a custom entrypoint means implementing a handful of async methods. The framework does the heavy lifting.


For more information about Solace Agent Mesh, visit the official Agent Mesh GitHub repository