@smthrs/flow 1.0.0-rc.12 → 1.0.0-rc.14

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Resume compatibility is weaker than result reuse: unchanged callback source
3
+ * may rebuild a recorded plan, but an opaque closure still cannot earn a cache
4
+ * hit. Only the interpreter's PlanRecorded guard uses this projection. Approval
5
+ * digests and dispatch keys keep the original entropy-bearing material.
6
+ *
7
+ * @since 1.0.0
8
+ * @internal
9
+ */
10
+ import { isRecord } from "@smthrs/canonical/Record";
11
+ import * as Graph from "../Graph.js";
12
+ /** Missing source metadata retains the old identity and fails closed. */
13
+ const callback = (value) => isRecord(value) && value._tag === "FunctionIdentity" && value.algorithm === "sha256-source-ephemeral/v4" &&
14
+ typeof value.sourceDigest === "string"
15
+ ? { _tag: value._tag, algorithm: value.algorithm, digest: value.sourceDigest }
16
+ : value;
17
+ /** Project only callback slots, never identity-shaped application data. */
18
+ const body = (value) => {
19
+ if (!isRecord(value))
20
+ return value;
21
+ switch (value._tag) {
22
+ case "FlowCall":
23
+ return isRecord(value.declaration)
24
+ ? { ...value, declaration: { ...value.declaration, body: callback(value.declaration.body) } }
25
+ : value;
26
+ case "Map":
27
+ return { ...value, mapper: callback(value.mapper) };
28
+ case "AndThen":
29
+ return { ...value, continuation: callback(value.continuation) };
30
+ case "Branch":
31
+ return { ...value, predicate: callback(value.predicate) };
32
+ case "Catch":
33
+ return { ...value, filterIdentity: callback(value.filterIdentity) };
34
+ default:
35
+ return value;
36
+ }
37
+ };
38
+ /**
39
+ * The same drafts, with only callback identities made restart-comparable.
40
+ *
41
+ * @since 1.0.0
42
+ * @private
43
+ */
44
+ export const recordedPlanDrafts = (graph) => Graph.drafts(graph).map((draft) => ({ ...draft, material: { ...draft.material, body: body(draft.material.body) } }));
45
+ //# sourceMappingURL=RecordedPlan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordedPlan.js","sourceRoot":"","sources":["../../../src/internal/RecordedPlan.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEnD,OAAO,KAAK,KAAK,MAAM,aAAa,CAAA;AAEpC,yEAAyE;AACzE,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAW,EAAE,CAC3C,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB,IAAI,KAAK,CAAC,SAAS,KAAK,4BAA4B;IACtG,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ;IACtC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,YAAY,EAAE;IAC9E,CAAC,CAAC,KAAK,CAAA;AAEX,2EAA2E;AAC3E,MAAM,IAAI,GAAG,CAAC,KAAc,EAAW,EAAE;IACvC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAClC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,UAAU;YACb,OAAO,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC;gBAChC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,WAAW,EAAE,EAAE,GAAG,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE;gBAC7F,CAAC,CAAC,KAAK,CAAA;QACX,KAAK,KAAK;YACR,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAA;QACrD,KAAK,SAAS;YACZ,OAAO,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAA;QACjE,KAAK,QAAQ;YACX,OAAO,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAA;QAC3D,KAAK,OAAO;YACV,OAAO,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAA;QACrE;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,KAAkB,EAAiC,EAAE,CACtF,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Resume compatibility is weaker than result reuse: unchanged callback source\n * may rebuild a recorded plan, but an opaque closure still cannot earn a cache\n * hit. Only the interpreter's PlanRecorded guard uses this projection. Approval\n * digests and dispatch keys keep the original entropy-bearing material.\n *\n * @since 1.0.0\n * @internal\n */\n\nimport { isRecord } from \"@smthrs/canonical/Record\"\nimport type * as Plan from \"@smthrs/plan/Plan\"\nimport * as Graph from \"../Graph.ts\"\n\n/** Missing source metadata retains the old identity and fails closed. */\nconst callback = (value: unknown): unknown =>\n isRecord(value) && value._tag === \"FunctionIdentity\" && value.algorithm === \"sha256-source-ephemeral/v4\" &&\n typeof value.sourceDigest === \"string\"\n ? { _tag: value._tag, algorithm: value.algorithm, digest: value.sourceDigest }\n : value\n\n/** Project only callback slots, never identity-shaped application data. */\nconst body = (value: unknown): unknown => {\n if (!isRecord(value)) return value\n switch (value._tag) {\n case \"FlowCall\":\n return isRecord(value.declaration)\n ? { ...value, declaration: { ...value.declaration, body: callback(value.declaration.body) } }\n : value\n case \"Map\":\n return { ...value, mapper: callback(value.mapper) }\n case \"AndThen\":\n return { ...value, continuation: callback(value.continuation) }\n case \"Branch\":\n return { ...value, predicate: callback(value.predicate) }\n case \"Catch\":\n return { ...value, filterIdentity: callback(value.filterIdentity) }\n default:\n return value\n }\n}\n\n/**\n * The same drafts, with only callback identities made restart-comparable.\n *\n * @since 1.0.0\n * @private\n */\nexport const recordedPlanDrafts = (graph: Graph.Graph): ReadonlyArray<Plan.NodeDraft> =>\n Graph.drafts(graph).map((draft) => ({ ...draft, material: { ...draft.material, body: body(draft.material.body) } }))\n"]}
@@ -19,7 +19,7 @@ engine, database, or jj: planning reads declarations only. They import
19
19
  add both at the version of `@smthrs/flows` you have:
20
20
 
21
21
  ```bash
22
- npm install @smthrs/flow@1.0.0-rc.12 @smthrs/plan@1.0.0-rc.12
22
+ npm install @smthrs/flow@1.0.0-rc.14 @smthrs/plan@1.0.0-rc.14
23
23
  ```
24
24
 
25
25
  `@smthrs/flows` re-exports the same modules, so `import { Graph } from "@smthrs/flows"`
@@ -13,8 +13,8 @@ npm install @smthrs/flow@next effect
13
13
 
14
14
  The `next` tag carries the 1.0 release candidates.
15
15
 
16
- `effect` is a peer dependency of this package, in the range `^4.0.4`. Add one
17
- `effect` in that range to your own dependencies: your declarations import
16
+ `effect` is a peer dependency of this package, at exactly `4.0.5`. Add one
17
+ `effect` at that version to your own dependencies: your declarations import
18
18
  `effect/Schema` and your implementations import `effect/Effect` directly, and two
19
19
  copies of `effect` in one program are two sets of service tags.
20
20
 
@@ -28,7 +28,7 @@ from, and the canonical JSON checks applied to values.
28
28
  ## Use the libraries
29
29
 
30
30
  Every `@smthrs` library installs from npm the same way. Name each package your
31
- code imports with the `next` tag, and add one `effect` in the `^4.0.4` range
31
+ code imports with the `next` tag, and add one `effect` in the `4.0.5` range
32
32
  the libraries peer. The [Quickstart](./quickstart.md) program needs these:
33
33
 
34
34
  ```bash
@@ -10,7 +10,7 @@ touches a database or the network, and the result arrives as a typed value
10
10
  decoded by the schema you declared.
11
11
 
12
12
  A runnable copy of this program is published in the Smithers examples,
13
- [`01-define-and-run.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/01-define-and-run.ts).
13
+ [`01-define-and-run.ts`](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/examples/01-define-and-run.ts).
14
14
 
15
15
  ## Prerequisites
16
16
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smthrs/flow",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.12",
4
+ "version": "1.0.0-rc.14",
5
5
  "license": "MIT",
6
6
  "smthrs": {
7
7
  "group": "engine"
@@ -289,21 +289,21 @@
289
289
  "tag": "next"
290
290
  },
291
291
  "dependencies": {
292
- "@smthrs/crypto": "1.0.0-rc.12",
293
- "@smthrs/keys": "1.0.0-rc.12",
294
- "@smthrs/plan": "1.0.0-rc.12",
295
- "@smthrs/canonical": "1.0.0-rc.12",
296
- "@smthrs/capability": "1.0.0-rc.12"
292
+ "@smthrs/crypto": "1.0.0-rc.14",
293
+ "@smthrs/keys": "1.0.0-rc.14",
294
+ "@smthrs/plan": "1.0.0-rc.14",
295
+ "@smthrs/canonical": "1.0.0-rc.14",
296
+ "@smthrs/capability": "1.0.0-rc.14"
297
297
  },
