Skip to content

Runtime

The ADL runtime wires process-level services: message and workflow stores, observers, template engine, and workflow context scope.

Construct the runtime in src/adl.ts (recommended) and set adl on adl.config.ts. Registry code should import { adl } from "#adl" — see Project Setup for the tsconfig alias. Tooling loads the runtime via loadAdlProject().getAdl() — not by importing src/adl.ts directly.

Drizzle/tRPC-style factory — primary app entrypoint. By default it loads .env* from process.cwd() (pass loadEnv: false to skip, or loadEnv: { root } for an explicit project root). If a module reads process.env at import time (for example to pick a default model id), call loadAdlEnv({ root }) before that read — ESM evaluates imports before createAdlRuntime runs.

import { createAdlRuntime, sqliteMessageStore, sqliteWorkflowStore } from "@agent-dev-lab/core";
import { openai } from "@ai-sdk/openai";
const adl = createAdlRuntime({
defaults: { model: openai("gpt-4o-mini") },
stores: {
message: sqliteMessageStore(),
workflow: sqliteWorkflowStore(),
},
observers: { workflows: [], agents: [] },
// optional: pin `version:<value>` on every workflow/agent run
// version: "1.0.0",
});

adl.createAgent / adl.createWorkflow bind the runtime automatically — use these in project code.

version controls the automatic provenance tag on every workflow.run() and agent.run() (version:<value> when set, otherwise commit:<id> from jj then git). Pass false to disable the lookup — createTestRuntime does. Caller tags are recorded as well; the inspector shows them in a footer. See Run tags.

Second argument on adl.createAgent / adl.createWorkflow:

import { inMemoryMessageStore, type AgentObserver, type MessageStore } from "@agent-dev-lab/core";
import { openai } from "@ai-sdk/openai";
// continues the createAdlRuntime example above
const episodeLogger: AgentObserver = {
onEvent: (event) => console.log(event.type, event),
};
const customStore: MessageStore = inMemoryMessageStore();
const researcher = adl.createAgent(
{ id: "researcher", model: openai("gpt-4o"), systemPrompt: "You are a research assistant." },
{
observers: { agents: [episodeLogger] }, // appended to runtime defaults
stores: { message: customStore }, // replaces default for this agent
},
);
  • stores.message / stores.workflow: replace the runtime default for this agent/workflow/run.
  • observers.workflows / observers.agents: append to lists from createAdlRuntime (not replace).

effectGate on createAdlRuntime is the host’s policy for effects that ask permission: tool calls from @agent-dev-lab/tools, workflow step entry, and ctx.requestApproval. A gate receives an EffectIntent and returns allow, deny, rewrite, or suspend. When omitted, the host policy allows everything. Only tools that are handed adl.services.effectGate go through it; tool factories never pick it up implicitly.

adl.services.effectGate is the composed gate: the inspection UI debugger first, then your policy. A suspend decision persists a suspend record on the run’s WorkflowStore, marks the run suspended, and waits until store.resolveSuspend(handleId, decision) records allow, deny, or rewrite. The inspection UI’s Allow / Deny buttons call it. Suspends need a workflow run: a standalone agent.run outside a workflow cannot park.

inMemoryEventLog() is a ring-buffer EventLog that also implements WorkflowObserver and AgentObserver. Register the same instance on both lists so workflow and agent events share one logSeq (process-wide, unlike per-run RunEvent.runSeq):

import { createAdlRuntime, inMemoryEventLog } from "@agent-dev-lab/core";
const eventLog = inMemoryEventLog(); // default cap 10_000
const adl = createAdlRuntime({
observers: {
workflows: [eventLog],
agents: [eventLog],
},
});

eventLog.list({ afterSeq, type, limit }) reads the buffer. waitForAppend(afterSeq, signal) resolves when a later event arrives (or the signal aborts) so an SSE tail does not miss events between list and wait. clear() empties the buffer.

The inspection UI attaches its own process singleton this way and hydrates an empty log from WorkflowStore on startup. A SQLite EventLog is not implemented yet.

