bazis 0.130.0 → 0.132.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.
Files changed (76) hide show
  1. package/cli/templates/process.ts +1 -1
  2. package/core/agent/AgentToolExecutor.ts +3 -0
  3. package/core/agent/Tool.contract.ts +1 -1
  4. package/core/agent/index.ts +1 -0
  5. package/core/agent/metadata.ts +9 -0
  6. package/core/agent/semantic.ts +8 -1
  7. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  8. package/core/workflow/{runtime_module/config/Runtime.config.ts → contracts/Workflow.config.ts} +15 -6
  9. package/core/workflow/contracts/WorkflowExtensions.ts +40 -0
  10. package/core/workflow/contracts/WorkflowInputValidator.ts +6 -5
  11. package/core/workflow/contracts/types.ts +37 -1
  12. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +18 -3
  13. package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -3
  14. package/core/workflow/definitions_module/access/Folder.references.ts +37 -0
  15. package/core/workflow/definitions_module/access/Process.references.ts +1 -1
  16. package/core/workflow/definitions_module/access/ProcessFolder.lookup.ts +20 -0
  17. package/core/workflow/definitions_module/code/CatalogImport.ts +95 -0
  18. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +26 -16
  19. package/core/workflow/definitions_module/code/codeProcesses.ts +7 -3
  20. package/core/workflow/definitions_module/code/defineProcess.ts +10 -4
  21. package/core/workflow/definitions_module/events/Definitions.events.ts +16 -0
  22. package/core/workflow/definitions_module/http/Processes.controller.ts +72 -25
  23. package/core/workflow/definitions_module/model/Definitions.dbContext.ts +2 -2
  24. package/core/workflow/definitions_module/model/WfProcess.model.ts +19 -5
  25. package/core/workflow/definitions_module/model/WfVersion.model.ts +1 -1
  26. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +49 -14
  27. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +128 -45
  28. package/core/workflow/generated/deps.ts +15 -5
  29. package/core/workflow/index.ts +17 -3
  30. package/core/workflow/internal/WorkflowEvents.ts +23 -0
  31. package/core/workflow/internal/WorkflowLanguages.ts +35 -0
  32. package/core/workflow/internal/canonicalJson.ts +8 -0
  33. package/core/workflow/internal/http.ts +53 -5
  34. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +0 -2
  35. package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
  36. package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
  37. package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
  38. package/core/workflow/runtime_module/events/Runtime.events.ts +30 -3
  39. package/core/workflow/runtime_module/http/Events.controller.ts +2 -1
  40. package/core/workflow/runtime_module/http/Instances.controller.ts +32 -21
  41. package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
  42. package/core/workflow/runtime_module/internal/ToolStep.ts +42 -10
  43. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +83 -31
  44. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +2 -4
  45. package/core/workflow/runtime_module/model/Runtime.dbContext.ts +2 -2
  46. package/core/workflow/runtime_module/model/WfHistory.model.ts +2 -2
  47. package/core/workflow/runtime_module/model/WfInstance.model.ts +28 -3
  48. package/core/workflow/runtime_module/model/WfJob.model.ts +17 -1
  49. package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
  50. package/core/workflow/runtime_module/model/WfToken.model.ts +11 -1
  51. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +19 -6
  52. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +20 -17
  53. package/core/workflow/tasks_module/events/Tasks.events.ts +15 -0
  54. package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -18
  55. package/core/workflow/tasks_module/model/Tasks.dbContext.ts +2 -2
  56. package/core/workflow/tasks_module/model/WfTask.model.ts +8 -4
  57. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +4 -4
  58. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +11 -8
  59. package/library/openapi/codegen.ts +12 -0
  60. package/library/validation/types/ValidatorOptions.ts +12 -0
  61. package/library/workflow/ProcessDocument.ts +59 -23
  62. package/library/workflow/expressionSyntax.ts +243 -0
  63. package/library/workflow/expressions.ts +343 -0
  64. package/library/workflow/formFields.ts +120 -34
  65. package/library/workflow/index.ts +48 -9
  66. package/library/workflow/localized.ts +35 -0
  67. package/library/workflow/masking.ts +58 -0
  68. package/library/workflow/mermaid.ts +18 -8
  69. package/library/workflow/nodeKinds.ts +88 -17
  70. package/library/workflow/palette.ts +108 -0
  71. package/library/workflow/paths.ts +31 -6
  72. package/library/workflow/structure.ts +282 -0
  73. package/library/workflow/upgradeDocument.ts +84 -0
  74. package/library/workflow/validateProcess.ts +130 -48
  75. package/package.json +1 -1
  76. package/library/workflow/conditions.ts +0 -44
