@styx-api/core 0.5.3 → 0.6.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/dist/index.mjs CHANGED
@@ -5400,7 +5400,29 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5400
5400
  names.params = (rootType.kind === "struct" ? namedTypes.get(structKey(rootType)) : void 0) ?? (rootType.kind === "union" ? namedTypes.get(unionKey(rootType)) : void 0) ?? names.params;
5401
5401
  const rootTypeTag = appId && pkg ? `${pkg}/${appId}` : void 0;
5402
5402
  const paramsType = rootType.kind === "struct" || rootType.kind === "union" ? names.params : mapType$1(rootType, resolveTypeName(namedTypes));
5403
+ const hostName = (childScope, wireKey) => childScope.add(pyScrubIdent(snakeCase(wireKey), PY_RESERVED));
5404
+ const resolve = resolveTypeName(namedTypes);
5403
5405
  const sigScope = scope.child(["params", "runner"]);
5406
+ const sigEntries = rootIsStruct && rootType.kind === "struct" ? buildSigEntries(rootType, collectFieldInfo(ctx, rootType), (wireKey) => hostName(sigScope, wireKey), pySigOptions(resolve)) : [];
5407
+ const rootStructKey = rootIsStruct && rootType.kind === "struct" ? structKey(rootType) : void 0;
5408
+ const nestedFactories = [];
5409
+ for (const decl of typeDecls) {
5410
+ if (decl.type.kind !== "struct") continue;
5411
+ const sKey = structKey(decl.type);
5412
+ if (sKey === rootStructKey) continue;
5413
+ const funcName = scope.add(pyScrubIdent(snakeCase(decl.name) + "_params", PY_RESERVED));
5414
+ const atType = decl.type.fields["@type"];
5415
+ const typeTag = atType && atType.kind === "literal" && typeof atType.value === "string" ? atType.value : void 0;
5416
+ const factoryScope = scope.child(["params"]);
5417
+ const factorySig = buildSigEntries(decl.type, collectFieldInfo(ctx, decl.type), (wireKey) => hostName(factoryScope, wireKey), pySigOptions(resolve));
5418
+ nestedFactories.push({
5419
+ typeName: decl.name,
5420
+ structKey: sKey,
5421
+ funcName,
5422
+ typeTag,
5423
+ sigEntries: factorySig
5424
+ });
5425
+ }
5404
5426
  return {
5405
5427
  appId,
5406
5428
  pkg,
@@ -5411,13 +5433,14 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5411
5433
  typeDecls,
5412
5434
  rootTypeTag,
5413
5435
  paramsType,
5414
- sigEntries: rootIsStruct && rootType.kind === "struct" ? buildSigEntries(rootType, collectFieldInfo(ctx, rootType), (wireKey) => sigScope.add(pyScrubIdent(wireKey, PY_RESERVED)), pySigOptions(resolveTypeName(namedTypes))) : []
5436
+ sigEntries,
5437
+ nestedFactories
5415
5438
  };
5416
5439
  }
5417
5440
  function generatePython(ctx, packageScope) {
5418
5441
  const cb = new CodeBuilder(" ");
5419
5442
  const scope = packageScope ?? new Scope(PY_RESERVED);
5420
- const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel(ctx, scope);
5443
+ const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries, nestedFactories } = buildEmitModel(ctx, scope);
5421
5444
  cb.comment("This file was auto generated by Styx.", "# ");
5422
5445
  cb.comment("Do not edit this file directly.", "# ");
5423
5446
  cb.blank();
@@ -5436,6 +5459,10 @@ function generatePython(ctx, packageScope) {
5436
5459
  emitParamsFactory$1(sigEntries, names.paramsFn, paramsType, rootTypeTag, cb);
5437
5460
  cb.blank();
5438
5461
  }
