octaflow 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,6 +202,7 @@ next to what your steps actually do.
202
202
  | 💤 | **Durable sleep** — hold a step in the queue for N ms (survives restarts) |
203
203
  | 🔀 | **Conditional branching** — `when` guards skip a step and its branch; `join: 'any'` converges |
204
204
  | 🎯 | **Decisions** — typed choices with a confidence from a swappable decider (a function, a model, your own); below a threshold a person settles it |
205
+ | 🧭 | **Case loops** — `defineCaseLoop` (`octaflow/ai`): a manager agent chooses every next move of a case, each move its own workflow ending in the same tail (result → decide → next); policy refuses a choice with its reason inside the exit's schema, a move with a step that could fail is refused at definition time, and a manager that cannot decide closes the case for a person |
205
206
  | ⏳ | **Deadlines** — a budget on a suspended step (fail, or continue with a stand-in answer) and on a whole run |
206
207
  | ♻️ | **Retry a failed run** — `retryWorkflow` resumes from the failure point; completed steps keep their output |
207
208
  | 🧹 | **Delete the runs about a record** — `purgeWorkflows({ entityRef })` cancels and deletes them with their sub-workflow runs and steps, for an erasure or a deleted record; refuses only while a step is executing |
@@ -215,7 +216,9 @@ next to what your steps actually do.
215
216
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
216
217
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
217
218
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
218
- | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, a `usage` sink that books each step into `@octabits-io/metering` (the paid APIs around the model in the same table, priced at read time), refusal-aware models, lenient JSON from local models (`withJsonRepair`), a vendor's refusal explained by key and hint (`explainProviderError`), a per-run circuit breaker, a connection test (`testProvider`) |
219
+ | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, a `usage` sink that books each step into `@octabits-io/metering` (the paid APIs around the model in the same table, priced at read time), refusal-aware models, lenient JSON from local models (`withJsonRepair`), a vendor's refusal explained by key and hint (`explainProviderError`), a per-run circuit breaker, a connection test (`testProvider`), Ollama's native API as language and embedding models (`createOllama` in `octaflow/ai/providers`: `num_ctx`, thinking and `keep_alive` per model or per call, no dependency) |
220
+ | 🗂️ | **Model providers an administrator manages** — `createModelProviders` (`octaflow/ai`): providers as rows, each with its kind, server, region, a key that crosses a cipher seam, and its models with declared capabilities (temperature, JSON mode, vision, video, tools, embedding); `createPgModelProviderStore` + `modelProviderDdl` (`octaflow/postgres`), `modelProviderColumns` (`octaflow/drizzle`); `createModelResolver` (`octaflow/ai/providers`) builds the AI SDK model from a row, shapes each call to what the model declares, hands the host its `destination` for the disclosure guard, and rebuilds after a write |
221
+ | 🏃 | **Model runners** — a model the app cannot reach, served by a process that connects *out*: a provider row of kind `runner` carries tags instead of a URL, a runner registered with a token (`octaflow/ai/runner`: `createModelRunnerHub`, shown once, kept as a hash) subscribes with its tags, claims the calls by long-poll through `@octabits-io/server/fetch/octaflow`'s `createModelRunnerHandler` and posts the answers back; the mailbox is two tables (`createPgModelRunnerStore` + `modelRunnerDdl`, `modelRunnerColumns` + `modelRunnerRequestColumns`) whose rows live only while a call is in flight; nobody online fails the call at once, so the breaker trips and the probe answers in milliseconds |
219
222
  | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
220
223
 
221
224
  ---
@@ -248,9 +251,10 @@ pnpm add ai @ai-sdk/provider
248
251
 
249
252
  ### The Postgres tables
250
253
 
