npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

agent-contracts-runtime

v0.13.1

Published

Runtime bridge for executing agent-contracts workflows on Agent SDKs

Readme

agent-contracts-runtime

Runtime bridge for executing agent-contracts agent teams from TypeScript programs.

agent-contracts defines business-level agent behavior in DSL: agent roles, task boundaries, workflows, handoff schemas, validations, and guardrails.

agent-contracts-runtime makes those DSL-defined workflows callable from ordinary TypeScript code.

It sits between your program and Agent SDKs:

TypeScript program
  ↓
WorkflowInvocation
  ↓
agent-contracts-runtime
  ↓
DSL-derived contracts
  ↓
Agent SDK adapter
  ↓
Cursor / Claude / OpenAI Agents SDK / custom runner

Agent SDKs execute LLM agents.

agent-contracts-runtime lets application code invoke reusable, DSL-defined agent workflows without hand-writing SDK-specific orchestration for each workflow.

Why this exists

Agent SDKs are good at executing agents.

But business-level agent behavior often involves more than one prompt or one SDK call.

A real agent team may define:

  • which agents exist
  • which tasks they can perform
  • who can delegate to whom
  • what handoff shape is expected
  • which workflow steps are allowed
  • what validation must happen before the next step
  • which guardrails apply

That behavior belongs in agent-contracts DSL.

agent-contracts-runtime exists so TypeScript programs can execute those DSL-defined workflows without rewriting the same SDK-specific orchestration code each time.

What it does

agent-contracts-runtime:

  • loads generated contracts from agent-contracts
  • accepts workflow invocations from TypeScript or CLI
  • resolves the DSL-defined workflow, task, agent, and handoff contracts
  • builds an SDK execution request
  • calls an Agent SDK through an adapter
  • validates the returned handoff
  • returns a structured workflow result

It is the execution bridge for DSL-defined agent teams.

Why not call the SDK directly?

You can.

For simple one-off agents, direct SDK code is enough.

agent-contracts-runtime is useful when the agent behavior is defined as reusable contracts:

  • multiple workflows use the same agent team
  • handoff schemas should be generated and validated
  • workflow steps should come from DSL, not scattered program code
  • different SDK adapters should run the same contract
  • application code should call business-level workflows, not low-level agent prompts

Instead of writing this repeatedly:

// resolve prompt
// choose agent
// build context
// call SDK
// parse output
// validate schema
// retry on malformed output
// route to next task

you call:

const result = await runtime
  .workflow("feature-implement")
  .handoff(handoffs.featureImplementationRequest({
    objective: "Add login endpoint with JWT",
    inputs: { repository: "." },
    expectedOutputs: ["implementation-diff"],
    completionCriteria: ["tests_passed"],
  }))
  .run();

Business behavior lives in the DSL

Prompts and SDK calls are execution details.

The reusable behavior of an agent team belongs in the DSL:

  • agent roles and responsibilities
  • task boundaries
  • delegation rules
  • workflow steps
  • handoff schemas
  • validations
  • guardrails

agent-contracts-runtime executes those generated contracts instead of duplicating the workflow logic in application code.

Install

npm install agent-contracts-runtime
npm install -D agent-contracts

Quick start

# 1. Initialize project scaffolding
agent-runtime init

# 2. Validate and lint your DSL
agent-contracts validate
agent-contracts lint --strict

# 3. Generate contracts and hooks
agent-runtime generate

# 4. Run a workflow
agent-runtime run feature-implement "Add login endpoint with JWT"

# 5. Verify project setup
agent-runtime doctor

How it works

