Skip to main content

External Agents

Agentium’s runtime (registry, Express/Socket.IO gateways, queue workers, observability) is framework-agnostic. Anything that satisfies the ServableAgent contract can be registered and served — the first-party Agent class is just one implementation.

The ServableAgent contract

interface ServableAgent {
  readonly kind: "agent";
  readonly name: string;
  run(input: MessageContent, opts?: RunOpts): Promise<RunOutput>;
  stream(input: MessageContent, opts?: RunOpts): AsyncIterable<StreamChunk>;

  // Optional — each field lights up extra runtime features when present:
  readonly eventBus?: EventBus;          // tracing, metrics, approval streams
  readonly modelId?: string;
  readonly providerId?: string;          // provider-specific API key routing
  readonly tools?: ReadonlyArray<{ name: string; description?: string }>;
  readonly instructions?: unknown;       // swagger docs, agent cards
  readonly hasStructuredOutput?: boolean;
  readonly structuredOutputSchema?: unknown;
  readonly memory?: unknown;             // enables the corrections endpoint
  readonly approvalManager?: unknown;    // enables HITL approval endpoints
  readonly checkpointManager?: unknown;  // enables checkpoint endpoints
}

defineExternalAgent()

The easiest way to wrap external logic. It normalizes your return value into a full RunOutput, provides a default stream() fallback, wires up an EventBus (so observability works out of the box), and auto-registers in the global registry.
import { defineExternalAgent } from "@agentium/core";

const researcher = defineExternalAgent({
  name: "researcher",
  run: async (input) => {
    // Wrap a LangGraph graph, a Claude Agent SDK call, or any custom code
    const result = await graph.invoke({ messages: [String(input)] });
    return result.messages.at(-1).content;   // plain string is fine
  },
});

// Immediately servable:
//   POST /agents/researcher/run
//   POST /agents/researcher/stream

Returning rich output

Return a partial RunOutput to include usage, tool calls, or structured data — missing fields are filled with defaults:
const agent = defineExternalAgent({
  name: "extractor",
  run: async (input) => ({
    text: extracted.summary,
    structured: extracted,
    usage: { promptTokens: 800, completionTokens: 200, totalTokens: 1000 },
  }),
});

Native streaming

By default, stream() runs to completion and yields the text as a single chunk. Provide your own generator for true streaming:
const agent = defineExternalAgent({
  name: "streamer",
  run: async (input) => fullResponse(input),
  stream: async function* (input) {
    for await (const token of llm.streamTokens(input)) {
      yield { type: "text", text: token };
    }
    yield { type: "finish", finishReason: "stop" };
  },
});

Configuration

PropertyTypeRequiredDefaultWhat it controls
namestringYesRegistry key and route path (/agents/:name/...)
run(input, opts) => Promise<string | Partial<RunOutput>>YesThe agent’s logic
stream(input, opts) => AsyncIterable<StreamChunk>Norun-then-yield fallbackNative streaming
instructionsstringNoShown in registry descriptions and swagger
modelId / providerIdstringNoMetadata for discovery cards and API key routing
registerbooleanNotrueAuto-register in the global registry
eventBusEventBusNofresh instanceBring your own bus for shared observability

Observability for free

defineExternalAgent emits the standard run lifecycle events (run.start, run.complete, run.error) on its event bus, so Tracer, MetricsExporter, and StructuredLogger work unchanged:
import { instrumentBus } from "@agentium/observability";

const agent = defineExternalAgent({ name: "researcher", run });
instrumentBus(agent.eventBus!, { tracing: true, metrics: true });

Registry names are labels (v2.3.2+)

Registering an agent with an existing name replaces the previous entry (last-write-wins) — names are labels, not unique keys. This means the same agent definition can be constructed repeatedly (loops, factories, concurrent requests) without Duplicate agent name errors. Gateways resolve a name to the most recently registered instance.
// Safe to call per-request — no name collision errors
function classify(email: string) {
  const agent = new Agent({ name: "email-classifier", model: getModel(), ... });
  return agent.run(email);
}
Pass register: false (on AgentConfig or ExternalAgentConfig) for ephemeral agents that should skip the registry entirely.

Cross-references