@smithers-orchestrator/engine 0.19.0 → 0.20.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.
@@ -0,0 +1,17 @@
1
+ import type { EventBus } from "./events.js";
2
+
3
+ export type AgentTraceCollectorOptions = {
4
+ eventBus: EventBus;
5
+ runId: string;
6
+ workflowPath?: string | null;
7
+ workflowHash?: string | null;
8
+ cwd: string;
9
+ nodeId: string;
10
+ iteration: number;
11
+ attempt: number;
12
+ agent: unknown;
13
+ agentId?: string;
14
+ model?: string;
15
+ logDir?: string;
16
+ annotations?: Record<string, string | number | boolean>;
17
+ };
@@ -789,25 +789,262 @@ function normalizeExecutionError(result) {
789
789
  }
790
790
  return new SmithersError("WORKFLOW_EXECUTION_FAILED", `Workflow execution ended with status "${result.status}"`, { status: result.status });
791
791
  }
792
+ /**
793
+ * @typedef {{ _tag: "WorkflowGraph"; expr: unknown; pipe: (...fns: Array<(g: any) => any>) => any }} WorkflowGraph
794
+ */
795
+
796
+ /**
797
+ * @param {unknown} value
798
+ * @returns {value is WorkflowGraph}
799
+ */
800
+ function isWorkflowGraph(value) {
801
+ return Boolean(value) && typeof value === "object" && /** @type {any} */ (value)._tag === "WorkflowGraph";
802
+ }
803
+
804
+ /**
805
+ * @param {unknown} expr
806
+ * @returns {WorkflowGraph}
807
+ */
808
+ function makeGraph(expr) {
809
+ /** @type {WorkflowGraph} */
810
+ const graph = /** @type {any} */ ({ _tag: "WorkflowGraph", expr });
811
+ graph.pipe = function pipe(...fns) {
812
+ return fns.reduce((acc, fn) => fn(acc), graph);
813
+ };
814
+ return graph;
815
+ }
816
+
817
+ /**
818
+ * Build the graph constructors shared by Smithers.workflow and Smithers.fragment.
819
+ */
820
+ function makeFactory() {
821
+ return {
822
+ /**
823
+ * @param {string} id
824
+ * @param {StepOptions} options
825
+ */
826
+ step: (id, options) => makeGraph({ _tag: "Step", id, options }),
827
+ /**
828
+ * @param {string} id
829
+ * @param {ApprovalOptions} options
830
+ */
831
+ approval: (id, options) => makeGraph({ _tag: "Approval", id, options }),
832
+ /**
833
+ * @param {WorkflowGraph[]} children
834
+ */
835
+ sequence: (...children) => makeGraph({ _tag: "Sequence", children }),
836
+ parallel: (...args) => {
837
+ let maxConcurrency;
838
+ const items = [...args];
839
+ const last = items[items.length - 1];
840
+ if (last &&
841
+ typeof last === "object" &&
842
+ !Array.isArray(last) &&
843
+ !isWorkflowGraph(last) &&
844
+ "maxConcurrency" in last) {
845
+ maxConcurrency = Number(last.maxConcurrency);
846
+ items.pop();
847
+ }
848
+ return makeGraph({ _tag: "Parallel", children: items, maxConcurrency });
849
+ },
850
+ match: (source, options) => makeGraph({
851
+ _tag: "Match",
852
+ source,
853
+ when: options.when,
854
+ then: options.then,
855
+ else: options.else,
856
+ }),
857
+ branch: (options) => makeGraph({
858
+ _tag: "Branch",
859
+ condition: options.condition,
860
+ needs: options.needs,
861
+ then: options.then,
862
+ else: options.else,
863
+ }),
864
+ loop: (options) => makeGraph({
865
+ _tag: "Loop",
866
+ id: options.id,
867
+ child: options.children,
868
+ until: options.until,
869
+ maxIterations: options.maxIterations,
870
+ onMaxReached: options.onMaxReached,
871
+ }),
872
+ worktree: (options) => makeGraph({
873
+ _tag: "Worktree",
874
+ id: options.id,
875
+ path: options.path,
876
+ branch: options.branch,
877
+ skipIf: options.skipIf,
878
+ needs: options.needs,
879
+ child: options.children,
880
+ }),
881
+ scope: (instanceId, child) => makeGraph({
882
+ _tag: "Scope",
883
+ instanceId,
884
+ child,
885
+ }),
886
+ };
887
+ }
888
+
889
+ /**
890
+ * @param {string} prefix
891
+ * @param {string | undefined} id
892
+ */
893
+ function applyPrefixId(prefix, id) {
894
+ if (!id) return id;
895
+ return prefix ? `${prefix}.${id}` : id;
896
+ }
897
+
898
+ /**
899
+ * @param {Record<string, WorkflowGraph> | undefined} needs
900
+ * @param {string} prefix
901
+ * @param {Map<string, Map<WorkflowGraph, BuilderNode>>} memo
902
+ */
903
+ function compileNeeds(needs, prefix, memo) {
904
+ if (!needs) return undefined;
905
+ const out = {};
906
+ for (const [key, dep] of Object.entries(needs)) {
907
+ out[key] = compileGraph(dep, prefix, memo);
908
+ }
909
+ return out;
910
+ }
911
+
912
+ /**
913
+ * Walk a graph expression tree and produce a BuilderNode tree at the active prefix.
914
+ * Memoizes per (prefix, graph) so a value referenced as both a child and a needs source
915
+ * compiles to a single handle, while reuse under different scopes produces distinct handles.
916
+ *
917
+ * @param {WorkflowGraph} graph
918
+ * @param {string} [prefix]
919
+ * @param {Map<string, Map<WorkflowGraph, BuilderNode>>} [memo]
920
+ * @returns {BuilderNode}
921
+ */
922
+ function compileGraph(graph, prefix = "", memo = new Map()) {
923
+ let perPrefix = memo.get(prefix);
924
+ if (!perPrefix) {
925
+ perPrefix = new Map();
926
+ memo.set(prefix, perPrefix);
927
+ }
928
+ const cached = perPrefix.get(graph);
929
+ if (cached) return cached;
930
+
931
+ const builder = createBuilder(prefix);
932
+ const expr = /** @type {any} */ (graph.expr);
933
+ let node;
934
+
935
+ switch (expr._tag) {
936
+ case "Step": {
937
+ const compiledOptions = {
938
+ ...expr.options,
939
+ needs: compileNeeds(expr.options.needs, prefix, memo) ?? {},
940
+ };
941
+ node = builder.step(expr.id, compiledOptions);
942
+ break;
943
+ }
944
+ case "Approval": {
945
+ const compiledOptions = {
946
+ ...expr.options,
947
+ needs: compileNeeds(expr.options.needs, prefix, memo) ?? {},
948
+ };
949
+ node = builder.approval(expr.id, compiledOptions);
950
+ break;
951
+ }
952
+ case "Sequence": {
953
+ node = {
954
+ kind: "sequence",
955
+ children: expr.children.map((c) => compileGraph(c, prefix, memo)),
956
+ };
957
+ break;
958
+ }
959
+ case "Parallel": {
960
+ node = {
961
+ kind: "parallel",
962
+ children: expr.children.map((c) => compileGraph(c, prefix, memo)),
963
+ maxConcurrency: expr.maxConcurrency,
964
+ };
965
+ break;
966
+ }
967
+ case "Match": {
968
+ node = {
969
+ kind: "match",
970
+ source: compileGraph(expr.source, prefix, memo),
971
+ when: expr.when,
972
+ then: compileGraph(expr.then, prefix, memo),
973
+ else: expr.else ? compileGraph(expr.else, prefix, memo) : undefined,
974
+ };
975
+ break;
976
+ }
977
+ case "Branch": {
978
+ node = {
979
+ kind: "branch",
980
+ condition: expr.condition,
981
+ needs: compileNeeds(expr.needs, prefix, memo),
982
+ then: compileGraph(expr.then, prefix, memo),
983
+ else: expr.else ? compileGraph(expr.else, prefix, memo) : undefined,
984
+ };
985
+ break;
986
+ }
987
+ case "Loop": {
988
+ node = {
989
+ kind: "loop",
990
+ id: applyPrefixId(prefix, expr.id),
991
+ children: compileGraph(expr.child, prefix, memo),
992
+ until: expr.until,
993
+ maxIterations: expr.maxIterations,
994
+ onMaxReached: expr.onMaxReached,
995
+ };
996
+ break;
997
+ }
998
+ case "Worktree": {
999
+ node = {
1000
+ kind: "worktree",
1001
+ id: applyPrefixId(prefix, expr.id),
1002
+ path: expr.path,
1003
+ branch: expr.branch,
1004
+ skipIf: expr.skipIf,
1005
+ needs: compileNeeds(expr.needs, prefix, memo),
1006
+ children: compileGraph(expr.child, prefix, memo),
1007
+ };
1008
+ break;
1009
+ }
1010
+ case "Scope": {
1011
+ const nextPrefix = prefix ? `${prefix}.${expr.instanceId}` : expr.instanceId;
1012
+ node = compileGraph(expr.child, nextPrefix, memo);
1013
+ break;
1014
+ }
1015
+ default:
1016
+ throw new SmithersError("UNKNOWN_GRAPH_EXPR", `Unknown graph expression _tag: "${expr._tag}"`);
1017
+ }
1018
+
1019
+ perPrefix.set(graph, node);
1020
+ return node;
1021
+ }
1022
+
792
1023
  /**
793
1024
  * @param {{ name: string; input: AnySchema }} options
794
1025
  */