agent-contracts.yaml (DSL)     bindings/runtime.yaml (guardrail impl)
        │                              │
        ▼                              ▼
  agent-runtime generate ──────────────┘
        │
        ├── agent/generated/agents.ts       (AgentContract + registry)
        ├── agent/generated/tasks.ts        (TaskContract + registry)
        ├── agent/generated/workflows.ts    (WorkflowContract + registry)
        ├── agent/generated/handoffs.ts     (Zod schemas + registry + factories)
        ├── agent/generated/index.ts        (barrel re-export)
        ├── agent/generated/hooks/guardrails.ts  (check functions)
        ├── agent/generated/hooks/index.ts       (unified hook adapter)
        └── agent/generated/.manifest.json       (DSL hash, metadata)

  agent/src/                             (user plugins, project guardrails)
        │
        ▼
  agent-runtime run <workflow> <request>
        │
        ├── Plugin: beforeWorkflow
        ├── For each step:
        │     ├── Plugin: beforeTask
        │     ├── Plugin: contextEnhancer (enrich structured context)
        │     ├── Plugin: promptBuilder (full override) or default buildTaskPrompt
        │     ├── Plugin: promptEnhancer (post-process)
        │     ├── SDK Adapter: send prompt to LLM
        │     ├── Extract structured result (YAML/JSON)
        │     ├── Validate against Zod handoff schema
        │     ├── followUp (lightweight, same session) on validation error
        │     ├── retry (heavyweight, new session) on persistent failure
        │     ├── Plugin: afterTask
        │     └── decideRetryStrategy callback for custom recovery logic
        └── Plugin: afterWorkflow

Layer separation

| Layer | Path | Owner | Description | |-------|------|-------|-------------| | Generated | agent/generated/ | Auto-generated | DSL-derived contracts, handoff factories, and hooks. Never edit manually. | | User code | agent/src/ | You | Plugins, project guardrails, interceptors. | | Runtime | node_modules/agent-contracts-runtime/ | npm package | Workflow runner, task runner, SDK adapters, generator. |

Generated contracts

The generate command reads your agent-contracts.yaml DSL and optional binding YAML files, then produces TypeScript contracts and guardrail hooks using Handlebars templates.

Two-phase generation

| Phase | Input | Output | Description | |-------|-------|--------|-------------| | 1. Contracts | DSL (agents, tasks, workflow, handoff_types) | agents.ts, tasks.ts, workflows.ts, handoffs.ts, index.ts | Typed contract interfaces, registries, and handoff factories | | 2. Guardrails | DSL guardrails + binding guardrail_impl + active policy | hooks/guardrails.ts, hooks/index.ts | Check functions for command, file path, file content |

Phase 2 requires a binding YAML (following the agent-contracts SoftwareBinding schema). If no bindings are configured, guardrail hooks are skipped.

Handoff factories

In addition to Zod schemas and type aliases, handoffs.ts generates type-safe factory functions for each handoff type:

import { handoffs } from "./agent/generated";

const envelope = handoffs.featureImplementationRequest({
  objective: "Add login endpoint with JWT",
  inputs: { repository: "." },
  expectedOutputs: ["implementation-diff"],
  completionCriteria: ["tests_passed"],
});
// => { type: "feature-implementation-request", version: 1, payload: { ... } }

The factory validates the payload against the Zod schema at construction time, catching type errors before the workflow starts.

YAML invocation files use the DSL-defined field names (snake_case). Generated TypeScript factories and APIs use camelCase.

Custom templates

Override any built-in Handlebars template by placing a file with the same name in a custom templates directory:

agent-runtime generate --templates ./my-templates

Or set templates_dir in agent-runtime.config.yaml.

Programmatic generation API

import { generate, checkFreshness } from "agent-contracts-runtime/generator";

const result = await generate({
  configPath: "./agent-runtime.config.yaml",
  clean: true,
});
console.log(result.filesGenerated);

const isFresh = await checkFreshness("./agent-runtime.config.yaml");

Programmatic API

The runtime provides three API levels:

| API | Use case | Input | |-----|----------|-------| | Simple API | Ad-hoc CLI-style invocation | user_request: string | | Structured API | Application integration, CI | WorkflowInvocation with typed handoff | | Builder API | Fluent programmatic usage | createRuntime → chained methods |

WorkflowInvocation

