diff --git a/components/home/CodeTabs.tsx b/components/home/CodeTabs.tsx index b70c839..4e3debc 100644 --- a/components/home/CodeTabs.tsx +++ b/components/home/CodeTabs.tsx @@ -50,16 +50,16 @@ const checked = validateLoopDefinition(definition) if (!checked.valid) { throw new Error(checked.errors.map((e) => e.message).join('; ')) }`, - run: `import { createMemoryLoopStorageAdapter } from '@loop-engine/adapter-memory' + run: `import { memoryStore } from '@loop-engine/adapter-memory' import { createLoopSystem } from '@loop-engine/sdk' // assumes \`definition\` from the Define tab const { engine } = await createLoopSystem({ loops: [definition], - storage: createMemoryLoopStorageAdapter() + store: memoryStore() }) -await engine.startLoop({ +await engine.start({ loopId: 'expense.approval', aggregateId: 'EXP-001', actor: { type: 'system', id: 'intake' } @@ -74,7 +74,7 @@ await engine.transition({ events: `// assumes \`createLoopSystem\` returned \`eventBus\` alongside \`engine\` const { engine, eventBus } = await createLoopSystem({ loops: [definition], - storage: createMemoryLoopStorageAdapter() + store: memoryStore() }) eventBus.subscribe(async (event) => { @@ -99,7 +99,7 @@ function renderHighlighted(code: string) { '$1' ); return withKeywords.replace( - /\b(parseLoopYaml|validateLoopDefinition|createLoopSystem|createMemoryLoopStorageAdapter)\b/g, + /\b(parseLoopYaml|validateLoopDefinition|createLoopSystem|memoryStore)\b/g, '$1' ); } diff --git a/content/docs/ai-and-automation/ai-as-actor.mdx b/content/docs/ai-and-automation/ai-as-actor.mdx index 69183d8..5b86475 100644 --- a/content/docs/ai-and-automation/ai-as-actor.mdx +++ b/content/docs/ai-and-automation/ai-as-actor.mdx @@ -112,9 +112,9 @@ The full dual-provider walkthrough is in `/docs/examples/ai-replenishment`, with | [`@loop-engine/adapter-openai`](/docs/packages/adapter-openai) | OpenAI (`gpt-4o`, o-series) | | [`@loop-engine/adapter-grok`](/docs/packages/adapter-grok) | Grok (`grok-3`, `grok-2`) via xAI | | [`@loop-engine/adapter-gemini`](/docs/packages/adapter-gemini) | Gemini (`gemini-1.5-pro`, `gemini-2.0-flash`) | -| [`@loop-engine/adapter-perplexity`](/docs/packages/adapter-perplexity) | Perplexity Sonar (`sonar`, `sonar-pro`, …) — `LLMAdapter` with citations for research steps | +| [`@loop-engine/adapter-perplexity`](/docs/packages/adapter-perplexity) | Perplexity Sonar (`sonar`, `sonar-pro`, …) — `ToolAdapter` with citations for research steps | -The Perplexity package implements `LLMAdapter.invoke()` (text + citations), not the actor `createSubmission` flow. Use it where you need grounded retrieval inside a loop; use the actor adapters above for structured signal decisions. +The Perplexity package implements `ToolAdapter.invoke()` (text + citations), not the actor `createSubmission` flow. Use it where you need grounded retrieval inside a loop; use the actor adapters above for structured signal decisions. ## Related diff --git a/content/docs/changelog.mdx b/content/docs/changelog.mdx index 81bb8fb..8c82814 100644 --- a/content/docs/changelog.mdx +++ b/content/docs/changelog.mdx @@ -14,8 +14,8 @@ Versioning: [Semantic Versioning](https://semver.org/) ## Unreleased ### Added -- `@loop-engine/adapter-perplexity` — Perplexity Sonar `LLMAdapter` with citations, retries, and `guardEvidence` integration -- `@loop-engine/core` — `LLMAdapter`, `AdapterInput` / `AdapterOutput`, and deep `guardEvidence()` for adapter audit redaction +- `@loop-engine/adapter-perplexity` — Perplexity Sonar `ToolAdapter` with citations, retries, and `guardEvidence` integration +- `@loop-engine/core` — `ToolAdapter`, `AdapterInput` / `AdapterOutput`, and deep `guardEvidence()` for adapter audit redaction - Docs: `docs/integrations-perplexity.md` (Sonar vs [Perplexity Computer skills](https://www.perplexity.ai/computer/skills)) - Release contract stub: `.rc/adapter-perplexity.json` (RC **DRAFT** until promoted **LOCKED**) diff --git a/content/docs/examples/postgres-persistence.mdx b/content/docs/examples/postgres-persistence.mdx index 4d61fdf..1678245 100644 --- a/content/docs/examples/postgres-persistence.mdx +++ b/content/docs/examples/postgres-persistence.mdx @@ -38,18 +38,18 @@ QUERY STATE ---> READ INSTANCE + READ HISTORY ```ts import { createLoopSystem } from "@loop-engine/sdk"; -import { createPostgresStore, createSchema } from "@loop-engine/adapter-postgres"; -import { createMemoryLoopStorageAdapter } from "@loop-engine/adapter-memory"; +import { postgresStore, createSchema } from "@loop-engine/adapter-postgres"; +import { memoryStore } from "@loop-engine/adapter-memory"; import { Pool } from "pg"; // Version A: in-memory (default local dev) -const memoryStore = createMemoryLoopStorageAdapter(); -const memoryRuntime = await createLoopSystem({ loops: [definition], store: memoryStore }); +const memStore = memoryStore(); +const memoryRuntime = await createLoopSystem({ loops: [definition], store: memStore }); // Version B: postgres (durable) const pool = new Pool({ connectionString: process.env.DATABASE_URL }); await createSchema(pool); -const pgStore = createPostgresStore(pool); +const pgStore = postgresStore(pool); const postgresRuntime = await createLoopSystem({ loops: [definition], store: pgStore }); // Loop definitions + transition calls are unchanged across adapters. @@ -79,5 +79,5 @@ pnpm dev ``` -The same contract pattern extends to `adapter-kafka` and `adapter-http`. Any backend implementing `LoopStorageAdapter` can be used. +The same contract pattern extends to `adapter-kafka` and `adapter-http`. Any backend implementing `LoopStore` can be used. diff --git a/content/docs/getting-started/installation.mdx b/content/docs/getting-started/installation.mdx index 82b91a0..1e37bed 100644 --- a/content/docs/getting-started/installation.mdx +++ b/content/docs/getting-started/installation.mdx @@ -38,7 +38,9 @@ Use at least: ## Install packages individually -If you do not want the SDK aggregate package, install only what you need: +If you do not want the SDK aggregate package, install only what you need. + +Core primitives: - `@loop-engine/core` - domain model types and branded IDs - `@loop-engine/dsl` - `LoopBuilder`, parser, schema validation @@ -50,11 +52,29 @@ If you do not want the SDK aggregate package, install only what you need: - `@loop-engine/observability` - metrics, timelines, replay - `@loop-engine/registry-client` - remote/local **loop catalog** client (package name retains `registry-client`) - `@loop-engine/ui-devtools` - React devtools components + +Stores: + - `@loop-engine/adapter-memory` - in-memory `LoopStore` - `@loop-engine/adapter-postgres` - PostgreSQL `LoopStore` adapter - `@loop-engine/adapter-kafka` - Kafka `EventBus` adapter - `@loop-engine/adapter-http` - HTTP webhook `EventBus` adapter +AI adapters: + +- `@loop-engine/adapter-anthropic` - Claude actor adapter +- `@loop-engine/adapter-openai` - GPT actor adapter +- `@loop-engine/adapter-gemini` - Gemini actor adapter +- `@loop-engine/adapter-grok` - Grok actor adapter +- `@loop-engine/adapter-perplexity` - Sonar grounded-search adapter + +Routing and framework adapters: + +- `@loop-engine/adapter-pagerduty` - PagerDuty approval routing +- `@loop-engine/adapter-openclaw` - OpenClaw skill / approval bridge +- `@loop-engine/adapter-vercel-ai` - Vercel AI SDK tool-call governance +- `@loop-engine/adapter-commerce-gateway` - Commerce Gateway tool routing + ## Runtime requirements - Node.js 18+ is required. diff --git a/content/docs/getting-started/quick-start.mdx b/content/docs/getting-started/quick-start.mdx index 246a975..8b45b4c 100644 --- a/content/docs/getting-started/quick-start.mdx +++ b/content/docs/getting-started/quick-start.mdx @@ -65,20 +65,19 @@ eventBus.subscribe(async (event) => console.log(event.type)) await engine.start({ loopId: 'expense.approval', aggregateId: aggregateId('EXP-2026-001'), - orgId: 'acme', - actor: { type: 'system', id: 'system:intake' } + actor: { type: 'automation', id: 'system:intake', serviceId: 'intake' } }) await engine.transition({ aggregateId: aggregateId('EXP-2026-001'), transitionId: transitionId('start_review'), - actor: { type: 'automation', id: 'system:router' } + actor: { type: 'automation', id: 'system:router', serviceId: 'router' } }) await engine.transition({ aggregateId: aggregateId('EXP-2026-001'), transitionId: transitionId('approve'), - actor: { type: 'human', id: 'manager@acme.com' }, + actor: { type: 'human', id: 'manager@acme.com', userId: 'manager-1', displayName: 'Manager' }, evidence: { comment: 'Approved for Q1 budget' } }) @@ -91,7 +90,7 @@ console.log(state?.status) // CLOSED - The loop moved through 3 states (`SUBMITTED -> UNDER_REVIEW -> APPROVED`) -- 3 actor types were used (`system`, `automation`, `human`) +- 2 actor types were used (`automation`, `human`) - Each successful transition emitted `loop.transition.executed` - Reaching a terminal state sets loop status to `CLOSED` diff --git a/content/docs/integrations/http.mdx b/content/docs/integrations/http.mdx index 8ff5422..e4a307c 100644 --- a/content/docs/integrations/http.mdx +++ b/content/docs/integrations/http.mdx @@ -11,7 +11,7 @@ section: Integrations ## What it does -Implements `LoopStorageAdapter` over HTTP. Any REST service that speaks the adapter protocol can back loop state storage. This is useful for integrating with existing services or remote storage planes. +Implements `LoopStore` over HTTP. Any REST service that speaks the adapter protocol can back loop state storage. This is useful for integrating with existing services or remote storage planes. ## Adapter protocol @@ -51,7 +51,7 @@ Expected backend endpoints: ## Note -If your backend is in the same process, implementing `LoopStorageAdapter` directly is usually simpler than standing up the HTTP protocol. +If your backend is in the same process, implementing `LoopStore` directly is usually simpler than standing up the HTTP protocol. ## Configuration reference diff --git a/content/docs/integrations/index.mdx b/content/docs/integrations/index.mdx index 88fa42a..b8fa13a 100644 --- a/content/docs/integrations/index.mdx +++ b/content/docs/integrations/index.mdx @@ -8,7 +8,7 @@ Connect Loop Engine to any AI provider, agentic platform, storage backend, or ob ## ⟩ AI Providers -Use any major LLM as a governed actor. Provider actor adapters produce the same `AIAgentActor` shape — switching providers requires changing one import. Perplexity Sonar covers **grounded retrieval with citations** as an `LLMAdapter` for research steps (see the package reference). +Use any major LLM as a governed actor. Provider actor adapters produce the same `AIAgentActor` shape — switching providers requires changing one import. Perplexity Sonar covers **grounded retrieval with citations** as a `ToolAdapter` for research steps (see the package reference).
` | `{}` | Optional action-to-signal mapping override | | `defaultActor` | `Actor` | `ai-agent/openclaw` | Fallback actor attribution metadata | | `onProposal` | `(proposal) => Promise` | — | Optional hook before transition submission | diff --git a/content/docs/integrations/perplexity.mdx b/content/docs/integrations/perplexity.mdx index 7454a75..da76e6c 100644 --- a/content/docs/integrations/perplexity.mdx +++ b/content/docs/integrations/perplexity.mdx @@ -18,7 +18,7 @@ section: Integrations ## What it does -`@loop-engine/adapter-perplexity` implements `LLMAdapter.invoke()` against the Sonar chat API. You get answer text plus structured citations for audit and evidence attachment. Use it where provenance matters more than open-ended generation. +`@loop-engine/adapter-perplexity` implements `ToolAdapter.invoke()` against the Sonar chat API. You get answer text plus structured citations for audit and evidence attachment. Use it where provenance matters more than open-ended generation. ## When to use it diff --git a/content/docs/integrations/postgres.mdx b/content/docs/integrations/postgres.mdx index 373f4a9..73829c6 100644 --- a/content/docs/integrations/postgres.mdx +++ b/content/docs/integrations/postgres.mdx @@ -11,7 +11,7 @@ section: Integrations ## What it does -Swaps the in-memory adapter for PostgreSQL persistence. Loop state, transitions, and events are stored in Postgres through the same `LoopStorageAdapter` interface, so loop definitions and business logic remain unchanged. +Swaps the in-memory adapter for PostgreSQL persistence. Loop state, transitions, and events are stored in Postgres through the same `LoopStore` interface, so loop definitions and business logic remain unchanged. ## Quick setup diff --git a/content/docs/packages/actors.mdx b/content/docs/packages/actors.mdx index 797f908..ddcd26f 100644 --- a/content/docs/packages/actors.mdx +++ b/content/docs/packages/actors.mdx @@ -17,28 +17,24 @@ npm install @loop-engine/actors ```ts interface HumanActor extends ActorRef { type: "human" - sessionId: string + userId: string + displayName: string + roles?: string[] } interface AutomationActor extends ActorRef { type: "automation" serviceId: string + version?: string } interface AIAgentActor extends ActorRef { type: "ai-agent" - agentId: string - gatewaySessionId: string - recommendedBy?: string -} - -interface WebhookActor extends ActorRef { - type: "webhook" - source: string -} - -interface SystemActor extends ActorRef { - type: "system" + modelId: string + provider: string + confidence?: number + promptHash?: string + toolsUsed?: string[] } ``` diff --git a/content/docs/packages/adapter-perplexity.mdx b/content/docs/packages/adapter-perplexity.mdx index ed58ca8..dff1b70 100644 --- a/content/docs/packages/adapter-perplexity.mdx +++ b/content/docs/packages/adapter-perplexity.mdx @@ -10,7 +10,7 @@ section: "packages" ## Overview -`@loop-engine/adapter-perplexity` wraps the Perplexity Sonar chat API as a Loop Engine `LLMAdapter`. Sonar adds grounded web retrieval with cited sources. You use it for Loop steps that need real-time, verifiable information — regulatory lookups, compliance research, supplier or market news. It is not a general-purpose generation adapter; for broad LLM actor flows, use [Anthropic](/docs/packages/adapter-anthropic) or [OpenAI](/docs/packages/adapter-openai) actor adapters. +`@loop-engine/adapter-perplexity` wraps the Perplexity Sonar chat API as a Loop Engine `ToolAdapter`. Sonar adds grounded web retrieval with cited sources. You use it for Loop steps that need real-time, verifiable information — regulatory lookups, compliance research, supplier or market news. It is not a general-purpose generation adapter; for broad LLM actor flows, use [Anthropic](/docs/packages/adapter-anthropic) or [OpenAI](/docs/packages/adapter-openai) actor adapters. ## Installation diff --git a/content/docs/packages/core.mdx b/content/docs/packages/core.mdx index e9418f3..a98cc38 100644 --- a/content/docs/packages/core.mdx +++ b/content/docs/packages/core.mdx @@ -99,12 +99,11 @@ export interface ActorRef { export interface LoopInstance { loopId: LoopId aggregateId: AggregateId - orgId: string currentState: StateId status: LoopStatus startedAt: string closedAt?: string - correlationId: CorrelationId + correlationId?: CorrelationId metadata?: Record } diff --git a/content/docs/packages/dsl.mdx b/content/docs/packages/dsl.mdx index f18820f..df5a6d2 100644 --- a/content/docs/packages/dsl.mdx +++ b/content/docs/packages/dsl.mdx @@ -29,7 +29,7 @@ Builder methods implemented in source: - `.build(): LoopDefinition` -`guard()`, `signal()`, and `actor()` are not LoopBuilder methods in `v0.1.0`. Guards are added inside `.transition({ guards: [...] })`. +`guard()`, `signal()`, and `actor()` are slated for `1.1.0+` as experimental builder methods and are not implemented in the current release. Guards are added today inside `.transition({ guards: [...] })`. ## Fluent example @@ -128,9 +128,9 @@ See `/docs/defining-loops/yaml-format` for the full format reference. ```ts import { parseLoopJson, - parseLoopFile, - serializeToJson, - serializeToYaml, + parseLoopYaml, + serializeLoopJson, + serializeLoopYaml, validateLoopDefinition } from "@loop-engine/dsl" ``` @@ -138,7 +138,18 @@ import { Validation signature: ```ts -validateLoopDefinition(input: unknown): { valid: boolean; errors: string[]; definition?: LoopDefinition } +validateLoopDefinition(definition: LoopDefinition): ValidationResult + +interface ValidationResult { + valid: boolean + errors: ValidationError[] +} + +interface ValidationError { + code: string + message: string + path?: string +} ``` Validation failure example: @@ -146,6 +157,12 @@ Validation failure example: ```ts { valid: false, - errors: ["initialState: initialState must exist in states"] + errors: [ + { + code: "INVALID_INITIAL_STATE", + message: 'initialState "DRAFT" does not exist in states', + path: "initialState" + } + ] } ``` diff --git a/content/docs/packages/events.mdx b/content/docs/packages/events.mdx index 59543ae..f150fc7 100644 --- a/content/docs/packages/events.mdx +++ b/content/docs/packages/events.mdx @@ -14,29 +14,28 @@ npm install @loop-engine/events ## Event catalog -The exported `LOOP_EVENT_TYPES` constants map to: +The exported `LOOP_EVENT_TYPES` constant enumerates the nine canonical lifecycle events: - `loop.started` +- `loop.completed` +- `loop.cancelled` +- `loop.failed` - `loop.transition.requested` - `loop.transition.executed` - `loop.transition.blocked` - `loop.guard.failed` -- `loop.completed` -- `loop.error` -- `loop.spawned` - `loop.signal.received` -- `loop.outcome.recorded` Every event extends: ```ts interface LoopEventBase { eventId: string + type: string loopId: LoopId aggregateId: AggregateId - orgId: string occurredAt: string - correlationId: CorrelationId + correlationId?: string causationId?: string } ``` @@ -70,9 +69,10 @@ unsubscribe() ```ts extractLearningSignal( completed: LoopCompletedEvent, - history: TransitionRecord[], - predicted?: Record + history: LoopTransitionExecutedEvent[], + definition: LoopDefinitionLike, + predicted?: Record ): LearningSignal ``` -This helper derives `actual`, `predicted`, and numeric `delta` fields from completion history. +The helper derives `actual`, `predicted`, and numeric `delta` fields from the completed event, the executed-transition history, and the loop definition's declared business metrics. `predicted` keys not declared in `definition.outcome.businessMetrics` are dropped with a warning. diff --git a/content/docs/packages/guards.mdx b/content/docs/packages/guards.mdx index 3e111a6..66b1b23 100644 --- a/content/docs/packages/guards.mdx +++ b/content/docs/packages/guards.mdx @@ -31,40 +31,37 @@ interface GuardResult { ```ts class GuardRegistry { - register(guardId: GuardId, fn: GuardFunction): void - get(guardId: GuardId): GuardFunction | undefined - createEvaluator(): GuardEvaluator + register(guardId: string, evaluator: GuardEvaluator): void + get(guardId: string): GuardEvaluator | undefined + registerBuiltIns(): void } ``` +`registerBuiltIns()` populates the registry with every guard exported from `@loop-engine/guards` (see "Built-in guards" below). Call it once on a fresh registry, or use the pre-populated `defaultRegistry` constant. + ```ts import { createGuardRegistry } from "@loop-engine/guards" import { guardId } from "@loop-engine/core" const registry = createGuardRegistry() -registry.register(guardId("budget_available"), async (context) => ({ - passed: context.evidence.budget_ok === true, - message: "Budget check failed" -})) +registry.register(guardId("budget_available"), { + async evaluate(context) { + return { + passed: context.evidence?.budget_ok === true, + message: "Budget check failed" + } + } +}) ``` ## Built-in guards -`defaultRegistry` pre-registers: - -- `actor_has_permission` (`actorPermissionGuard`) -- `approval_obtained` (`approvalObtainedGuard`) -- `deadline_not_exceeded` (`deadlineNotExceededGuard`) -- `duplicate_check_passed` (`duplicateCheckPassedGuard`) -- `field_value_constraint` (`fieldValueConstraintGuard`) - -Built-in evidence keys: +`defaultRegistry` (and `GuardRegistry.registerBuiltIns()`) pre-registers: -- `approval_obtained` reads `evidence.approved === true` -- `actor_has_permission` reads `required_role` and `roles` -- `deadline_not_exceeded` reads `deadline_iso` -- `duplicate_check_passed` reads `duplicate_found` -- `field_value_constraint` reads `constraint` with operators `eq | gt | lt | in` +- `confidence-threshold` (`ConfidenceThresholdGuard`) +- `human-only` (`HumanOnlyGuard`) +- `evidence-required` (`EvidenceRequiredGuard`) +- `cooldown` (`CooldownGuard`) ## Hard and soft behavior diff --git a/content/docs/packages/index.mdx b/content/docs/packages/index.mdx index 4b5f303..0a9b643 100644 --- a/content/docs/packages/index.mdx +++ b/content/docs/packages/index.mdx @@ -11,12 +11,12 @@ Every additional package is opt-in based on your stack. | Layer | What it is | Required | Current packages | Planned packages | | --- | --- | --- | --- | --- | -| Core | Canonical types and runtime execution. Everything else depends on these. | always | sdk, runtime | Internal (not published): core, dsl, guards, actors, events, signals | +| Core | Canonical types and runtime execution. Everything else depends on these. | always | sdk, runtime, core, dsl, guards, actors, events, signals | — | | Stores | Pluggable persistence for loop state. Swap without changing loop logic. Default to adapter-memory in dev. | pick one | adapter-memory, adapter-postgres, adapter-kafka | adapter-redis, adapter-sqlite, adapter-dynamodb | | AI adapters | Governed LLM actors with confidence scoring, prompt attribution, and hard guard enforcement at runtime; Sonar for grounded retrieval with citations. | pick one per LLM / use-case | adapter-anthropic, adapter-openai, adapter-gemini, adapter-grok, adapter-perplexity | adapter-ollama, adapter-cohere, adapter-mistral | | Routing adapters | Consume PENDING_HUMAN_APPROVAL events and route them to where the human already lives. | pick one for human approval delivery | adapter-pagerduty, adapter-openclaw, adapter-vercel-ai | adapter-slack, adapter-teams, adapter-discord, adapter-webhook | -| Framework adapters | Drop Loop Engine into your existing tool-call layer with zero rebuild. requiresApproval() gates wrap any tool call structurally. | optional | adapter-vercel-ai, adapter-openclaw | adapter-n8n, adapter-temporal, adapter-langchain | -| Observability | Metrics, event timelines, replay, and devtools. Consumes the event bus — no changes to loop definitions needed. | optional | observability, ui-devtools, playground, registry-client (loop catalog) | adapter-datadog, adapter-grafana | +| Framework adapters | Drop Loop Engine into your existing tool-call layer with zero rebuild. requiresApproval() gates wrap any tool call structurally. | optional | adapter-vercel-ai, adapter-openclaw, adapter-commerce-gateway | adapter-n8n, adapter-temporal, adapter-langchain | +| Observability | Metrics, event timelines, replay, and devtools. Consumes the event bus — no changes to loop definitions needed. | optional | observability, ui-devtools, registry-client (loop catalog) | adapter-datadog, adapter-grafana | | Verticals | Pre-assembled, domain-specific loop definitions for regulated industries. An enterprise installs one package instead of assembling manually. | optional | — (in development) | loops-healthcare, loops-fintech, loops-supply-chain, loops-hr-ops, loops-legaltech, loops-construction | ## Minimum install @@ -27,7 +27,7 @@ npm install @loop-engine/sdk This is always the floor. Add layers as your stack demands them. -Core primitives are bundled in `@loop-engine/sdk`. The internal packages are available in the monorepo for contributors. +Core primitives are bundled in `@loop-engine/sdk`. They are also published as standalone `@loop-engine/*` packages for consumers that want to install only the layers they need. ## Common install recipes diff --git a/content/docs/packages/observability.mdx b/content/docs/packages/observability.mdx index ba42f2e..05434c0 100644 --- a/content/docs/packages/observability.mdx +++ b/content/docs/packages/observability.mdx @@ -22,7 +22,7 @@ computeMetrics( ): LoopMetrics ``` -`LoopMetrics` includes `completionRate`, `avgDurationMs`, `medianDurationMs`, `p95DurationMs`, `aiActorRate`, `humanActorRate`, and `avgTransitionCount`. +`LoopMetrics` includes `loopId`, `period`, `totalInstances`, `openInstances`, `closedInstances`, `errorInstances`, `avgDurationMs`, `medianDurationMs`, `p95DurationMs`, `completionRate`, `guardFailureRate`, `aiActorRate`, `humanActorRate`, and `avgTransitionCount`. ```ts import { computeMetrics } from "@loop-engine/observability" diff --git a/content/docs/packages/registry-client.mdx b/content/docs/packages/registry-client.mdx index 8daf8f8..5f2c9e4 100644 --- a/content/docs/packages/registry-client.mdx +++ b/content/docs/packages/registry-client.mdx @@ -86,3 +86,39 @@ interface LoopRegistry { ## SDK integration `createLoopSystem({ loops, registry })` merges **catalog** results with local loops. Local `loops[]` definitions override matching catalog IDs, and catalog load failures fall back to local-only startup. + +## Error classes + +The package exports three typed errors. Catch by class to distinguish missing definitions from conflicts and from network/IO failures. + +```ts +import { + RegistryNotFoundError, + RegistryConflictError, + RegistryNetworkError +} from "@loop-engine/registry-client" +``` + +### RegistryNotFoundError + +Thrown by `get()` and `getVersion()` when a loop (or specific version) is not found. Exposes `loopId` and optional `version`. + +### RegistryConflictError + +Thrown by `register()` when a loop with the same `loopId@version` already exists. Pass `{ force: true }` (development only) to overwrite. Exposes `loopId` and `version`. + +### RegistryNetworkError + +Thrown by `httpRegistry` and the Better Data adapter on network or HTTP failures. Exposes `url`, optional `statusCode`, and the underlying `cause` when available. + +```ts +try { + await registry.register(definition) +} catch (error) { + if (error instanceof RegistryConflictError) { + console.warn(`Already registered: ${error.loopId}@${error.version}`) + return + } + throw error +} +``` diff --git a/content/docs/packages/runtime.mdx b/content/docs/packages/runtime.mdx index 6b432d1..98bf090 100644 --- a/content/docs/packages/runtime.mdx +++ b/content/docs/packages/runtime.mdx @@ -39,7 +39,7 @@ start(options: StartOptions): Promise transition(options: TransitionOptions): Promise getState(aggregateId: AggregateId): Promise getHistory(aggregateId: AggregateId): Promise -listOpen(loopId: string, orgId: string): Promise +listOpen(loopId: string): Promise registerSideEffectHandler(sideEffectId: string, handler: SideEffectHandler): void ``` @@ -71,7 +71,7 @@ interface LoopStore { saveInstance(instance: LoopInstance): Promise getTransitionHistory(aggregateId: AggregateId): Promise saveTransitionRecord(record: TransitionRecord): Promise - listOpenInstances(loopId: LoopId, orgId: string): Promise + listOpenInstances(loopId: LoopId): Promise } interface EventBus { diff --git a/content/docs/packages/sdk.mdx b/content/docs/packages/sdk.mdx index 85fc43e..9401e11 100644 --- a/content/docs/packages/sdk.mdx +++ b/content/docs/packages/sdk.mdx @@ -17,7 +17,7 @@ npm install @loop-engine/sdk Source-verified exports include: - `LoopBuilder` -- `createLoopSystem` and `createLoopEngine` +- `createLoopSystem` (auto-wired aggregate; the runtime factory `createLoopEngine` lives in `@loop-engine/runtime`) - `guardRegistry` and `createSignalEngine` - `InMemoryEventBus` - `computeMetrics` and `buildTimeline` diff --git a/content/docs/packages/signals.mdx b/content/docs/packages/signals.mdx index 923dbfc..234feaa 100644 --- a/content/docs/packages/signals.mdx +++ b/content/docs/packages/signals.mdx @@ -1,10 +1,10 @@ --- title: "@loop-engine/signals" -description: SignalEngine evaluates event streams with rule functions and emits Signal records for detection workflows. +description: SignalRegistry is the current signal-spec surface; SignalEngine pattern detection lands in 1.1.0 as experimental. section: Packages --- -`@loop-engine/signals` detects patterns from `LoopEvent[]` and emits typed `Signal` objects. +`@loop-engine/signals` ships `SignalRegistry` for declaring and validating signal specs. The pattern-detection engine (`SignalEngine`) is on the roadmap for `1.1.0+`. ## Install @@ -12,47 +12,73 @@ section: Packages npm install @loop-engine/signals ``` -## Engine API +## SignalRegistry + +The current public surface is a registry that declares each signal's identity, optional Zod schema, and human-readable metadata. Runtime code uses it to look up and validate signal payloads. ```ts -interface SignalEngine { - registerRule(rule: SignalRule): void - process(events: LoopEvent[]): Signal[] - subscribe(handler: (signal: Signal) => void): () => void +class SignalRegistry { + register(spec: SignalSpec): void + get(signalId: SignalId): SignalSpec | undefined + validatePayload(signalId: SignalId, payload: unknown): { valid: boolean; error?: string } + list(): SignalSpec[] } -``` -Factory: +interface SignalSpec { + signalId: SignalId + name: string + description?: string + schema?: ZodType + tags?: string[] +} +``` ```ts -createSignalEngine(): SignalEngine -``` +import { z } from "zod" +import { SignalRegistry } from "@loop-engine/signals" +import { signalId } from "@loop-engine/core" + +const registry = new SignalRegistry() +registry.register({ + signalId: signalId("expense.submitted"), + name: "Expense submitted", + description: "A user has submitted an expense report for review.", + schema: z.object({ + amount: z.number().positive(), + currency: z.string().length(3) + }) +}) -`createSignalEngine()` pre-registers: +const check = registry.validatePayload(signalId("expense.submitted"), { + amount: 1200, + currency: "USD" +}) +// => { valid: true } +``` -- `threshold-breach` -- `state-dwell` -- `repeated-guard-failure` -- `loop-not-started` +## SignalEngine (experimental, 1.1.0+) -## Built-in rule factories + +`SignalEngine`, `createSignalEngine()`, and the built-in rule factories listed below are slated for `1.1.0+` as experimental APIs and are **not** present in the current release. The shapes shown here are roadmap intent, not a contract — expect them to change before they ship. + ```ts -thresholdBreachRule(config: ThresholdRuleConfig): SignalRule -stateDwellRule(config: StateDwellRuleConfig): SignalRule -repeatedGuardFailureRule(config: RepeatedGuardFailureConfig): SignalRule -loopNotStartedRule(config: LoopNotStartedConfig): SignalRule +interface SignalEngine { + registerRule(rule: SignalRule): void + process(events: LoopEvent[]): Signal[] + subscribe(handler: (signal: Signal) => void): () => void +} + +createSignalEngine(): SignalEngine ``` -```ts -import { createSignalEngine } from "@loop-engine/signals" +Planned built-in rule factories: -const signalEngine = createSignalEngine() -signalEngine.subscribe((signal) => { - console.log(signal.type, signal.subject, signal.confidence) -}) -``` +- `thresholdBreachRule(config: ThresholdRuleConfig): SignalRule` +- `stateDwellRule(config: StateDwellRuleConfig): SignalRule` +- `repeatedGuardFailureRule(config: RepeatedGuardFailureConfig): SignalRule` +- `loopNotStartedRule(config: LoopNotStartedConfig): SignalRule` -## Detection flow +## Detection model -Signals are detections, not actors. Application code decides what to do with them, including whether to start or transition loops. +Signals are detections, not actors. Application code decides what to do with them — including whether to start or transition loops in response. diff --git a/content/docs/running-loops/event-subscriptions.mdx b/content/docs/running-loops/event-subscriptions.mdx index 01d0fbd..8e6a661 100644 --- a/content/docs/running-loops/event-subscriptions.mdx +++ b/content/docs/running-loops/event-subscriptions.mdx @@ -16,15 +16,14 @@ subscribe(handler: (event: LoopEvent) => Promise): () => void ## Event types - `loop.started` +- `loop.completed` +- `loop.cancelled` +- `loop.failed` - `loop.transition.requested` - `loop.transition.executed` - `loop.transition.blocked` - `loop.guard.failed` -- `loop.completed` -- `loop.error` -- `loop.spawned` - `loop.signal.received` -- `loop.outcome.recorded` ## Subscribe patterns @@ -37,7 +36,7 @@ eventBus.subscribe(async (event) => { ```ts eventBus.subscribe(async (event) => { if (event.type === 'loop.completed') { - console.log('Closed:', event.outcomeId, `${event.durationMs}ms`) + console.log('Closed:', event.finalState, `${event.durationMs}ms`) } }) ```