agent-contracts-runtime
v0.13.1
Published
Runtime bridge for executing agent-contracts workflows on Agent SDKs
Maintainers
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 runnerAgent 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 taskyou 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-contractsQuick 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 doctorHow 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: afterWorkflowLayer 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-templatesOr 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 filesCreates 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/ entryThe 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 checksagent-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: 1agent-runtime run --file ./invocation.yaml --adapter claude
# or: --adapter cursor, --adapter mockFor 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-sdkrequireszod@^4.0.0as a peer dependency. This runtime useszod@^4.0.0natively.
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/agentsrequireszod@^4.0.0as a peer dependency. This runtime useszod@^4.0.0natively.
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
↓
afterWorkflowbeforeTask 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 patternsfile_glob— Match file paths against glob patternscontent_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 templatesExports
| 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