All API levels resolve to the same internal model:

type WorkflowInvocation = {
  workflow: string;

  handoff: {
    type: string;
    version?: number;
    payload: unknown;
  };

  runtime?: {
    maxFollowUps?: number;
    maxRetries?: number;
    dryRun?: boolean;
    readonly?: boolean;
  };

  hooks?: {
    onStepComplete?: (event: StepCompleteEvent) => void;
    onGate?: (gateKind: string, description: string) => Promise<boolean>;
    decideRetryStrategy?: (
      outcome: TaskOutcome,
      attempt: number
    ) => Promise<"follow_up" | "retry" | "abort">;
  };

  context?: {
    variables?: Record<string, unknown>;
    artifacts?: Record<string, string>;
  };
};

Simple API

For quick scripts and CLI-compatible usage:

import { runWorkflow } from "agent-contracts-runtime";
import { CursorSdkAdapter } from "agent-contracts-runtime/adapters/cursor-sdk";

const adapter = await CursorSdkAdapter.create({
  apiKey: process.env.CURSOR_API_KEY!,
  model: "composer-2",
  cwd: process.cwd(),
});

const result = await runWorkflow(adapter, "feature-implement", {
  user_request: "Add login endpoint with JWT",
  maxFollowUps: 3,
  maxRetries: 1,
});

console.log(`${result.workflow_id}: ${result.status} (${result.total_elapsed_ms}ms)`);

Or with the Claude adapter:

import { runWorkflow } from "agent-contracts-runtime";
import { ClaudeAgentSdkAdapter } from "agent-contracts-runtime/adapters/claude-agent-sdk";

const adapter = new ClaudeAgentSdkAdapter({
  cwd: process.cwd(),
});

const result = await runWorkflow(adapter, "feature-implement", {
  user_request: "Add login endpoint with JWT",
  maxFollowUps: 3,
  maxRetries: 1,
});

Or with the OpenAI Agents SDK adapter:

import { runWorkflow } from "agent-contracts-runtime";
import { OpenAIAgentsSdkAdapter } from "agent-contracts-runtime/adapters/openai-agents-sdk";

const adapter = new OpenAIAgentsSdkAdapter({
  model: "gpt-4.1",
});

const result = await runWorkflow(adapter, "feature-implement", {
  user_request: "Add login endpoint with JWT",
  maxFollowUps: 3,
  maxRetries: 1,
});

For production integration, prefer the Structured API or Builder API so that input handoffs are validated before execution.

Structured API

Use WorkflowInvocation for typed handoff input with Zod validation on both input and output:

import { runWorkflow } from "agent-contracts-runtime";
import { handoffs } from "./agent/generated";

const result = await runWorkflow(adapter, {
  workflow: "feature-implement",

  handoff: handoffs.featureImplementationRequest({
    objective: "Add login endpoint with JWT",
    inputs: {
      repository: ".",
      apiSpec: "./docs/openapi.yaml",
    },
    constraints: {
      allowedPaths: ["src/**", "test/**"],
      deniedPaths: ["infra/**"],
    },
    expectedOutputs: ["implementation-diff", "test-report"],
    completionCriteria: ["tests_passed", "review_approved"],
  }),

  runtime: {
    maxFollowUps: 3,
    maxRetries: 1,
  },

  hooks: {
    onGate: async (gateKind, description) => true,
  },
});

The WorkflowInvocation envelope is SDK-independent. SDK-specific options belong on the adapter constructor:

const adapter = await CursorSdkAdapter.create({
  apiKey: process.env.CURSOR_API_KEY!,
  model: "composer-2",
  cwd: process.cwd(),
});

Builder API

For fluent programmatic usage:

import { createRuntime } from "agent-contracts-runtime";
import { CursorSdkAdapter } from "agent-contracts-runtime/adapters/cursor-sdk";
import { handoffs } from "./agent/generated";