251
- `octaflow/postgres` owns five tables and ships their DDL: `flowStoreDdl()` (`flow_workflow`,
252
- `flow_workflow_step`), `flowEventDdl()` (`flow_step_event`) and `flowGateDdl()`
253
- (`flow_rate_bucket`, `flow_step_lease`). Each is idempotent `CREATE TABLE IF NOT EXISTS`, takes
254
+ `octaflow/postgres` owns eight tables and ships their DDL: `flowStoreDdl()` (`flow_workflow`,
255
+ `flow_workflow_step`), `flowEventDdl()` (`flow_step_event`), `flowGateDdl()`
256
+ (`flow_rate_bucket`, `flow_step_lease`), `modelProviderDdl()` (`model_provider`) and
257
+ `modelRunnerDdl()` (`model_runner`, `model_runner_request`). Each is idempotent `CREATE TABLE IF NOT EXISTS`, takes
254
258
  a `schema`, and is meant to be pasted into your own migration system; `applySchema(pool, ddl)`
255
259
  is the dev and test shortcut.
256
260
 
@@ -268,8 +272,9 @@ column set, so a host on Drizzle declares it in its own schema and drizzle-kit o
268
272
  migration. Where the store's queries are one SQL statement by nature — every one of octaflow's
269
273
  is — `./drizzle` ships the columns only, and the store stays the SQL one. So
270
274
  `octaflow/drizzle` exports `flowWorkflowColumns`, `flowWorkflowStepColumns`,
271
- `flowStepEventColumns`, `flowRateBucketColumns` and `flowStepLeaseColumns` (`drizzle-orm` an
272
- optional peer) and nothing that runs a query. The foreign keys, the composite keys, the unique
275
+ `flowStepEventColumns`, `flowRateBucketColumns`, `flowStepLeaseColumns`, `modelProviderColumns`,
276
+ `modelRunnerColumns` and `modelRunnerRequestColumns` (`drizzle-orm` an optional peer) and
277
+ nothing that runs a query. The foreign keys, the composite keys, the unique
273
278
  constraint and the indexes are yours to declare on the `pgTable`; each column set's doc lists
274
279
  the ones its DDL creates, and two of them the store relies on:
275
280
 