795
- export function createWorkflow(options) {
1026
+ export function workflow(options) {
1027
+ const factory = makeFactory();
796
1028
  return {
1029
+ ...factory,
797
1030
  /**
798
- * @param {($: BuilderApi) => BuilderNode} buildGraph
799
- * @returns {BuiltSmithersWorkflow}
800
- */
801
- build(buildGraph) {
802
- const root = buildGraph(createBuilder());
1031
+ * Finalize the workflow definition. Compiles the graph expression tree to a
1032
+ * BuilderNode tree, allocates step handles with active prefixes, and returns
1033
+ * a runnable workflow.
1034
+ *
1035
+ * @param {WorkflowGraph} graph
1036
+ */
1037
+ from(graph) {
1038
+ const root = compileGraph(graph);
803
1039
  annotateLoops(root);
804
1040
  const handles = collectHandles(root);
805
1041
  assertUniqueHandleIds(handles);
806
1042
  return {
1043
+ node: root,
807
1044
  /**
808
- * @param {unknown} input
809
- * @param {Omit<Parameters<typeof runWorkflow>[1], "input">} [opts]
810
- */
1045
+ * @param {unknown} input
1046
+ * @param {Omit<Parameters<typeof runWorkflow>[1], "input">} [opts]
1047
+ */
811
1048
  execute(input, opts) {
812
1049
  return Effect.gen(function* () {
813
1050
  const env = yield* Effect.context();
@@ -815,12 +1052,12 @@ export function createWorkflow(options) {
815
1052
  const decodedInput = decodeSchema(options.input, input);
816
1053
  const encodedInput = JSON.parse(JSON.stringify(encodeSchema(options.input, decodedInput) ?? {}));
817
1054
  return yield* Effect.acquireUseRelease(Effect.sync(() => createBuilderDb(sqliteConfig.filename, handles)), (runtime) => Effect.promise(async () => {
818
- const workflow = {
1055
+ const wf = {
819
1056
  db: runtime.db,
820
- build: (ctx) => React.createElement(Workflow, { name: options.name }, renderNode(ctx && root ? root : root, ctx, decodedInput, env)),
1057
+ build: (ctx) => React.createElement(Workflow, { name: options.name }, renderNode(root, ctx, decodedInput, env)),
821
1058
  opts: {},
822
1059
  };
823
- const result = await Effect.runPromise(runWorkflow(workflow, {
1060
+ const result = await Effect.runPromise(runWorkflow(wf, {
824
1061
  ...opts,
825
1062
  input: encodedInput,
826
1063
  }));
@@ -839,39 +1076,28 @@ export function createWorkflow(options) {
839
1076
  },
840
1077
  };
841
1078
  }
1079
+
842
1080
  /**
843
- * @param {{ name: string; params?: Record<string, unknown> }} options
1081
+ * Build a graph fragment whose steps live across workflows. Same constructors as
1082
+ * a workflow handle, minus `from` — fragments are values, not workflows. They compile
1083
+ * when mounted into a real workflow via `G.scope(instanceId, fragment)`.
1084
+ *
1085
+ * @param {AnySchema} _inputSchema
844
1086
  */
845
- export function createComponent(options) {
846
- return {
847
- /**
848
- * @param {($: BuilderApi, params: Record<string, unknown>) => BuilderNode} buildGraph
849
- * @returns {ComponentDefinition}
850
- */
851
- build(buildGraph) {
852
- return {
853
- kind: "component-definition",
854
- name: options.name,
855
- /**
856
- * @param {string} prefix
857
- * @param {Record<string, unknown>} params
858
- */
859
- buildWithPrefix(prefix, params) {
860
- return buildGraph(createBuilder(prefix), params);
861
- },
862
- };
863
- },
864
- };
1087
+ export function fragment(_inputSchema) {
1088
+ return makeFactory();
865
1089
  }
1090
+
866
1091
  /**
867
1092
  * @param {SmithersSqliteOptions} options
868
1093
  */
869
1094
  function sqlite(options) {
870
1095
  return Layer.succeed(SmithersSqlite, options);
871
1096
  }
872
- /** @type {{ sqlite: typeof sqlite; createWorkflow: typeof createWorkflow; createComponent: typeof createComponent }} */
1097
+
1098
+ /** @type {{ sqlite: typeof sqlite; workflow: typeof workflow; fragment: typeof fragment }} */
873
1099
  export const Smithers = {
874
1100
  sqlite,
875
- createWorkflow,
876
- createComponent,
1101
+ workflow,
1102
+ fragment,
877
1103
  };