const adapter = await CursorSdkAdapter.create({
  apiKey: process.env.CURSOR_API_KEY!,
  model: "composer-2",
  cwd: process.cwd(),
});

const runtime = createRuntime({ adapter });

const result = await runtime
  .workflow("feature-implement")
  .handoff(handoffs.featureImplementationRequest({
    objective: "Add login endpoint with JWT",
    inputs: { repository: "." },
    expectedOutputs: ["implementation-diff"],
    completionCriteria: ["tests_passed"],
  }))
  .maxFollowUps(3)
  .maxRetries(1)
  .onStepComplete((event) => {
    console.log(event.task_id, event.outcome_status);
  })
  .onGate(async () => true)
  .run();

The builder can also accept a plain string for quick usage:

const result = await runtime
  .workflow("feature-implement")
  .request("Add login endpoint with JWT")
  .run();

Run a single task

import { runTask } from "agent-contracts-runtime";

const result = await runTask(adapter, "run-tests", {
  user_request: "Run all tests and report results",
});

if (result.outcome.status === "success") {
  console.log("Completed:", result.outcome.data);
} else if (result.outcome.status === "validation_error") {
  console.error("Schema mismatch — followUps used:", result.follow_ups_used);
} else if (result.outcome.status === "escalation") {
  console.warn("Escalation:", result.outcome.reason);
}

Workflow result

type WorkflowResult = {
  workflow_id: string;
  status: "success" | "failed" | "escalation" | "cancelled";
  steps: StepResult[];
  final_handoff?: HandoffEnvelope;
  total_elapsed_ms: number;
};

Each step result includes task ID, outcome status, validation errors, follow-up count, retry count, and elapsed time.

CLI

| Command | Description | |---------|-------------| | agent-runtime init | Initialize runtime scaffolding | | agent-runtime generate | Generate contracts and hooks from DSL | | agent-runtime run <workflow> <request> | Execute a workflow | | agent-runtime run --file <path> | Execute from a YAML invocation file | | agent-runtime list <resource> | List workflows, tasks, or agents | | agent-runtime show-prompt <task> | Display the generated prompt for a task | | agent-runtime doctor | Verify configuration and connectivity |

agent-runtime init

Scaffolds a new project with configuration and user code templates:

agent-runtime init                    # Scaffold in current directory with mock adapter
agent-runtime init --adapter claude   # Configure for Claude adapter
agent-runtime init --output ./my-proj # Scaffold in a specific directory
agent-runtime init --force            # Overwrite existing files

Creates the following structure:

<output-dir>/
├── agent-runtime.config.yaml           # Runtime configuration
├── agent/
│   └── src/
│       ├── plugins/
│       │   └── example-plugin.ts       # AgentPlugin skeleton with hook examples
│       └── guardrails/
│           └── custom-guardrails.ts    # Project-specific guardrail checks
└── .gitignore                          # Adds agent/generated/ entry

The generated agent-runtime.config.yaml points to ./agent-contracts.yaml for DSL input and ./agent/generated/ for generated output. Edit this file to configure bindings, guardrail policies, and custom templates.

Key options:

agent-runtime generate --check       # CI: verify generated files are up to date
agent-runtime generate --clean       # Delete and regenerate
agent-runtime generate -t ./my-tpl   # Use custom Handlebars templates
agent-runtime run ... --dry-run      # Simulate without calling SDK
agent-runtime run ... --adapter mock # Use mock adapter for testing
agent-runtime run --file ./invocation.yaml          # Run from YAML manifest
agent-runtime show-prompt plan-and-implement                  # Preview prompt
agent-runtime show-prompt audit-tests -u "Check test quality" # With custom request
agent-runtime show-prompt plan-and-implement --format json    # JSON output
agent-runtime show-prompt plan-and-implement --with-plugins   # Apply plugin enhancers
agent-runtime doctor                         # Run all diagnostic checks

agent-runtime doctor

Runs diagnostic checks and reports pass/fail/warn status for each:

| Check | What it verifies | |-------|------------------| | config_exists | agent-runtime.config.yaml is found and parses correctly | | dsl_exists | agent-contracts.yaml (per config) exists and is valid YAML | | manifest_fresh | Generated files match the current DSL hash | | adapter_configured | At least one SDK adapter has its API key env var set | | plugin_entrypoint | All plugin files listed in config exist on disk | | bindings_valid | All binding YAML files listed in config parse correctly |

$ agent-runtime doctor
  ✓ config_exists        PASS  Loaded ./agent-runtime.config.yaml
  ✓ dsl_exists           PASS  Parsed OK — 3 agent(s), 4 task(s), 2 workflow(s)
  ✓ manifest_fresh       PASS  Generated files are up to date
  ! adapter_configured   WARN  No SDK adapter API keys found. Set one of: ...
  ✓ plugin_entrypoint    PASS  No plugins configured
  ✓ bindings_valid       PASS  No bindings configured

All checks passed.

Outputs a DoctorResult JSON object to stdout. Exits with code 0 when all checks pass (warnings are OK), code 1 when any check fails.

YAML invocation file

For CI or programmatic invocation, define the full request as a YAML file:

# invocation.yaml
workflow: feature-implement
handoff:
  type: feature-implementation-request
  payload:
    objective: Add login endpoint with JWT
    inputs:
      repository: .
    expected_outputs:
      - implementation-diff
    completion_criteria:
      - tests_passed
runtime:
  max_follow_ups: 3
  max_retries: 1
agent-runtime run --file ./invocation.yaml --adapter claude
# or: --adapter cursor, --adapter mock

For full CLI reference with exit codes and output schemas, see docs/cli-reference.md.

The CLI specification is managed contract-first via cli-contracts in cli-contract.yaml.

SDK adapters

| Adapter | SDK | Status | |---------|-----|--------| | cursor | Cursor Cloud Agents (@cursor/sdk) | Implemented | | claude | Claude Agent SDK (@anthropic-ai/claude-agent-sdk) | Implemented | | gemini | Google Gemini (@google/genai) | Implemented | | openai | OpenAI Agents SDK (@openai/agents) | Implemented | | mock | Simulated responses for testing/demo | Implemented |

Adapter interface

The minimal adapter interface requires only send:

interface SdkAdapter {
  send(prompt: string, options: AdapterSendOptions): Promise<string>;
  followUp?(message: string): Promise<string>;
}

For adapters that need full contract context, implement sendExecution:

interface SdkAdapter {
  send(prompt: string, options: AdapterSendOptions): Promise<string>;
  followUp?(message: string): Promise<string>;
  sendExecution?(request: AgentExecutionRequest): Promise<string>;
}

When sendExecution is implemented, the runtime prefers it over send and passes the full AgentExecutionRequest containing agent/task IDs, handoff info, Zod schema metadata, and task context.

Choosing adapter methods

| Method | Use when | |--------|----------| | send(prompt, options) | Your SDK only needs a prompt string | | sendExecution(request) | Your SDK needs agent/task IDs, handoff metadata, schema info, tools, or context | | followUp(message) | Your SDK supports continuing the same session after validation errors |

Adapters may start with send and later upgrade to sendExecution without changing workflow code.

SDK-specific options belong on the adapter constructor, not on the shared interface:

// Cursor SDK
const cursorAdapter = await CursorSdkAdapter.create({
  apiKey: process.env.CURSOR_API_KEY!,
  model: "composer-2",
  cwd: process.cwd(),
  guardrailHooks,
});

// Claude Agent SDK
const claudeAdapter = new ClaudeAgentSdkAdapter({
  model: "claude-sonnet-4-20250514",
  cwd: process.cwd(),
  guardrailHooks,
});

// OpenAI Agents SDK
const openaiAdapter = new OpenAIAgentsSdkAdapter({
  model: "gpt-4.1",
  maxTurns: 20,
  guardrailHooks,
});

Claude Agent SDK adapter