5462
+ for (const nf of nestedFactories) {
5463
+ emitParamsFactory$1(nf.sigEntries, nf.funcName, nf.typeName, nf.typeTag, cb);
5464
+ cb.blank();
5465
+ }
5439
5466
  emitValidate$1(ctx, rootType, ctx.expr, paramsType, names.validate, resolveTypeName(namedTypes), scope, cb);
5440
5467
  cb.blank();
5441
5468
  emitBuildCargs$1(ctx, rootType, paramsType, names.cargs, cb);
@@ -5455,6 +5482,7 @@ function generatePython(ctx, packageScope) {
5455
5482
  names.cargs,
5456
5483
  ...[names.outputsFn],
5457
5484
  ...rootIsStruct ? [names.paramsFn, names.execute] : [],
5485
+ ...nestedFactories.map((nf) => nf.funcName),
5458
5486
  names.validate,
5459
5487
  names.wrapper
5460
5488
  ];
@@ -5622,9 +5650,13 @@ function structAtType(type) {
5622
5650
  }
5623
5651
  /**
5624
5652
  * Render a struct config as a host object literal (Python dict / TS object).
5625
- * Keys are the Boutiques wire names (the generated TypedDict / interface keys) -
5626
- * nested structs have no constructor function in the generated code, so callers
5627
- * build them as plain object literals.
5653
+ * Keys are the Boutiques wire names (the generated TypedDict / interface keys).
5654
+ *
5655
+ * When the dialect supplies `structConstructor` (Python, which generates a
5656
+ * per-struct `_params` builder), the struct is rendered as that factory call
5657
+ * instead - e.g. `tool_corrected_output_params(file="...")`. Backends without
5658
+ * per-struct builders (TypeScript) leave the hook unset and build the plain
5659
+ * object literal below.
5628
5660
  *
5629
5661
  * `@type` is emitted from the struct's literal discriminator field when present
5630
5662
  * (union variants carry a required, load-bearing `@type`); for the root call the
@@ -5634,6 +5666,10 @@ function structAtType(type) {
5634
5666
  */
