@sanoma-ai/workflows 0.6.0 → 0.7.1

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 (166) hide show
  1. package/README.md +52 -14
  2. package/dist/apply.d.ts +2 -1
  3. package/dist/apply.js +29 -7
  4. package/dist/apply.js.map +1 -1
  5. package/dist/approvals.d.ts +27 -2
  6. package/dist/approvals.js +73 -9
  7. package/dist/approvals.js.map +1 -1
  8. package/dist/call.d.ts +3 -2
  9. package/dist/call.js +38 -30
  10. package/dist/call.js.map +1 -1
  11. package/dist/cedar.js +24 -9
  12. package/dist/cedar.js.map +1 -1
  13. package/dist/change.d.ts +219 -0
  14. package/dist/change.js +150 -0
  15. package/dist/change.js.map +1 -0
  16. package/dist/chats.d.ts +43 -0
  17. package/dist/chats.js +48 -0
  18. package/dist/chats.js.map +1 -0
  19. package/dist/client.d.ts +52 -6
  20. package/dist/client.js +88 -47
  21. package/dist/client.js.map +1 -1
  22. package/dist/config.d.ts +75 -8
  23. package/dist/config.js +103 -13
  24. package/dist/config.js.map +1 -1
  25. package/dist/constructors.d.ts +37 -1
  26. package/dist/constructors.js +6 -2
  27. package/dist/constructors.js.map +1 -1
  28. package/dist/database.d.ts +83 -0
  29. package/dist/database.js +92 -0
  30. package/dist/database.js.map +1 -0
  31. package/dist/db.d.ts +19 -1
  32. package/dist/db.js +51 -7
  33. package/dist/db.js.map +1 -1
  34. package/dist/deadlines.d.ts +91 -0
  35. package/dist/deadlines.js +165 -0
  36. package/dist/deadlines.js.map +1 -0
  37. package/dist/define.d.ts +25 -4
  38. package/dist/define.js +40 -0
  39. package/dist/define.js.map +1 -1
  40. package/dist/demo.d.ts +28 -0
  41. package/dist/demo.js +72 -0
  42. package/dist/demo.js.map +1 -0
  43. package/dist/describe.d.ts +5 -2
  44. package/dist/describe.js +10 -6
  45. package/dist/describe.js.map +1 -1
  46. package/dist/errors.d.ts +10 -1
  47. package/dist/errors.js +13 -0
  48. package/dist/errors.js.map +1 -1
  49. package/dist/fake.d.ts +34 -0
  50. package/dist/fake.js +134 -1
  51. package/dist/fake.js.map +1 -1
  52. package/dist/grid.d.ts +34 -0
  53. package/dist/grid.js +62 -0
  54. package/dist/grid.js.map +1 -0
  55. package/dist/host.d.ts +12 -2
  56. package/dist/host.js +2 -2
  57. package/dist/host.js.map +1 -1
  58. package/dist/index.d.ts +15 -8
  59. package/dist/index.js +10 -5
  60. package/dist/index.js.map +1 -1
  61. package/dist/ledger.d.ts +11 -2
  62. package/dist/ledger.js.map +1 -1
  63. package/dist/lint.js +2 -2
  64. package/dist/lint.js.map +1 -1
  65. package/dist/op.d.ts +21 -2
  66. package/dist/op.js.map +1 -1
  67. package/dist/person.d.ts +5 -0
  68. package/dist/person.js +3 -1
  69. package/dist/person.js.map +1 -1
  70. package/dist/policy.d.ts +7 -1
  71. package/dist/policy.js +2 -1
  72. package/dist/policy.js.map +1 -1
  73. package/dist/resource.d.ts +13 -1
  74. package/dist/resource.js +9 -1
  75. package/dist/resource.js.map +1 -1
  76. package/dist/resources.d.ts +2 -0
  77. package/dist/resources.js +15 -4
  78. package/dist/resources.js.map +1 -1
  79. package/dist/run.d.ts +5 -0
  80. package/dist/run.js.map +1 -1
  81. package/dist/sandbox.d.ts +38 -10
  82. package/dist/sandbox.js +66 -35
  83. package/dist/sandbox.js.map +1 -1
  84. package/dist/scenario.d.ts +18 -1
  85. package/dist/scenario.js +77 -27
  86. package/dist/scenario.js.map +1 -1
  87. package/dist/scenarios.d.ts +10 -1
  88. package/dist/scenarios.js +19 -4
  89. package/dist/scenarios.js.map +1 -1
  90. package/dist/schedules.d.ts +66 -0
  91. package/dist/schedules.js +158 -0
  92. package/dist/schedules.js.map +1 -0
  93. package/dist/schema.d.ts +6 -0
  94. package/dist/schema.js +13 -3
  95. package/dist/schema.js.map +1 -1
  96. package/dist/shared.d.ts +30 -2
  97. package/dist/shared.js +42 -4
  98. package/dist/shared.js.map +1 -1
  99. package/dist/source.d.ts +185 -2
  100. package/dist/source.js +9 -0
  101. package/dist/source.js.map +1 -1
  102. package/dist/supervisor.d.ts +39 -0
  103. package/dist/supervisor.js +68 -0
  104. package/dist/supervisor.js.map +1 -0
  105. package/dist/version.d.ts +3 -1
  106. package/dist/version.js +3 -1
  107. package/dist/version.js.map +1 -1
  108. package/dist/wakeup-driver.d.ts +128 -0
  109. package/dist/wakeup-driver.js +216 -0
  110. package/dist/wakeup-driver.js.map +1 -0
  111. package/dist/wakeup-service.d.ts +83 -0
  112. package/dist/wakeup-service.js +415 -0
  113. package/dist/wakeup-service.js.map +1 -0
  114. package/dist/wakeup-type.d.ts +43 -0
  115. package/dist/wakeup-type.js +80 -0
  116. package/dist/wakeup-type.js.map +1 -0
  117. package/dist/wakeup.d.ts +77 -0
  118. package/dist/wakeup.js +78 -0
  119. package/dist/wakeup.js.map +1 -0
  120. package/dist/worker.d.ts +39 -1
  121. package/dist/worker.js +125 -38
  122. package/dist/worker.js.map +1 -1
  123. package/oxlint.json +3 -1
  124. package/package.json +10 -2
  125. package/src/apply.ts +25 -6
  126. package/src/approvals.ts +89 -16
  127. package/src/call.ts +40 -29
  128. package/src/cedar.ts +24 -6
  129. package/src/change.ts +222 -0
  130. package/src/chats.ts +114 -0
  131. package/src/client.ts +145 -55
  132. package/src/config.ts +149 -21
  133. package/src/constructors.ts +8 -2
  134. package/src/database.ts +152 -0
  135. package/src/db.ts +58 -9
  136. package/src/deadlines.ts +228 -0
  137. package/src/define.ts +71 -4
  138. package/src/demo.ts +87 -0
  139. package/src/describe.ts +18 -6
  140. package/src/errors.ts +14 -0
  141. package/src/fake.ts +149 -0
  142. package/src/grid.ts +100 -0
  143. package/src/host.ts +12 -4
  144. package/src/index.ts +66 -3
  145. package/src/ledger.ts +15 -3
  146. package/src/lint.ts +2 -2
  147. package/src/op.ts +22 -2
  148. package/src/person.ts +4 -1
  149. package/src/policy.ts +7 -1
  150. package/src/resource.ts +22 -3
  151. package/src/resources.ts +17 -4
  152. package/src/run.ts +5 -0
  153. package/src/sandbox.ts +97 -37
  154. package/src/scenario.ts +97 -28
  155. package/src/scenarios.ts +21 -5
  156. package/src/schedules.ts +206 -0
  157. package/src/schema.ts +16 -3
  158. package/src/shared.ts +51 -4
  159. package/src/source.ts +176 -3
  160. package/src/supervisor.ts +102 -0
  161. package/src/version.ts +3 -1
  162. package/src/wakeup-driver.ts +270 -0
  163. package/src/wakeup-service.ts +504 -0
  164. package/src/wakeup-type.ts +90 -0
  165. package/src/wakeup.ts +139 -0
  166. package/src/worker.ts +176 -43