The Claude adapter wraps @anthropic-ai/claude-agent-sdk, which runs Claude as a stateful coding agent with built-in tool execution (Read, Edit, Bash, etc.).

import { ClaudeAgentSdkAdapter } from "agent-contracts-runtime/adapters/claude-agent-sdk";

const adapter = new ClaudeAgentSdkAdapter({
  cwd: process.cwd(),
  model: "claude-sonnet-4-20250514",   // optional, uses SDK default
  permissionMode: "bypassPermissions", // default for automated workflows
  maxTurns: 20,                        // optional turn limit
  guardrailHooks,                      // optional guardrail enforcement
});

Internally the adapter calls the SDK's query() function, which returns an AsyncGenerator of SDK events. The adapter iterates the stream and extracts the final result text.

followUp() resumes the same session via the SDK's resume option, so the agent retains full conversation context when correcting output format.

| Config option | Description | Default | |---------------|-------------|---------| | cwd | Working directory | process.cwd() | | model | Claude model identifier | SDK default | | tools | Available tools (string array or { type: 'preset', preset: 'claude_code' }) | Auto-selected based on readonly | | permissionMode | "default" / "acceptEdits" / "bypassPermissions" / "plan" | "bypassPermissions" | | maxTurns | Maximum conversation turns | No limit | | guardrailHooks | Runtime guardrail hooks (mapped to SDK PreToolUse hooks) | None |

Note: @anthropic-ai/claude-agent-sdk requires zod@^4.0.0 as a peer dependency. This runtime uses zod@^4.0.0 natively.

OpenAI Agents SDK adapter

The OpenAI adapter wraps @openai/agents, which provides a lightweight agent framework with built-in tool execution, guardrails, handoffs, and tracing.

import { OpenAIAgentsSdkAdapter } from "agent-contracts-runtime/adapters/openai-agents-sdk";

const adapter = new OpenAIAgentsSdkAdapter({
  model: "gpt-4.1",          // optional, uses SDK default
  maxTurns: 20,               // optional turn limit (default: 10)
  guardrailHooks,             // optional guardrail enforcement
});

Internally the adapter creates a fresh Agent for each send() call with the contract prompt as instructions, then calls the SDK's run() function and extracts finalOutput as the result text.

followUp() resumes the same conversation via the SDK's previousResponseId option, so the model retains full context when correcting output format.

| Config option | Description | Default | |---------------|-------------|---------| | model | Model identifier (e.g. "gpt-4.1", "gpt-5.5") | SDK default | | maxTurns | Maximum agent loop turns | 10 (SDK default) | | tools | Additional tools to pass to the Agent | None | | agentName | Name for the Agent instance | "contract-agent" | | guardrailHooks | Runtime guardrail hooks (mapped to SDK InputGuardrail) | None | | signal | AbortSignal for cancellation | None |

Note: @openai/agents requires zod@^4.0.0 as a peer dependency. This runtime uses zod@^4.0.0 natively.

Google Gemini adapter

The Gemini adapter wraps @google/genai, Google's TypeScript SDK for Gemini models.

import { GeminiSdkAdapter } from "agent-contracts-runtime/adapters/gemini-sdk";

const adapter = new GeminiSdkAdapter({
  model: "gemini-2.5-flash",       // optional, defaults to gemini-2.5-flash
  apiKey: process.env.GEMINI_API_KEY,  // optional, falls back to env var
  temperature: 0.7,                // optional
  maxOutputTokens: 8192,           // optional
  guardrailHooks,                  // optional guardrail enforcement
});

Internally the adapter creates a chat session via ai.chats.create() for each send() call, then calls chat.sendMessage(). This maintains conversation state for followUp() within the same chat session.