5635
5667
  function renderStructLiteral(value, type, indent, d, injectAtType) {
5636
5668
  const obj = isRecord(value) ? value : {};
5669
+ if (injectAtType === void 0 && d.structConstructor) {
5670
+ const call = d.structConstructor(type, obj, indent, d);
5671
+ if (call !== void 0) return call;
5672
+ }
5637
5673
  const inner = indent + d.indentUnit;
5638
5674
  const lines = [];
5639
5675
  const atType = structAtType(type) ?? injectAtType;
@@ -5709,6 +5745,41 @@ const pyDialect = {
5709
5745
  objKey: (k) => pyStr(k)
5710
5746
  };
5711
5747
  /**
5748
+ * Build a Python dialect that renders nested structs as `_params` factory calls
5749
+ * (matching the generated code's per-struct builders) rather than plain dicts.
5750
+ * Each factory is looked up by the struct's structural key; field values use the
5751
+ * factory's scrubbed host kwarg names (from the same `buildSigEntries` the
5752
+ * generated factory is built from), and the function name is package-qualified
5753
+ * so the call resolves through `from <root> import <pkg>`. A struct with no
5754
+ * registered factory (shouldn't happen for well-formed nested types) falls back
5755
+ * to the plain dict literal.
5756
+ */
5757
+ function pyDialectWithFactories(model, pkg) {
5758
+ const byKey = /* @__PURE__ */ new Map();
5759
+ for (const f of model.nestedFactories) byKey.set(f.structKey, {
5760
+ call: pkg ? `${pkg}.${f.funcName}` : f.funcName,
5761
+ nameByWire: new Map(f.sigEntries.map((e) => [e.wireKey, e.name]))
5762
+ });
5763
+ return {
5764
+ ...pyDialect,
5765
+ structConstructor(type, value, indent, d) {
5766
+ const entry = byKey.get(structKey(type));
5767
+ if (!entry) return void 0;
5768
+ const inner = indent + d.indentUnit;
5769
+ const lines = [];
5770
+ for (const [wireKey, fieldType] of Object.entries(type.fields)) {
5771
+ if (wireKey === "@type") continue;
5772
+ if (fieldType.kind === "literal") continue;
5773
+ if (!(wireKey in value)) continue;
5774
+ const name = entry.nameByWire.get(wireKey) ?? wireKey;
5775
+ lines.push(`${inner}${name}=${renderValue(value[wireKey], fieldType, inner, d)},`);
5776
+ }
5777
+ if (lines.length === 0) return `${entry.call}()`;
5778
+ return `${entry.call}(\n${lines.join("\n")}\n${indent})`;
5779
+ }
5780
+ };
5781
+ }
5782
+ /**
5712
5783
  * Render a Python call snippet for one tool from a config object (the params
5713
5784
  * dict the form produces, keyed by Boutiques wire names).
5714
5785
  *
@@ -5717,8 +5788,9 @@ const pyDialect = {
5717
5788
  * *scrubbed host* identifiers (`float` -> `float_`), not the wire keys; the
5718
5789
  * per-field mapping comes from the same `buildSigEntries` the generated wrapper
5719
5790
  * is built from, so the snippet matches the real signature. Nested structs /
5720
- * union variants / lists-of-structs have no constructor in the generated code,
5721
- * so they render as plain dict literals keyed by wire names.
5791
+ * union variants / lists-of-structs render as calls to their generated
5792
+ * `_params` factory (`fsl.bet_x_params(...)`), matching the per-struct builders
5793
+ * the Python backend emits.
5722
5794
  *
5723
5795
  * Union- (or otherwise non-struct-) rooted tools have no kwarg wrapper; the
5724
5796
  * single dict-style `<tool>` entry is called with one object-literal argument.
@@ -5739,21 +5811,22 @@ function renderPythonCall(ctx, config, opts = {}) {
5739
5811
  const model = buildEmitModel(ctx);
5740
5812
  const pkg = ctx.package?.name;
5741
5813
  const callee = pkg ? `${pkg}.${model.names.wrapper}` : model.names.wrapper;
5742
- const call = model.rootIsStruct && model.rootType.kind === "struct" ? renderKwargCall(callee, model.sigEntries, model.rootType, config) : `${callee}(${renderValue(config, model.rootType, "", pyDialect)})`;
5814
+ const dialect = pyDialectWithFactories(model, pkg);
5815
+ const call = model.rootIsStruct && model.rootType.kind === "struct" ? renderKwargCall(callee, model.sigEntries, model.rootType, config, dialect) : `${callee}(${renderValue(config, model.rootType, "", dialect)})`;
5743
5816
  if (opts.includeImport === false || !pkg) return call;
5744
5817
  const root = opts.packageRoot ?? ctx.project?.name;
5745
5818
  return `${root ? `from ${root} import ${pkg}` : `import ${pkg}`}\n\n${call}`;
5746
5819
  }
5747
5820
  /** Render `callee(name=value, ...)` for a struct root using scrubbed kwarg names. */
5748
- function renderKwargCall(callee, sigEntries, rootType, config) {
5821
+ function renderKwargCall(callee, sigEntries, rootType, config, dialect) {
5749
5822
  const nameFor = new Map(sigEntries.map((e) => [e.wireKey, e.name]));
5750
- const indent = pyDialect.indentUnit;
5823
+ const indent = dialect.indentUnit;
5751
5824
  const lines = [];
5752
5825
  for (const [wireKey, fieldType] of Object.entries(rootType.fields)) {
5753
5826
  if (fieldType.kind === "literal") continue;
5754
5827
  if (!(wireKey in config)) continue;
5755
5828
  const name = nameFor.get(wireKey) ?? wireKey;
5756
- lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, pyDialect)},`);
5829
+ lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, dialect)},`);
5757
5830
  }
5758
5831
  if (lines.length === 0) return `${callee}()`;
5759
5832
  return `${callee}(\n${lines.join("\n")}\n)`;