bazis 0.132.0 → 0.134.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/core/agent/AgentToolExecutor.ts +5 -0
- package/core/agent/Tool.contract.ts +2 -2
- package/core/agent/metadata.ts +11 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/di-generate.ts +8 -0
- package/core/tools/StandardTools.module.ts +14 -0
- package/core/tools/generated/deps.ts +10 -0
- package/core/tools/index.ts +8 -0
- package/core/tools/orm/OrmEntity.references.ts +71 -0
- package/core/tools/orm/ormEntityTools.ts +423 -0
- package/core/workflow/contracts/WorkflowError.ts +2 -1
- package/core/workflow/contracts/WorkflowPartitions.ts +29 -0
- package/core/workflow/contracts/WorkflowProcessUsage.ts +8 -0
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +7 -2
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -2
- package/core/workflow/definitions_module/access/ProcessPartition.lookup.ts +27 -0
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +5 -4
- package/core/workflow/definitions_module/events/Definitions.events.ts +1 -1
- package/core/workflow/definitions_module/http/Processes.controller.ts +21 -5
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +10 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +28 -2
- package/core/workflow/generated/deps.ts +12 -2
- package/core/workflow/index.ts +5 -0
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +9 -0
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +2 -2
- package/core/workflow/runtime_module/events/Runtime.events.ts +4 -4
- package/core/workflow/runtime_module/http/Instances.controller.ts +14 -2
- package/core/workflow/runtime_module/internal/ChildCalls.ts +186 -0
- package/core/workflow/runtime_module/internal/EventDelivery.ts +234 -0
- package/core/workflow/runtime_module/internal/GraphCache.ts +7 -1
- package/core/workflow/runtime_module/internal/RunLimits.ts +134 -0
- package/core/workflow/runtime_module/internal/RuntimeProcessUsage.ts +13 -0
- package/core/workflow/runtime_module/internal/Schedules.ts +97 -30
- package/core/workflow/runtime_module/internal/TimerStep.ts +11 -1
- package/core/workflow/runtime_module/internal/ToolStep.ts +4 -4
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +544 -132
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +44 -6
- package/core/workflow/runtime_module/model/WfHistory.model.ts +1 -1
- package/core/workflow/runtime_module/model/WfInstance.model.ts +7 -4
- package/core/workflow/runtime_module/model/WfJob.model.ts +18 -8
- package/core/workflow/runtime_module/model/WfToken.model.ts +5 -3
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +59 -4
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +72 -42
- package/core/workflow/tasks_module/events/TaskEvents.ts +4 -8
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +1 -1
- package/library/workflow/ProcessDocument.ts +14 -0
- package/library/workflow/cron.ts +103 -24
- package/library/workflow/expressions.ts +5 -1
- package/library/workflow/index.ts +2 -2
- package/library/workflow/masking.ts +17 -3
- package/library/workflow/mermaid.ts +22 -6
- package/library/workflow/nodeKinds.ts +123 -9
- package/library/workflow/palette.ts +3 -1
- package/library/workflow/paths.ts +10 -4
- package/library/workflow/structure.ts +33 -4
- package/library/workflow/validateProcess.ts +141 -17
- package/package.json +2 -1
|
@@ -1,43 +1,76 @@
|
|
|
1
|
-
import { inputSpecOf, parseCron, validateForm } from "../../../../library/workflow";
|
|
1
|
+
import { inputSpecOf, parseCron, validateForm, type ProcessNode } from "../../../../library/workflow";
|
|
2
|
+
import type { ServiceProvider } from "../../../di";
|
|
3
|
+
import { WorkflowPartitions, type ProcessPartition } from "../../contracts/WorkflowPartitions";
|
|
2
4
|
import type { IWorkflowDefinitions, VersionDetails } from "../../definitions_module/services/IWorkflowDefinitions.service";
|
|
5
|
+
import type { WorkflowLanguages } from "../../internal/WorkflowLanguages";
|
|
3
6
|
import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
4
7
|
import { WfJob } from "../model/WfJob.model";
|
|
5
8
|
import type { WorkflowEngine } from "./WorkflowEngine";
|
|
6
9
|
|
|
7
10
|
const SCHEDULE_ACTOR = { kind: "system", id: "schedule" } as const;
|
|
11
|
+
const MINUTE = 60_000;
|
|
12
|
+
|
|
13
|
+
/** The `config` of a `scheduleStart`, as validation lets it through. */
|
|
14
|
+
interface ScheduleConfig {
|
|
15
|
+
readonly cron: string;
|
|
16
|
+
readonly timezone?: string;
|
|
17
|
+
readonly from?: string;
|
|
18
|
+
readonly until?: string;
|
|
19
|
+
readonly overlap?: "skip" | "queue" | "parallel";
|
|
20
|
+
readonly catchUp?: boolean;
|
|
21
|
+
readonly partitions?: "none" | "all" | readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One place a node fires in: a partition with its time zone. */
|
|
25
|
+
interface Target {
|
|
26
|
+
readonly partition: string | null;
|
|
27
|
+
readonly timezone: string | undefined;
|
|
28
|
+
}
|
|
8
29
|
|
|
9
30
|
/**
|
|
10
|
-
* Scheduled starts (`scheduleStart
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* starts the
|
|
14
|
-
* transaction, so a crash never starts one firing twice.
|
|
15
|
-
*
|
|
31
|
+
* Scheduled starts (`scheduleStart`). Every node of a latest published version
|
|
32
|
+
* has a `schedule` job per partition it runs in, due at its next firing.
|
|
33
|
+
* `reconcile` keeps the jobs in line with the catalog and `WorkflowPartitions`;
|
|
34
|
+
* `fire` starts the instance of a due job and plans the next firing in the same
|
|
35
|
+
* transaction, so a crash never starts one firing twice. With `catchUp` the next
|
|
36
|
+
* firing is planned from the one just made, so every missed firing runs;
|
|
37
|
+
* otherwise from now, and those missed while no worker ran are gone.
|
|
16
38
|
*/
|
|
17
39
|
export class Schedules {
|
|
18
40
|
constructor(
|
|
19
41
|
private readonly db: RuntimeDbContext,
|
|
20
42
|
private readonly engine: WorkflowEngine,
|
|
21
43
|
private readonly definitions: IWorkflowDefinitions,
|
|
44
|
+
private readonly languages: WorkflowLanguages,
|
|
45
|
+
/** The scope: the application's own `WorkflowPartitions` replaces the default. */
|
|
46
|
+
private readonly services: ServiceProvider,
|
|
22
47
|
) {}
|
|
23
48
|
|
|
24
|
-
/** Adds jobs for new scheduled
|
|
49
|
+
/** Adds jobs for new scheduled nodes and partitions and drops those no longer wanted. */
|
|
25
50
|
async reconcile(now = this.engine.now()): Promise<void> {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
51
|
+
const wanted = new Map<string, { readonly version: VersionDetails; readonly nodeId: string; readonly config: ScheduleConfig; readonly target: Target }>();
|
|
52
|
+
const invalid: string[] = [];
|
|
53
|
+
for (const version of await this.definitions.published()) {
|
|
54
|
+
for (const [nodeId, config] of scheduleNodes(version)) {
|
|
55
|
+
for (const target of await this.targetsOf(version.key, config)) {
|
|
56
|
+
if (parseCron(config.cron, target.timezone) === undefined) invalid.push(`${version.key}: time zone ${String(target.timezone)} of partition ${String(target.partition)}`);
|
|
57
|
+
else wanted.set(keyOf(version.id, nodeId, target.partition), { version, nodeId, config, target });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
28
61
|
const added: WfJob[] = [];
|
|
29
62
|
await this.db.transactionScope(async () => {
|
|
30
63
|
// One reconciler at a time across application instances.
|
|
31
64
|
await this.db.database.querySqlRaw("SELECT pg_advisory_xact_lock(hashtext('bazis.workflow.schedules'))");
|
|
32
65
|
const jobs = await this.db.jobs.asNoTracking().where((j) => j.kind.eq("schedule")).toList();
|
|
33
|
-
const stale = jobs.filter((job) =>
|
|
66
|
+
const stale = jobs.filter((job) => !wanted.has(keyOf(job.versionId, job.nodeId, job.partition))).map((job) => job.id);
|
|
34
67
|
if (stale.length > 0) await this.db.jobs.asNoTracking().where((j) => j.id.in(stale)).executeDelete();
|
|
35
|
-
const planned = new Set(jobs.map((job) => job.versionId));
|
|
36
|
-
for (const version of
|
|
37
|
-
if (planned.has(
|
|
38
|
-
const dueAt = nextFiring(
|
|
68
|
+
const planned = new Set(jobs.map((job) => keyOf(job.versionId, job.nodeId, job.partition)));
|
|
69
|
+
for (const [key, { version, nodeId, config, target }] of wanted) {
|
|
70
|
+
if (planned.has(key)) continue;
|
|
71
|
+
const dueAt = nextFiring(config, target, now);
|
|
39
72
|
if (dueAt === undefined) continue;
|
|
40
|
-
const job = Object.assign(new WfJob(), { id: Bun.randomUUIDv7(), kind: "schedule" as const, versionId: version.id, dueAt });
|
|
73
|
+
const job = Object.assign(new WfJob(), { id: Bun.randomUUIDv7(), kind: "schedule" as const, versionId: version.id, nodeId, partition: target.partition, dueAt });
|
|
41
74
|
this.db.jobs.add(job);
|
|
42
75
|
added.push(job);
|
|
43
76
|
}
|
|
@@ -45,40 +78,74 @@ export class Schedules {
|
|
|
45
78
|
});
|
|
46
79
|
// The jobs belong to the worker from now on: this context must not hold them.
|
|
47
80
|
for (const job of added) this.db.detach(job);
|
|
81
|
+
if (invalid.length > 0) throw new Error(`Scheduled starts are not planned for ${invalid.join("; ")}`);
|
|
48
82
|
}
|
|
49
83
|
|
|
50
|
-
/** A `schedule` job is due: start an instance
|
|
84
|
+
/** A `schedule` job is due: start an instance unless `overlap` says otherwise, and plan the next firing. */
|
|
51
85
|
async fire(job: WfJob): Promise<void> {
|
|
52
86
|
const version = job.versionId === null ? undefined : await this.definitions.versionById(job.versionId);
|
|
53
87
|
const latest = version === undefined ? undefined : (await this.definitions.published()).find((item) => item.id === version.id);
|
|
54
|
-
|
|
88
|
+
const config = latest === undefined || job.nodeId === null ? undefined : scheduleConfig(latest.document.nodes[job.nodeId]);
|
|
89
|
+
const target = latest === undefined || config === undefined ? undefined : (await this.targetsOf(latest.key, config)).find((item) => item.partition === job.partition);
|
|
90
|
+
if (!latest || !config || !target) {
|
|
55
91
|
await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
56
92
|
return;
|
|
57
93
|
}
|
|
58
|
-
const due = new Date(Math.floor(job.dueAt.getTime() /
|
|
94
|
+
const due = new Date(Math.floor(job.dueAt.getTime() / MINUTE) * MINUTE);
|
|
59
95
|
const graph = this.engine.cache(latest);
|
|
96
|
+
const { overlap = "parallel" } = config;
|
|
60
97
|
await this.db.transactionScope(async () => {
|
|
61
|
-
// Planned from now, not from the missed firing: a stopped worker does not catch up.
|
|
62
98
|
const now = this.engine.now();
|
|
63
|
-
const next = nextFiring(
|
|
99
|
+
const next = nextFiring(config, target, config.catchUp === true || now <= due ? due : now);
|
|
64
100
|
if (next === undefined) await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
65
101
|
else await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeUpdate({ dueAt: next, lockedUntil: null, attempts: 0, lastError: null });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await this.engine.
|
|
102
|
+
let queued = false;
|
|
103
|
+
if (overlap !== "parallel") {
|
|
104
|
+
await this.engine.lockProcess(latest.key);
|
|
105
|
+
if (await this.engine.scheduledLive(latest.key, target.partition) > 0) {
|
|
106
|
+
if (overlap === "skip") return;
|
|
107
|
+
queued = true;
|
|
108
|
+
}
|
|
69
109
|
}
|
|
110
|
+
await this.engine.begin(graph, job.nodeId!, { input: validateForm(inputSpecOf(latest.document), {}).value, partition: target.partition, businessKey: null, startedVia: "schedule", actor: SCHEDULE_ACTOR, queued, scheduledAt: due });
|
|
70
111
|
});
|
|
71
112
|
}
|
|
113
|
+
|
|
114
|
+
/** Where a node runs: `none` without a partition, `all` in every enabled one, a list in those not switched off. */
|
|
115
|
+
private async targetsOf(processKey: string, config: ScheduleConfig): Promise<readonly Target[]> {
|
|
116
|
+
const { partitions = "none", timezone } = config;
|
|
117
|
+
if (partitions === "none") return [{ partition: null, timezone }];
|
|
118
|
+
const known = await this.partitionsOf(processKey);
|
|
119
|
+
if (partitions === "all") return known.filter((item) => item.enabled).map((item) => ({ partition: item.partition, timezone: timezone ?? item.timezone }));
|
|
120
|
+
return partitions.flatMap((partition) => {
|
|
121
|
+
const entry = known.find((item) => item.partition === partition);
|
|
122
|
+
return entry?.enabled === false ? [] : [{ partition, timezone: timezone ?? entry?.timezone }];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async partitionsOf(processKey: string): Promise<readonly ProcessPartition[]> {
|
|
127
|
+
return await this.services.resolve(WorkflowPartitions).of(processKey, { partition: null, language: this.languages.default, actor: null });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function keyOf(versionId: string | null, nodeId: string | null, partition: string | null): string {
|
|
132
|
+
return JSON.stringify([versionId, nodeId, partition]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scheduleConfig(node: ProcessNode | undefined): ScheduleConfig | undefined {
|
|
136
|
+
return node?.kind === "scheduleStart" && typeof node.config?.cron === "string" ? node.config as unknown as ScheduleConfig : undefined;
|
|
72
137
|
}
|
|
73
138
|
|
|
74
|
-
function scheduleNodes(version: VersionDetails) {
|
|
139
|
+
function scheduleNodes(version: VersionDetails): readonly (readonly [string, ScheduleConfig])[] {
|
|
75
140
|
return Object.entries(version.document.nodes).flatMap(([id, node]) => {
|
|
76
|
-
const
|
|
77
|
-
return
|
|
141
|
+
const config = scheduleConfig(node);
|
|
142
|
+
return config === undefined ? [] : [[id, config] as const];
|
|
78
143
|
});
|
|
79
144
|
}
|
|
80
145
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
146
|
+
/** The first firing after `after` inside the period of the node. */
|
|
147
|
+
function nextFiring(config: ScheduleConfig, target: Target, after: Date): Date | undefined {
|
|
148
|
+
const from = config.from === undefined ? undefined : new Date(Date.parse(config.from) - MINUTE);
|
|
149
|
+
const next = parseCron(config.cron, target.timezone)?.next(from !== undefined && from > after ? from : after);
|
|
150
|
+
return next === undefined || (config.until !== undefined && next.getTime() > Date.parse(config.until)) ? undefined : next;
|
|
84
151
|
}
|
|
@@ -2,7 +2,7 @@ import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
|
2
2
|
import type { WfJob } from "../model/WfJob.model";
|
|
3
3
|
import type { WorkflowEngine } from "./WorkflowEngine";
|
|
4
4
|
|
|
5
|
-
/** One `timer` job: a `timerCatch` step or a boundary timer is due. A closed token drops it. */
|
|
5
|
+
/** One `timer` job: a `timerCatch` step or a boundary timer is due. A closed token drops it. A `loop` job works the same way. */
|
|
6
6
|
export class TimerStep {
|
|
7
7
|
constructor(
|
|
8
8
|
private readonly db: RuntimeDbContext,
|
|
@@ -18,4 +18,14 @@ export class TimerStep {
|
|
|
18
18
|
});
|
|
19
19
|
if (fired === null) await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
async continueLoop(job: WfJob): Promise<void> {
|
|
23
|
+
const went = job.instanceId === null || job.tokenId === null
|
|
24
|
+
? null
|
|
25
|
+
: await this.engine.inStep(job.instanceId, async (step) => {
|
|
26
|
+
await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
27
|
+
return await this.engine.continueLoop(step, job.tokenId!);
|
|
28
|
+
});
|
|
29
|
+
if (went === null) await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
30
|
+
}
|
|
21
31
|
}
|
|
@@ -4,13 +4,13 @@ import type { WorkflowToolMetadata } from "../../contracts/types";
|
|
|
4
4
|
import type { WorkflowTools } from "../../internal/WorkflowTools";
|
|
5
5
|
import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
6
6
|
import type { WfJob } from "../model/WfJob.model";
|
|
7
|
-
import { dataOf, type WorkflowEngine } from "./WorkflowEngine";
|
|
7
|
+
import { dataOf, runActorOf, type WorkflowEngine } from "./WorkflowEngine";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* One `tool` job: call the service task's Tool and apply the result. The Tool
|
|
11
11
|
* runs outside a transaction, the result is applied in a step transaction; a
|
|
12
12
|
* token closed meanwhile (a cancel) drops it. The Tool runs on behalf of who
|
|
13
|
-
* started the instance, with the token id as its idempotency key: after a crash
|
|
13
|
+
* started the instance or of the process (`runAs`), with the token id as its idempotency key: after a crash
|
|
14
14
|
* between the call and the step the call may repeat (TOOLS-001 §7).
|
|
15
15
|
*/
|
|
16
16
|
/** How long a claimed job belongs to its worker; if it dies, another one takes the job after that. */
|
|
@@ -35,7 +35,7 @@ export class ToolStep {
|
|
|
35
35
|
const graph = await this.engine.graphOf(instance.versionId);
|
|
36
36
|
const node = graph.document.nodes[token.nodeId];
|
|
37
37
|
const name = String(node?.config?.tool ?? "");
|
|
38
|
-
const input = resolveInput(node?.config?.input ?? {}, dataOf(instance, this.engine.now()));
|
|
38
|
+
const input = resolveInput(node?.config?.input ?? {}, dataOf(instance, this.engine.now(), token));
|
|
39
39
|
const timeoutMs = stepTimeoutMs(node?.config?.timeout, graph.document);
|
|
40
40
|
// A step may run longer than the lease: another worker must not take the job and call the Tool again meanwhile.
|
|
41
41
|
if (timeoutMs !== undefined && timeoutMs >= LEASE_MS) {
|
|
@@ -46,7 +46,7 @@ export class ToolStep {
|
|
|
46
46
|
? { callId: token.id, name, status: "error", error: { code: "TOOL_NOT_FOUND", message: `Tool ${name} is not available to processes` }, metadata: {} } as AgentToolResult
|
|
47
47
|
: await runner.run(name, input, {
|
|
48
48
|
idempotencyKey: token.id,
|
|
49
|
-
actor:
|
|
49
|
+
actor: runActorOf(graph.document, instance),
|
|
50
50
|
metadata: { instanceId: instance.id, processKey: instance.processKey, nodeId: token.nodeId, partition: instance.partition, test: false } satisfies WorkflowToolMetadata,
|
|
51
51
|
signal,
|
|
52
52
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|