| Config option | Description | Default | |---------------|-------------|---------| | apiKey | Gemini API key (or set GEMINI_API_KEY env var) | Env var | | model | Model identifier (e.g. "gemini-2.5-flash", "gemini-2.5-pro") | "gemini-2.5-flash" | | systemInstruction | System instruction prepended to conversations | None | | temperature | Temperature for generation (0.0–2.0) | SDK default | | maxOutputTokens | Maximum output tokens | SDK default | | guardrailHooks | Runtime guardrail hooks (evaluated locally on responses) | None |

Handoff validation

The runtime validates both input and output handoffs against generated Zod schemas.

Input validation happens when a WorkflowInvocation includes a typed handoff (Structured API or Builder API). The handoff factory validates the payload at construction time.

Output validation happens after each SDK execution. The runtime extracts the structured result from the agent's output and validates it against the expected handoff schema for that workflow step.

If validation fails, the runtime attempts a followUp (lightweight, same session) to correct the output format before falling back to a full retry.

Follow-up and retry

| | followUp | retry | |---|---|---| | Method | adapter.followUp() | adapter.send() | | Cost | Lightweight | Heavy | | Use case | Output format correction | Full task re-execution | | Default limit | maxFollowUps: 2 | maxRetries: 0 (opt-in) | | DSL mapping | Independent (always available) | step.max_retries in workflow DSL | | Trigger | Zod schema validation error | Empty output, or decideRetryStrategy | | Session | Same session continues (Cursor: same agent, Claude: resume with session ID) | New session |

The prompt includes the full handoff schema field table and a YAML example, so the agent can produce valid output on the first attempt without guessing the format.

Inject custom recovery logic via decideRetryStrategy:

const result = await runTask(adapter, "plan-and-implement", {
  user_request: "Add login",
  decideRetryStrategy: async (outcome, attempt) => {
    if (attempt >= 2) return "abort";
    if (outcome.status === "validation_error") return "follow_up";
    return "retry";
  },
});

Runtime hooks and plugins

Plugins let project code customize execution around DSL-defined workflows without changing the DSL or runtime core.

interface AgentPlugin {
  readonly id: string;
  beforeTask?(taskId: string, context: TaskContext): Promise<TaskContext | null>;
  contextEnhancer?(taskId: string, context: TaskContext): TaskContext;
  afterTask?(taskId: string, outcome: TaskOutcome): Promise<TaskOutcome>;
  promptEnhancer?(taskId: string, prompt: string, context: TaskContext): string;
  promptBuilder?(args: PromptBuilderArgs): string | null;
  customGuardrails?: { /* evaluateCommand, evaluateFilePath, evaluateFileContent */ };
  beforeWorkflow?(workflowId: string, userRequest: string): Promise<void>;
  afterWorkflow?(workflowId: string, result: WorkflowResult): Promise<void>;
}

Hook execution order

beforeWorkflow
  ↓
beforeTask        ← skip task (return null) or modify context
  ↓
contextEnhancer   ← enrich structured context (variables, handoff_input, etc.)
  ↓
promptBuilder     ← full prompt override, or null to use default
  ↓
promptEnhancer    ← lightweight post-processing on prompt string
  ↓
SDK Adapter send
  ↓
afterTask
  ↓
afterWorkflow

beforeTask vs contextEnhancer

| Hook | Purpose | Side effects | |------|---------|--------------| | beforeTask | Skip task (return null), replace context entirely, perform gating decisions | May have side effects | | contextEnhancer | Add structured variables, paths, or metadata to context | Should be side-effect free |

Context vs prompt

The runtime treats prompt as a derived artifact from structured context. Plugins should prefer modifying TaskContext via contextEnhancer over string manipulation in promptEnhancer when the data is structured:

const contextPlugin: AgentPlugin = {
  id: "context-enricher",
  contextEnhancer(taskId, context) {
    return {
      ...context,
      relevant_paths: ["src/auth/**", "src/middleware/**"],
      variables: {
        ...context.variables,
        dbType: "PostgreSQL",
        orm: "TypeORM",
      },
    };
  },
};

Prompt customization hooks

