octaflow 0.32.0 → 0.33.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 +1 -0
- package/dist/ai/index.d.ts +151 -3
- package/dist/ai/index.js +276 -2
- package/dist/ai/providers/index.d.ts +1 -1
- package/dist/{core-DBqxBk3R.js → core-COMWflJl.js} +2 -0
- package/dist/{dispatcher-B3ruILCN.d.ts → dispatcher-BQErUGy_.d.ts} +1 -1
- package/dist/{hooks-C46TPBEg.d.ts → hooks-DHomzpiq.d.ts} +1 -1
- package/dist/{index-D0XhoTkE.d.ts → index-DZUO24Rz.d.ts} +11 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{observability-0MLzBjuV.d.ts → observability-BLUEjdto.d.ts} +1 -1
- package/dist/pg-boss/index.d.ts +2 -2
- package/dist/postgres/index.d.ts +2 -2
- package/dist/postgres/index.js +1 -1
- package/dist/{provider-probe-_rio4ykx.d.ts → provider-probe-BDg4c1Rk.d.ts} +1 -1
- package/package.json +1 -1
- package/skills/add-a-case-loop/SKILL.md +136 -0
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 |
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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-
|
|
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-
|
|
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-
|
|
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
6
|
import { z } from "zod";
|
|
7
7
|
import { ToolSet } from "ai";
|
|
8
8
|
import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4Usage, SharedV4ProviderMetadata } from "@ai-sdk/provider";
|
|
@@ -866,4 +866,152 @@ export declare function withJsonRepair(model: LanguageModelV4, options?: JsonRep
|
|
|
866
866
|
*/
|
|
867
867
|
export declare function addUsage(a: LanguageModelV4Usage, b: LanguageModelV4Usage): LanguageModelV4Usage;
|
|
868
868
|
//#endregion
|
|
869
|
+
//#region src/ai/case-loop.d.ts
|
|
870
|
+
/** What a move leaves for the manager: a summary, and an outcome when the move itself ends the case. */
|
|
871
|
+
export type CaseMoveResult<TOutcome extends string, TResult extends Record<string, unknown>> = TResult & {
|
|
872
|
+
summary: string;
|
|
873
|
+
closes?: TOutcome;
|
|
874
|
+
};
|
|
875
|
+
/** One finished move, as the next moves see it. */
|
|
876
|
+
export interface CaseMoveRecord<TMove extends string, TOutcome extends string, TResult extends Record<string, unknown>> {
|
|
877
|
+
no: number;
|
|
878
|
+
move: TMove;
|
|
879
|
+
/** The manager's reason for it — its exit's `why`, when the exit has one. */
|
|
880
|
+
why: string;
|
|
881
|
+
result: CaseMoveResult<TOutcome, TResult>;
|
|
882
|
+
}
|
|
883
|
+
/** A move's input: the case's own fields, plus where the case stands. */
|
|
884
|
+
export type CaseMoveInput<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>> = TCase & {
|
|
885
|
+
caseId: string;
|
|
886
|
+
move: TMove;
|
|
887
|
+
/** 1 for the first move. */
|
|
888
|
+
moveNo: number;
|
|
889
|
+
/** The manager's arguments for this move — its exit's value. */
|
|
890
|
+
brief: Record<string, any>;
|
|
891
|
+
moves: CaseMoveRecord<TMove, TOutcome, TResult>[];
|
|
892
|
+
startedAt: string;
|
|
893
|
+
};
|
|
894
|
+
/** The manager's choice, as `decide` outputs it. `stuck` when it could not choose within its limits. */
|
|
895
|
+
export type CaseDecision<TExit extends string = string> = {
|
|
896
|
+
exit: TExit | 'stuck';
|
|
897
|
+
value: Record<string, any>;
|
|
898
|
+
/** Choices the policy refused on the way, each with its reason, de-duplicated. */
|
|
899
|
+
refusals: Array<{
|
|
900
|
+
choice: string;
|
|
901
|
+
reason: string;
|
|
902
|
+
}>;
|
|
903
|
+
trace: AgentTrace;
|
|
904
|
+
};
|
|
905
|
+
/** What `next` did: the case's outcome, or `continued` with the run it started. */
|
|
906
|
+
export type CaseNext<TOutcome extends string = string> = {
|
|
907
|
+
outcome: TOutcome | 'continued';
|
|
908
|
+
detail: string;
|
|
909
|
+
nextWorkflowId?: number;
|
|
910
|
+
};
|
|
911
|
+
type Ctx<THost> = AiContext<THost>;
|
|
912
|
+
type AnyStep<THost> = TypedStep<any, any, any, Ctx<THost>>;
|
|
913
|
+
export interface CaseLoopConfig<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, TDecisions extends AgentExits, TFacts, THost> {
|
|
914
|
+
/** Prefix of every workflow and step type: move `write` runs as `<name>:write`, its tail as `<name>:write:decide`. */
|
|
915
|
+
name: string;
|
|
916
|
+
moves: readonly [TMove, ...TMove[]];
|
|
917
|
+
/** The case's own fields, carried by every move. */
|
|
918
|
+
case: z.ZodType<TCase>;
|
|
919
|
+
/** The move result's own fields, beside `summary` and `closes`. */
|
|
920
|
+
result: z.ZodType<TResult>;
|
|
921
|
+
/** What a result step that failed stands in with — `summary` says it failed. */
|
|
922
|
+
failedResult: TResult;
|
|
923
|
+
outcomes: readonly [TOutcome, ...TOutcome[]];
|
|
924
|
+
/** The outcome of a case whose manager could not decide — a person takes it over. */
|
|
925
|
+
stuck: NoInfer<TOutcome>;
|
|
926
|
+
/** The exits the manager may take. An exit named after a move starts it; any other must be in `closers`. */
|
|
927
|
+
decisions: TDecisions;
|
|
928
|
+
/** The exits that close the case, each turning its value into the outcome. */
|
|
929
|
+
closers?: { [K in keyof TDecisions & string]?: (value: any) => {
|
|
930
|
+
outcome: NoInfer<TOutcome>;
|
|
931
|
+
detail: string;
|
|
932
|
+
}; };
|
|
933
|
+
/** The facts the policy is checked against, read once per decision — the move's own result included. */
|
|
934
|
+
facts: (ctx: TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, any, Ctx<THost>>, result: CaseMoveResult<TOutcome, TResult>) => Promise<TFacts> | TFacts;
|
|
935
|
+
/** The policy on one choice: a reason refuses it. */
|
|
936
|
+
refuse?: (choice: keyof TDecisions & string, value: any, facts: TFacts, input: CaseMoveInput<TMove, TOutcome, TCase, TResult>) => string | null;
|
|
937
|
+
/** The most moves a case takes; from the last one on, only `atLimit` is allowed. */
|
|
938
|
+
maxMoves: number;
|
|
939
|
+
atLimit: ReadonlyArray<keyof TDecisions & string>;
|
|
940
|
+
manager: {
|
|
941
|
+
system: string;
|
|
942
|
+
/** The case file the manager decides on. */
|
|
943
|
+
prompt: (ctx: TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, any, Ctx<THost>>, result: CaseMoveResult<TOutcome, TResult>, facts: TFacts) => string;
|
|
944
|
+
/** Read-only tools: the manager looks things up and chooses — the moves do the work. */
|
|
945
|
+
tools?: (ctx: TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, any, Ctx<THost>>) => ToolSet;
|
|
946
|
+
maxRounds?: number;
|
|
947
|
+
maxTokens?: number;
|
|
948
|
+
maxOutputTokens?: number;
|
|
949
|
+
resources?: readonly string[];
|
|
950
|
+
retry?: RetryPolicy;
|
|
951
|
+
heartbeatTimeoutMs?: number;
|
|
952
|
+
/** The `detail` of a case closed as `stuck`. */
|
|
953
|
+
stuckDetail?: string;
|
|
954
|
+
};
|
|
955
|
+
startMove: (host: THost, input: CaseMoveInput<TMove, TOutcome, TCase, TResult>) => Promise<number>;
|
|
956
|
+
closeCase: (host: THost, closed: {
|
|
957
|
+
caseId: string;
|
|
958
|
+
outcome: TOutcome;
|
|
959
|
+
detail: string;
|
|
960
|
+
}) => Promise<void>;
|
|
961
|
+
}
|
|
962
|
+
export interface CaseMoveConfig<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, THost> {
|
|
963
|
+
/** The move's own steps, by key. `result`, `decide` and `next` are the tail's. */
|
|
964
|
+
steps: Record<string, AnyStep<THost>>;
|
|
965
|
+
/** The steps `result` reads — it runs once any of them completed and the others stopped. */
|
|
966
|
+
ends: Record<string, AnyStep<THost>>;
|
|
967
|
+
/** What the move achieved. `deps` holds the outputs of the `ends` that completed. */
|
|
968
|
+
summarize: (ctx: Omit<TypedStepContext<CaseMoveInput<TMove, TOutcome, TCase, TResult>, {}, Ctx<THost>>, 'deps'> & {
|
|
969
|
+
deps: Record<string, any>;
|
|
970
|
+
}) => Promise<CaseMoveResult<TOutcome, TResult>> | CaseMoveResult<TOutcome, TResult>;
|
|
971
|
+
}
|
|
972
|
+
export interface CaseLoop<TMove extends string, TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, TDecisions extends AgentExits, TFacts, THost> {
|
|
973
|
+
readonly name: string;
|
|
974
|
+
readonly inputSchema: z.ZodType<CaseMoveInput<TMove, TOutcome, TCase, TResult>>;
|
|
975
|
+
readonly resultSchema: z.ZodType<CaseMoveResult<TOutcome, TResult>>;
|
|
976
|
+
readonly decisionSchema: z.ZodType<CaseDecision<keyof TDecisions & string>>;
|
|
977
|
+
/** One move as its own workflow: its steps, then the tail. Throws on a step that can end in failure. */
|
|
978
|
+
move(name: TMove, config: CaseMoveConfig<TMove, TOutcome, TCase, TResult, THost>): TypedWorkflow<CaseMoveInput<TMove, TOutcome, TCase, TResult>, Ctx<THost>>;
|
|
979
|
+
/** The policy on one choice, with the move ceiling — what `decide` holds the manager to, for a page that shows it. */
|
|
980
|
+
refusal(choice: keyof TDecisions & string, value: unknown, facts: TFacts, input: CaseMoveInput<TMove, TOutcome, TCase, TResult>): string | null;
|
|
981
|
+
/** The input of a case's first move. */
|
|
982
|
+
first(fields: TCase & {
|
|
983
|
+
caseId: string;
|
|
984
|
+
move: TMove;
|
|
985
|
+
brief?: Record<string, any>;
|
|
986
|
+
startedAt?: string;
|
|
987
|
+
}): CaseMoveInput<TMove, TOutcome, TCase, TResult>;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Define a case loop: a manager agent that chooses every next move, each move
|
|
991
|
+
* its own workflow ending in the same tail — see the module comment.
|
|
992
|
+
*
|
|
993
|
+
* ```ts
|
|
994
|
+
* const loop = defineCaseLoop({
|
|
995
|
+
* name: 'move',
|
|
996
|
+
* moves: ['open', 'investigate', 'write', 'wait'],
|
|
997
|
+
* case: z.object({ invoice: z.string() }),
|
|
998
|
+
* result: z.object({ mailSent: z.boolean() }),
|
|
999
|
+
* failedResult: { mailSent: false },
|
|
1000
|
+
* outcomes: ['paid', 'unresolved'],
|
|
1001
|
+
* stuck: 'unresolved',
|
|
1002
|
+
* decisions: { investigate: …, write: …, wait: …, close: … },
|
|
1003
|
+
* closers: { close: (v) => ({ outcome: 'paid', detail: v.summary }) },
|
|
1004
|
+
* facts: async (ctx) => readBooks(ctx.workflowInput.caseId),
|
|
1005
|
+
* refuse: (choice, value, facts) => (choice === 'close' && facts.balance > 0 ? 'The balance is open.' : null),
|
|
1006
|
+
* maxMoves: 12,
|
|
1007
|
+
* atLimit: ['close'],
|
|
1008
|
+
* manager: { system: '…', prompt: (ctx, result) => caseFile(ctx.workflowInput, result) },
|
|
1009
|
+
* startMove: (host, input) => host.startMove(input),
|
|
1010
|
+
* closeCase: (host, c) => host.cases.close(c.caseId, c.outcome, c.detail),
|
|
1011
|
+
* });
|
|
1012
|
+
* const write = loop.move('write', { steps: { compose, approval, send }, ends: { approval, send }, summarize });
|
|
1013
|
+
* ```
|
|
1014
|
+
*/
|
|
1015
|
+
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>;
|
|
1016
|
+
//#endregion
|
|
869
1017
|
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 };
|
package/dist/ai/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as err, r as ok, t as noopLogger } from "../logger-epkdcXDA.js";
|
|
2
|
-
import { C as nonRetryableError, _ as defineStep, m as buildWorkflow } from "../core-
|
|
2
|
+
import { C as nonRetryableError, _ as defineStep, m as buildWorkflow } from "../core-COMWflJl.js";
|
|
3
3
|
import { S as readRefusalDetails, _ as maskKeys, a as testProvider, b as createRefusalAwareModel, c as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, d as PROVIDER_ERROR_REDACTED, f as PROVIDER_ERROR_STATUS, g as isProviderError, h as isFatalProviderError, i as testOllamaModel, l as FATAL_PROVIDER_ERROR_KEYS, m as explainProviderError, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as createCircuitBreaker, p as ProviderError, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as withCircuitBreaker, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as PROVIDER_ERROR_PHRASES, v as AiRefusalError, x as isAiRefusalError, y as REFUSAL_FINISH_REASON } from "../provider-probe-CGN2MpaZ.js";
|
|
4
4
|
import { a as EMBEDDING_PROVIDERS, c as OPENAI_COMPATIBLE_PROVIDER_ID, d as resolveEndpointResidency, i as DEFAULT_EMBEDDING_PROVIDER, l as embeddingDimensionsFor, n as AI_PROVIDER_IDS, o as EMBEDDING_PROVIDER_IDS, r as DEFAULT_AI_PROVIDER, s as OPENAI_COMPATIBLE_PROVIDER, t as AI_PROVIDERS, u as isSubprocessorResidency } from "../provider-registry-B91lgOmu.js";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -1285,4 +1285,278 @@ function isRecord(value) {
|
|
|
1285
1285
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1286
1286
|
}
|
|
1287
1287
|
//#endregion
|
|
1288
|
-
|
|
1288
|
+
//#region src/ai/case-loop.ts
|
|
1289
|
+
const NO_TRACE = {
|
|
1290
|
+
rounds: 0,
|
|
1291
|
+
toolCalls: [],
|
|
1292
|
+
tokens: 0
|
|
1293
|
+
};
|
|
1294
|
+
const TAIL = [
|
|
1295
|
+
"result",
|
|
1296
|
+
"decide",
|
|
1297
|
+
"next"
|
|
1298
|
+
];
|
|
1299
|
+
/** Why a step can end in failure, or null when it cannot. */
|
|
1300
|
+
function failureOf(step) {
|
|
1301
|
+
if (step.waitForEvent) {
|
|
1302
|
+
if (step.timeoutMs === void 0) return "a wait step without timeoutMs can hold the case forever";
|
|
1303
|
+
if (typeof step.onTimeout !== "object") return "a wait step needs onTimeout: { output } — an expiry would fail the move";
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof step.onFailure !== "object") return "a step needs onFailure: { output } — its failure would skip the tail and leave the case open";
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Define a case loop: a manager agent that chooses every next move, each move
|
|
1311
|
+
* its own workflow ending in the same tail — see the module comment.
|
|
1312
|
+
*
|
|
1313
|
+
* ```ts
|
|
1314
|
+
* const loop = defineCaseLoop({
|
|
1315
|
+
* name: 'move',
|
|
1316
|
+
* moves: ['open', 'investigate', 'write', 'wait'],
|
|
1317
|
+
* case: z.object({ invoice: z.string() }),
|
|
1318
|
+
* result: z.object({ mailSent: z.boolean() }),
|
|
1319
|
+
* failedResult: { mailSent: false },
|
|
1320
|
+
* outcomes: ['paid', 'unresolved'],
|
|
1321
|
+
* stuck: 'unresolved',
|
|
1322
|
+
* decisions: { investigate: …, write: …, wait: …, close: … },
|
|
1323
|
+
* closers: { close: (v) => ({ outcome: 'paid', detail: v.summary }) },
|
|
1324
|
+
* facts: async (ctx) => readBooks(ctx.workflowInput.caseId),
|
|
1325
|
+
* refuse: (choice, value, facts) => (choice === 'close' && facts.balance > 0 ? 'The balance is open.' : null),
|
|
1326
|
+
* maxMoves: 12,
|
|
1327
|
+
* atLimit: ['close'],
|
|
1328
|
+
* manager: { system: '…', prompt: (ctx, result) => caseFile(ctx.workflowInput, result) },
|
|
1329
|
+
* startMove: (host, input) => host.startMove(input),
|
|
1330
|
+
* closeCase: (host, c) => host.cases.close(c.caseId, c.outcome, c.detail),
|
|
1331
|
+
* });
|
|
1332
|
+
* const write = loop.move('write', { steps: { compose, approval, send }, ends: { approval, send }, summarize });
|
|
1333
|
+
* ```
|
|
1334
|
+
*/
|
|
1335
|
+
function defineCaseLoop(config) {
|
|
1336
|
+
const { name, moves, outcomes, decisions } = config;
|
|
1337
|
+
const closers = config.closers ?? {};
|
|
1338
|
+
if (!outcomes.includes(config.stuck)) throw new Error(`[${name}] stuck outcome '${config.stuck}' is not one of the outcomes`);
|
|
1339
|
+
for (const exit of Object.keys(decisions)) {
|
|
1340
|
+
const isMove = moves.includes(exit);
|
|
1341
|
+
if (isMove && closers[exit]) throw new Error(`[${name}] exit '${exit}' is a move and a closer`);
|
|
1342
|
+
if (!isMove && !closers[exit]) throw new Error(`[${name}] exit '${exit}' is neither a move nor a closer`);
|
|
1343
|
+
}
|
|
1344
|
+
if (Object.hasOwn(decisions, "stuck")) throw new Error(`[${name}] 'stuck' is reserved`);
|
|
1345
|
+
for (const exit of config.atLimit) if (!Object.hasOwn(decisions, exit)) throw new Error(`[${name}] atLimit names '${exit}', not an exit`);
|
|
1346
|
+
if (!(config.maxMoves >= 1)) throw new Error(`[${name}] maxMoves must be at least 1`);
|
|
1347
|
+
const resultSchema = z.intersection(config.result, z.object({
|
|
1348
|
+
summary: z.string(),
|
|
1349
|
+
closes: z.enum(outcomes).optional()
|
|
1350
|
+
}));
|
|
1351
|
+
const recordSchema = z.object({
|
|
1352
|
+
no: z.number().int().min(1),
|
|
1353
|
+
move: z.enum(moves),
|
|
1354
|
+
why: z.string(),
|
|
1355
|
+
result: resultSchema
|
|
1356
|
+
});
|
|
1357
|
+
const inputSchema = z.intersection(config.case, z.object({
|
|
1358
|
+
caseId: z.string(),
|
|
1359
|
+
move: z.enum(moves),
|
|
1360
|
+
moveNo: z.number().int().min(1),
|
|
1361
|
+
brief: z.record(z.string(), z.any()),
|
|
1362
|
+
moves: z.array(recordSchema),
|
|
1363
|
+
startedAt: z.string()
|
|
1364
|
+
}));
|
|
1365
|
+
const exitNames = Object.keys(decisions);
|
|
1366
|
+
const decisionSchema = z.object({
|
|
1367
|
+
exit: z.enum([...exitNames, "stuck"]),
|
|
1368
|
+
value: z.record(z.string(), z.any()),
|
|
1369
|
+
refusals: z.array(z.object({
|
|
1370
|
+
choice: z.string(),
|
|
1371
|
+
reason: z.string()
|
|
1372
|
+
})),
|
|
1373
|
+
trace: AGENT_TRACE_SCHEMA
|
|
1374
|
+
});
|
|
1375
|
+
const nextSchema = z.object({
|
|
1376
|
+
outcome: z.enum([...outcomes, "continued"]),
|
|
1377
|
+
detail: z.string(),
|
|
1378
|
+
nextWorkflowId: z.number().optional()
|
|
1379
|
+
});
|
|
1380
|
+
const refusal = (choice, value, facts, input) => {
|
|
1381
|
+
if (input.moveNo >= config.maxMoves && !config.atLimit.includes(choice)) return `${config.maxMoves} moves are spent — choose one of: ${config.atLimit.join(", ")}.`;
|
|
1382
|
+
return config.refuse?.(choice, value, facts, input) ?? null;
|
|
1383
|
+
};
|
|
1384
|
+
const stuckDetail = config.manager.stuckDetail ?? "The manager could not decide — a person takes over.";
|
|
1385
|
+
function tail(move, ends, summarize) {
|
|
1386
|
+
const result = defineAiStep({
|
|
1387
|
+
type: `${name}:${move}:result`,
|
|
1388
|
+
workflowInputSchema: inputSchema,
|
|
1389
|
+
outputSchema: resultSchema,
|
|
1390
|
+
dependencies: ends,
|
|
1391
|
+
join: "any",
|
|
1392
|
+
onFailure: { output: {
|
|
1393
|
+
...config.failedResult,
|
|
1394
|
+
summary: `The ${move} move's result could not be read.`
|
|
1395
|
+
} },
|
|
1396
|
+
handler: async (ctx) => summarize(ctx)
|
|
1397
|
+
});
|
|
1398
|
+
const m = config.manager;
|
|
1399
|
+
const decide = defineAiStep({
|
|
1400
|
+
type: `${name}:${move}:decide`,
|
|
1401
|
+
workflowInputSchema: inputSchema,
|
|
1402
|
+
outputSchema: decisionSchema,
|
|
1403
|
+
dependencies: { result },
|
|
1404
|
+
when: (ctx) => !ctx.deps.result.closes,
|
|
1405
|
+
resources: m.resources,
|
|
1406
|
+
retry: m.retry,
|
|
1407
|
+
heartbeatTimeoutMs: m.heartbeatTimeoutMs,
|
|
1408
|
+
onFailure: { output: {
|
|
1409
|
+
exit: "stuck",
|
|
1410
|
+
value: { why: stuckDetail },
|
|
1411
|
+
refusals: [],
|
|
1412
|
+
trace: NO_TRACE
|
|
1413
|
+
} },
|
|
1414
|
+
handler: async (ctx) => {
|
|
1415
|
+
const input = ctx.workflowInput;
|
|
1416
|
+
const facts = await config.facts(ctx, ctx.deps.result);
|
|
1417
|
+
const refusals = [];
|
|
1418
|
+
const exits = Object.fromEntries(Object.entries(decisions).map(([exit, d]) => [exit, {
|
|
1419
|
+
description: d.description,
|
|
1420
|
+
schema: d.schema.superRefine((value, issue) => {
|
|
1421
|
+
const reason = refusal(exit, value, facts, input);
|
|
1422
|
+
if (!reason) return;
|
|
1423
|
+
if (!refusals.some((r) => r.choice === exit && r.reason === reason)) refusals.push({
|
|
1424
|
+
choice: exit,
|
|
1425
|
+
reason
|
|
1426
|
+
});
|
|
1427
|
+
issue.addIssue({
|
|
1428
|
+
code: "custom",
|
|
1429
|
+
message: `Refused by policy: ${reason}`
|
|
1430
|
+
});
|
|
1431
|
+
})
|
|
1432
|
+
}]));
|
|
1433
|
+
const run = await runAgentToExit({
|
|
1434
|
+
model: ctx.context.model,
|
|
1435
|
+
system: m.system,
|
|
1436
|
+
prompt: m.prompt(ctx, ctx.deps.result, facts),
|
|
1437
|
+
tools: m.tools?.(ctx),
|
|
1438
|
+
exits,
|
|
1439
|
+
maxRounds: m.maxRounds ?? 10,
|
|
1440
|
+
maxTokens: m.maxTokens,
|
|
1441
|
+
maxOutputTokens: m.maxOutputTokens,
|
|
1442
|
+
heartbeat: ctx.heartbeat,
|
|
1443
|
+
abortSignal: ctx.signal
|
|
1444
|
+
});
|
|
1445
|
+
return {
|
|
1446
|
+
exit: run.exit,
|
|
1447
|
+
value: run.value ?? {},
|
|
1448
|
+
refusals,
|
|
1449
|
+
trace: run.trace
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
return {
|
|
1454
|
+
result,
|
|
1455
|
+
decide,
|
|
1456
|
+
next: defineAiStep({
|
|
1457
|
+
type: `${name}:${move}:next`,
|
|
1458
|
+
workflowInputSchema: inputSchema,
|
|
1459
|
+
outputSchema: nextSchema,
|
|
1460
|
+
dependencies: {
|
|
1461
|
+
result,
|
|
1462
|
+
decide
|
|
1463
|
+
},
|
|
1464
|
+
join: "any",
|
|
1465
|
+
retry: {
|
|
1466
|
+
maxAttempts: 10,
|
|
1467
|
+
backoff: "exponential",
|
|
1468
|
+
initialDelayMs: 1e3,
|
|
1469
|
+
maxDelayMs: 6e4
|
|
1470
|
+
},
|
|
1471
|
+
handler: async (ctx) => {
|
|
1472
|
+
const i = ctx.workflowInput;
|
|
1473
|
+
const r = ctx.deps.result;
|
|
1474
|
+
const d = ctx.deps.decide;
|
|
1475
|
+
const host = ctx.context.host;
|
|
1476
|
+
const close = async (outcome, detail) => {
|
|
1477
|
+
await config.closeCase(host, {
|
|
1478
|
+
caseId: i.caseId,
|
|
1479
|
+
outcome,
|
|
1480
|
+
detail
|
|
1481
|
+
});
|
|
1482
|
+
return {
|
|
1483
|
+
outcome,
|
|
1484
|
+
detail
|
|
1485
|
+
};
|
|
1486
|
+
};
|
|
1487
|
+
if (r.closes) return close(r.closes, r.summary);
|
|
1488
|
+
if (!d || d.exit === "stuck") return close(config.stuck, String(d?.value.why ?? stuckDetail));
|
|
1489
|
+
const closer = closers[d.exit];
|
|
1490
|
+
if (closer) {
|
|
1491
|
+
const c = closer(d.value);
|
|
1492
|
+
return close(c.outcome, c.detail);
|
|
1493
|
+
}
|
|
1494
|
+
const record = {
|
|
1495
|
+
no: i.moveNo,
|
|
1496
|
+
move: i.move,
|
|
1497
|
+
why: String(i.brief.why ?? ""),
|
|
1498
|
+
result: r
|
|
1499
|
+
};
|
|
1500
|
+
const caseFields = Object.fromEntries(Object.entries(i).filter(([k]) => ![
|
|
1501
|
+
"move",
|
|
1502
|
+
"moveNo",
|
|
1503
|
+
"brief",
|
|
1504
|
+
"moves",
|
|
1505
|
+
"startedAt"
|
|
1506
|
+
].includes(k)));
|
|
1507
|
+
const nextWorkflowId = await config.startMove(host, {
|
|
1508
|
+
...caseFields,
|
|
1509
|
+
move: d.exit,
|
|
1510
|
+
moveNo: i.moveNo + 1,
|
|
1511
|
+
brief: d.value,
|
|
1512
|
+
moves: [...i.moves, record],
|
|
1513
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1514
|
+
});
|
|
1515
|
+
return {
|
|
1516
|
+
outcome: "continued",
|
|
1517
|
+
detail: `${d.exit}: ${String(d.value.why ?? "")}`,
|
|
1518
|
+
nextWorkflowId
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
})
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
name,
|
|
1526
|
+
inputSchema,
|
|
1527
|
+
resultSchema,
|
|
1528
|
+
decisionSchema,
|
|
1529
|
+
refusal,
|
|
1530
|
+
first(fields) {
|
|
1531
|
+
const { brief, startedAt, ...rest } = fields;
|
|
1532
|
+
return {
|
|
1533
|
+
...rest,
|
|
1534
|
+
moveNo: 1,
|
|
1535
|
+
brief: brief ?? {},
|
|
1536
|
+
moves: [],
|
|
1537
|
+
startedAt: startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1538
|
+
};
|
|
1539
|
+
},
|
|
1540
|
+
move(move, { steps, ends, summarize }) {
|
|
1541
|
+
if (!moves.includes(move)) throw new Error(`[${name}] '${move}' is not a move`);
|
|
1542
|
+
for (const key of TAIL) if (Object.hasOwn(steps, key)) throw new Error(`[${name}:${move}] step key '${key}' is the tail's`);
|
|
1543
|
+
for (const [key, step] of Object.entries(steps)) {
|
|
1544
|
+
const why = failureOf(step);
|
|
1545
|
+
if (why) throw new Error(`[${name}:${move}] step '${key}' (${step.type}): ${why}`);
|
|
1546
|
+
}
|
|
1547
|
+
const owned = new Set(Object.values(steps));
|
|
1548
|
+
for (const [key, step] of Object.entries(ends)) if (!owned.has(step)) throw new Error(`[${name}:${move}] ends.${key} is not one of the move's steps`);
|
|
1549
|
+
if (Object.keys(ends).length === 0) throw new Error(`[${name}:${move}] ends names no step`);
|
|
1550
|
+
return buildAiWorkflow({
|
|
1551
|
+
type: `${name}:${move}`,
|
|
1552
|
+
inputSchema,
|
|
1553
|
+
steps: {
|
|
1554
|
+
...steps,
|
|
1555
|
+
...tail(move, ends, summarize)
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
//#endregion
|
|
1562
|
+
export { AGENT_TRACE_SCHEMA, AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_JSON_EXCERPT_LENGTH, DEFAULT_MODEL_PRICING, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, FATAL_PROVIDER_ERROR_KEYS, MODEL_JSON_PROVIDER_METADATA_KEY, MODEL_JSON_REPAIRED_ATTRIBUTE, ModelJsonError, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProviderError, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, addUsage, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCircuitBreaker, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createRefusalAwareModel, createUsageAccumulator, defineAgentStep, defineAiStep, defineCaseLoop, embeddingDimensionsFor, estimateCostMicros, excerptOf, explainProviderError, isAiRefusalError, isFatalProviderError, isModelJsonError, isProviderError, isSubprocessorResidency, maskKeys, mergeModelJsonObjects, modelJsonRepairText, parseModelJson, readRefusalDetails, recoverModelJson, resolveEndpointResidency, runAgent, runAgentToExit, supportsAdaptiveThinking, supportsToolSearch, testOllamaModel, testProvider, thinkingSettingsFor, withCircuitBreaker, withJsonRepair };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b as OpenAiCompatibleProviderId, g as EmbeddingProviderId, i as AiEndpointResidency, o as AiProviderId } from "../../provider-registry-C_mOfdHH.js";
|
|
2
|
-
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, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, o as ProbeableProvider, p as CircuitBreakerOptions, s as ProviderProbeFailure, 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-
|
|
2
|
+
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, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, o as ProbeableProvider, p as CircuitBreakerOptions, s as ProviderProbeFailure, 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";
|
|
3
3
|
import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
|
|
4
4
|
//#region src/ai/providers/index.d.ts
|
|
5
5
|
export type LanguageModelFactory = (modelId: string) => LanguageModelV4;
|
|
@@ -459,6 +459,7 @@ function defineSleepStep(config) {
|
|
|
459
459
|
outputSchema: emptyObjectSchema,
|
|
460
460
|
dependencies: config.dependencies,
|
|
461
461
|
delayMs: config.sleepMs,
|
|
462
|
+
onFailure: config.onFailure,
|
|
462
463
|
handler: async () => ({})
|
|
463
464
|
});
|
|
464
465
|
}
|
|
@@ -635,6 +636,7 @@ function defineDecisionStep(config) {
|
|
|
635
636
|
isRetryable: config.isRetryable,
|
|
636
637
|
timeoutMs: config.timeoutMs,
|
|
637
638
|
resources: config.resources,
|
|
639
|
+
onFailure: config.onFailure,
|
|
638
640
|
handler: async (ctx) => {
|
|
639
641
|
const response = await decider.decide({
|
|
640
642
|
questions,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { St as Result, lt as WorkflowId, nt as StepId, xt as FlowErrorShape } from "./index-
|
|
1
|
+
import { St as Result, lt as WorkflowId, nt as StepId, xt as FlowErrorShape } from "./index-DZUO24Rz.js";
|
|
2
2
|
//#region src/core/dispatcher.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* What a step job is for. A queue carries two kinds of work for a step, and
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { St as Result, W as StartOptions, ct as WorkflowDefinition, ft as WorkflowRecord, lt as WorkflowId, nt as StepId, rt as StepRecord, xt as FlowErrorShape } from "./index-
|
|
1
|
+
import { St as Result, W as StartOptions, ct as WorkflowDefinition, ft as WorkflowRecord, lt as WorkflowId, nt as StepId, rt as StepRecord, xt as FlowErrorShape } from "./index-DZUO24Rz.js";
|
|
2
2
|
//#region src/core/hooks.d.ts
|
|
3
3
|
interface BeforeStartArgs {
|
|
4
4
|
definition: WorkflowDefinition;
|
|
@@ -779,6 +779,13 @@ declare function defineSleepStep<TContext = unknown, TDeps extends Record<string
|
|
|
779
779
|
/** How long to sleep once the step becomes ready, in ms — fixed, or computed from the run's input then. */
|
|
780
780
|
sleepMs: StepDelay;
|
|
781
781
|
dependencies?: TDeps;
|
|
782
|
+
/**
|
|
783
|
+
* What a terminal failure does — the sleep's job exhausted, its worker
|
|
784
|
+
* presumed dead. `{ output: {} }` wakes the run anyway. Default `'fail'`.
|
|
785
|
+
*/
|
|
786
|
+
onFailure?: 'fail' | {
|
|
787
|
+
output: Record<string, never>;
|
|
788
|
+
};
|
|
782
789
|
}): TypedStep<Record<string, never>, Record<string, never>, TDeps, TContext>;
|
|
783
790
|
/** Minimal engine surface `buildWorkflow().start()` needs — avoids a circular import. */
|
|
784
791
|
interface StartableEngine {
|
|
@@ -908,6 +915,10 @@ interface DefineDecisionStepConfig<Q extends DecisionQuestions, TInput extends R
|
|
|
908
915
|
timeoutMs?: number;
|
|
909
916
|
/** Resources the decider draws on (a model, say), for the admission gate — see `defineStep`. */
|
|
910
917
|
resources?: readonly string[];
|
|
918
|
+
/** What a terminal failure does — see `defineStep`'s `onFailure`. Default `'fail'`. */
|
|
919
|
+
onFailure?: 'fail' | {
|
|
920
|
+
output: DecisionOutput<Q>;
|
|
921
|
+
};
|
|
911
922
|
}
|
|
912
923
|
/**
|
|
913
924
|
* Define a **decision** step: the decider answers each question with one of
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as StepHandler, A as StepOutput, B as defineWaitStep, C as defineDecisionOutcomeStep, Ct as err, D as resolveDecision, E as functionDecider, F as buildWorkflow, G as StepCompensateHandler, H as InvalidWorkflowDefinitionError, I as defineMapStep, J as StepDefinition, K as StepCompensationContext, L as defineSleepStep, M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, P as WorkflowOutput, Q as StepFailurePolicy, R as defineStep, S as DecisionStep, St as Result, T as defineDecisionStep, U as RetryPolicy, V as FlowError, W as StartOptions, X as StepError, Y as StepDelay, Z as StepExecutionContext, _ as DecisionOutput, _t as PlannedSkip, a as STEP_DISPLAY_STATUS, at as StepStatus, b as DecisionResponse, bt as isTerminalStepStatus, c as WORKFLOW_STATUS_SCHEMA, ct as WorkflowDefinition, d as toPublicWorkflow, dt as WorkflowNotRetryableError, et as StepHandlerNotFoundError, f as Decider, ft as WorkflowRecord, g as DecisionOutcome, gt as JoinRule, h as DecisionChoices, ht as WorkflowWithSteps, i as PublicWorkflowStep, it as StepRegistration, j as TypedStep, k as StepDeps, l as toDisplayStepStatus, lt as WorkflowId, m as DecisionAnswers, mt as WorkflowStepRunningError, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepId, o as STEP_DISPLAY_STATUS_SCHEMA, ot as WaitTimeoutPolicy, p as DecisionAnswer, pt as WorkflowStatus, q as StepConditionHandler, r as PublicWorkflow, rt as StepRecord, s as StepDisplayStatus, st as WorkflowCreatedResult, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepHandlerRegistry, u as toPublicStep, ut as WorkflowNotFoundError, v as DecisionQuestions, vt as ReadinessPlan, w as defineDecisionReviewStep, wt as ok, x as DecisionReview, xt as FlowErrorShape, y as DecisionRequest, yt as computeReadiness, z as defineSubWorkflowStep } from "./index-
|
|
1
|
+
import { $ as StepHandler, A as StepOutput, B as defineWaitStep, C as defineDecisionOutcomeStep, Ct as err, D as resolveDecision, E as functionDecider, F as buildWorkflow, G as StepCompensateHandler, H as InvalidWorkflowDefinitionError, I as defineMapStep, J as StepDefinition, K as StepCompensationContext, L as defineSleepStep, M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, P as WorkflowOutput, Q as StepFailurePolicy, R as defineStep, S as DecisionStep, St as Result, T as defineDecisionStep, U as RetryPolicy, V as FlowError, W as StartOptions, X as StepError, Y as StepDelay, Z as StepExecutionContext, _ as DecisionOutput, _t as PlannedSkip, a as STEP_DISPLAY_STATUS, at as StepStatus, b as DecisionResponse, bt as isTerminalStepStatus, c as WORKFLOW_STATUS_SCHEMA, ct as WorkflowDefinition, d as toPublicWorkflow, dt as WorkflowNotRetryableError, et as StepHandlerNotFoundError, f as Decider, ft as WorkflowRecord, g as DecisionOutcome, gt as JoinRule, h as DecisionChoices, ht as WorkflowWithSteps, i as PublicWorkflowStep, it as StepRegistration, j as TypedStep, k as StepDeps, l as toDisplayStepStatus, lt as WorkflowId, m as DecisionAnswers, mt as WorkflowStepRunningError, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepId, o as STEP_DISPLAY_STATUS_SCHEMA, ot as WaitTimeoutPolicy, p as DecisionAnswer, pt as WorkflowStatus, q as StepConditionHandler, r as PublicWorkflow, rt as StepRecord, s as StepDisplayStatus, st as WorkflowCreatedResult, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepHandlerRegistry, u as toPublicStep, ut as WorkflowNotFoundError, v as DecisionQuestions, vt as ReadinessPlan, w as defineDecisionReviewStep, wt as ok, x as DecisionReview, xt as FlowErrorShape, y as DecisionRequest, yt as computeReadiness, z as defineSubWorkflowStep } from "./index-DZUO24Rz.js";
|
|
2
2
|
import { n as Logger, r as noopLogger, t as LogAttributes } from "./logger-BCus3AOa.js";
|
|
3
|
-
import { A as FinishWorkflowParams, C as gateRuleKeys, D as CreateWorkflowStep, E as CreateWorkflowParams, F as WorkflowTreeFilter, M as ReopenWorkflowParams, N as TransactionalScope, O as CreatedWorkflow, P as WorkflowStore, S as createInMemoryStepGate, T as CompleteStepParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ListWorkflowsFilters, k as FailStepParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as AddChildStep, x as StepGateRequest, y as StepGate } from "./observability-
|
|
4
|
-
import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-
|
|
5
|
-
import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-
|
|
3
|
+
import { A as FinishWorkflowParams, C as gateRuleKeys, D as CreateWorkflowStep, E as CreateWorkflowParams, F as WorkflowTreeFilter, M as ReopenWorkflowParams, N as TransactionalScope, O as CreatedWorkflow, P as WorkflowStore, S as createInMemoryStepGate, T as CompleteStepParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ListWorkflowsFilters, k as FailStepParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as AddChildStep, x as StepGateRequest, y as StepGate } from "./observability-BLUEjdto.js";
|
|
4
|
+
import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-BQErUGy_.js";
|
|
5
|
+
import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-DHomzpiq.js";
|
|
6
6
|
//#region src/core/registry.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Creates an in-memory step handler registry.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as err, r as ok, t as noopLogger } from "./logger-epkdcXDA.js";
|
|
2
2
|
import { n as gateRuleKeys, t as createInMemoryStepGate } from "./gate-BRq-t3K_.js";
|
|
3
|
-
import { C as nonRetryableError, S as markRetryable, _ as defineStep, a as WORKFLOW_STATUS_SCHEMA, b as explicitRetryability, c as toPublicWorkflow, d as defineDecisionStep, f as functionDecider, g as defineSleepStep, h as defineMapStep, i as STEP_DISPLAY_STATUS_SCHEMA, l as defineDecisionOutcomeStep, m as buildWorkflow, n as PUBLIC_WORKFLOW_STEP_SCHEMA, o as toDisplayStepStatus, p as resolveDecision, r as STEP_DISPLAY_STATUS, s as toPublicStep, t as PUBLIC_WORKFLOW_SCHEMA, u as defineDecisionReviewStep, v as defineSubWorkflowStep, w as retryableError, x as isRetryableError, y as defineWaitStep } from "./core-
|
|
3
|
+
import { C as nonRetryableError, S as markRetryable, _ as defineStep, a as WORKFLOW_STATUS_SCHEMA, b as explicitRetryability, c as toPublicWorkflow, d as defineDecisionStep, f as functionDecider, g as defineSleepStep, h as defineMapStep, i as STEP_DISPLAY_STATUS_SCHEMA, l as defineDecisionOutcomeStep, m as buildWorkflow, n as PUBLIC_WORKFLOW_STEP_SCHEMA, o as toDisplayStepStatus, p as resolveDecision, r as STEP_DISPLAY_STATUS, s as toPublicStep, t as PUBLIC_WORKFLOW_SCHEMA, u as defineDecisionReviewStep, v as defineSubWorkflowStep, w as retryableError, x as isRetryableError, y as defineWaitStep } from "./core-COMWflJl.js";
|
|
4
4
|
//#region src/core/readiness.ts
|
|
5
5
|
/** Statuses a step can no longer move out of on its own. */
|
|
6
6
|
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ft as WorkflowRecord, ht as WorkflowWithSteps, lt as WorkflowId, nt as StepId, pt as WorkflowStatus, rt as StepRecord } from "./index-
|
|
1
|
+
import { ft as WorkflowRecord, ht as WorkflowWithSteps, lt as WorkflowId, nt as StepId, pt as WorkflowStatus, rt as StepRecord } from "./index-DZUO24Rz.js";
|
|
2
2
|
//#region src/core/store.d.ts
|
|
3
3
|
interface CreateWorkflowStep {
|
|
4
4
|
key: string;
|
package/dist/pg-boss/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import "../index-
|
|
1
|
+
import "../index-DZUO24Rz.js";
|
|
2
2
|
import { n as Logger } from "../logger-BCus3AOa.js";
|
|
3
|
-
import { r as Dispatcher } from "../dispatcher-
|
|
3
|
+
import { r as Dispatcher } from "../dispatcher-BQErUGy_.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { PgBoss, Schedule } from "pg-boss";
|
|
6
6
|
//#region src/pg-boss/payload.d.ts
|
package/dist/postgres/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { lt as WorkflowId } from "../index-
|
|
2
|
-
import { P as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-
|
|
1
|
+
import { lt as WorkflowId } from "../index-DZUO24Rz.js";
|
|
2
|
+
import { P as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-BLUEjdto.js";
|
|
3
3
|
import { Pool } from "pg";
|
|
4
4
|
//#region src/postgres/ddl.d.ts
|
|
5
5
|
/**
|
package/dist/postgres/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octaflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss, with an optional AI add-on (token/cost/quota). Layered single package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-a-case-loop
|
|
3
|
+
description: Add an octaflow case loop — a manager agent chooses every next move of a case (investigate, draft, wait, ask a person, escalate, close), each move its own workflow with its own shape, all ending in the same tail; the policy refuses a choice with its reason inside the exit's schema so the agent chooses again, every outward step waits for a person, and no failure leaves a case open. Use when the sequence of work is not known in advance; when it is, write one workflow, and when the model only picks among options, use add-a-decision.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a case loop
|
|
7
|
+
|
|
8
|
+
> Paths under `apps/` and `packages/` are in the platform repository,
|
|
9
|
+
> <https://github.com/octabits-io/octabits>. In your own app, read a package's README at
|
|
10
|
+
> `node_modules/octaflow/README.md`.
|
|
11
|
+
|
|
12
|
+
A case loop is for work whose order nobody can write down: chasing an overdue invoice, checking
|
|
13
|
+
a claim until it holds or falls. A **manager** agent reads the case file and takes one exit per
|
|
14
|
+
decision; each exit named after a **move** starts that move's workflow, each other exit closes
|
|
15
|
+
the case.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌──────── move N (its own steps) ────────┐
|
|
19
|
+
… body … ─► result ─► decide (manager) ─► next ─► move N+1 | case closed
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Shown end to end in `apps/demo-task-loop/src/moves.ts` (Postgres, pg-boss, a customer agent
|
|
23
|
+
that answers by resuming the move that waits for it), with whole cases on scripted models in
|
|
24
|
+
`apps/demo-task-loop/src/moves.test.ts`. The loop's own tests are
|
|
25
|
+
`packages/octaflow/src/ai/case-loop.test.ts`.
|
|
26
|
+
|
|
27
|
+
**Which pattern?**
|
|
28
|
+
|
|
29
|
+
| The model… | Pattern |
|
|
30
|
+
|---|---|
|
|
31
|
+
| chooses what happens next, and the next thing is a whole piece of work | a case loop (this skill) |
|
|
32
|
+
| chooses among options you can list for one record | `add-a-decision` (beside this skill) |
|
|
33
|
+
| writes content a person always reviews | `add-an-ai-proposal-workflow` (beside this skill) |
|
|
34
|
+
|
|
35
|
+
## 1. Define the loop
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { defineCaseLoop } from 'octaflow/ai';
|
|
39
|
+
|
|
40
|
+
export const caseLoop = defineCaseLoop({
|
|
41
|
+
name: 'move', // workflow types move:<move>, tail steps move:<move>:result|decide|next
|
|
42
|
+
moves: ['open', 'investigate', 'write', 'wait', 'escalate'],
|
|
43
|
+
case: z.object({ invoiceId: z.string() }), // the case's own fields, carried by every move
|
|
44
|
+
result: z.object({ mailSent: z.boolean() }), // a move's result beside `summary` and `closes`
|
|
45
|
+
failedResult: { mailSent: false },
|
|
46
|
+
outcomes: ['paid', 'collection', 'unresolved'],
|
|
47
|
+
stuck: 'unresolved', // a manager that cannot decide hands the case to a person
|
|
48
|
+
decisions: {
|
|
49
|
+
investigate: { description: '…', schema: z.object({ question: z.string(), why: z.string() }) },
|
|
50
|
+
write: { description: '…', schema: z.object({ purpose: z.string(), why: z.string() }) },
|
|
51
|
+
wait: { description: '…', schema: z.object({ days: z.number().int().min(1).max(14), why: z.string() }) },
|
|
52
|
+
escalate: { description: '…', schema: z.object({ reason: z.string(), why: z.string() }) },
|
|
53
|
+
close: { description: 'Close: paid.', schema: z.object({ summary: z.string(), why: z.string() }) },
|
|
54
|
+
},
|
|
55
|
+
closers: { close: (v) => ({ outcome: 'paid', detail: v.summary }) },
|
|
56
|
+
facts: async (ctx, result) => readFacts(ctx.workflowInput, result), // once per decision, this move's result included
|
|
57
|
+
refuse: (choice, value, facts) =>
|
|
58
|
+
choice === 'close' && facts.balance > 0 ? `The open balance is ${facts.balance}.` : null,
|
|
59
|
+
maxMoves: 12,
|
|
60
|
+
atLimit: ['escalate', 'close'],
|
|
61
|
+
manager: {
|
|
62
|
+
system: 'You decide the next move, one at a time, by calling exactly one decision tool. …',
|
|
63
|
+
prompt: (ctx, result, facts) => caseFile(ctx.workflowInput, result, facts),
|
|
64
|
+
tools: (ctx) => readOnlyTools(ctx), // the manager looks things up; moves do the work
|
|
65
|
+
resources: ['llm'],
|
|
66
|
+
retry: { maxAttempts: 3 },
|
|
67
|
+
},
|
|
68
|
+
startMove: (host: Host, input) => host.startMove(input),
|
|
69
|
+
closeCase: (host: Host, c) => host.cases.close(c.caseId, c.outcome, c.detail),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Give every exit a `why`: the next move's input keeps it as the record's reason.
|
|
74
|
+
- The policy belongs in `refuse`, never in the prompt alone. A refused choice is an invalid
|
|
75
|
+
tool call answered with "Refused by policy: <reason>"; the manager chooses again, and
|
|
76
|
+
`decide`'s output lists the refusals for the page.
|
|
77
|
+
- `caseLoop.refusal(choice, value, facts, input)` is the same check, ceiling included — use it
|
|
78
|
+
on the page that shows what the manager may choose next.
|
|
79
|
+
|
|
80
|
+
## 2. Write each move
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const write = caseLoop.move('write', {
|
|
84
|
+
steps: { compose, riskCritic, factCritic, approval, send, reply },
|
|
85
|
+
ends: { compose, approval, send, reply }, // what `result` reads; it runs once one completed and the rest stopped
|
|
86
|
+
summarize: (ctx) => mailSummary(ctx.deps),
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`move()` throws at definition time on a step that can end in failure, because a failed step
|
|
91
|
+
skips the tail and nothing would close the case:
|
|
92
|
+
|
|
93
|
+
- every step declares `onFailure: { output }` — a stand-in the move's summary can say out loud
|
|
94
|
+
("the draft failed", "sending failed"), with `when` guards so nothing downstream acts on it;
|
|
95
|
+
- every wait step has `timeoutMs` and `onTimeout: { output }` (`{ decision: 'expired' }`);
|
|
96
|
+
- a sleep step takes `onFailure: { output: {} }`.
|
|
97
|
+
|
|
98
|
+
Anything that leaves the house — a mail, a booking, a filing — is a wait step for a person
|
|
99
|
+
followed by a step that does exactly what the person saw, with no model in between. Critics see
|
|
100
|
+
the purpose and the draft, never the writer's trace.
|
|
101
|
+
|
|
102
|
+
## 3. Wire the host
|
|
103
|
+
|
|
104
|
+
`startMove` starts `workflows[input.move]` with `{ idempotencyKey: \`${caseId}:${moveNo}\` }`, so
|
|
105
|
+
a re-run `next` starts the move once. Register every move's workflow; start a case with
|
|
106
|
+
`caseLoop.first({ caseId, ...caseFields, move: 'open' })`. Another workflow (a customer, a child
|
|
107
|
+
check) answers a move by `engine.resumeStep(moveWorkflowId, 'reply', payload)`; a `false` result
|
|
108
|
+
means the move stopped waiting — keep the answer where the next `wait` move finds it.
|
|
109
|
+
|
|
110
|
+
## Prove it works
|
|
111
|
+
|
|
112
|
+
A test on the in-memory engine with one scripted model whose lines are routed by the system
|
|
113
|
+
prompt — `apps/demo-task-loop/src/moves.test.ts` is the template — that asserts:
|
|
114
|
+
|
|
115
|
+
- a whole case: the moves in order, the outcome the host recorded;
|
|
116
|
+
- a refused choice is in `decide`'s `refusals` with its reason, and the next choice is taken;
|
|
117
|
+
- at `maxMoves` only the `atLimit` exits pass;
|
|
118
|
+
- a manager with no lines left closes the case as `stuck`;
|
|
119
|
+
- a drafting step that fails reaches no person and the manager decides again.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm --filter octaflow test:unit
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Green: `src/ai/case-loop.test.ts` passes, including "refuses a step that can end in failure"
|
|
126
|
+
and "closes the case as stuck when the manager cannot decide".
|
|
127
|
+
|
|
128
|
+
## Checklist
|
|
129
|
+
|
|
130
|
+
- [ ] Every exit has a `why`; every non-move exit has a closer
|
|
131
|
+
- [ ] The policy is in `refuse` and reads facts from the store, not from the model's words
|
|
132
|
+
- [ ] The manager's tools only read
|
|
133
|
+
- [ ] Every outward step waits for a person and sends exactly what they released
|
|
134
|
+
- [ ] `startMove` passes the `caseId:moveNo` idempotency key
|
|
135
|
+
- [ ] The manager's model goes through the disclosure gate
|
|
136
|
+
- [ ] A test plays whole cases on scripted models
|