Status: Documentation split in progress.
This document describes the step plugin and executor extension model used by the Deterministic AI Runtime.
The complete technical reference is currently preserved in:
The runtime is designed to orchestrate execution without hardcoding every possible AI, RAG, tool, or decision behavior inside the DAG engine.
Instead, runtime behavior is extended through step plugins.
A step plugin allows the runtime to support new step types while keeping the execution engine focused on:
- DAG orchestration
- dependency evaluation
- distributed claiming
- retry and recovery
- retention and compaction
- concurrency admission
- execution control
- deterministic convergence
The plugin owns domain-specific execution logic.
The runtime owns execution guarantees.
The main principle is:
The DAG engine coordinates execution.
Step plugins execute domain behavior.
This keeps responsibilities clean.
The engine should not need to know how to:
- call a specific LLM provider
- query a specific RAG backend
- merge a specific document format
- score a business decision
- call a domain API
- write to an external system
The engine only needs to know how to safely run a step.
A step plugin is a registered execution component that can handle a specific stepKey.
A pipeline step declares:
{
"name": "retrieve-context",
"stepKey": "rag.retrieval",
"config": {
"provider": "postgres",
"operation": "candidate.byId"
}
}The runtime uses stepKey to resolve the correct executor.
The executor receives resolved input and configuration, then returns a structured result.
The stepKey is the stable identifier for a step executor.
Examples:
rag.retrieval
rag.merge
rag.compose
llm.summary
tool.execute
decision.score
The stepKey should be:
- stable
- explicit
- unique enough to avoid ambiguity
- registered with the runtime
- meaningful for diagnostics and observability
A step cannot execute safely if its stepKey cannot be resolved.
The runtime separates step name from step key.
step name
= unique step instance inside a pipeline
stepKey
= registered executor type / behavior
Example:
{
"name": "candidate",
"stepKey": "rag.retrieval"
}Here:
candidate
= this specific step inside the pipeline
rag.retrieval
= the executor behavior used to run it
This separation allows multiple steps to use the same executor type.
A step executor is responsible for executing domain logic.
It may:
- read resolved input
- read step configuration
- call a provider
- run custom logic
- return a structured result
- emit diagnostics
- throw controlled failures when required
A step executor should not directly own global orchestration.
It should not decide DAG convergence.
It should not bypass runtime state transitions.
The runtime is responsible for:
- validating the pipeline
- resolving step dependencies
- determining step readiness
- claiming the step atomically
- applying retry rules
- applying concurrency admission
- applying execution control
- persisting results
- applying retention
- finalizing the execution
The runtime decides when and whether a step can run.
The plugin decides what happens inside the step.
A simplified execution flow is:
DAG engine finds eligible step
↓
Execution control gate allows advancement
↓
Concurrency admission succeeds
↓
Redis Lua claim succeeds
↓
Runtime resolves step executor by stepKey
↓
Runtime resolves inputs and config
↓
Executor runs domain logic
↓
Executor returns result or failure
↓
Runtime persists completion/failure transition
This keeps plugin execution inside the runtime safety boundary.
Step plugins are configured through the pipeline definition.
A plugin can receive configuration such as:
- provider
- provider key
- model
- operation
- source step
- source steps
- composer
- retry configuration
- retention configuration
- concurrency configuration
- plugin-specific options
Example:
{
"name": "summarize",
"stepKey": "llm.summary",
"dependsOn": [
"retrieve-context"
],
"config": {
"provider": "openai",
"model": "gpt-4.1",
"operation": "llm.chat"
}
}The plugin should treat configuration as its execution contract.
Step plugins can be declared using a class-level attribute.
The attribute binds a concrete executor class to a stable stepKey.
Example:
[AiStep("decision.score")]
public sealed class CandidateScoreStep : IAiStepExecutor
{
public Task<AiStepExecutionResult> ExecuteAsync(
AiStepExecutionContext context,
CancellationToken cancellationToken)
{
// Domain-specific logic belongs here.
// Runtime guarantees are still handled by the runtime.
return Task.FromResult(
AiStepExecutionResult.Success(new
{
score = 92,
reason = "Strong profile match"
}));
}
}The important idea is:
Class attribute
↓
declares the stepKey handled by the executor
Assembly scanning
↓
discovers attributed executor classes
Step registry
↓
maps stepKey to executor implementation
Pipeline JSON
↓
references stepKey
Runtime
↓
resolves the executor automatically
The exact attribute class name should match the implementation in the repository.
The documentation should keep the concept clear:
A step executor should be discoverable from its class metadata.
The pipeline should not manually construct executor instances.
The runtime can register step plugins by scanning one or more assemblies.
A typical registration pattern is:
services.AddAiStepsFromAssemblies(
typeof(AiRuntimeAssemblyMarker).Assembly,
typeof(MyApplicationAssemblyMarker).Assembly);The assembly scanner should discover step executors that are marked with the step class attribute.
This allows new plugins to be added without modifying the DAG engine core.
The high-level flow is:
Assembly contains attributed step classes
↓
AddAiStepsFromAssemblies scans assembly
↓
Step metadata is read from class attributes
↓
Executors are registered in DI
↓
Step registry maps stepKey to executor
↓
Pipeline can use the stepKey
This makes the extension model scalable:
- runtime-owned steps can live in runtime assemblies
- test steps can live in test assemblies
- application steps can live in application assemblies
- domain-specific plugins can live in separate packages
A safe attribute-based discovery model should ensure:
- each discovered step has a stable
stepKey - the executor implements the required step executor contract
- duplicate
stepKeyregistrations are rejected - missing step keys fail before unsafe execution
- only intended assemblies are scanned
- step metadata is explicit and observable
- registration is deterministic
Duplicate step keys must not silently override each other.
A duplicate registration should fail fast because it creates ambiguity.
Plugins must be registered with the runtime.
The registration mechanism may use dependency injection, class attributes, and assembly scanning.
A typical pattern is:
Executor class has step attribute
↓
Assembly is scanned
↓
Runtime reads stepKey from attribute
↓
Executor is registered in DI
↓
Step registry binds stepKey to executor
↓
Pipeline references stepKey
↓
Runtime resolves executor at execution time
The exact registration implementation may evolve, but the principle remains:
No registered executor
↓
step cannot execute safely
The runtime supports or can support assembly-based discovery of step plugins.
This allows new step executors to be added without modifying the DAG engine core.
A safe discovery model should ensure:
- only intended assemblies are scanned
- duplicate step keys are detected
- missing step keys fail early
- attributed classes are validated
- plugin registration is explicit enough for production debugging
- the final registry can be inspected or logged
Assembly scanning is a convenience mechanism.
It must not weaken runtime safety.
Many plugins should not hardcode a specific provider.
Instead, a step may use provider abstractions.
Examples:
rag.retrieval
↓
provider = relational
providerKey = sqlserver
rag.retrieval
↓
provider = relational
providerKey = postgres
rag.retrieval
↓
provider = vector
providerKey = redis-vector
This allows the same step type to work with different backends.
RAG-oriented steps are a natural fit for the plugin model.
Examples include:
rag.retrievalrag.mergerag.compose
A retrieval plugin may retrieve data from:
- SQL Server
- PostgreSQL
- MongoDB
- Redis vector
- vector databases
- external APIs
- document indexes
A merge plugin may combine upstream results.
A compose plugin may transform merged data into deterministic context.
The DAG engine does not need to know the internals of these operations.
LLM or prompt steps can also be implemented as plugins.
An LLM plugin may:
- build a prompt
- resolve context
- call an AI provider
- parse a response
- return structured output
- emit provider/model/operation metadata
The runtime can then apply:
- retry policies
- provider throttling
- model throttling
- operation throttling
- observability
- retention
without the LLM plugin controlling those runtime guarantees directly.
Tool or action plugins can execute application-specific behavior.
Examples:
- call an external API
- send a notification
- write to a database
- update a CRM
- run a calculation
- execute a domain command
Tool plugins should still run inside the runtime execution model.
They should respect:
- runtime retry behavior
- cancellation boundaries
- concurrency admission
- observability
- idempotency expectations where required
Decision plugins can evaluate structured data and return a deterministic decision.
Examples:
- score a candidate
- classify a document
- decide whether to continue
- select a downstream path
- validate a business rule
Decision plugins are important because AI workflows often combine probabilistic AI outputs with deterministic business logic.
Step plugins should receive resolved inputs from the runtime.
Inputs may come from:
- initial execution state
- previous step outputs
- nested result data
- configuration
- external payload resolver
Example binding:
{
"input": {
"candidateId": "state.candidateId",
"context": "steps.merge.result.data"
}
}The plugin should not manually scan unrelated runtime state if the input resolver can provide the required input.
Step plugins should return structured outputs.
A plugin output may include:
- status
- data
- metadata
- diagnostics
- provider information
- payload information
The runtime is responsible for persisting the result through controlled state transitions.
Large outputs may later be externalized by retention.
Plugins should not implement hidden retry loops that bypass runtime retry state.
Instead:
Plugin fails
↓
Runtime captures failure
↓
Retry engine resolves config.retry
↓
Retry policies decide
↓
Runtime schedules retry through state
This keeps retry behavior:
- observable
- deterministic
- policy-driven
- distributed-safe
Plugins may classify or expose failure metadata, but the runtime owns retry scheduling.
Plugins should not bypass runtime concurrency control.
Before a plugin executes, the runtime may apply:
- execution control gate
- concurrency policies
- provider/model/operation throttling
- Redis lease acquisition
- atomic DAG claim
Provider/model/operation metadata in step config helps the runtime apply correct concurrency rules.
Plugins may produce large outputs.
The runtime retention system decides whether to:
- keep output inline
- compact payload
- externalize payload
- evict hot data later
- preserve resolver references
A plugin should return structured data and let the runtime manage memory boundaries.
Plugins should support replay-safe execution patterns where possible.
Replay foundations rely on:
- stable step keys
- stable step names
- deterministic result representation
- durable payload references
- configuration context
- replay-safe metadata
A plugin should avoid hiding important execution context outside runtime state.
Plugins should emit or expose useful diagnostics.
Useful plugin diagnostics include:
- provider
- provider key
- model
- operation
- input source
- output size
- failure reason
- external call latency
- result metadata
The runtime should correlate this with:
ExecutionId- step name
- step key
- retry state
- concurrency decision
- trace/timeline events
Plugins must not bypass the runtime safety model.
A plugin should not directly:
- claim steps
- complete steps
- fail steps
- mutate Redis DAG state
- mutate execution control state
- schedule retries independently
- acquire distributed concurrency leases independently
- finalize executions
- delete runtime payloads outside retention rules
The plugin performs domain work.
The runtime controls execution state.
Some plugins may call external systems.
Those operations may not be naturally idempotent.
The runtime can prevent duplicate ownership, but plugin authors should still design external side effects carefully.
For example:
- use idempotency keys where possible
- include
ExecutionIdand step name in external calls - avoid irreversible side effects without safeguards
- make retries safe when possible
- distinguish read-only steps from mutating actions
This is especially important for tool/action steps.
Plugins may evolve over time.
Future runtime hardening may require:
- versioned step keys
- versioned pipeline definitions
- compatibility rules
- migration guidance
- replay-safe plugin metadata
For now, stable stepKey and pipeline versioning are the main foundations.
A simplified process for adding a new plugin is:
1. Define a stable stepKey
2. Implement the executor
3. Add the step class attribute with the stepKey
4. Ensure the executor implements the runtime step executor contract
5. Register the assembly with AddAiStepsFromAssemblies
6. Add step config schema/expectations
7. Add tests for success/failure behavior
8. Add retry/concurrency/retention compatibility tests if needed
9. Reference the stepKey in a pipeline definition
Example executor:
[AiStep("decision.score")]
public sealed class CandidateScoreStep : IAiStepExecutor
{
public Task<AiStepExecutionResult> ExecuteAsync(
AiStepExecutionContext context,
CancellationToken cancellationToken)
{
return Task.FromResult(
AiStepExecutionResult.Success(new
{
score = 92
}));
}
}Example service registration:
services.AddAiStepsFromAssemblies(
typeof(AiRuntimeAssemblyMarker).Assembly,
typeof(CandidateScoreStep).Assembly);Example pipeline step:
{
"name": "score-candidate",
"stepKey": "decision.score",
"dependsOn": [
"compose"
],
"config": {
"operation": "candidate.score",
"strategy": "deterministic"
}
}| Scenario | Runtime Behavior |
|---|---|
Unknown stepKey |
Runtime should fail safely before execution. |
| Missing step class attribute | Assembly scanning should not register the executor. |
| Duplicate step plugin registration | Runtime should detect ambiguity and fail fast. |
| Executor does not implement required contract | Registration should reject it or fail clearly. |
| Plugin throws exception | Runtime captures failure and applies retry rules. |
| Plugin returns large payload | Retention can externalize and compact output. |
| Plugin calls provider | Concurrency and provider throttling can apply before execution. |
| Plugin is slow or worker crashes | Recovery can release stale ownership. |
| Plugin output is needed later | Resolver and retention preserve access. |
| Plugin has side effects | Plugin should use idempotency safeguards where possible. |
The step plugin model is validated through runtime usage and tests covering:
- step execution by
stepKey - registered step executors
- class-attribute-based step metadata
- assembly-based discovery
- config-driven step behavior
- RAG retrieval step pattern
- RAG merge step pattern
- RAG compose step pattern
- provider-based retrieval abstraction
- runtime-controlled retry around plugins
- runtime-controlled concurrency around plugins
- runtime-controlled retention around plugin outputs
- failure when a step key cannot be resolved
Public plugin SDK polish and versioned plugin contracts remain planned work.
| Capability | Status |
|---|---|
Step execution by stepKey |
Implemented / validated |
| Registered step executors | Implemented / validated |
| Class-attribute-based step metadata | Implemented / validated |
| Assembly-based step discovery | Implemented / validated |
AddAiStepsFromAssemblies registration pattern |
Implemented / validated |
| Config-driven step behavior | Implemented / validated |
| RAG retrieval step pattern | Implemented / foundation available |
| RAG merge step pattern | Implemented / foundation available |
| RAG compose step pattern | Implemented / foundation available |
| Provider-based retrieval abstraction | Implemented / foundation available |
| LLM/prompt step extension model | Foundation available |
| Tool/action step extension model | Foundation available |
| Decision step extension model | Foundation available |
| Runtime-controlled retry around plugins | Implemented / validated |
| Runtime-controlled concurrency around plugins | Implemented / validated |
| Runtime-controlled retention around plugin outputs | Implemented / validated |
| Public plugin SDK polish | Planned |
| Versioned plugin contract | Planned |
| Component | Responsibility |
|---|---|
| DAG engine | Coordinates step eligibility and execution flow. |
| Step attribute | Declares the stable stepKey handled by an executor class. |
| Assembly scanner | Discovers attributed step executor classes. |
| Step registry | Maps stepKey values to step executors. |
| Step executor | Executes domain-specific behavior. |
| Input resolver | Resolves step inputs from state and previous results. |
| Retry engine | Handles plugin failures through runtime retry state. |
| Concurrency engine | Applies admission and throttling before plugin execution. |
| Retention system | Handles large plugin outputs safely. |
| Observability layer | Records plugin execution diagnostics. |
Step plugins allow the runtime to stay generic while supporting specialized behavior.
The DAG engine does not need to know how RAG retrieval, LLM calls, tools, or domain decisions work.
Step executors are discovered through explicit metadata, registered through assembly scanning, resolved by stepKey, and executed inside the runtime safety boundary.
This keeps the runtime extensible without weakening deterministic execution guarantees.
- Architecture Overview
- Config-Driven Runtime
- Policy-Driven Execution
- RAG Pipelines
- Retry and Recovery
- Retention and Compaction
- Distributed Concurrency and Throttling
- Replay and Audit
- Testing Strategy
This document is a focused extraction from the complete technical reference.
The original technical depth remains preserved in:
Do not remove content from runtime-internals.md until the extracted documentation has been reviewed and validated.