| Hook | Purpose | Execution order | |------|---------|-----------------| | promptBuilder | Full prompt override. Return a string to replace the default prompt, or null to use the default. | 1st (before promptEnhancer) | | promptEnhancer | Lightweight post-processor. Receives the built prompt and returns a modified version. | 2nd (after promptBuilder) |

Register a plugin

import { pluginRegistry, type AgentPlugin } from "agent-contracts-runtime";

const myPlugin: AgentPlugin = {
  id: "my-plugin",
  async beforeTask(taskId, context) {
    return context; // return null to skip
  },
  contextEnhancer(taskId, context) {
    return {
      ...context,
      variables: { ...context.variables, projectFramework: "NestJS" },
    };
  },
  async afterTask(taskId, outcome) {
    return outcome;
  },
  promptEnhancer(taskId, prompt) {
    return prompt + "\n\nAlways write tests first.";
  },
};

pluginRegistry.register(myPlugin);

Guardrails

Guardrails evaluate commands, file paths, and file content before execution.

Generated guardrail hooks (from DSL + binding) and plugin custom guardrails are merged and evaluated together, producing one of four actions:

| Action | Effect | |--------|--------| | block | Deny the operation | | warn | Allow with warning (can fail with fail_on_guardrail_warning) | | info | Allow with informational context to user/agent | | shadow | Report only, no effect on execution |

Guardrail checks are defined in binding YAML files (not in the DSL directly), following the agent-contracts SoftwareBinding schema with guardrail_impl entries. Three matcher types are supported:

  • command_regex — Match shell commands against regex patterns
  • file_glob — Match file paths against glob patterns
  • content_regex — Match file content against regex patterns

Binding file

# bindings/runtime.yaml
software: agent-runtime
version: 1

guardrail_impl:
  no-force-push:
    checks:
      - matcher:
          type: command_regex
          pattern: "(^|[|;&]\\s*)git\\s+push\\s.*(--force|-f)\\b"
        message: "Force push is forbidden."
  block-env-files:
    checks:
      - matcher:
          type: file_glob
          pattern: "**/{.env,.env.*}"
        message: "Writing to .env file is blocked."

Configuration

agent-runtime.config.yaml:

dsl: ./agent-contracts.yaml
generated_dir: ./agent/generated          # default: ./agent/generated
bindings:                                  # optional, for guardrail hook generation
  - ./bindings/runtime.yaml
active_guardrail_policy: default           # which guardrail_policy to activate
templates_dir: ./custom-templates          # optional, overrides built-in templates

Exports

| Import path | Description | |-------------|-------------| | agent-contracts-runtime | Core runtime API (runWorkflow, runTask, createRuntime, buildTaskPrompt, pluginRegistry, zodSchemaToPromptDescription) | | agent-contracts-runtime | Types (WorkflowInvocation, HandoffInput, AgentExecutionRequest, WorkflowRegistries, SdkAdapter, AdapterSendOptions, TaskContext, TaskOutcome, etc.) | | agent-contracts-runtime/generator | Generator API (generate, checkFreshness, buildContractContext) | | agent-contracts-runtime/adapters/cursor-sdk | Cursor SDK adapter | | agent-contracts-runtime/adapters/claude-agent-sdk | Claude Agent SDK adapter | | agent-contracts-runtime/adapters/gemini-sdk | Google Gemini adapter | | agent-contracts-runtime/adapters/openai-agents-sdk | OpenAI Agents SDK adapter | | agent-contracts-runtime/adapters/mock | Mock adapter for testing |

Requirements

  • Node.js 20+
  • TypeScript 5.x (ESM, strict mode)
  • agent-contracts (optional peer dependency for DSL resolution)
  • @anthropic-ai/claude-agent-sdk (optional, for Claude adapter — requires zod ^4.0.0)
  • @cursor/sdk (optional, for Cursor adapter)
  • @google/genai (optional, for Gemini adapter)
  • @openai/agents (optional, for OpenAI adapter — requires zod ^4.0.0)

License

MIT