This document describes what is implemented today in /packages and /loops.
If a behavior is not in source, it is called out explicitly.
A LoopDefinition (from @loop-engine/core) contains:
id: stable loop identifierversion: semver string (x.y.z)description: human-readable summarydomain: domain label (scm,crm, etc.)states: list ofStateSpecinitialState: starting state id (must exist instates)transitions: list ofTransitionSpecoutcome:OutcomeSpecfor measurable completion- optional
participants,spawnableLoops,metadata
Minimal YAML shape:
id: example.procurement
version: 1.0.0
domain: scm
description: Minimal loop definition
states:
- id: OPEN
- id: SETTLED
isTerminal: true
initialState: OPEN
transitions:
- id: settle
from: OPEN
to: SETTLED
allowedActors: [human, automation]
outcome:
id: po_settled
description: Purchase order settled
valueUnit: po_settled
measurable: trueStateSpec fields:
id: unique state idisTerminal?: marks lifecycle completion (statusbecomesCLOSEDin runtime)isError?: marks error terminal path (statusbecomesERRORin runtime)
TransitionSpec fields:
id: transition idfrom: source state idto: destination state idallowedActors: allowed actor types- optional
guards,sideEffects,description
Actor types implemented in @loop-engine/actors:
humanautomationai-agentwebhooksystem
Every transition evaluation includes an actor, and runtime authorization is checked by
canActorExecuteTransition(...).
Canonical ActorRef shape (@loop-engine/core):
type ActorRef = {
type: "human" | "automation" | "ai-agent" | "webhook" | "system";
id: string;
displayName?: string;
sessionId?: string;
agentId?: string;
};GuardSpec fields:
iddescriptionfailureMessageseverity:hard|softevaluatedBy:runtime|module|external
Runtime behavior:
hard: blocks transition, emitsloop.guard.failed, state does not advancesoft: transition proceeds; warning is appended to transition evidence as_softGuardWarnings
Built-in guards in @loop-engine/guards:
actor_has_permission(actorPermissionGuard) - checksevidence.required_rolevsevidence.rolesapproval_obtained(approvalObtainedGuard) - requiresevidence.approved === truedeadline_not_exceeded(deadlineNotExceededGuard) - validatesevidence.deadline_isois in the futureduplicate_check_passed(duplicateCheckPassedGuard) - fails whenevidence.duplicate_found === truefield_value_constraint(fieldValueConstraintGuard) - evaluatesevidence.constraint(eq,gt,lt,in)
Event types defined in @loop-engine/events:
loop.started: emitted whenengine.start(...)creates a new instance; includesinitialState,actorloop.transition.requested: schema/type exists; runtime engine does not currently emit this eventloop.transition.executed: emitted after successful transition persistence; includesfromState,toState,transitionId,evidenceloop.transition.blocked: schema/type exists; runtime engine currently returns rejected status rather than emitting this eventloop.guard.failed: emitted when a hard guard fails; includesattemptedTransitionId,guardId,guardFailureMessageloop.completed: emitted when transition enters terminal state (isTerminal); includesterminalState,durationMs,transitionCount,outcomeId,valueUnitloop.error: schema/type exists; current runtime does not emit this eventloop.spawned: schema/type exists; current runtime does not emit this eventloop.signal.received: schema/type exists; current runtime does not emit this eventloop.outcome.recorded: schema/type exists; current runtime does not emit this event
LearningSignal is implemented in @loop-engine/events and produced by
extractLearningSignal(completedEvent, history, predicted?).
It contains:
predicted: model or baseline estimate mapactual: observed values map (currently computescycle_time_dayswhen history exists)delta: numeric difference (actual - predicted) for overlapping numeric keys
This is computed from completion event + transition history; it is not auto-emitted by runtime today.
Verified from package dependencies:
@loop-engine/sdk
├── @loop-engine/runtime
│ ├── @loop-engine/core
│ ├── @loop-engine/loop-definition
│ ├── @loop-engine/events
│ ├── @loop-engine/actors
│ └── @loop-engine/guards
├── @loop-engine/loop-definition
├── @loop-engine/signals
├── @loop-engine/events
├── @loop-engine/observability
├── @loop-engine/core
├── @loop-engine/guards
└── @loop-engine/adapter-memory (default store)
engine.transition(...) execution path in packages/runtime/src/engine.ts:
- Load instance from
store.getInstance(aggregateId)and load definition from registry. - Reject if loop is closed/error (
state.isTerminal,state.isError, or instance status). - Resolve transition by
(transitionId, from=currentState); reject withinvalid_transitionif not found. - Check actor authorization via
canActorExecuteTransition(...); may returnrejectedorpending_approval. - Evaluate guards through
guardEvaluator.evaluate(...), collecting hard/soft failures. - If hard guard fails, emit
loop.guard.failedand returnguard_failedwithout state advance. - Advance
currentState, derive instance status (IN_PROGRESS/CLOSED/ERROR), persist transition record + instance. - Emit
loop.transition.executed; if nowCLOSED, emitloop.completed. - Run registered side-effect handlers for transition
sideEffectsids.
Notes on implemented behavior:
- Soft guard failures are stored in transition evidence under
_softGuardWarnings. loop.transition.requested,loop.transition.blocked,loop.error,loop.spawned,loop.signal.received, andloop.outcome.recordedare typed/schematized but not emitted by the current runtime implementation.
LoopStore interface (@loop-engine/runtime) requires:
getInstance(aggregateId)saveInstance(instance)getTransitionHistory(aggregateId)saveTransitionRecord(record)listOpenInstances(loopId, orgId)
Available adapters in repo:
@loop-engine/adapter-memory- complete in-memoryLoopStore(used by default in SDK)@loop-engine/adapter-postgres- includescreateSchema(pool)andpostgresStore(pool)API; runtime methods are currently markedpending@loop-engine/adapter-kafka-kafkaEventBus(...)implementation forEventBus@loop-engine/adapter-http-httpEventBus(...)implementation for webhook emission
Default memory usage via SDK:
import { createLoopSystem } from "@loop-engine/sdk";
const { engine } = createLoopSystem({ loops: [definition] });Postgres store wiring (current API surface):
import { createSchema, postgresStore } from "@loop-engine/adapter-postgres";
import { createLoopSystem } from "@loop-engine/sdk";
await createSchema(pool);
const { engine } = createLoopSystem({
loops: [definition],
store: postgresStore(pool)
});The Postgres adapter methods currently throw "postgresStore runtime implementation pending" until completed.