package/src/grid.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { ResolvedConfig } from "./config.ts";
2
+ import type { Principal } from "./define.ts";
3
+ import type { ConfigDescription } from "./describe.ts";
4
+ import type { PreviewAnswer, PreviewRequest } from "./policy.ts";
5
+ import {
6
+ AGENTS,
7
+ ANONYMOUS,
8
+ ANONYMOUS_PRINCIPAL,
9
+ groupNamesOf,
10
+ previewRequests,
11
+ VISITORS,
12
+ visitorPrincipal,
13
+ } from "./shared.ts";
14
+
15
+ // The Permissions grid: what the policy would answer each group, for every workflow's start, the
16
+ // operations it may call, and each direct operation. A pure function of a resolved config's policy
17
+ // and its description, so the app's Permissions page and `sanoma check` (a draft's grid, on its
18
+ // `checked` event) compute it the one way, and a change is reviewed as one grid against another.
19
+
20
+ /** A row of the grid: a workflow (its start) or an operation, and the policy's answer for each column. */
21
+ export interface PermissionsRow {
22
+ id: string;
23
+ title?: string;
24
+ cells: PreviewAnswer[];
25
+ }
26
+
27
+ /**
28
+ * The grid: its columns (`null` for someone in no group, then the declared groups, `agents` and
29
+ * `visitors`, sorted, then `anonymous`), a row for each workflow's start, with a row for each
30
+ * operation it uses (`ops`, in its order), and a row for each operation a person may call
31
+ * directly, each with a cell per column.
32
+ */
33
+ export interface PermissionsGrid {
34
+ groups: (string | null)[];
35
+ workflows: (PermissionsRow & { ops: PermissionsRow[] })[];
36
+ ops: PermissionsRow[];
37
+ }
38
+
39
+ /**
40
+ * Whom the `agents` column's agent acts for: a hold `@hold("on-behalf-of")` asks them. Only an
41
+ * agent acts for someone, so in any other column that hold refuses, as it does a person.
42
+ */
43
+ const ACTS_FOR = "whom it acts for";
44
+
45
+ /** Whom a column of the grid asks about: someone in the group alone, a visitor, or `anonymous`. */
46
+ export function columnPrincipal(group: string | null): Principal {
47
+ if (group === null) return { id: "group:", groups: [] };
48
+ if (group === ANONYMOUS) return ANONYMOUS_PRINCIPAL;
49
+ if (group === VISITORS) return visitorPrincipal("grid", "visitor");
50
+ return { id: `group:${group}`, groups: [group], ...(group === AGENTS && { onBehalfOf: ACTS_FOR }) };
51
+ }
52
+
53
+ /**
54
+ * The grid of `resolved`'s policy over `description`: for someone in no group, then for each
55
+ * group, the declared ones, `agents` and `visitors` (the visitors' only beside a public site),
56
+ * then for `anonymous`, what the policy would answer someone in it alone (`columnPrincipal`), the
57
+ * input unknown (`Policy.preview`), for the start of each of the config's own workflows, by name,
58
+ * each with the operations it uses, in the order it declares them (`previewRequests`), then each
59
+ * operation a person may call directly, by id.
60
+ */
61
+ export function permissionsGrid(
62
+ { policy }: Pick<ResolvedConfig, "policy">,
63
+ description: ConfigDescription,
64
+ ): PermissionsGrid {
65
+ // A visitor's columns only beside a public site, where there are visitors.
66
+ const visitors = description.sites.some((site) => site.public);
67
+ const groups = [
68
+ null,
69
+ ...groupNamesOf(description.resources).filter((g) => visitors || g !== VISITORS),
70
+ ...(visitors ? [ANONYMOUS] : []),
71
+ ];
72
+ const opsById = new Map(description.ops.map((op) => [op.id, op]));
73
+ const workflows = description.workflows.filter((w) => !w.builtin);
74
+ const direct = description.ops.filter((op) => op.direct).toSorted((a, b) => a.id.localeCompare(b.id));
75
+ // Every row's question, in the grid's order: each workflow's start, then the operations it may
76
+ // call, as the run form asks them; then each direct operation.
77
+ const asked = workflows.map((w) => previewRequests(w, opsById));
78
+ const requests: Omit<PreviewRequest, "principal">[] = [
79
+ ...asked.flat(),
80
+ ...direct.map(({ id, effect }) => ({ action: { id, effect } })),
81
+ ];
82
+ const columns = groups.map((group) => {
83
+ const principal = columnPrincipal(group);
84
+ return policy.preview(requests.map((request) => ({ ...request, principal })));
85
+ });
86
+ const rows = requests.map(({ action: { id } }, i): PermissionsRow => ({
87
+ id,
88
+ cells: columns.map((answers) => answers[i]!),
89
+ }));
90
+ // Each workflow's rows, as many as it asked: its start, then one per operation it may call.
91
+ let at = 0;
92
+ return {
93
+ groups,
94
+ workflows: workflows.map(({ name, title }, i) => {
95
+ const [start, ...called] = rows.slice(at, (at += asked[i]!.length));
96
+ return { ...start!, id: name, ...(title !== undefined && { title }), ops: called };
97
+ }),
98
+ ops: rows.slice(at),
99
+ };
100
+ }
package/src/host.ts CHANGED
@@ -4,8 +4,16 @@ 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
- /** How a host keeps the runtime up: `long-lived`, or (later) woken on a schedule. */
8
- export type HostLifetime = "long-lived" | "wake-on-schedule";
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-when-quiet`: the host stops the instance after a while without inbound requests, and
11
+ * starts it again on the next request (a free Render service, Cloudflare Containers).
12
+ *
13
+ * A host that stops needs a declared `Wakeup` for what is due at a time while it is stopped.
14
+ * Later: `stops-on-exit`, with a host that stops the instance when its process exits (Fly).
15
+ */
16
+ export type HostLifetime = "long-lived" | "stops-when-quiet";
9
17
 
10
18
  /** What a host can do, for choosing one. */
11
19
  export interface HostCapabilities {
@@ -53,10 +61,10 @@ export interface HostSpec {
53
61
  outputs: (state: Record<string, unknown>) => HostOutputs;
54
62
  }
55
63
 
56
- const LIFETIMES: readonly HostLifetime[] = ["long-lived", "wake-on-schedule"];
64
+ const LIFETIMES: readonly HostLifetime[] = ["long-lived", "stops-when-quiet"];
57
65
 
58
66
  /** 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 {
67
+ export function hasPath(schema: z.ZodType, path: string): boolean {
60
68
  let at: z.ZodType | undefined = schema;
61
69
  for (const key of path.split(".")) {
62
70
  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 { defineConfig, resolveConfig, resolveDatabaseUrl, type ResolvedConfig, type SanomaConfig } from "./config.ts";
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,13 @@ 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
+ export { nextFire, nextScheduledFire } from "./schedules.ts";
130
153
  // `secret` itself is at `@sanoma-ai/workflows/resources`, for data files; this is the drivers' side.
131
154
  export {
132
155
  generateSecret,
@@ -137,10 +160,43 @@ export {
137
160
  type Secret,
138
161
  type Secrets,
139
162
  } from "./secret.ts";
140
- export { resolveRevision, SOURCE_RESOLVE, type Source } from "./source.ts";
163
+ export {
164
+ type CheckResult,
165
+ type Draft,
166
+ type DraftDiff,
167
+ type HostEnforcement,
168
+ type PullRequest,
169
+ type RemoteContext,
170
+ type RemoteEvent,
171
+ type RemoteHalf,
172
+ remoteFor,
173
+ type RepoRef,
174
+ resolveRevision,
175
+ SOURCE_RESOLVE,
176
+ type Source,
177
+ type SourceHost,
178
+ } from "./source.ts";
141
179
  export type { HostCapabilities, HostLifetime, HostOutputs, HostSpec } from "./host.ts";
180
+ export {
181
+ checkDatabase,
182
+ type DatabaseCapabilities,
183
+ type DatabaseOutputs,
184
+ type DatabaseSpec,
185
+ databaseUrlSecret,
186
+ storeDatabaseUrl,
187
+ } from "./database.ts";
188
+ export type { Chat, Chats } from "./chats.ts";
189
+ export {
190
+ checkWakeup,
191
+ type DeclaredWakeup,
192
+ type WakeupOutputs,
193
+ type WakeupRegistration,
194
+ type WakeupSpec,
195
+ wakeupHostTypes,
196
+ } from "./wakeup.ts";
142
197
  export {
143
198
  type CallOptions,
199
+ type ClientOptions,
144
200
  SanomaClient,
145
201
  type RunsFilter,
146
202
  type RunSummary,
@@ -156,11 +212,18 @@ export {
156
212
  ANONYMOUS_PRINCIPAL,
157
213
  approverLabel,
158
214
  approverOf,
215
+ CHANGE_CLOSE,
216
+ CHANGE_EVENT,
217
+ CHANGE_MERGE,
218
+ CHANGE_OPEN,
219
+ CHANGE_REVIEW,
220
+ CHANGE_WORKFLOW,
159
221
  declaresPeople,
160
222
  type PeopleResource,
161
223
  personOf,
162
224
  principalOf,
163
225
  RUNTIME_AGENTS,
226
+ SCHEDULE_AGENT,
164
227
  credentialReady,
165
228
  DEFAULT_SLEEP_NAME,
166
229
  ENDED_STATUSES,
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 decided, `<runId>:approval.refused:<approvalId>:refused:<n>` for
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
- | { type: "run.started"; input: unknown }
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
@@ -29,7 +29,7 @@ const ALLOWED_PACKAGE = new RegExp(`^(${[...AUTHOR_PACKAGES, "@sanoma-ai/connect
29
29
  const REFUSED_PACKAGES: [RegExp, string][] = [
30
30
  [/^@sanoma-ai\/testing(\/|$)/, `fakes and test helpers belong in tests; ${THROUGH_CTX}`],
31
31
  [/^@sanoma-ai\/app(\/|$)/, "the app starts runs and decides approvals, so a workflow could approve itself"],
32
- [/^@sanoma-ai\/connector-[^/]+\/(fake|driver)(\/|$)/, `${THROUGH_CTX}; import the connector itself`],
32
+ [/^@sanoma-ai\/connector-[^/]+\/(fake|driver|remote)(\/|$)/, `${THROUGH_CTX}; import the connector itself`],
33
33
  [/^sanoma\//, "`sanoma` has no entry points for a workflow: import its names from `sanoma` itself"],
34
34
  ];
35
35
 
@@ -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,6 +1,8 @@
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";
5
+ import type { RemoteHalf } from "./source.ts";
4
6
  import type { Steps } from "./steps.ts";
5
7
 
6
8
  /**
@@ -306,8 +308,9 @@ export interface Driver {
306
308
  * What a connector package attaches to its connector (`withSetup`), for the config's resolver
307
309
  * to call: the config then names the connector alone, and never a driver, a fake, steps or a
308
310
  * 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, and one with no login half signs
310
- * no visitor in.
311
+ * refused, a connector with no fake has none for sandbox runs, one with no login half signs no
312
+ * visitor in, one with no model half gives the app no models, and one with no remote half hosts
313
+ * no repository a change is reviewed on.
311
314
  */
312
315
  export interface ConnectorSetup {
313
316
  /** Makes the live driver, which reads its credentials from the environment when called: once per resolved config. */
@@ -321,8 +324,25 @@ export interface ConnectorSetup {
321
324
  * starts; a login that needs tables migrates them here (serialized, idempotent).
322
325
  */
323
326
  login?: (context: LoginContext) => LoginHalf | Promise<LoginHalf>;
327
+ /**
328
+ * Makes the connector's AI SDK provider, the vendor's adapter as the transport: by the app,
329
+ * once per resolved config and client (again after one that throws), whose registry names its
330
+ * models `<vendor>/<model-id>`. It answers `languageModel("default")` with the connector's own
331
+ * default model, and reads its credentials through `context.secrets` when a model is called, so
332
+ * a key set on the connector page later is used (unless the environment sets one, which wins),
333
+ * and a missing one fails that call.
334
+ */
335
+ model?: (context: ModelContext) => ProviderV4 | Promise<ProviderV4>;
336
+ /**
337
+ * Makes the connector's remote half, a git host's pull requests, statuses and webhooks: once per
338
+ * resolved config (`ResolvedConfig.remotes`). It reads its credentials when called.
339
+ */
340
+ remote?: () => RemoteHalf;
324
341
  }
325
342
 
343
+ /** What the app gives a connector's model half (`ConnectorSetup.model`): the secrets, as a login half reads them. */
344
+ export type ModelContext = Pick<LoginContext, "secrets">;
345
+
326
346
  /** What the app gives a connector's login half (`ConnectorSetup.login`) when it makes it for a public site. */
327
347
  export interface LoginContext {
328
348
  /** The company's database (`resolved.databaseUrl`), where a login like Better Auth keeps its users. */
package/src/person.ts CHANGED
@@ -68,8 +68,11 @@ const group = defineResource({
68
68
  */
69
69
  export const sanoma = { person, agent, group };
70
70
 
71
+ /** The runtime's own vendor, as a data file imports its constructors: `import { sanoma } from "sanoma/resources"`. */
72
+ export const SANOMA_INFO = { title: "Sanoma", package: "sanoma" };
73
+
71
74
  /**
72
75
  * The runtime's own connector of the people types: every config's, so its data files may declare
73
76
  * them. Its package is `sanoma`, whose `/resources` entry exports `sanoma`.
74
77
  */
75
- export const peopleConnector = defineConnector("sanoma", sanoma, { title: "Sanoma", package: "sanoma" });
78
+ export const peopleConnector = defineConnector("sanoma", sanoma, SANOMA_INFO);
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<ResourceSpec<S>, "identity" | "find" | "writable" | "host" | "stateless" | "afterCreate" | "vendorless">
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
@@ -12,7 +12,8 @@ import {
12
12
  import { type Binding, type Bindings, LOCKFILE, readLockfile } from "./lockfile.ts";
13
13
  import { type Connector, VENDOR } from "./op.ts";
14
14
  import { filesUnder } from "./paths.ts";
15
- import { peopleConnector } from "./person.ts";
15
+ import { peopleConnector, sanoma } from "./person.ts";
16
+ import { wakeupOps } from "./wakeup-type.ts";
16
17
  import type { Span } from "./outline.ts";
17
18
  import { type Declared, type Resource, resourceTypesOf } from "./resource.ts";
18
19
  import { Principal } from "./define.ts";
@@ -112,7 +113,7 @@ export interface DeclaredPeople {
112
113
  }
113
114
 
114
115
  /** 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["']/;
116
+ export const DECLARES_PEOPLE = /\{[^}]*\bsanoma\b[^}]*\}\s*from\s*["'](?:sanoma|@sanoma-ai\/workflows)\/resources["']/;
116
117
  /** The last read of the people under each root, by what its data files were then (`filesStamp`). */
117
118
  const peopleRead = new Map<string, { stamp: string; read: DeclaredPeople }>();
118
119
 
@@ -144,8 +145,9 @@ export function readPeople(root: string | undefined): DeclaredPeople {
144
145
  const stamp = filesStamp(root);
145
146
  const cached = peopleRead.get(root);
146
147
  if (cached?.stamp === stamp) return cached.read;
147
- const read = readDataFiles(root, [peopleConnector], undefined, (source) => DECLARES_PEOPLE.test(source));
148
- const people = read.resources;
148
+ // With the wakeup's type, which the same constructors declare: read, and only the people's kept.
149
+ const read = readDataFiles(root, [peopleConnector, wakeupOps], undefined, (source) => DECLARES_PEOPLE.test(source));
150
+ const people = read.resources.filter((r) => Object.hasOwn(sanoma, r.type));
149
151
  // The people are never bound, so the lockfile's problems are not theirs.
150
152
  const own = read.problems.filter((p) => p.file !== LOCKFILE);
151
153
  const ids = new Map<string, DeclaredResource>();
@@ -424,6 +426,17 @@ export function readDataFiles(
424
426
  done.set(id, undefined);
425
427
  }
426
428
  }
429
+ // An instance registers with one wakeup: several are each a problem, and left out, as is any that names one.
430
+ const wakeups = [...declarations].filter(
431
+ ([, { file, exp }]) => types.get(`${vendors.get(file)!.get(exp.vendor)}.${exp.type}`)?.wakeup,
432
+ );
433
+ if (wakeups.length > 1) {
434
+ for (const [id, { file, exp }] of wakeups) {
435
+ const others = wakeups.filter(([other]) => other !== id).map(([other]) => other);
436
+ file.problem(exp.start, `an instance has one wakeup: leave out this one or ${others.join(", ")}`);
437
+ done.set(id, undefined);
438
+ }
439
+ }
427
440
  const keys = new Set([...files.values()].flatMap((f) => f.data.declared.map((name) => `${f.rel}#${name}`)));
428
441
  const orphans = Object.entries(lock.bindings)
429
442
  .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. */