@sanoma-ai/workflows 0.5.0 → 0.7.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 +49 -15
- package/dist/apply.d.ts +2 -1
- package/dist/apply.js +29 -7
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +27 -2
- package/dist/approvals.js +73 -9
- package/dist/approvals.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +38 -30
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +24 -9
- package/dist/cedar.js.map +1 -1
- package/dist/change.d.ts +219 -0
- package/dist/change.js +150 -0
- package/dist/change.js.map +1 -0
- package/dist/chats.d.ts +43 -0
- package/dist/chats.js +48 -0
- package/dist/chats.js.map +1 -0
- package/dist/client.d.ts +52 -6
- package/dist/client.js +88 -47
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +47 -5
- package/dist/config.js +71 -7
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +83 -0
- package/dist/database.js +92 -0
- package/dist/database.js.map +1 -0
- package/dist/db.d.ts +31 -1
- package/dist/db.js +89 -7
- package/dist/db.js.map +1 -1
- package/dist/deadlines.d.ts +88 -0
- package/dist/deadlines.js +171 -0
- package/dist/deadlines.js.map +1 -0
- package/dist/define.d.ts +25 -4
- package/dist/define.js +40 -0
- package/dist/define.js.map +1 -1
- package/dist/demo.d.ts +28 -0
- package/dist/demo.js +72 -0
- package/dist/demo.js.map +1 -0
- package/dist/describe.d.ts +6 -3
- package/dist/describe.js +11 -7
- package/dist/describe.js.map +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/fake.d.ts +8 -0
- package/dist/fake.js +11 -1
- package/dist/fake.js.map +1 -1
- package/dist/grid.d.ts +34 -0
- package/dist/grid.js +62 -0
- package/dist/grid.js.map +1 -0
- package/dist/host.d.ts +13 -2
- package/dist/host.js +2 -2
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +16 -8
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +11 -2
- package/dist/ledger.js.map +1 -1
- package/dist/lint.js +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/op.d.ts +14 -2
- package/dist/op.js.map +1 -1
- package/dist/policy.d.ts +7 -1
- package/dist/policy.js +2 -1
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +13 -1
- package/dist/resource.js +9 -1
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +10 -1
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +5 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +38 -10
- package/dist/sandbox.js +66 -35
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +18 -1
- package/dist/scenario.js +77 -27
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +10 -1
- package/dist/scenarios.js +19 -4
- package/dist/scenarios.js.map +1 -1
- package/dist/schedules.d.ts +66 -0
- package/dist/schedules.js +158 -0
- package/dist/schedules.js.map +1 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.js +13 -3
- package/dist/schema.js.map +1 -1
- package/dist/shared.d.ts +38 -2
- package/dist/shared.js +50 -4
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +38 -1
- package/dist/sites.js +153 -26
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +61 -2
- package/dist/source.js.map +1 -1
- package/dist/supervisor.d.ts +38 -0
- package/dist/supervisor.js +77 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/version.d.ts +3 -1
- package/dist/version.js +3 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-service.d.ts +83 -0
- package/dist/wakeup-service.js +421 -0
- package/dist/wakeup-service.js.map +1 -0
- package/dist/wakeup.d.ts +84 -0
- package/dist/wakeup.js +87 -0
- package/dist/wakeup.js.map +1 -0
- package/dist/worker.d.ts +39 -1
- package/dist/worker.js +130 -39
- package/dist/worker.js.map +1 -1
- package/oxlint.json +2 -1
- package/package.json +8 -2
- package/src/apply.ts +25 -6
- package/src/approvals.ts +89 -16
- package/src/call.ts +40 -29
- package/src/cedar.ts +24 -6
- package/src/change.ts +222 -0
- package/src/chats.ts +114 -0
- package/src/client.ts +145 -55
- package/src/config.ts +110 -13
- package/src/database.ts +152 -0
- package/src/db.ts +108 -9
- package/src/deadlines.ts +234 -0
- package/src/define.ts +71 -4
- package/src/demo.ts +87 -0
- package/src/describe.ts +19 -7
- package/src/errors.ts +14 -0
- package/src/fake.ts +12 -0
- package/src/grid.ts +100 -0
- package/src/host.ts +13 -4
- package/src/index.ts +62 -3
- package/src/ledger.ts +15 -3
- package/src/lint.ts +1 -1
- package/src/op.ts +15 -2
- package/src/policy.ts +7 -1
- package/src/resource.ts +22 -3
- package/src/resources.ts +12 -1
- package/src/run.ts +5 -0
- package/src/sandbox.ts +97 -37
- package/src/scenario.ts +97 -28
- package/src/scenarios.ts +21 -5
- package/src/schedules.ts +206 -0
- package/src/schema.ts +16 -3
- package/src/shared.ts +62 -4
- package/src/sites.ts +198 -33
- package/src/source.ts +58 -3
- package/src/supervisor.ts +107 -0
- package/src/version.ts +3 -1
- package/src/wakeup-service.ts +511 -0
- package/src/wakeup.ts +150 -0
- package/src/worker.ts +181 -44
package/src/host.ts
CHANGED
|
@@ -4,8 +4,17 @@ import type { z } from "zod";
|
|
|
4
4
|
// a Fly app) declares about itself, so hosts are swappable and one conformance kit decides
|
|
5
5
|
// whether one works. A host is a resource type like any other; this is what sets it apart.
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* How a host keeps the runtime up:
|
|
9
|
+
* - `long-lived`: the process is restarted if it exits, and never stopped for being quiet (Docker).
|
|
10
|
+
* - `stops-on-exit`: the host stops the instance when its process exits, and starts it again on
|
|
11
|
+
* the next request (Fly).
|
|
12
|
+
* - `stops-when-quiet`: the host stops the instance after a while without inbound requests, and
|
|
13
|
+
* starts it again on the next request (a free Render service, Cloudflare Containers).
|
|
14
|
+
*
|
|
15
|
+
* A host that stops needs a declared `Wakeup` for what is due at a time while it is stopped.
|
|
16
|
+
*/
|
|
17
|
+
export type HostLifetime = "long-lived" | "stops-on-exit" | "stops-when-quiet";
|
|
9
18
|
|
|
10
19
|
/** What a host can do, for choosing one. */
|
|
11
20
|
export interface HostCapabilities {
|
|
@@ -53,10 +62,10 @@ export interface HostSpec {
|
|
|
53
62
|
outputs: (state: Record<string, unknown>) => HostOutputs;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
const LIFETIMES: readonly HostLifetime[] = ["long-lived", "
|
|
65
|
+
const LIFETIMES: readonly HostLifetime[] = ["long-lived", "stops-on-exit", "stops-when-quiet"];
|
|
57
66
|
|
|
58
67
|
/** True when `schema` has a field at the dotted `path`, looking through optional, nullable and default wrappers. */
|
|
59
|
-
function hasPath(schema: z.ZodType, path: string): boolean {
|
|
68
|
+
export function hasPath(schema: z.ZodType, path: string): boolean {
|
|
60
69
|
let at: z.ZodType | undefined = schema;
|
|
61
70
|
for (const key of path.split(".")) {
|
|
62
71
|
at = unwrap(at);
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
isSvgElement,
|
|
15
15
|
type LoginContext,
|
|
16
16
|
type LoginHalf,
|
|
17
|
+
type ModelContext,
|
|
17
18
|
type Op,
|
|
18
19
|
type OpSpec,
|
|
19
20
|
type ResourceGroup,
|
|
@@ -33,6 +34,7 @@ export {
|
|
|
33
34
|
type Ctx,
|
|
34
35
|
type SleepRequest,
|
|
35
36
|
type StepOptions,
|
|
37
|
+
type Trigger,
|
|
36
38
|
type Use,
|
|
37
39
|
type WorkflowDefinition,
|
|
38
40
|
} from "./define.ts";
|
|
@@ -51,7 +53,14 @@ export {
|
|
|
51
53
|
type ResourceSpec,
|
|
52
54
|
withoutWriteOnly,
|
|
53
55
|
} from "./resource.ts";
|
|
54
|
-
export {
|
|
56
|
+
export {
|
|
57
|
+
declaredWakeup,
|
|
58
|
+
defineConfig,
|
|
59
|
+
resolveConfig,
|
|
60
|
+
resolveDatabaseUrl,
|
|
61
|
+
type ResolvedConfig,
|
|
62
|
+
type SanomaConfig,
|
|
63
|
+
} from "./config.ts";
|
|
55
64
|
// `sanoma` itself (the people types' constructors) is at `@sanoma-ai/workflows/resources`, for data files.
|
|
56
65
|
// `collection()` itself is at `@sanoma-ai/workflows/data`, for `resources/data.ts`; this tells one apart, as the launcher does.
|
|
57
66
|
export { isCollection } from "./collection.ts";
|
|
@@ -66,6 +75,16 @@ export {
|
|
|
66
75
|
type PlanStep,
|
|
67
76
|
} from "./apply.ts";
|
|
68
77
|
export { CALL_WORKFLOW, CallInput } from "./call-workflow.ts";
|
|
78
|
+
export {
|
|
79
|
+
type ChangeEvent,
|
|
80
|
+
ChangeFacts,
|
|
81
|
+
ChangeInput,
|
|
82
|
+
changeOps,
|
|
83
|
+
type ChangeReport,
|
|
84
|
+
type ChangeState,
|
|
85
|
+
runtimeHold,
|
|
86
|
+
touchesOf,
|
|
87
|
+
} from "./change.ts";
|
|
69
88
|
export { isDirect } from "./direct.ts";
|
|
70
89
|
export {
|
|
71
90
|
DRIFT_WORKFLOW,
|
|
@@ -113,6 +132,7 @@ export {
|
|
|
113
132
|
PolicyDeniedError,
|
|
114
133
|
RejectedError,
|
|
115
134
|
SanomaError,
|
|
135
|
+
WithdrawnError,
|
|
116
136
|
} from "./errors.ts";
|
|
117
137
|
export {
|
|
118
138
|
jsonlLedger,
|
|
@@ -123,10 +143,15 @@ export {
|
|
|
123
143
|
type LedgerRecord,
|
|
124
144
|
type LedgerStore,
|
|
125
145
|
} from "./ledger.ts";
|
|
126
|
-
export { ApprovalMessage, APPROVALS_EVENT, decisionEventOf } from "./approvals.ts";
|
|
146
|
+
export { ApprovalMessage, APPROVALS_EVENT, ApprovalWithdrawal, decisionEventOf } from "./approvals.ts";
|
|
127
147
|
export { RUNTIME_VERSION } from "./version.ts";
|
|
128
148
|
export type { RunArgs } from "./run.ts";
|
|
129
149
|
export { startWorker, type Worker, type WorkerOptions } from "./worker.ts";
|
|
150
|
+
export { superviseWorker, type Supervisor, type SupervisorOptions } from "./supervisor.ts";
|
|
151
|
+
export type { DeadlineSource, Deadlines, SelfStop, Wait } from "./deadlines.ts";
|
|
152
|
+
// Whether a wake is the one the instance's worker last registered, for the `/wake` route.
|
|
153
|
+
export { shouldWake } from "./db.ts";
|
|
154
|
+
export { nextFire, nextScheduledFire } from "./schedules.ts";
|
|
130
155
|
// `secret` itself is at `@sanoma-ai/workflows/resources`, for data files; this is the drivers' side.
|
|
131
156
|
export {
|
|
132
157
|
generateSecret,
|
|
@@ -137,10 +162,36 @@ export {
|
|
|
137
162
|
type Secret,
|
|
138
163
|
type Secrets,
|
|
139
164
|
} from "./secret.ts";
|
|
140
|
-
export {
|
|
165
|
+
export {
|
|
166
|
+
type CheckResult,
|
|
167
|
+
type Draft,
|
|
168
|
+
type DraftDiff,
|
|
169
|
+
resolveRevision,
|
|
170
|
+
SOURCE_RESOLVE,
|
|
171
|
+
type Source,
|
|
172
|
+
} from "./source.ts";
|
|
141
173
|
export type { HostCapabilities, HostLifetime, HostOutputs, HostSpec } from "./host.ts";
|
|
174
|
+
export {
|
|
175
|
+
checkDatabase,
|
|
176
|
+
type DatabaseCapabilities,
|
|
177
|
+
type DatabaseOutputs,
|
|
178
|
+
type DatabaseSpec,
|
|
179
|
+
databaseUrlSecret,
|
|
180
|
+
storeDatabaseUrl,
|
|
181
|
+
} from "./database.ts";
|
|
182
|
+
export type { Chat, Chats } from "./chats.ts";
|
|
183
|
+
export {
|
|
184
|
+
checkWakeup,
|
|
185
|
+
type DeclaredWakeup,
|
|
186
|
+
type WakeupOutputs,
|
|
187
|
+
type WakeupRegistration,
|
|
188
|
+
type WakeupSpec,
|
|
189
|
+
wakeGeneration,
|
|
190
|
+
wakeupHostTypes,
|
|
191
|
+
} from "./wakeup.ts";
|
|
142
192
|
export {
|
|
143
193
|
type CallOptions,
|
|
194
|
+
type ClientOptions,
|
|
144
195
|
SanomaClient,
|
|
145
196
|
type RunsFilter,
|
|
146
197
|
type RunSummary,
|
|
@@ -156,11 +207,18 @@ export {
|
|
|
156
207
|
ANONYMOUS_PRINCIPAL,
|
|
157
208
|
approverLabel,
|
|
158
209
|
approverOf,
|
|
210
|
+
CHANGE_CLOSE,
|
|
211
|
+
CHANGE_EVENT,
|
|
212
|
+
CHANGE_MERGE,
|
|
213
|
+
CHANGE_OPEN,
|
|
214
|
+
CHANGE_REVIEW,
|
|
215
|
+
CHANGE_WORKFLOW,
|
|
159
216
|
declaresPeople,
|
|
160
217
|
type PeopleResource,
|
|
161
218
|
personOf,
|
|
162
219
|
principalOf,
|
|
163
220
|
RUNTIME_AGENTS,
|
|
221
|
+
SCHEDULE_AGENT,
|
|
164
222
|
credentialReady,
|
|
165
223
|
DEFAULT_SLEEP_NAME,
|
|
166
224
|
ENDED_STATUSES,
|
|
@@ -175,6 +233,7 @@ export {
|
|
|
175
233
|
type RunStatus,
|
|
176
234
|
VISITORS,
|
|
177
235
|
visitorPrincipal,
|
|
236
|
+
warningLine,
|
|
178
237
|
} from "./shared.ts";
|
|
179
238
|
// describeConfig, with the outline and every problem lintWorkflow finds, lives at
|
|
180
239
|
// `@sanoma-ai/workflows/describe`: what a UI renders from, apart from what runs. The worker reads
|
package/src/ledger.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { errorMessage } from "./shared.ts";
|
|
|
17
17
|
* decision, every approval, and how it ended. Records are append-only.
|
|
18
18
|
*
|
|
19
19
|
* `id` is deterministic (`<runId>:<type>:<seq>`, `<runId>:<type>:<approvalId>` for an
|
|
20
|
-
* approval requested or
|
|
20
|
+
* approval requested, decided or withdrawn, `<runId>:approval.refused:<approvalId>:refused:<n>` for
|
|
21
21
|
* the nth message an approval ignored) so that when DBOS replays a run after a restart,
|
|
22
22
|
* the records it writes again have the same ids and the store skips them. `seq` orders a
|
|
23
23
|
* run's records. `v` is the record format's version; `app` is the config's `appName`.
|
|
@@ -34,7 +34,12 @@ export type LedgerRecord = {
|
|
|
34
34
|
/** Set on every record written while a `ctx.all` member runs; absent outside one. */
|
|
35
35
|
group?: LedgerGroup;
|
|
36
36
|
} & (
|
|
37
|
-
| {
|
|
37
|
+
| {
|
|
38
|
+
type: "run.started";
|
|
39
|
+
input: unknown;
|
|
40
|
+
/** For a run its schedule started (`trigger: { schedule }`): the tick's time, ISO 8601, which may be before `at`. */
|
|
41
|
+
scheduledFor?: string;
|
|
42
|
+
}
|
|
38
43
|
| {
|
|
39
44
|
type: "op.called";
|
|
40
45
|
op: string;
|
|
@@ -61,7 +66,7 @@ export type LedgerRecord = {
|
|
|
61
66
|
durationMs: number;
|
|
62
67
|
/** Which try produced the output or the final error, from 1. Idempotent operations are retried. */
|
|
63
68
|
attempt?: number;
|
|
64
|
-
/** The approval that held the call, when its rejection stopped it. */
|
|
69
|
+
/** The approval that held the call, when its rejection or withdrawal stopped it. */
|
|
65
70
|
approval?: string;
|
|
66
71
|
}
|
|
67
72
|
| {
|
|
@@ -94,6 +99,13 @@ export type LedgerRecord = {
|
|
|
94
99
|
/** Set when `by` started the run: nobody else could decide (`mayDecide`). */
|
|
95
100
|
alone?: true;
|
|
96
101
|
}
|
|
102
|
+
/** The approval no longer stands: withdrawn while pending, or retired by its run once approved. */
|
|
103
|
+
| {
|
|
104
|
+
type: "approval.withdrawn";
|
|
105
|
+
approval: string;
|
|
106
|
+
/** Who withdrew it, when someone did rather than its run. */ by?: string;
|
|
107
|
+
reason: string;
|
|
108
|
+
}
|
|
97
109
|
/**
|
|
98
110
|
* A `ctx.sleep` began; `until` is when it ends, in ms since the epoch: the time asked for, or
|
|
99
111
|
* for a duration the record's `at` plus the duration, as the runtime saw it. Written
|
package/src/lint.ts
CHANGED
|
@@ -55,7 +55,7 @@ const NOT_ALLOWED =
|
|
|
55
55
|
* copy, which is no instance of them, so a branch on it goes another way than the first time
|
|
56
56
|
* and the run's steps fall out of step. Read the code with `errorCode(err)` instead.
|
|
57
57
|
*/
|
|
58
|
-
const ERROR_CLASSES = new Set(["DriverError", "SanomaError", "PolicyDeniedError", "RejectedError"]);
|
|
58
|
+
const ERROR_CLASSES = new Set(["DriverError", "SanomaError", "PolicyDeniedError", "RejectedError", "WithdrawnError"]);
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Checks a workflow file. Imports allowed: the names in `WORKFLOW_IMPORTS` and any
|
package/src/op.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderV4 } from "@ai-sdk/provider";
|
|
1
2
|
import type { z } from "zod";
|
|
2
3
|
import type { Fake } from "./fake.ts";
|
|
3
4
|
import type { Secrets } from "./secret.ts";
|
|
@@ -306,8 +307,8 @@ export interface Driver {
|
|
|
306
307
|
* What a connector package attaches to its connector (`withSetup`), for the config's resolver
|
|
307
308
|
* to call: the config then names the connector alone, and never a driver, a fake, steps or a
|
|
308
309
|
* login. Any may be missing: a workflow using an operation of a connector with no driver is
|
|
309
|
-
* refused, a connector with no fake has none for sandbox runs,
|
|
310
|
-
* no
|
|
310
|
+
* refused, a connector with no fake has none for sandbox runs, one with no login half signs no
|
|
311
|
+
* visitor in, and one with no model half gives the app no models.
|
|
311
312
|
*/
|
|
312
313
|
export interface ConnectorSetup {
|
|
313
314
|
/** Makes the live driver, which reads its credentials from the environment when called: once per resolved config. */
|
|
@@ -321,8 +322,20 @@ export interface ConnectorSetup {
|
|
|
321
322
|
* starts; a login that needs tables migrates them here (serialized, idempotent).
|
|
322
323
|
*/
|
|
323
324
|
login?: (context: LoginContext) => LoginHalf | Promise<LoginHalf>;
|
|
325
|
+
/**
|
|
326
|
+
* Makes the connector's AI SDK provider, the vendor's adapter as the transport: by the app,
|
|
327
|
+
* once per resolved config and client (again after one that throws), whose registry names its
|
|
328
|
+
* models `<vendor>/<model-id>`. It answers `languageModel("default")` with the connector's own
|
|
329
|
+
* default model, and reads its credentials through `context.secrets` when a model is called, so
|
|
330
|
+
* a key set on the connector page later is used (unless the environment sets one, which wins),
|
|
331
|
+
* and a missing one fails that call.
|
|
332
|
+
*/
|
|
333
|
+
model?: (context: ModelContext) => ProviderV4 | Promise<ProviderV4>;
|
|
324
334
|
}
|
|
325
335
|
|
|
336
|
+
/** What the app gives a connector's model half (`ConnectorSetup.model`): the secrets, as a login half reads them. */
|
|
337
|
+
export type ModelContext = Pick<LoginContext, "secrets">;
|
|
338
|
+
|
|
326
339
|
/** What the app gives a connector's login half (`ConnectorSetup.login`) when it makes it for a public site. */
|
|
327
340
|
export interface LoginContext {
|
|
328
341
|
/** The company's database (`resolved.databaseUrl`), where a login like Better Auth keeps its users. */
|
package/src/policy.ts
CHANGED
|
@@ -32,6 +32,11 @@ export interface PolicyCall {
|
|
|
32
32
|
run: { id: string; workflow: string; approvals: readonly ApprovalState[] };
|
|
33
33
|
/** For a collection's `save` or `delete`: the record as it was, when there was one. */
|
|
34
34
|
before?: unknown;
|
|
35
|
+
/**
|
|
36
|
+
* The attributes of the entity the call acts on, when its action is on one with attributes: a
|
|
37
|
+
* `sanoma.change.merge`'s `Sanoma::Change`, the change's facts (`ChangeFacts`).
|
|
38
|
+
*/
|
|
39
|
+
entity?: Record<string, unknown>;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
@@ -145,7 +150,8 @@ export const denyAll = (reason: string): Policy => {
|
|
|
145
150
|
/**
|
|
146
151
|
* True when an approval in the run was approved, covers the operation, and was addressed to
|
|
147
152
|
* `approver`. Check all three: a workflow can request an approval covering any operation from
|
|
148
|
-
* anyone it names, so an approval's `covers` alone does not say the right person approved.
|
|
153
|
+
* anyone it names, so an approval's `covers` alone does not say the right person approved. A
|
|
154
|
+
* withdrawn approval is no longer approved, so it lifts nothing.
|
|
149
155
|
*/
|
|
150
156
|
export function approvedFor(approvals: readonly ApprovalState[], opId: string, approver: Approver): boolean {
|
|
151
157
|
return approvals.some(
|
package/src/resource.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { checkDatabase, type DatabaseSpec } from "./database.ts";
|
|
2
3
|
import { checkHost, type HostSpec } from "./host.ts";
|
|
3
4
|
import { type Connector, type OpSpec, RESOURCE, VENDOR } from "./op.ts";
|
|
4
5
|
import { isSanomaSecret, isSecret, type Secret } from "./secret.ts";
|
|
6
|
+
import { checkWakeup, type WakeupSpec } from "./wakeup.ts";
|
|
5
7
|
|
|
6
8
|
/** Which fields of a resource are flagged, by dotted path (`pages.html_url`; a list's items have no index). */
|
|
7
9
|
export interface ResourceFields {
|
|
@@ -79,6 +81,16 @@ export interface ResourceSpec<S extends z.ZodObject = z.ZodObject> {
|
|
|
79
81
|
writable?: boolean;
|
|
80
82
|
/** Set when the type meets the `Host` contract: it runs the company. Such a type is `writable` and `stateless`. */
|
|
81
83
|
host?: HostSpec;
|
|
84
|
+
/**
|
|
85
|
+
* Set when the type meets the `Wakeup` contract: it wakes a stopped instance at a time. Such a
|
|
86
|
+
* type is `writable`, and its connector has the operation `register` names.
|
|
87
|
+
*/
|
|
88
|
+
wakeup?: WakeupSpec;
|
|
89
|
+
/**
|
|
90
|
+
* Set when the type meets the `Database` contract: it holds the company's database. Such a type
|
|
91
|
+
* is `writable`, not `stateless`, and its `afterCreate` calls the operation `storeUrl` names.
|
|
92
|
+
*/
|
|
93
|
+
database?: DatabaseSpec;
|
|
82
94
|
/**
|
|
83
95
|
* True when its objects hold no data, so one gone at the vendor is created again by apply
|
|
84
96
|
* without asking (a host running the company's code); otherwise apply asks first, as a
|
|
@@ -269,10 +281,13 @@ export interface Resource<
|
|
|
269
281
|
extends
|
|
270
282
|
Readonly<
|
|
271
283
|
Required<
|
|
272
|
-
Omit<
|
|
284
|
+
Omit<
|
|
285
|
+
ResourceSpec<S>,
|
|
286
|
+
"identity" | "find" | "writable" | "host" | "wakeup" | "database" | "stateless" | "afterCreate" | "vendorless"
|
|
287
|
+
>
|
|
273
288
|
>
|
|
274
289
|
>,
|
|
275
|
-
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "afterCreate">> {
|
|
290
|
+
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "wakeup" | "database" | "afterCreate">> {
|
|
276
291
|
(desired: Declaring<z.input<S>, R>): Declared<Id, z.input<S>>;
|
|
277
292
|
/** `read` and `import`, each effect `read`, idempotent, with the id as `target`; for a writable type, the change operations too. */
|
|
278
293
|
readonly ops: ResourceOps<S, W>;
|
|
@@ -536,10 +551,12 @@ export function defineResource<
|
|
|
536
551
|
>(
|
|
537
552
|
spec: ResourceSpec<S> & { vendor: V; type: T; fields: { references?: R }; writable?: W },
|
|
538
553
|
): Resource<S, `${V}.${T}`, R, W> {
|
|
539
|
-
const { vendor, type, schema, fields, find, identity, host } = spec;
|
|
554
|
+
const { vendor, type, schema, fields, find, identity, host, wakeup, database } = spec;
|
|
540
555
|
const writable = (spec.writable ?? false) as W;
|
|
541
556
|
const what: `${V}.${T}` = `${vendor}.${type}`;
|
|
542
557
|
if (host) checkHost(what, host, spec);
|
|
558
|
+
if (wakeup) checkWakeup(what, wakeup, spec);
|
|
559
|
+
if (database) checkDatabase(what, database, spec);
|
|
543
560
|
if (spec.vendorless && (writable || host))
|
|
544
561
|
throw new Error(`${what}: a vendorless type has no object to change or host`);
|
|
545
562
|
const title = spec.title.toLowerCase();
|
|
@@ -677,6 +694,8 @@ export function defineResource<
|
|
|
677
694
|
normalize: spec.normalize ?? ((state: State, desired: z.input<S>) => compareDeclared(fields, state, desired)),
|
|
678
695
|
writable,
|
|
679
696
|
...(host && { host }),
|
|
697
|
+
...(wakeup && { wakeup }),
|
|
698
|
+
...(database && { database }),
|
|
680
699
|
stateless: spec.stateless ?? host !== undefined,
|
|
681
700
|
vendorless: spec.vendorless ?? false,
|
|
682
701
|
...(spec.afterCreate && { afterCreate: spec.afterCreate }),
|
package/src/resources.ts
CHANGED
|
@@ -112,7 +112,7 @@ export interface DeclaredPeople {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/** A file that imports the runtime's constructors, `sanoma` (by any local name): the files `readPeople` reads. */
|
|
115
|
-
const DECLARES_PEOPLE = /\{[^}]*\bsanoma\b[^}]*\}\s*from\s*["'](?:sanoma|@sanoma-ai\/workflows)\/resources["']/;
|
|
115
|
+
export const DECLARES_PEOPLE = /\{[^}]*\bsanoma\b[^}]*\}\s*from\s*["'](?:sanoma|@sanoma-ai\/workflows)\/resources["']/;
|
|
116
116
|
/** The last read of the people under each root, by what its data files were then (`filesStamp`). */
|
|
117
117
|
const peopleRead = new Map<string, { stamp: string; read: DeclaredPeople }>();
|
|
118
118
|
|
|
@@ -424,6 +424,17 @@ export function readDataFiles(
|
|
|
424
424
|
done.set(id, undefined);
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
|
+
// An instance registers with one wakeup: several are each a problem, and left out, as is any that names one.
|
|
428
|
+
const wakeups = [...declarations].filter(
|
|
429
|
+
([, { file, exp }]) => types.get(`${vendors.get(file)!.get(exp.vendor)}.${exp.type}`)?.wakeup,
|
|
430
|
+
);
|
|
431
|
+
if (wakeups.length > 1) {
|
|
432
|
+
for (const [id, { file, exp }] of wakeups) {
|
|
433
|
+
const others = wakeups.filter(([other]) => other !== id).map(([other]) => other);
|
|
434
|
+
file.problem(exp.start, `an instance has one wakeup: leave out this one or ${others.join(", ")}`);
|
|
435
|
+
done.set(id, undefined);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
427
438
|
const keys = new Set([...files.values()].flatMap((f) => f.data.declared.map((name) => `${f.rel}#${name}`)));
|
|
428
439
|
const orphans = Object.entries(lock.bindings)
|
|
429
440
|
.filter(([key]) => !keys.has(key))
|
package/src/run.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { Collection } from "./collection.ts";
|
|
3
|
+
import type { DeadlineRegistry } from "./deadlines.ts";
|
|
3
4
|
import type { ApprovalState, Approver, Principal, WorkflowDefinition } from "./define.ts";
|
|
4
5
|
import type { Fake } from "./fake.ts";
|
|
5
6
|
import type { LedgerStore } from "./ledger.ts";
|
|
@@ -45,6 +46,8 @@ export interface WorkerState {
|
|
|
45
46
|
drivers: Map<string, DriverFn>;
|
|
46
47
|
/** The makers of the config's fake vendors, by vendor id: a sandbox run makes its own of each when it is seeded. */
|
|
47
48
|
fakes: Map<string, () => Fake<any, any>>;
|
|
49
|
+
/** The instance's fakes, by vendor id, when it runs on them (`fakes: "instance"`): its sandbox runs seed and call these, not fakes of their own. */
|
|
50
|
+
instanceFakes?: ReadonlyMap<string, Fake<any, any>>;
|
|
48
51
|
/** Where sandbox runs' scenarios are, the resource types their steps may name, and the steps the connectors ship. */
|
|
49
52
|
scenarios?: URL;
|
|
50
53
|
/** The worker's `source`, when it was given one. */
|
|
@@ -75,6 +78,8 @@ export interface WorkerState {
|
|
|
75
78
|
* how it really ends (see `isInfrastructureError`).
|
|
76
79
|
*/
|
|
77
80
|
stopped: boolean;
|
|
81
|
+
/** Which of its runs are executing and which wait, until when: what the worker's `idle` reads. */
|
|
82
|
+
deadlines: DeadlineRegistry;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
/** What a run keeps in memory. Rebuilt the same way when DBOS replays the run. */
|
package/src/sandbox.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { opIdsOf } from "./define.ts";
|
|
|
4
4
|
import { SanomaError } from "./errors.ts";
|
|
5
5
|
import type { Fake, ResourceFake } from "./fake.ts";
|
|
6
6
|
import { entry, type LedgerRecord, write } from "./ledger.ts";
|
|
7
|
-
import type { DriverFn } from "./op.ts";
|
|
7
|
+
import type { CallContext, DriverFn, Op } from "./op.ts";
|
|
8
8
|
import type { Run } from "./run.ts";
|
|
9
9
|
import { memorySecrets } from "./secret.ts";
|
|
10
10
|
import { type Given, loadScenarios, type Scenario } from "./scenario.ts";
|
|
@@ -58,7 +58,7 @@ export function seedResource(
|
|
|
58
58
|
scenario: string,
|
|
59
59
|
copy: ResourceFake,
|
|
60
60
|
given: Extract<Given, { resource: string }>,
|
|
61
|
-
):
|
|
61
|
+
): Seeds[number] {
|
|
62
62
|
const { resource, type, name } = given;
|
|
63
63
|
try {
|
|
64
64
|
if ("gone" in given) {
|
|
@@ -77,16 +77,81 @@ export function seedResource(
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** What a scenario's `Given` steps seeded, as its `scenario.seeded` record has it. */
|
|
81
|
+
export type Seeds = Extract<LedgerRecord, { type: "scenario.seeded" }>["seeds"];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Where a scenario's `Given` steps go (`applyGivens`): a sandbox run's own fakes (`run`), or the
|
|
85
|
+
* instance a demo runs (`instance`), its collections in the config's database and every vendor
|
|
86
|
+
* on its one fake, which every run shares, so a fault, injected into a call some other run would
|
|
87
|
+
* make, is refused there.
|
|
88
|
+
*/
|
|
89
|
+
export interface GivenTarget {
|
|
90
|
+
kind: "run" | "instance";
|
|
91
|
+
/** What a `Given` operation is called through, by operation id. */
|
|
92
|
+
drivers: ReadonlyMap<string, DriverFn>;
|
|
93
|
+
/** The fakes, by vendor id: what a `Given` resource is put in, and a `run`'s faults injected into. */
|
|
94
|
+
fakes: ReadonlyMap<string, Fake<unknown>>;
|
|
95
|
+
/** The operations by id, for a fault's vendor. */
|
|
96
|
+
ops: ReadonlyMap<string, Op>;
|
|
97
|
+
/** The context of the call the `i`th `Given` makes. */
|
|
98
|
+
call(i: number, op: string): CallContext;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Applies the scenario's `Given` steps to the target: calls each operation through its drivers,
|
|
103
|
+
* puts or removes each resource in its vendor's fake, then, for a `run`, injects the faults.
|
|
104
|
+
* Checks first that every operation it seeds has a driver there, that every resource's vendor has
|
|
105
|
+
* a fake holding resources, and, for an `instance`, that it injects no fault. Returns what it
|
|
106
|
+
* seeded, in order.
|
|
107
|
+
*/
|
|
108
|
+
export async function applyGivens(scenario: Pick<Scenario, "name" | "given">, target: GivenTarget): Promise<Seeds> {
|
|
109
|
+
const { name, given } = scenario;
|
|
110
|
+
const fault = given.find((g) => "fault" in g);
|
|
111
|
+
if (fault && target.kind === "instance") {
|
|
112
|
+
throw new SanomaError(
|
|
113
|
+
"invalid_input",
|
|
114
|
+
`Scenario "${name}" injects a fault ("${fault.step}"), but its Givens go to the instance, whose fakes every run shares`,
|
|
115
|
+
{ scenario: name, step: fault.step },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const missing = [...new Set(given.flatMap((g) => ("op" in g && !target.drivers.has(g.op) ? [g.op] : [])))];
|
|
119
|
+
if (missing.length) {
|
|
120
|
+
throw new SanomaError(
|
|
121
|
+
"invalid_input",
|
|
122
|
+
`Scenario "${name}" seeds ${missing.join(", ")}, but no fake its connectors attach implements ${missing.length > 1 ? "them" : "it"}`,
|
|
123
|
+
{ scenario: name, missing },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const plain = missingResourceFake(scenario, target.fakes);
|
|
127
|
+
if (plain) throw plain;
|
|
128
|
+
const seeded: Seeds = [];
|
|
129
|
+
for (const [i, g] of given.entries()) {
|
|
130
|
+
if ("fault" in g) continue;
|
|
131
|
+
if ("op" in g) {
|
|
132
|
+
const output = await target.drivers.get(g.op)!(g.input, target.call(i, g.op));
|
|
133
|
+
seeded.push({ op: g.op, input: g.input, output });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Checked above: the vendor's fake holds resources.
|
|
137
|
+
seeded.push(seedResource(name, target.fakes.get(g.vendor) as Fake<unknown> & ResourceFake, g));
|
|
138
|
+
}
|
|
139
|
+
for (const g of given) {
|
|
140
|
+
if ("fault" in g) target.fakes.get(target.ops.get(g.op)!.vendor);
|
|
141
|
+
}
|
|
142
|
+
return seeded;
|
|
143
|
+
}
|
|
144
|
+
|
|
80
145
|
/**
|
|
81
146
|
* Seeds a sandbox run of the scenario `name`, from the worker, in one step: reads the scenario,
|
|
82
|
-
* checks that it runs this workflow,
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* run whose seeding is replayed from it has none, and fails,
|
|
89
|
-
* survive a worker restart.
|
|
147
|
+
* checks that it runs this workflow, picks the run's fakes, checks that every operation the
|
|
148
|
+
* workflow uses has one, then applies the scenario's `Given` steps to them
|
|
149
|
+
* (`applyGivens`) and puts their functions on the run. The run then calls those only. Its fakes
|
|
150
|
+
* are its own (each vendor's maker, called once for the run), so sandbox runs share no state when
|
|
151
|
+
* the makers make a new fake each; on an instance that runs on fakes (`instanceFakes`) they are
|
|
152
|
+
* the instance's, which its sandbox runs share, as its live drivers are. The step's checkpoint
|
|
153
|
+
* never stands in for a run's own: a run whose seeding is replayed from it has none, and fails,
|
|
154
|
+
* since a sandbox run does not survive a worker restart; on an instance's fakes it goes on with them.
|
|
90
155
|
*/
|
|
91
156
|
export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
92
157
|
const { state } = run;
|
|
@@ -103,19 +168,22 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
|
103
168
|
throw new SanomaError("invalid_input", `No scenario named "${name}"${why}`, { scenario: name });
|
|
104
169
|
}
|
|
105
170
|
if (scenario.workflow !== run.workflow) {
|
|
106
|
-
|
|
171
|
+
const runs = scenario.workflow === undefined ? "is a world, which runs nothing" : `runs ${scenario.workflow}`;
|
|
172
|
+
throw new SanomaError("invalid_input", `Scenario "${name}" ${runs}, not ${run.workflow}`, {
|
|
107
173
|
scenario: name,
|
|
108
|
-
workflow: scenario.workflow,
|
|
174
|
+
...(scenario.workflow !== undefined && { workflow: scenario.workflow }),
|
|
109
175
|
});
|
|
110
176
|
}
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
state.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
177
|
+
const instance = state.instanceFakes;
|
|
178
|
+
const fakes = instance ?? new Map([...state.fakes].map(([vendor, make]) => [vendor, make()]));
|
|
179
|
+
const drivers = instance
|
|
180
|
+
? state.drivers
|
|
181
|
+
: indexDrivers(
|
|
182
|
+
[...fakes.values()].map((f) => f.driver),
|
|
183
|
+
state.ops,
|
|
184
|
+
);
|
|
185
|
+
// What the scenario seeds is checked by `applyGivens`.
|
|
186
|
+
const missing = opIdsOf(state.workflows.get(run.workflow)!).filter((id) => !drivers.has(id));
|
|
119
187
|
if (missing.length) {
|
|
120
188
|
throw new SanomaError(
|
|
121
189
|
"invalid_input",
|
|
@@ -123,28 +191,20 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
|
123
191
|
{ scenario: name, missing },
|
|
124
192
|
);
|
|
125
193
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const output = await drivers.get(given.op)!(given.input, call);
|
|
134
|
-
seeded.push({ op: given.op, input: given.input, output });
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
// Checked above: the vendor's fake holds resources.
|
|
138
|
-
seeded.push(seedResource(name, fakes.get(given.vendor) as Fake<unknown> & ResourceFake, given));
|
|
139
|
-
}
|
|
140
|
-
for (const given of scenario.given) {
|
|
141
|
-
if ("fault" in given) fakes.get(state.ops.get(given.op)!.vendor);
|
|
142
|
-
}
|
|
194
|
+
const seeded = await applyGivens(scenario, {
|
|
195
|
+
kind: instance ? "instance" : "run",
|
|
196
|
+
drivers,
|
|
197
|
+
fakes,
|
|
198
|
+
ops: state.ops,
|
|
199
|
+
call: (i, opId) => ({ idempotencyKey: `${run.id}:seed:${i}`, runId: run.id, opId, attempt: 1, secrets }),
|
|
200
|
+
});
|
|
143
201
|
copies = drivers;
|
|
144
202
|
return { seeds: seeded };
|
|
145
203
|
},
|
|
146
204
|
{ name: "sandbox:seed" },
|
|
147
205
|
);
|
|
206
|
+
// Replayed on an instance that runs on fakes: they are the instance's, still there in this process.
|
|
207
|
+
if (!copies && state.instanceFakes) copies = state.drivers;
|
|
148
208
|
if (!copies) {
|
|
149
209
|
// Replayed from its checkpoint (after a worker restart, or in a fork), so the run has no fakes.
|
|
150
210
|
// The records written before the restart hold the seqs from here on: the failure goes after them.
|