298
298
  "devDependencies": {
299
299
  "@effect/language-service": "0.87.2",
300
- "@effect/platform-node": "4.0.4",
301
- "@effect/vitest": "4.0.4",
300
+ "@effect/platform-node": "4.0.5",
301
+ "@effect/vitest": "4.0.5",
302
302
  "@eslint/js": "9.39.5",
303
303
  "@types/node": "26.4.1",
304
304
  "@vitest/coverage-v8": "5.0.0",
305
305
  "dprint": "0.57.1",
306
- "effect": "4.0.4",
306
+ "effect": "4.0.5",
307
307
  "esbuild": "0.28.2",
308
308
  "eslint": "9.39.5",
309
309
  "eslint-import-resolver-typescript": "4.4.5",
@@ -316,7 +316,7 @@
316
316
  "vitest": "5.0.0"
317
317
  },
318
318
  "peerDependencies": {
319
- "effect": "^4.0.4"
319
+ "effect": "4.0.5"
320
320
  },
321
321
  "scripts": {
322
322
  "lint": "eslint src examples --max-warnings=0 && dprint check",
package/src/Flow/make.ts CHANGED
@@ -84,6 +84,20 @@ interface MakeOptions<
84
84
  * reach.
85
85
  */
86
86
  readonly modelInvocable?: boolean | undefined
87
+ /**
88
+ * The tokens, milliseconds and dollars one run asks a control plane to
89
+ * approve, and what exceeding them does, as a literal.
90
+ *
91
+ * Discovery reads it from the source text onto the descriptor's budget,
92
+ * exactly as Markdown frontmatter's `budget` is read; the control plane, not
93
+ * this flow, enforces it.
94
+ */
95
+ readonly budget?: {
96
+ readonly tokens?: number | undefined
97
+ readonly milliseconds?: number | undefined
98
+ readonly usd?: number | undefined
99
+ readonly onExceeded?: "fail" | "warn" | "skip-remaining" | "park" | undefined
100
+ } | undefined
87
101
  readonly idempotencyKey?:
88
102
  | ((
89
103
  payload: Payload extends Schema.Struct.Fields ? Schema.Struct.Type<Payload>
package/src/HumanTask.ts CHANGED
@@ -1004,4 +1004,18 @@ export const answer = (options: {
1004
1004
  export const decode =
1005
1005
  <S extends Schema.ConstraintDecoder<unknown>>(schema: S) =>
1006
1006
  <E, R>(self: Node.Node<typeof Schema.Json.Type, E, R>): Node.Node<S["Type"], E, R> =>
1007
- Node.map(self, Schema.decodeUnknownSync(schema))
1007
+ Node.map(self, answerDecoder(schema))
1008
+
1009
+ // Schema decoders are opaque callbacks. Reuse one per immutable schema so a
1010
+ // same-process rebuild preserves its identity without claiming portable
1011
+ // identity for arbitrary schema transformations or checks.
1012
+ const answerDecoders = new WeakMap<object, (input: unknown) => unknown>()
1013
+
1014
+ const answerDecoder = <S extends Schema.ConstraintDecoder<unknown>>(schema: S): (input: unknown) => S["Type"] => {
1015
+ let decoder = answerDecoders.get(schema)
1016
+ if (decoder === undefined) {
1017
+ decoder = Schema.decodeUnknownSync(schema)
1018
+ answerDecoders.set(schema, decoder)
1019
+ }
1020
+ return decoder
1021
+ }
@@ -80,6 +80,7 @@ import * as HumanTask from "./HumanTask.ts"
80
80
  import * as BoundedJson from "./internal/BoundedJson.ts"
81
81
  import { ExecutionMiddleware } from "./internal/ExecutionMiddleware.ts"
82
82
  import { OutcomeValueTypeId } from "./internal/OutcomeMarker.ts"
83
+ import { recordedPlanDrafts } from "./internal/RecordedPlan.ts"
83
84
  import * as Sleep from "./Sleep.ts"
84
85
  import * as WaitFor from "./WaitFor.ts"
85
86
 
@@ -272,7 +273,7 @@ const planPages = (
272
273
  > =>
273
274
  Effect.gen(function*() {
274
275
  const identity = yield* StepKey.content({
275
- body: { flow, nodes: Graph.drafts(graph), edges: Graph.edges(graph) },
276
+ body: { flow, nodes: recordedPlanDrafts(graph), edges: Graph.edges(graph) },
276
277
  inputs: {},
277
278
  layers: [],
278
279
  capabilities: {}
@@ -1486,12 +1487,27 @@ export const layer = <
1486
1487
 
1487
1488
  // These layers only register handlers at construction. Crypto is supplied by
1488
1489
  // the executing interpreter (Services), not required to register a flow.
1489
- const systemActions = Layer.mergeAll(HumanTask.layer, Sleep.layer, WaitFor.layer) as Layer.Layer<
1490
+ const systemHandlers = Layer.mergeAll(HumanTask.layer, Sleep.layer, WaitFor.layer) as Layer.Layer<
1490
1491
  never,
1491
1492
  never,
1492
1493
  FlowRuntime
1493
1494
  >
1494
1495
 
1496
+ // Independent module/refresh layer builds can share one implementation table.
1497
+ // Memoize the built-ins by that owner, with Layer retaining each live scope's
1498
+ // reference, rather than registering a second handler or overriding the first.
1499
+ const systemMemoMaps = new WeakMap<Implementations["Service"], Layer.MemoMap>()
1500
+ const systemActions = Layer.fromBuild((currentMemoMap, scope) =>
1501
+ Effect.flatMap(Implementations, (table) => {
1502
+ let memoMap = systemMemoMaps.get(table)
1503
+ if (memoMap === undefined) {
1504
+ memoMap = currentMemoMap
1505
+ systemMemoMaps.set(table, memoMap)
1506
+ }
1507
+ return Layer.buildWithMemoMap(systemHandlers, memoMap, scope)
1508
+ })
1509
+ )
1510
+
1495
1511
  /** Registers the interpreter and reachable child flows under one policy. @private */
1496
1512
  const makeLayer = <
1497
1513
  Tag extends string,
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Resume compatibility is weaker than result reuse: unchanged callback source
3
+ * may rebuild a recorded plan, but an opaque closure still cannot earn a cache
4
+ * hit. Only the interpreter's PlanRecorded guard uses this projection. Approval
5
+ * digests and dispatch keys keep the original entropy-bearing material.
6
+ *
7
+ * @since 1.0.0
8
+ * @internal
9
+ */
10
+
11
+ import { isRecord } from "@smthrs/canonical/Record"
12
+ import type * as Plan from "@smthrs/plan/Plan"
13
+ import * as Graph from "../Graph.ts"
14
+
15
+ /** Missing source metadata retains the old identity and fails closed. */
16
+ const callback = (value: unknown): unknown =>
17
+ isRecord(value) && value._tag === "FunctionIdentity" && value.algorithm === "sha256-source-ephemeral/v4" &&
18
+ typeof value.sourceDigest === "string"
19
+ ? { _tag: value._tag, algorithm: value.algorithm, digest: value.sourceDigest }
20
+ : value
21
+
22
+ /** Project only callback slots, never identity-shaped application data. */
23
+ const body = (value: unknown): unknown => {
24
+ if (!isRecord(value)) return value
25
+ switch (value._tag) {
26
+ case "FlowCall":
27
+ return isRecord(value.declaration)
28
+ ? { ...value, declaration: { ...value.declaration, body: callback(value.declaration.body) } }
29
+ : value
30
+ case "Map":
31
+ return { ...value, mapper: callback(value.mapper) }
32
+ case "AndThen":
33
+ return { ...value, continuation: callback(value.continuation) }
34
+ case "Branch":
35
+ return { ...value, predicate: callback(value.predicate) }
36
+ case "Catch":
37
+ return { ...value, filterIdentity: callback(value.filterIdentity) }
38
+ default:
39
+ return value
40
+ }
41
+ }
42
+
43
+ /**
44
+ * The same drafts, with only callback identities made restart-comparable.
45
+ *
46
+ * @since 1.0.0
47
+ * @private
48
+ */
49
+ export const recordedPlanDrafts = (graph: Graph.Graph): ReadonlyArray<Plan.NodeDraft> =>
50
+ Graph.drafts(graph).map((draft) => ({ ...draft, material: { ...draft.material, body: body(draft.material.body) } }))