@@ -313,6 +318,56 @@ The store stays plain SQL, bound to the `schema` you hand `createPgWorkflowStore
313
318
  declare the tables under the same schema and names. A test in the package holds every column
314
319
  set and its DDL equal, column for column: a column added to one side alone fails the build.
315
320
 
321
+ ### Model runners — a model that connects out
322
+
323
+ A model the app cannot reach: a laptop with the model loaded for a demo, a GPU box behind
324
+ someone's firewall, a machine that is only on when it is needed. The runner never opens a
325
+ port. It connects *out* to the app with a token, subscribes with its tags, claims the model
326
+ calls that carry those tags and posts the answers back — the shape of a self-hosted CI
327
+ runner. The mailbox is Postgres, because the call runs in the worker and the door the runner
328
+ knocks on runs in the API; a request row lives only while the call is in flight.
329
+
330
+ ```ts
331
+ // the app — once, beside the providers
332
+ import { createModelRunnerHub } from 'octaflow/ai/runner';
333
+ import { createPgModelRunnerStore } from 'octaflow/postgres';
334
+
335
+ const runners = createModelRunnerHub({ store: createPgModelRunnerStore({ executor }) });
336
+ const resolver = createModelResolver({ providers, runners, wrap });
337
+
338
+ // an administrator: the provider row carries tags, the runner gets its token once
339
+ await providers.save({ id: 'mac', kind: 'runner', label: 'The Mac', region: 'self-hosted', tags: ['qwen'], models: [{ id: 'qwen3:32b', settings: { numCtx: 16_384 } }] });
340
+ const { token } = (await runners.registerRunner({ id: 'dani-mac', label: "Dani's Mac", tags: ['qwen', 'mac'] })).value;
341
+
342
+ // the API: the door, behind the host's rate limit
343
+ import { createModelRunnerHandler } from '@octabits-io/server/fetch/octaflow';
344
+ export const POST = compose(withErrorMapping())(createModelRunnerHandler({ prefix: '/api/model-runner', hub: runners }));
345
+
346
+ // the runner, on the machine with the model
347
+ import { createHttpRunnerClient, runModelRunner } from 'octaflow/ai/runner';
348
+ import { createOllama } from 'octaflow/ai/providers';
349
+ await runModelRunner({
350
+ client: createHttpRunnerClient({ url: 'https://app.example/api/model-runner', token }),
351
+ backend: createOllama({ baseUrl: 'http://localhost:11434' }),
352
+ });
353
+ ```
354
+
355
+ A runner may claim a request when it has **every tag** of the request — the provider row's
356
+ tags — as a CI job's labels must all be on the runner. A call with nobody online for its
357
+ tags fails at once with `provider_unreachable`, so `withCircuitBreaker` trips and
358
+ `testProvider` reports it in milliseconds instead of a timeout; whether another provider is
359
+ tried is the host's resolver's choice, through the same disclosure gate. A claim lasts 30 s
360
+ without a heartbeat; an expired claim is requeued once and failed the second time. The
361
+ region is the administrator's declaration about where runners with these tags are, as for
362
+ every provider, and the disclosure gate reads it the same way.
363
+
364
+ The wire is the AI SDK's own call and answer as JSON — binary file data as base64, a URL
365
+ restored on the other side — so the runner's backend is any `LanguageModelV4`: `createOllama`
366
+ from this package, or `@ai-sdk/openai-compatible` for mlx_lm.server, vLLM and llama.cpp. The
367
+ first version answers whole: `streamText` over a runner model works, emitted as one text
368
+ block when the answer is there. Files reach the runner as bytes, since the runner model
369
+ declares no URL support and the SDK downloads on the app's side. Embeddings go the same way.
370
+
316
371
  ---
317
372
 
318
373
  ---
@@ -1,8 +1,9 @@
1
- import { M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, St as Result, U as RetryPolicy, f as Decider, gt as JoinRule, j as TypedStep, xt as FlowErrorShape } from "../index-D0XhoTkE.js";
1
+ import { M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, St as Result, U as RetryPolicy, f as Decider, gt as JoinRule, j as TypedStep, xt as FlowErrorShape } from "../index-DZUO24Rz.js";
2
2
  import { n as Logger } from "../logger-BCus3AOa.js";
3
- import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-C46TPBEg.js";
3
+ import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-DHomzpiq.js";
4
4
  import { C as isSubprocessorResidency, S as embeddingDimensionsFor, _ as EmbeddingProviderMeta, a as AiProcessingRegion, b as OpenAiCompatibleProviderId, c as AiProviderModelOption, d as AiVendorResidency, f as DEFAULT_AI_PROVIDER, g as EmbeddingProviderId, h as EMBEDDING_PROVIDER_IDS, i as AiEndpointResidency, l as AiResidencyError, m as EMBEDDING_PROVIDERS, n as AI_PROVIDER_IDS, o as AiProviderId, p as DEFAULT_EMBEDDING_PROVIDER, r as AiEndpointRegion, s as AiProviderMeta, t as AI_PROVIDERS, u as AiResidencyResult, v as OPENAI_COMPATIBLE_PROVIDER, w as resolveEndpointResidency, x as OpenAiCompatibleProviderMeta, y as OPENAI_COMPATIBLE_PROVIDER_ID } from "../provider-registry-C_mOfdHH.js";
5
- import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, b as PROVIDER_ERROR_REDACTED, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, g as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as ProbeableProvider, p as CircuitBreakerOptions, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as ProviderProbeFailure, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../provider-probe-_rio4ykx.js";
5
+ import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, b as PROVIDER_ERROR_REDACTED, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, g as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as ProbeableProvider, p as CircuitBreakerOptions, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as ProviderProbeFailure, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../provider-probe-BDg4c1Rk.js";
6
+ import { C as modelProviderInputSchema, S as modelEntrySchema, _ as StoredModelProvider, a as ModelCapabilities, b as maskModelProvider, c as ModelProvider, d as ModelProviderError, f as ModelProviderInput, g as ModelProviders, h as ModelProviderStore, i as MODEL_PROVIDER_REGIONS, l as ModelProviderChange, m as ModelProviderRegion, n as MODEL_JSON_MODES, o as ModelEntry, p as ModelProviderKind, r as MODEL_PROVIDER_KINDS, s as ModelJsonMode, t as CreateModelProvidersOptions, u as ModelProviderCipher, v as createInMemoryModelProviderStore, x as modelCapabilitiesSchema, y as createModelProviders } from "../model-providers-DQU57vAf.js";
6
7
  import { z } from "zod";
7
8
  import { ToolSet } from "ai";
8
9
  import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4Usage, SharedV4ProviderMetadata } from "@ai-sdk/provider";
@@ -866,4 +867,152 @@ export declare function withJsonRepair(model: LanguageModelV4, options?: JsonRep
866
867
  */
867
868
  export declare function addUsage(a: LanguageModelV4Usage, b: LanguageModelV4Usage): LanguageModelV4Usage;
868
869
  //#endregion
869
- export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, CircuitBreaker, CircuitBreakerOptions, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, ExplainProviderErrorOptions, FATAL_PROVIDER_ERROR_KEYS, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, OllamaProbeOptions, OllamaProbeReport, OpenAiCompatibleProviderId, OpenAiCompatibleProviderMeta, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProbeableProvider, ProviderError, ProviderErrorExplanation, ProviderErrorKey, ProviderErrorPhrase, ProviderErrorPreset, ProviderProbeFailure, ProviderProbeOptions, ProviderProbeReport, createCircuitBreaker, embeddingDimensionsFor, explainProviderError, isFatalProviderError, isProviderError, isSubprocessorResidency, maskKeys, resolveEndpointResidency, testOllamaModel, testProvider, withCircuitBreaker };
870
+ //#region src/ai/case-loop.d.ts
871
+ /** What a move leaves for the manager: a summary, and an outcome when the move itself ends the case. */
872
+ export type CaseMoveResult<TOutcome extends string, TResult extends Record<string, unknown>> = TResult & {
873
+ summary: string;
874
+ closes?: TOutcome;
875
+ };
876
+ /** One finished move, as the next moves see it. */
877
+ export interface CaseMoveRecord<TMove extends string, TOutcome extends string, TResult extends Record<string, unknown>> {
878
+ no: number;
879
+ move: TMove;
880
+ /** The manager's reason for it — its exit's `why`, when the exit has one. */
881
+ why: string;
882
+ result: CaseMoveResult<TOutcome, TResult>;
883
+ }
884
+ /** A move's input: the case's own fields, plus where the case stands. */
885
+ export type CaseMoveInput<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>> = TCase & {
886
+ caseId: string;
887
+ move: TMove;
888
+ /** 1 for the first move. */
889
+ moveNo: number;
890
+ /** The manager's arguments for this move — its exit's value. */
891
+ brief: Record<string, any>;
892
+ moves: CaseMoveRecord<TMove, TOutcome, TResult>[];
893
+ startedAt: string;
894
+ };
895
+ /** The manager's choice, as `decide` outputs it. `stuck` when it could not choose within its limits. */
896
+ export type CaseDecision<TExit extends string = string> = {
897
+ exit: TExit | 'stuck';
898
+ value: Record<string, any>;
899
+ /** Choices the policy refused on the way, each with its reason, de-duplicated. */
900
+ refusals: Array<{
901
+ choice: string;
902
+ reason: string;
903
+ }>;
904
+ trace: AgentTrace;
905
+ };
906
+ /** What `next` did: the case's outcome, or `continued` with the run it started. */
907
+ export type CaseNext<TOutcome extends string = string> = {
908
+ outcome: TOutcome | 'continued';
909
+ detail: string;
910
+ nextWorkflowId?: number;
911
+ };
912
+ type Ctx<THost> = AiContext<THost>;
913
+ type AnyStep<THost> = TypedStep<any, any, any, Ctx<THost>>;
914
+ export interface CaseLoopConfig<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, TDecisions extends AgentExits, TFacts, THost> {
915
+ /** Prefix of every workflow and step type: move `write` runs as `<name>:write`, its tail as `<name>:write:decide`. */
916
+ name: string;
917
+ moves: readonly [TMove, ...TMove[]];
918
+ /** The case's own fields, carried by every move. */
919
+ case: z.ZodType<TCase>;
920
+ /** The move result's own fields, beside `summary` and `closes`. */
921
+ result: z.ZodType<TResult>;
922
+ /** What a result step that failed stands in with — `summary` says it failed. */
923
+ failedResult: TResult;
924
+ outcomes: readonly [TOutcome, ...TOutcome[]];
925
+ /** The outcome of a case whose manager could not decide — a person takes it over. */
926
+ stuck: NoInfer<TOutcome>;
927
+ /** The exits the manager may take. An exit named after a move starts it; any other must be in `closers`. */
928
+ decisions: TDecisions;
929
+ /** The exits that close the case, each turning its value into the outcome. */
930
+ closers?: { [K in keyof TDecisions & string]?: (value: any) => {
931
+ outcome: NoInfer<TOutcome>;
932
+ detail: string;
933
+ }; };
934
+ /** The facts the policy is checked against, read once per decision — the move's own result included. */
935
+ facts: (ctx: TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, any, Ctx<THost>>, result: CaseMoveResult<TOutcome, TResult>) => Promise<TFacts> | TFacts;
936
+ /** The policy on one choice: a reason refuses it. */
937
+ refuse?: (choice: keyof TDecisions & string, value: any, facts: TFacts, input: CaseMoveInput<TMove, TOutcome, TCase, TResult>) => string | null;
938
+ /** The most moves a case takes; from the last one on, only `atLimit` is allowed. */
939
+ maxMoves: number;
940
+ atLimit: ReadonlyArray<keyof TDecisions & string>;
941
+ manager: {
942
+ system: string;
943
+ /** The case file the manager decides on. */
944
+ prompt: (ctx: TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, any, Ctx<THost>>, result: CaseMoveResult<TOutcome, TResult>, facts: TFacts) => string;
945
+ /** Read-only tools: the manager looks things up and chooses — the moves do the work. */
946
+ tools?: (ctx: TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, any, Ctx<THost>>) => ToolSet;
947
+ maxRounds?: number;
948
+ maxTokens?: number;
949
+ maxOutputTokens?: number;
950
+ resources?: readonly string[];
951
+ retry?: RetryPolicy;
952
+ heartbeatTimeoutMs?: number;
953
+ /** The `detail` of a case closed as `stuck`. */
954
+ stuckDetail?: string;
955
+ };
956
+ startMove: (host: THost, input: CaseMoveInput<TMove, TOutcome, TCase, TResult>) => Promise<number>;
957
+ closeCase: (host: THost, closed: {
958
+ caseId: string;
959
+ outcome: TOutcome;
960
+ detail: string;
961
+ }) => Promise<void>;
962
+ }
963
+ export interface CaseMoveConfig<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, THost> {
964
+ /** The move's own steps, by key. `result`, `decide` and `next` are the tail's. */
965
+ steps: Record<string, AnyStep<THost>>;
966
+ /** The steps `result` reads — it runs once any of them completed and the others stopped. */
967
+ ends: Record<string, AnyStep<THost>>;
968
+ /** What the move achieved. `deps` holds the outputs of the `ends` that completed. */
969
+ summarize: (ctx: Omit<TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, {}, Ctx<THost>>, 'deps'> & {
970
+ deps: Record<string, any>;
971
+ }) => Promise<CaseMoveResult<TOutcome, TResult>> | CaseMoveResult<TOutcome, TResult>;
972
+ }
973
+ export interface CaseLoop<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, TDecisions extends AgentExits, TFacts, THost> {
974
+ readonly name: string;
975
+ readonly inputSchema: z.ZodType<CaseMoveInput<TMove, TOutcome, TCase, TResult>>;
976
+ readonly resultSchema: z.ZodType<CaseMoveResult<TOutcome, TResult>>;
977
+ readonly decisionSchema: z.ZodType<CaseDecision<keyof TDecisions & string>>;
978
+ /** One move as its own workflow: its steps, then the tail. Throws on a step that can end in failure. */
979
+ move(name: TMove, config: CaseMoveConfig<TMove, TOutcome, TCase, TResult, THost>): TypedWorkflow<CaseMoveInput<TMove, TOutcome, TCase, TResult>, Ctx<THost>>;
980
+ /** The policy on one choice, with the move ceiling — what `decide` holds the manager to, for a page that shows it. */
981
+ refusal(choice: keyof TDecisions & string, value: unknown, facts: TFacts, input: CaseMoveInput<TMove, TOutcome, TCase, TResult>): string | null;
982
+ /** The input of a case's first move. */
983
+ first(fields: TCase & {
984
+ caseId: string;
985
+ move: TMove;
986
+ brief?: Record<string, any>;
987
+ startedAt?: string;
988
+ }): CaseMoveInput<TMove, TOutcome, TCase, TResult>;
989
+ }
990
+ /**
991
+ * Define a case loop: a manager agent that chooses every next move, each move
992
+ * its own workflow ending in the same tail — see the module comment.
993
+ *
994
+ * ```ts
995
+ * const loop = defineCaseLoop({
996
+ * name: 'move',
997
+ * moves: ['open', 'investigate', 'write', 'wait'],
998
+ * case: z.object({ invoice: z.string() }),
999
+ * result: z.object({ mailSent: z.boolean() }),
1000
+ * failedResult: { mailSent: false },
1001
+ * outcomes: ['paid', 'unresolved'],
1002
+ * stuck: 'unresolved',
1003
+ * decisions: { investigate: …, write: …, wait: …, close: … },
1004
+ * closers: { close: (v) => ({ outcome: 'paid', detail: v.summary }) },
1005
+ * facts: async (ctx) => readBooks(ctx.workflowInput.caseId),
1006
+ * refuse: (choice, value, facts) => (choice === 'close' && facts.balance > 0 ? 'The balance is open.' : null),
1007
+ * maxMoves: 12,
1008
+ * atLimit: ['close'],
1009
+ * manager: { system: '…', prompt: (ctx, result) => caseFile(ctx.workflowInput, result) },
1010
+ * startMove: (host, input) => host.startMove(input),
1011
+ * closeCase: (host, c) => host.cases.close(c.caseId, c.outcome, c.detail),
1012
+ * });
1013
+ * const write = loop.move('write', { steps: { compose, approval, send }, ends: { approval, send }, summarize });
1014
+ * ```
1015
+ */
1016
+ export declare function defineCaseLoop<const TMove extends string, const TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, TDecisions extends AgentExits, TFacts, THost = any>(config: CaseLoopConfig<TMove, TOutcome, TCase, TResult, TDecisions, TFacts, THost>): CaseLoop<TMove, TOutcome, TCase, TResult, TDecisions, TFacts, THost>;
1017
+ //#endregion
1018
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, CircuitBreaker, CircuitBreakerOptions, CreateModelProvidersOptions, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, ExplainProviderErrorOptions, FATAL_PROVIDER_ERROR_KEYS, MODEL_JSON_MODES, MODEL_PROVIDER_KINDS, MODEL_PROVIDER_REGIONS, ModelCapabilities, ModelEntry, ModelJsonMode, ModelProvider, ModelProviderChange, ModelProviderCipher, ModelProviderError, ModelProviderInput, ModelProviderKind, ModelProviderRegion, ModelProviderStore, ModelProviders, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, OllamaProbeOptions, OllamaProbeReport, OpenAiCompatibleProviderId, OpenAiCompatibleProviderMeta, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProbeableProvider, ProviderError, ProviderErrorExplanation, ProviderErrorKey, ProviderErrorPhrase, ProviderErrorPreset, ProviderProbeFailure, ProviderProbeOptions, ProviderProbeReport, StoredModelProvider, createCircuitBreaker, createInMemoryModelProviderStore, createModelProviders, embeddingDimensionsFor, explainProviderError, isFatalProviderError, isProviderError, isSubprocessorResidency, maskKeys, maskModelProvider, modelCapabilitiesSchema, modelEntrySchema, modelProviderInputSchema, resolveEndpointResidency, testOllamaModel, testProvider, withCircuitBreaker };