@@ -5,7 +5,7 @@ import { WorkflowClock } from "../../contracts/WorkflowClock";
5
5
  import { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
6
6
  import { WORKFLOW_INSTANCES_DELETED } from "../events/Runtime.events";
7
7
  import { RuntimeDbContext } from "../model/Runtime.dbContext";
8
- import type { WorkflowSettings } from "../config/Runtime.config";
8
+ import type { WorkflowSettings } from "../../contracts/Workflow.config";
9
9
 
10
10
  const DAY_MS = 86_400_000;
11
11
 
@@ -2,14 +2,14 @@ import { BackgroundService, delay } from "../../../background";
2
2
  import type { ServiceProvider } from "../../../di";
3
3
  import { WorkflowClock } from "../../contracts/WorkflowClock";
4
4
  import type { ConfigView } from "../../../kernel";
5
- import type { WorkflowSettings } from "../config/Runtime.config";
5
+ import type { WorkflowSettings } from "../../contracts/Workflow.config";
6
6
  import { WorkflowJobs } from "../internal/WorkflowJobs";
7
7
 
8
8
  /** How often the scheduled starts are matched against the published versions. */
9
9
  const RECONCILE_MS = 60_000;
10
10
 
11
11
  /**
12
- * Takes due jobs of `bazis_wf_run_jobs` every `worker.intervalMs`, up to
12
+ * Takes due jobs of `bazis_wf2_run_jobs` every `worker.intervalMs`, up to
13
13
  * `worker.batchSize` at a time ({@link WorkflowJobs}). A full batch is
14
14
  * followed by the next one at once.
15
15
  */
@@ -1,6 +1,7 @@
1
1
  import { createEventToken } from "../../../kernel";
2
- import type { FormSpec } from "../../../../library/workflow";
3
- import type { InstanceStatus } from "../model/WfInstance.model";
2
+ import type { FormSpec, LocalizedText } from "../../../../library/workflow";
3
+ import type { WorkflowActor, WorkflowRequest } from "../../contracts/types";
4
+ import type { InstanceStatus, StartedVia } from "../model/WfInstance.model";
4
5
 
5
6
  /** A token reached a user task. Published after the step commits. */
6
7
  export interface WorkflowTaskRequested {
@@ -9,7 +10,7 @@ export interface WorkflowTaskRequested {
9
10
  readonly processKey: string;
10
11
  readonly nodeId: string;
11
12
  readonly partition: string | null;
12
- readonly title: string;
13
+ readonly title: LocalizedText;
13
14
  readonly assignee: { readonly role: string } | { readonly user: string };
14
15
  readonly form: FormSpec | null;
15
16
  /** When the task is due, from the node's `dueIn`. */
@@ -37,7 +38,33 @@ export interface WorkflowInstancesDeleted {
37
38
  readonly instanceIds: readonly string[];
38
39
  }
39
40
 
41
+ /** What the audit events of an instance share. Published after the commit. */
42
+ interface WorkflowInstanceAudit {
43
+ readonly instanceId: string;
44
+ readonly processKey: string;
45
+ readonly partition: string | null;
46
+ readonly actor: WorkflowActor;
47
+ readonly request: WorkflowRequest | null;
48
+ }
49
+
50
+ /** An instance started: by hand, from an event or on schedule. */
51
+ export interface WorkflowInstanceStarted extends WorkflowInstanceAudit {
52
+ readonly version: number;
53
+ readonly businessKey: string | null;
54
+ readonly startedVia: StartedVia;
55
+ }
56
+
57
+ export interface WorkflowInstanceCancelled extends WorkflowInstanceAudit {
58
+ readonly reason: string | null;
59
+ }
60
+
61
+ /** The failed steps of an instance run again. */
62
+ export type WorkflowInstanceRetried = WorkflowInstanceAudit;
63
+
40
64
  export const WORKFLOW_TASK_REQUESTED = createEventToken<WorkflowTaskRequested>("workflow.task-requested");
41
65
  export const WORKFLOW_TOKEN_CLOSED = createEventToken<WorkflowTokenClosed>("workflow.token-closed");
42
66
  export const WORKFLOW_INSTANCE_FINISHED = createEventToken<WorkflowInstanceFinished>("workflow.instance-finished");
43
67
  export const WORKFLOW_INSTANCES_DELETED = createEventToken<WorkflowInstancesDeleted>("workflow.instances-deleted");
68
+ export const WORKFLOW_INSTANCE_STARTED = createEventToken<WorkflowInstanceStarted>("workflow.instance-started");
69
+ export const WORKFLOW_INSTANCE_CANCELLED = createEventToken<WorkflowInstanceCancelled>("workflow.instance-cancelled");
70
+ export const WORKFLOW_INSTANCE_RETRIED = createEventToken<WorkflowInstanceRetried>("workflow.instance-retried");
@@ -15,11 +15,12 @@ export class EventsController {
15
15
  const eventKey = param(ctx, "eventKey");
16
16
  const request = await body(ctx);
17
17
  const partition = optionalString(request.partition, "partition") ?? null;
18
- const { actor } = await callerOf(ctx, "workflow.events.emit", { scope: partition, event: eventKey });
18
+ const { actor, request: from } = await callerOf(ctx, "workflow.events.emit", { scope: partition, event: eventKey });
19
19
  const result = await this.runtime.emit(eventKey, request.payload ?? {}, {
20
20
  correlation: optionalString(request.correlation, "correlation") ?? null,
21
21
  partition,
22
22
  actor,
23
+ request: from,
23
24
  });
24
25
  ctx.res.status(202);
25
26
  return result;
@@ -1,14 +1,19 @@
1
1
  import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
2
  import { can, type AccessObject } from "../../../access";
3
3
  import { Authorize, Controller, Created, Get, Post, type HttpContext } from "../../../http";
4
- import { body, callerOf, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
4
+ import { buildListDocument } from "../../../../library/jsonapi";
5
+ import { body, callerOf, filterOf, listQuery, optionalString, pagingOfList, param, requiredString } from "../../internal/http";
5
6
  import type { InstanceStatus } from "../model/WfInstance.model";
7
+ import { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
6
8
  import { IWorkflowRuntime, type InstanceSummary } from "../services/IWorkflowRuntime.service";
7
9
 
8
10
  /** Instances over HTTP (WORKFLOW-001 §4); rights are `workflow.instances.*` (WORKFLOW-002 §6.2). */
9
11
  @Controller("workflow/instances")
10
12
  export class InstancesController {
11
- constructor(private readonly runtime: IWorkflowRuntime) {}
13
+ constructor(
14
+ private readonly runtime: IWorkflowRuntime,
15
+ private readonly definitions: IWorkflowDefinitions,
16
+ ) {}
12
17
 
13
18
  @Post()
14
19
  @Authorize(can("workflow.instances.start"))
@@ -16,12 +21,14 @@ export class InstancesController {
16
21
  const request = await body(ctx);
17
22
  const processKey = requiredString(request.processKey, "processKey", 64);
18
23
  const partition = optionalString(request.partition, "partition") ?? null;
19
- const { actor } = await callerOf(ctx, "workflow.instances.start", { scope: partition, process: processKey });
24
+ const { actor, request: from, language } = await callerOf(ctx, "workflow.instances.start", { scope: partition, process: processKey, folder: await this.definitions.folderOf(processKey) });
20
25
  const instance = await this.runtime.start(processKey, {
21
26
  input: request.input,
22
27
  partition,
23
28
  businessKey: optionalString(request.businessKey, "businessKey") ?? null,
24
29
  actor,
30
+ request: from,
31
+ language,
25
32
  });
26
33
  return Created(`${ctx.path}/${instance.id}`, instance);
27
34
  }
@@ -30,23 +37,27 @@ export class InstancesController {
30
37
  @Authorize(can("workflow.instances.read"))
31
38
  async list(ctx: HttpContext) {
32
39
  const { access } = await callerOf(ctx);
40
+ const list = listQuery(ctx, {
41
+ filter: { partition: ["eq"], processKey: ["eq"], status: ["eq"], businessKey: ["eq"], startedById: ["eq"] },
42
+ sort: ["createdAt", "endedAt", "status", "processKey"],
43
+ });
33
44
  const page = await this.runtime.instances({
34
- ...pagingQuery(ctx),
45
+ ...pagingOfList(list),
35
46
  access: await access.filter("workflow.instances.read"),
36
- partition: optionalString(ctx.query("partition"), "partition"),
37
- processKey: optionalString(ctx.query("processKey"), "processKey"),
38
- status: optionalString(ctx.query("status"), "status") as InstanceStatus | undefined,
39
- businessKey: optionalString(ctx.query("businessKey"), "businessKey"),
40
- startedById: optionalString(ctx.query("startedById"), "startedById"),
47
+ partition: filterOf(list, "partition"),
48
+ processKey: filterOf(list, "processKey"),
49
+ status: filterOf(list, "status") as InstanceStatus | undefined,
50
+ businessKey: filterOf(list, "businessKey"),
51
+ startedById: filterOf(list, "startedById"),
41
52
  });
42
- return { data: page.items, meta: { total: page.total } };
53
+ return buildListDocument(page.items, list, page.total, { basePath: ctx.path });
43
54
  }
44
55
 
45
56
  @Get(":id")
46
57
  @Authorize(can("workflow.instances.read"))
47
58
  async get(ctx: HttpContext) {
48
59
  const instance = await this.runtime.instance(param(ctx, "id"));
49
- await callerOf(ctx, "workflow.instances.read", objectOf(instance));
60
+ await callerOf(ctx, "workflow.instances.read", await this.objectOf(instance));
50
61
  return instance;
51
62
  }
52
63
 
@@ -54,7 +65,7 @@ export class InstancesController {
54
65
  @Authorize(can("workflow.instances.read"))
55
66
  async history(ctx: HttpContext) {
56
67
  const instance = await this.runtime.instance(param(ctx, "id"));
57
- await callerOf(ctx, "workflow.instances.read", objectOf(instance));
68
+ await callerOf(ctx, "workflow.instances.read", await this.objectOf(instance));
58
69
  return { data: await this.runtime.history(instance.id) };
59
70
  }
60
71
 
@@ -63,22 +74,22 @@ export class InstancesController {
63
74
  async cancel(ctx: HttpContext) {
64
75
  const instance = await this.runtime.instance(param(ctx, "id"));
65
76
  const request = await body(ctx);
66
- const { actor } = await callerOf(ctx, "workflow.instances.manage", objectOf(instance));
67
- return await this.runtime.cancel(instance.id, { reason: optionalString(request.reason, "reason", 1000) ?? null, actor });
77
+ const { actor, request: from } = await callerOf(ctx, "workflow.instances.manage", await this.objectOf(instance));
78
+ return await this.runtime.cancel(instance.id, { reason: optionalString(request.reason, "reason", 1000) ?? null, actor, request: from });
68
79
  }
69
80
 
70
81
  @Post(":id/retry")
71
82
  @Authorize(can("workflow.instances.manage"))
72
83
  async retry(ctx: HttpContext) {
73
84
  const instance = await this.runtime.instance(param(ctx, "id"));
74
- const { actor } = await callerOf(ctx, "workflow.instances.manage", objectOf(instance));
75
- return await this.runtime.retry(instance.id, { actor });
85
+ const { actor, request } = await callerOf(ctx, "workflow.instances.manage", await this.objectOf(instance));
86
+ return await this.runtime.retry(instance.id, { actor, request });
76
87
  }
77
- }
78
- contextOnlyController(InstancesController);
79
88
 
80
- /** The instance as the object of a permission. */
81
- function objectOf(instance: InstanceSummary): AccessObject {
82
- return { scope: instance.partition, process: instance.processKey, startedById: instance.startedBy.id };
89
+ /** The instance as the object of a permission. */
90
+ private async objectOf(instance: InstanceSummary): Promise<AccessObject> {
91
+ return { scope: instance.partition, process: instance.processKey, folder: await this.definitions.folderOf(instance.processKey), startedById: instance.startedBy.id };
92
+ }
83
93
  }
94
+ contextOnlyController(InstancesController);
84
95
 
@@ -1,4 +1,4 @@
1
- import { parseCron } from "../../../../library/workflow";
1
+ import { inputSpecOf, parseCron, validateForm } from "../../../../library/workflow";
2
2
  import type { IWorkflowDefinitions, VersionDetails } from "../../definitions_module/services/IWorkflowDefinitions.service";
3
3
  import type { RuntimeDbContext } from "../model/Runtime.dbContext";
4
4
  import { WfJob } from "../model/WfJob.model";
@@ -65,7 +65,7 @@ export class Schedules {
65
65
  else await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeUpdate({ dueAt: next, lockedUntil: null, attempts: 0, lastError: null });
66
66
  for (const [nodeId, cron] of scheduleNodes(latest)) {
67
67
  if (!cron.matches(due)) continue;
68
- await this.engine.begin(graph, nodeId, { input: {}, partition: null, businessKey: null, startedVia: "schedule", actor: SCHEDULE_ACTOR });
68
+ await this.engine.begin(graph, nodeId, { input: validateForm(inputSpecOf(latest.document), {}).value, partition: null, businessKey: null, startedVia: "schedule", actor: SCHEDULE_ACTOR });
69
69
  }
70
70
  });
71
71
  }
@@ -1,5 +1,6 @@
1
- import type { AgentToolResult } from "../../../agent";
2
- import { durationMs, resolveInput } from "../../../../library/workflow";
1
+ import type { AgentSchemaContract, AgentToolResult } from "../../../agent";
2
+ import { durationMs, maskByMapping, maskBySchema, masksPersonal, personalPaths, resolveInput, type ProcessDocument } from "../../../../library/workflow";
3
+ import type { WorkflowToolMetadata } from "../../contracts/types";
3
4
  import type { WorkflowTools } from "../../internal/WorkflowTools";
4
5
  import type { RuntimeDbContext } from "../model/Runtime.dbContext";
5
6
  import type { WfJob } from "../model/WfJob.model";
@@ -12,6 +13,11 @@ import { dataOf, type WorkflowEngine } from "./WorkflowEngine";
12
13
  * started the instance, with the token id as its idempotency key: after a crash
13
14
  * between the call and the step the call may repeat (TOOLS-001 §7).
14
15
  */
16
+ /** How long a claimed job belongs to its worker; if it dies, another one takes the job after that. */
17
+ export const LEASE_MS = 5 * 60_000;
18
+
19
+ const TIMEOUT_CODES: ReadonlySet<string> = new Set(["TOOL_TIMEOUT", "TOOL_TIMEOUT_OUTCOME_UNKNOWN"]);
20
+
15
21
  export class ToolStep {
16
22
  constructor(
17
23
  private readonly db: RuntimeDbContext,
@@ -29,21 +35,29 @@ export class ToolStep {
29
35
  const graph = await this.engine.graphOf(instance.versionId);
30
36
  const node = graph.document.nodes[token.nodeId];
31
37
  const name = String(node?.config?.tool ?? "");
32
- const input = resolveInput(node?.config?.input ?? {}, dataOf(instance));
38
+ const input = resolveInput(node?.config?.input ?? {}, dataOf(instance, this.engine.now()));
39
+ const timeoutMs = stepTimeoutMs(node?.config?.timeout, graph.document);
40
+ // A step may run longer than the lease: another worker must not take the job and call the Tool again meanwhile.
41
+ if (timeoutMs !== undefined && timeoutMs >= LEASE_MS) {
42
+ await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeUpdate({ lockedUntil: new Date(this.engine.now().getTime() + timeoutMs + LEASE_MS) });
43
+ }
33
44
  const runner = this.tools.bound;
34
45
  const result: AgentToolResult = runner === undefined
35
46
  ? { callId: token.id, name, status: "error", error: { code: "TOOL_NOT_FOUND", message: `Tool ${name} is not available to processes` }, metadata: {} } as AgentToolResult
36
47
  : await runner.run(name, input, {
37
48
  idempotencyKey: token.id,
38
49
  actor: { kind: instance.startedByKind, id: instance.startedById },
39
- metadata: { instanceId: instance.id, partition: instance.partition, processKey: instance.processKey, nodeId: token.nodeId },
50
+ metadata: { instanceId: instance.id, processKey: instance.processKey, nodeId: token.nodeId, partition: instance.partition, test: false } satisfies WorkflowToolMetadata,
40
51
  signal,
52
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
41
53
  });
42
54
 
43
- const reason = reasonOf(result);
55
+ const timedOut = TIMEOUT_CODES.has(result.error?.code ?? "");
56
+ const reason = timedOut ? "timeout" : reasonOf(result);
57
+ const payloads = graph.document.settings?.log?.toolPayloads === true ? this.payloads(graph.document, name, node?.config?.input, input, result) : {};
44
58
  await this.engine.inStep(instance.id, async (step) => {
45
59
  const current = step.tokens.find((t) => t.id === token.id);
46
- const retryAt = current?.status === "waiting" && step.instance.status === "running" && result.status === "error" && reason === undefined
60
+ const retryAt = current?.status === "waiting" && step.instance.status === "running" && result.status === "error" && (reason === undefined || timedOut)
47
61
  ? nextAttempt(node?.config?.retry, job.attempts, this.engine.now())
48
62
  : undefined;
49
63
  if (retryAt !== undefined) {
@@ -58,6 +72,7 @@ export class ToolStep {
58
72
  status: result.status,
59
73
  attempt: job.attempts,
60
74
  durationMs: result.durationMs ?? null,
75
+ ...payloads,
61
76
  ...(result.error ? { error: { code: result.error.code ?? null, message: result.error.message, ...(reason === undefined ? {} : { reason }) } } : {}),
62
77
  ...(retryAt === undefined ? {} : { retryAt: retryAt.toISOString() }),
63
78
  });
@@ -73,6 +88,15 @@ export class ToolStep {
73
88
  }
74
89
  });
75
90
  }
91
+
92
+ /** The input and output for the history under `log.toolPayloads`; personal data is masked unless `log.maskPersonal` is `false`. */
93
+ private payloads(document: ProcessDocument, name: string, mapping: unknown, input: unknown, result: AgentToolResult): Record<string, unknown> {
94
+ if (!masksPersonal(document)) return { input, output: result.output ?? null };
95
+ const contract = this.tools.bound?.describe(name);
96
+ const schemaOf = (side: AgentSchemaContract | undefined) => (side?.kind === "json-schema" ? side.schema : undefined);
97
+ const masked = maskByMapping(mapping ?? {}, maskBySchema(input, schemaOf(contract?.input)), personalPaths(document));
98
+ return { input: masked, output: maskBySchema(result.output ?? null, schemaOf(contract?.output)) };
99
+ }
76
100
  }
77
101
 
78
102
  /** The business reason of a `ToolError`, if the Tool gave one. */
@@ -82,15 +106,23 @@ function reasonOf(result: AgentToolResult): string | undefined {
82
106
  return typeof reason === "string" ? reason : undefined;
83
107
  }
84
108
 
109
+ /** How long a step's Tool may run: its `timeout`, else `settings.stepTimeout`; `undefined` leaves the Tool's own limit. */
110
+ function stepTimeoutMs(timeout: unknown, document: ProcessDocument): number | undefined {
111
+ const value = typeof timeout === "string" ? timeout : document.settings?.stepTimeout;
112
+ return value === undefined ? undefined : durationMs(value);
113
+ }
114
+
85
115
  /**
86
116
  * When to try a failed call again under the node's `retry` policy, or
87
- * `undefined` when the attempts are used up. The pause doubles from `backoff`
88
- * (10 seconds by default) and stops growing at a day.
117
+ * `undefined` when the attempts are used up. The pause starts at `backoff` (10
118
+ * seconds by default), grows by `factor` (2 by default) unless `growth` is
119
+ * `fixed`, and stops growing at a day.
89
120
  */
90
121
  export function nextAttempt(policy: unknown, attempts: number, now: Date): Date | undefined {
91
122
  if (typeof policy !== "object" || policy === null) return undefined;
92
- const { maxAttempts, backoff } = policy as { maxAttempts?: unknown; backoff?: unknown };
123
+ const { maxAttempts, backoff, growth, factor } = policy as { maxAttempts?: unknown; backoff?: unknown; growth?: unknown; factor?: unknown };
93
124
  if (typeof maxAttempts !== "number" || attempts >= maxAttempts) return undefined;
94
125
  const base = typeof backoff === "string" ? durationMs(backoff) ?? 10_000 : 10_000;
95
- return new Date(now.getTime() + Math.min(base * 2 ** Math.max(attempts - 1, 0), 86_400_000));
126
+ const scale = growth === "fixed" ? 1 : (typeof factor === "number" ? factor : 2) ** Math.max(attempts - 1, 0);
127
+ return new Date(now.getTime() + Math.min(base * scale, 86_400_000));
96
128
  }
@@ -1,10 +1,10 @@
1
- import type { ServiceProvider } from "../../../di";
2
- import { EventBus, type EventToken, type Logger } from "../../../kernel";
3
- import { durationMs, evaluateCondition, nodeKindOf, readPath, type FormSpec, type InstanceData, type ProcessFlow, type ProcessNode } from "../../../../library/workflow";
4
- import type { WorkflowActor } from "../../contracts/types";
1
+ import type { EventToken } from "../../../kernel";
2
+ import { durationMs, evaluateExpression, initialVarsOf, isTruthy, masksPersonal, maskByMapping, nodeKindOf, outputOf, personalPaths, personalVariables, PERSONAL_MASK, readPath, type Expr, type FormSpec, type InstanceData, type ProcessFlow, type ProcessNode } from "../../../../library/workflow";
3
+ import type { WorkflowActor, WorkflowRequest } from "../../contracts/types";
5
4
  import type { WorkflowClock } from "../../contracts/WorkflowClock";
6
5
  import type { IWorkflowDefinitions, VersionDetails } from "../../definitions_module/services/IWorkflowDefinitions.service";
7
- import { WORKFLOW_INSTANCE_FINISHED, WORKFLOW_TASK_REQUESTED, WORKFLOW_TOKEN_CLOSED } from "../events/Runtime.events";
6
+ import type { WorkflowEvents } from "../../internal/WorkflowEvents";
7
+ import { WORKFLOW_INSTANCE_CANCELLED, WORKFLOW_INSTANCE_FINISHED, WORKFLOW_INSTANCE_RETRIED, WORKFLOW_INSTANCE_STARTED, WORKFLOW_TASK_REQUESTED, WORKFLOW_TOKEN_CLOSED } from "../events/Runtime.events";
8
8
  import type { RuntimeDbContext } from "../model/Runtime.dbContext";
9
9
  import { WfHistory, type HistoryType } from "../model/WfHistory.model";
10
10
  import { WfInstance, type StartedVia } from "../model/WfInstance.model";
@@ -36,6 +36,10 @@ export interface BeginOptions {
36
36
  readonly businessKey: string | null;
37
37
  readonly startedVia: StartedVia;
38
38
  readonly actor: WorkflowActor;
39
+ /** Where the start came from; `null` for code, schedules and the worker. */
40
+ readonly request?: WorkflowRequest | null;
41
+ /** The event that starts it: `$event` and `$trigger.eventKey`. */
42
+ readonly event?: { readonly key: string; readonly payload: unknown };
39
43
  }
40
44
 
41
45
  const LIVE = new Set(["active", "waiting", "failed"]);
@@ -51,11 +55,8 @@ export class WorkflowEngine {
51
55
  private readonly db: RuntimeDbContext,
52
56
  private readonly definitions: IWorkflowDefinitions,
53
57
  private readonly graphs: GraphCache,
54
- private readonly events: EventBus,
55
- /** The scope of the engine: event handlers run in it. */
56
- private readonly scope: ServiceProvider,
58
+ private readonly events: WorkflowEvents,
57
59
  private readonly clock: WorkflowClock,
58
- private readonly logger?: Logger,
59
60
  ) {}
60
61
 
61
62
  /** The engine's time ({@link WorkflowClock}). */
@@ -87,6 +88,14 @@ export class WorkflowEngine {
87
88
  version: graph.version,
88
89
  partition: options.partition,
89
90
  input: options.input,
91
+ vars: initialVarsOf(graph.document),
92
+ event: options.event?.payload ?? null,
93
+ trigger: {
94
+ via: options.startedVia,
95
+ at: this.now().toISOString(),
96
+ actor: options.actor,
97
+ ...(options.event === undefined ? {} : { eventKey: options.event.key }),
98
+ },
90
99
  businessKey: options.businessKey,
91
100
  startedVia: options.startedVia,
92
101
  startedByKind: options.actor.kind,
@@ -97,6 +106,10 @@ export class WorkflowEngine {
97
106
  await this.db.saveChanges();
98
107
  const step: Step = { instance, tokens: [], graph, events: [] };
99
108
  this.history(step, null, null, "started", { via: options.startedVia }, options.actor);
109
+ step.events.push({
110
+ event: WORKFLOW_INSTANCE_STARTED as EventToken<unknown>,
111
+ payload: { instanceId: instance.id, processKey: graph.processKey, version: graph.version, partition: options.partition, businessKey: options.businessKey, startedVia: options.startedVia, actor: options.actor, request: options.request ?? null },
112
+ });
100
113
  await this.advance(step, [this.spawn(step, startNodeId, null)]);
101
114
  await this.db.saveChanges();
102
115
  this.publishAfter(transaction, step);
@@ -147,13 +160,20 @@ export class WorkflowEngine {
147
160
  /**
148
161
  * A step's Tool failed for good. An error boundary attached to the step takes
149
162
  * the token (first one whose `errorCode` equals the Tool's `reason` or error
150
- * code, else one without `errorCode`); otherwise the instance fails.
163
+ * code, else one without `errorCode`); otherwise `onError: "skip"` moves on with
164
+ * `{ skipped: true, error }`; otherwise the instance fails.
151
165
  */
152
166
  async failStep(step: Step, token: WfToken, error: { readonly code: string; readonly message: string; readonly reason?: string }): Promise<void> {
153
167
  const boundaries = (step.graph.boundaries.get(token.nodeId) ?? []).filter(([, node]) => node.kind === "boundaryError");
154
168
  const matching = boundaries.find(([, node]) => node.config?.errorCode !== undefined && (node.config.errorCode === error.reason || node.config.errorCode === error.code))
155
169
  ?? boundaries.find(([, node]) => node.config?.errorCode === undefined);
156
170
  if (!matching) {
171
+ if (step.graph.document.nodes[token.nodeId]?.config?.onError === "skip") {
172
+ const skipped = { code: error.code, message: error.message, ...(error.reason === undefined ? {} : { reason: error.reason }) };
173
+ this.history(step, token, token.nodeId, "skipped", skipped);
174
+ await this.complete(step, token.id, { skipped: true, error: skipped });
175
+ return;
176
+ }
157
177
  this.fail(step, token, error.code, error.message);
158
178
  return;
159
179
  }
@@ -194,6 +214,7 @@ export class WorkflowEngine {
194
214
  const token = step.tokens.find((t) => t.id === tokenId);
195
215
  if (!token || token.status !== "waiting" || step.instance.status !== "running") return false;
196
216
  const node = step.graph.document.nodes[token.nodeId];
217
+ if (node?.kind === "eventWait" || node?.kind === "boundaryEvent") step.instance.event = payload;
197
218
  if (node?.kind === "eventWait") {
198
219
  this.history(step, token, token.nodeId, "event", { event: eventKey });
199
220
  return await this.complete(step, tokenId, payload);
@@ -231,8 +252,12 @@ export class WorkflowEngine {
231
252
  }
232
253
 
233
254
  /** Retries the failed steps: their tokens are back on their nodes and the instance runs again. */
234
- async retry(step: Step, actor: WorkflowActor): Promise<void> {
255
+ async retry(step: Step, actor: WorkflowActor, request: WorkflowRequest | null = null): Promise<void> {
235
256
  const failed = step.tokens.filter((t) => t.status === "failed");
257
+ step.events.push({
258
+ event: WORKFLOW_INSTANCE_RETRIED as EventToken<unknown>,
259
+ payload: { instanceId: step.instance.id, processKey: step.instance.processKey, partition: step.instance.partition, actor, request },
260
+ });
236
261
  step.instance.status = "running";
237
262
  step.instance.error = null;
238
263
  for (const token of failed) {
@@ -243,8 +268,12 @@ export class WorkflowEngine {
243
268
  }
244
269
 
245
270
  /** Closes every live token, job and subscription; the application drops the tasks on `WORKFLOW_TOKEN_CLOSED`. */
246
- async cancel(step: Step, reason: string | null, actor: WorkflowActor): Promise<void> {
271
+ async cancel(step: Step, reason: string | null, actor: WorkflowActor, request: WorkflowRequest | null = null): Promise<void> {
247
272
  const open = step.tokens.filter((t) => LIVE.has(t.status));
273
+ step.events.push({
274
+ event: WORKFLOW_INSTANCE_CANCELLED as EventToken<unknown>,
275
+ payload: { instanceId: step.instance.id, processKey: step.instance.processKey, partition: step.instance.partition, reason, actor, request },
276
+ });
248
277
  for (const token of open) {
249
278
  token.status = "cancelled";
250
279
  token.endedAt = this.now();
@@ -287,6 +316,7 @@ export class WorkflowEngine {
287
316
  if (step.instance.status === "running" && !step.tokens.some((t) => LIVE.has(t.status))) {
288
317
  step.instance.status = "completed";
289
318
  step.instance.endedAt = this.now();
319
+ step.instance.output = outputOf(step.graph.document, step.instance.vars);
290
320
  this.history(step, null, null, "completed", null);
291
321
  this.finished(step, "completed");
292
322
  }
@@ -308,6 +338,8 @@ export class WorkflowEngine {
308
338
  // An error boundary that caught a step's failure: it only leads on.
309
339
  if (role === "boundary") return this.leave(step, token, this.single(step, token.nodeId));
310
340
  switch (node.kind) {
341
+ case "assign":
342
+ return this.assign(step, token, node);
311
343
  case "serviceTask":
312
344
  return this.arm(step, token, this.waitForTool(step, token));
313
345
  case "userTask":
@@ -326,8 +358,28 @@ export class WorkflowEngine {
326
358
  }
327
359
  }
328
360
 
361
+ /**
362
+ * An `assign` step: each value sees the ones before it. The history names the
363
+ * variables; their values go there only under `log.variables`, personal ones masked.
364
+ */
365
+ private assign(step: Step, token: WfToken, node: ProcessNode): WfToken[] {
366
+ const { document } = step.graph;
367
+ const assignments = (node.config?.assignments ?? []) as readonly { readonly var: string; readonly value: Expr }[];
368
+ const personal = masksPersonal(document) ? personalVariables(document) : new Set<string>();
369
+ const reads = masksPersonal(document) ? personalPaths(document) : new Set<string>();
370
+ const logged: Record<string, unknown> = {};
371
+ for (const { var: name, value } of assignments) {
372
+ const result = evaluateExpression(value, dataOf(step.instance, this.now())) ?? null;
373
+ step.instance.vars = { ...step.instance.vars, [name]: result };
374
+ logged[name] = personal.has(name) ? PERSONAL_MASK : maskByMapping(value, result, reads);
375
+ }
376
+ const log = document.settings?.log?.variables === true;
377
+ this.history(step, token, token.nodeId, "assigned", log ? { values: logged } : { vars: assignments.map((item) => item.var) });
378
+ return this.leave(step, token, this.single(step, token.nodeId));
379
+ }
380
+
329
381
  private waitForTimer(step: Step, token: WfToken, node: ProcessNode): WfToken[] {
330
- const dueAt = timerDue(node.config ?? {}, dataOf(step.instance), this.now());
382
+ const dueAt = timerDue(node.config ?? {}, dataOf(step.instance, this.now()), this.now());
331
383
  if (dueAt === undefined) {
332
384
  this.fail(step, token, "bad_timer", `the timer of ${token.nodeId} has no valid date`);
333
385
  return [];
@@ -345,7 +397,7 @@ export class WorkflowEngine {
345
397
  if (host.status !== "waiting") return next;
346
398
  for (const [id, node] of step.graph.boundaries.get(host.nodeId) ?? []) {
347
399
  if (node.kind === "boundaryEvent") {
348
- const correlation = correlationOf(node.config ?? {}, dataOf(step.instance));
400
+ const correlation = correlationOf(node.config ?? {}, dataOf(step.instance, this.now()));
349
401
  if (correlation === undefined) {
350
402
  this.fail(step, host, "bad_correlation", `the correlation of ${id} has no value`);
351
403
  return [];
@@ -356,7 +408,7 @@ export class WorkflowEngine {
356
408
  continue;
357
409
  }
358
410
  if (node.kind !== "boundaryTimer") continue;
359
- const dueAt = timerDue(node.config ?? {}, dataOf(step.instance), this.now());
411
+ const dueAt = timerDue(node.config ?? {}, dataOf(step.instance, this.now()), this.now());
360
412
  if (dueAt === undefined) {
361
413
  this.fail(step, host, "bad_timer", `the timer of ${id} has no valid date`);
362
414
  return [];
@@ -378,7 +430,7 @@ export class WorkflowEngine {
378
430
  }
379
431
 
380
432
  private waitForEvent(step: Step, token: WfToken, node: ProcessNode): WfToken[] {
381
- const correlation = correlationOf(node.config ?? {}, dataOf(step.instance));
433
+ const correlation = correlationOf(node.config ?? {}, dataOf(step.instance, this.now()));
382
434
  if (correlation === undefined) {
383
435
  this.fail(step, token, "bad_correlation", `the correlation of ${token.nodeId} has no value`);
384
436
  return [];
@@ -430,8 +482,8 @@ export class WorkflowEngine {
430
482
  private choose(step: Step, token: WfToken): WfToken[] {
431
483
  const flows = step.graph.outgoing.get(token.nodeId) ?? [];
432
484
  if (flows.length === 1) return this.leave(step, token, flows[0]!);
433
- const data = dataOf(step.instance);
434
- const chosen = flows.find(([, flow]) => flow.condition !== undefined && evaluateCondition(flow.condition, data))
485
+ const data = dataOf(step.instance, this.now());
486
+ const chosen = flows.find(([, flow]) => flow.condition !== undefined && isTruthy(evaluateExpression(flow.condition, data)))
435
487
  ?? flows.find(([, flow]) => flow.isDefault === true);
436
488
  if (!chosen) {
437
489
  this.fail(step, token, "no_branch", "no condition matched and there is no default flow");
@@ -493,21 +545,12 @@ export class WorkflowEngine {
493
545
  });
494
546
  }
495
547
 
496
- /**
497
- * Events go out after the commit, so a handler never sees a step that rolled
498
- * back. A failing handler is logged: the step is already saved.
499
- */
548
+ /** Events go out after the commit, so a handler never sees a step that rolled back. */
500
549
  private publishAfter(transaction: { afterCommit(callback: () => void | Promise<void>): void }, step: Step): void {
501
550
  if (step.events.length === 0) return;
502
551
  const events = [...step.events];
503
552
  transaction.afterCommit(async () => {
504
- for (const { event, payload } of events) {
505
- try {
506
- await this.events.publishScoped(this.scope, event, payload);
507
- } catch (error) {
508
- this.logger?.error("workflow event handler failed", { event: event.name, instance: step.instance.id, error: error instanceof Error ? error.message : String(error) });
509
- }
510
- }
553
+ for (const { event, payload } of events) await this.events.publish(event, payload);
511
554
  });
512
555
  }
513
556
  }
@@ -537,6 +580,15 @@ export function correlationOf(config: Readonly<Record<string, unknown>>, data: I
537
580
  return value === undefined || value === null || value === "" || typeof value === "object" ? undefined : String(value);
538
581
  }
539
582
 
540
- export function dataOf(instance: WfInstance): InstanceData {
541
- return { input: instance.input, vars: instance.vars, nodes: instance.nodes };
583
+ /** What expressions read in an instance at this moment of the engine's clock. */
584
+ export function dataOf(instance: WfInstance, now: Date): InstanceData {
585
+ return {
586
+ input: instance.input,
587
+ vars: instance.vars,
588
+ nodes: instance.nodes,
589
+ event: instance.event,
590
+ trigger: instance.trigger,
591
+ run: { id: instance.id, processKey: instance.processKey, version: instance.version, partition: instance.partition, test: instance.test },
592
+ now: now.toISOString(),
593
+ };
542
594
  }
@@ -4,11 +4,9 @@ import { RuntimeDbContext } from "../model/Runtime.dbContext";
4
4
  import type { WfJob } from "../model/WfJob.model";
5
5
  import { Schedules } from "./Schedules";
6
6
  import { TimerStep } from "./TimerStep";
7
- import { ToolStep } from "./ToolStep";
7
+ import { LEASE_MS, ToolStep } from "./ToolStep";
8
8
  import { WorkflowEngine } from "./WorkflowEngine";
9
9
 
10
- /** How long a claimed job belongs to its worker; if it dies, another one takes the job after that. */
11
- const LEASE_MS = 5 * 60_000;
12
10
  /** A job that keeps failing outside its Tool (the database, a lost version) fails its step after this many tries. */
13
11
  const MAX_JOB_ATTEMPTS = 5;
14
12
 
@@ -16,7 +14,7 @@ const MAX_JOB_ATTEMPTS = 5;
16
14
  export type JobFailureReport = (message: string, error: unknown, fields?: Readonly<Record<string, unknown>>) => void;
17
15
 
18
16
  /**
19
- * Claiming and running jobs of `bazis_wf_run_jobs`: the work of the worker, and
17
+ * Claiming and running jobs of `bazis_wf2_run_jobs`: the work of the worker, and
20
18
  * of `drain` in tests. Every job runs in its own DI scope.
21
19
  */
22
20
  export class WorkflowJobs {
@@ -19,7 +19,7 @@ export const RUNTIME_ENTITIES = [WfInstance, WfToken, WfJob, WfSubscription, WfH
19
19
  /** The tables of the Runtime part: bazis creates and checks them in the application database. */
20
20
  export const runtimeStore = defineOrmOwnedStoreV1({
21
21
  contract: "bazis.orm-owned-store/v1",
22
- storeKey: "bazis.workflow.runtime",
22
+ storeKey: "bazis.workflow.runtime.v2",
23
23
  formatVersion: 1,
24
- ownedScope: { schema: "public", tablePrefix: "bazis_wf_run_" },
24
+ ownedScope: { schema: "public", tablePrefix: "bazis_wf2_run_" },
25
25
  });
@@ -1,12 +1,12 @@
1
1
  import { Column, Entity, ForeignKey, Index, UUID } from "../../../orm";
2
2
  import { WfInstance } from "./WfInstance.model";
3
3
 
4
- export type HistoryType = "started" | "entered" | "left" | "tool" | "timer" | "event" | "error" | "retried" | "cancelled" | "completed";
4
+ export type HistoryType = "started" | "entered" | "left" | "tool" | "assigned" | "skipped" | "timer" | "event" | "error" | "retried" | "cancelled" | "completed";
5
5
 
6
6
  /** The journal of an instance, append only. Its v7 ids give the order. */
7
7
  @ForeignKey(() => WfInstance, { properties: ["instanceId"], onDelete: "cascade" })
8
8
  @Index(["instanceId", "id"])
9
- @Entity({ table: "bazis_wf_run_history" })
9
+ @Entity({ table: "bazis_wf2_run_history" })
10
10
  export class WfHistory {
11
11
  @UUID({ version: "v7" })
12
12
  id = "";