Observer lists are not pinned across watchAdlProject reloads — late-attached observers (including the inspector log) must be pushed again onto the new arrays.

Project code should use adl.createAgent, adl.createWorkflow, and adl.createTemplate. The package also exports createAgent(runtime, …), createWorkflow(runtime, …), and createTemplate(runtime, …) for unit tests and libraries that need an explicit runtime handle without a project adl module. Same behavior — prefer the bound methods in application code.

ALS is not used for runtime services (stores, observers) — those are passed explicitly.

ALS is used for:

  • Workflow context: when agent.run / agent.stream is called inside a workflow body or step, the active WorkflowContext is available so agents attach to the correct workflowRunId / stepId without manual wiring. Callers can still pass workflow: { workflowRunId, stepId } explicitly — that takes priority over ALS.
  • Conversation title re-entrancy: while titleWorkflow runs, a flag prevents a nested agent.run from starting another auto-title.

WorkflowContext is a host object. step and emit close over parent services, workflowRunId, and step registry.

  • Child contexts are built from the parent host when ctx.step("name", async ({ ctx }) => …) runs.
  • step, emit, setTitle, memoryScopeWithSuffix, and runLocalScope are bound on the context instance, so const { step } = ctx is safe. Inside a workflow step (researcher defined in registry; query from workflow input):
await ctx.step("research", async ({ ctx: child }) => {
await researcher.run({
memoryScope: child.memoryScopeWithSuffix("notes"),
user: query,
});
});

Tools created via adl.createToolFromAgent / adl.createToolFromWorkflow work outside a workflow as well. When the tool runs inside a workflow body or step, ALS still supplies meta.ctx so the tool can nest on the parent run. adl.createWorkflowFromAgent wraps an agent as a workflow that accepts a string user message.

const handle = review.run(input);
handle.workflowRunId;
await handle.result;
  • Public API: run(input) for root runs (CLI, UI).
  • Author API: inside a workflow, otherWorkflow.run(input) nests via ALS (new workflowRunId, parentWorkflowRunId / rootWorkflowRunId on live ctx). memoryScopeWithSuffix keys the root id; runLocalScope keys the child — see Nested and Isolated Runs. Pass { isolated: true } for an unlinked persisted run.
  • No { project } on the execution path.

Nested runs can pass parentCtx explicitly:

// inside literature-review run; searchPapers imported from workflows/search-papers.ts
await ctx.step("search", async ({ ctx: child }) => {
const result = await searchPapers.run({ topic: input.topic }, { parentCtx: child }).result;
});

When stores are omitted, createAdlRuntime uses in-memory message and workflow stores (fine for tests). For durable runs, pass sqliteMessageStore() / sqliteWorkflowStore() (default file .data/agent-dev-lab.sqlite, overridable with ADL_SQLITE_PATH).

defaults.model and tools on createAdlRuntime apply to every agent that omits those fields (agent values win). Set these on the runtime — not on adl.config — because agents are created before the config object finishes loading.

adl.config.tools is registry-only (documentation / future inspection UI: list tools and trigger a manual run). It does not merge into agents at load time.

SQLite event rows store per-run order as run_seq (the same value as RunEvent.runSeq). Process-wide tails use logSeq on the in-memory event log.

import { createTestRuntime } from "@agent-dev-lab/core";
const adl = createTestRuntime(); // in-memory stores

Pass defaults.model (or a mock LanguageModel) when the test constructs agents.

RunRecorder already mirrors run events onto the active OpenTelemetry span. Agent episodes also forward AI SDK telemetry on streamText so OpenTelemetry model and tool spans nest under the agent span — this is not Vercel product analytics. createAdlRuntime registers peer @ai-sdk/otel unless disabled with createAdlRuntime({ telemetry: { isEnabled: false } }). The option type is AdlOpenTelemetrySettings. Install an OTel SDK exporter in the application; ADL does not ship a parallel tracing API.