@styx-api/core 0.5.2 → 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
@@ -3781,7 +3781,8 @@ function emitMetadata$1(ctx, metaConst, cb) {
3781
3781
  cb.line(`name=${pyStr(name)},`);
3782
3782
  cb.line(`package=${pyStr(pkg)},`);
3783
3783
  if (ctx.app?.doc?.literature?.length) cb.line(`citations=[${ctx.app.doc.literature.map(pyStr).join(", ")}],`);
3784
- if (ctx.app?.container?.image) cb.line(`container_image_tag=${pyStr(ctx.app.container.image)},`);
3784
+ const containerImage = ctx.package?.docker ?? ctx.app?.container?.image;
3785
+ if (containerImage) cb.line(`container_image_tag=${pyStr(containerImage)},`);
3785
3786
  });
3786
3787
  cb.line(")");
3787
3788
  }
@@ -5399,7 +5400,29 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5399
5400
  names.params = (rootType.kind === "struct" ? namedTypes.get(structKey(rootType)) : void 0) ?? (rootType.kind === "union" ? namedTypes.get(unionKey(rootType)) : void 0) ?? names.params;
5400
5401
  const rootTypeTag = appId && pkg ? `${pkg}/${appId}` : void 0;
5401
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);
5402
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
+ }
5403
5426
  return {
5404
5427
  appId,
5405
5428
  pkg,
@@ -5410,13 +5433,14 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5410
5433
  typeDecls,
5411
5434
  rootTypeTag,
5412
5435
  paramsType,
5413
- sigEntries: rootIsStruct && rootType.kind === "struct" ? buildSigEntries(rootType, collectFieldInfo(ctx, rootType), (wireKey) => sigScope.add(pyScrubIdent(wireKey, PY_RESERVED)), pySigOptions(resolveTypeName(namedTypes))) : []
5436
+ sigEntries,
5437
+ nestedFactories
5414
5438
  };
5415
5439
  }
5416
5440
  function generatePython(ctx, packageScope) {
5417
5441
  const cb = new CodeBuilder(" ");
5418
5442
  const scope = packageScope ?? new Scope(PY_RESERVED);
5419
- 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);
5420
5444
  cb.comment("This file was auto generated by Styx.", "# ");
5421
5445
  cb.comment("Do not edit this file directly.", "# ");
5422
5446
  cb.blank();
@@ -5435,6 +5459,10 @@ function generatePython(ctx, packageScope) {
5435
5459
  emitParamsFactory$1(sigEntries, names.paramsFn, paramsType, rootTypeTag, cb);
5436
5460
  cb.blank();
5437
5461
  }
5462
+ for (const nf of nestedFactories) {
5463
+ emitParamsFactory$1(nf.sigEntries, nf.funcName, nf.typeName, nf.typeTag, cb);
5464
+ cb.blank();
5465
+ }
5438
5466
  emitValidate$1(ctx, rootType, ctx.expr, paramsType, names.validate, resolveTypeName(namedTypes), scope, cb);
5439
5467
  cb.blank();
5440
5468
  emitBuildCargs$1(ctx, rootType, paramsType, names.cargs, cb);
@@ -5454,6 +5482,7 @@ function generatePython(ctx, packageScope) {
5454
5482
  names.cargs,
5455
5483
  ...[names.outputsFn],
5456
5484
  ...rootIsStruct ? [names.paramsFn, names.execute] : [],
5485
+ ...nestedFactories.map((nf) => nf.funcName),
5457
5486
  names.validate,
5458
5487
  names.wrapper
5459
5488
  ];
@@ -5621,9 +5650,13 @@ function structAtType(type) {
5621
5650
  }
5622
5651
  /**
5623
5652
  * Render a struct config as a host object literal (Python dict / TS object).
5624
- * Keys are the Boutiques wire names (the generated TypedDict / interface keys) -
5625
- * nested structs have no constructor function in the generated code, so callers
5626
- * 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.
5627
5660
  *
5628
5661
  * `@type` is emitted from the struct's literal discriminator field when present
5629
5662
  * (union variants carry a required, load-bearing `@type`); for the root call the
@@ -5633,6 +5666,10 @@ function structAtType(type) {
5633
5666
  */
5634
5667
  function renderStructLiteral(value, type, indent, d, injectAtType) {
5635
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
+ }
5636
5673
  const inner = indent + d.indentUnit;
5637
5674
  const lines = [];
5638
5675
  const atType = structAtType(type) ?? injectAtType;
@@ -5708,6 +5745,41 @@ const pyDialect = {
5708
5745
  objKey: (k) => pyStr(k)
5709
5746
  };
5710
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
+ /**
5711
5783
  * Render a Python call snippet for one tool from a config object (the params
5712
5784
  * dict the form produces, keyed by Boutiques wire names).
5713
5785
  *
@@ -5716,8 +5788,9 @@ const pyDialect = {
5716
5788
  * *scrubbed host* identifiers (`float` -> `float_`), not the wire keys; the
5717
5789
  * per-field mapping comes from the same `buildSigEntries` the generated wrapper
5718
5790
  * is built from, so the snippet matches the real signature. Nested structs /
5719
- * union variants / lists-of-structs have no constructor in the generated code,
5720
- * 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.
5721
5794
  *
5722
5795
  * Union- (or otherwise non-struct-) rooted tools have no kwarg wrapper; the
5723
5796
  * single dict-style `<tool>` entry is called with one object-literal argument.
@@ -5738,21 +5811,22 @@ function renderPythonCall(ctx, config, opts = {}) {
5738
5811
  const model = buildEmitModel(ctx);
5739
5812
  const pkg = ctx.package?.name;
5740
5813
  const callee = pkg ? `${pkg}.${model.names.wrapper}` : model.names.wrapper;
5741
- 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)})`;
5742
5816
  if (opts.includeImport === false || !pkg) return call;
5743
5817
  const root = opts.packageRoot ?? ctx.project?.name;
5744
5818
  return `${root ? `from ${root} import ${pkg}` : `import ${pkg}`}\n\n${call}`;
5745
5819
  }
5746
5820
  /** Render `callee(name=value, ...)` for a struct root using scrubbed kwarg names. */
5747
- function renderKwargCall(callee, sigEntries, rootType, config) {
5821
+ function renderKwargCall(callee, sigEntries, rootType, config, dialect) {
5748
5822
  const nameFor = new Map(sigEntries.map((e) => [e.wireKey, e.name]));
5749
- const indent = pyDialect.indentUnit;
5823
+ const indent = dialect.indentUnit;
5750
5824
  const lines = [];
5751
5825
  for (const [wireKey, fieldType] of Object.entries(rootType.fields)) {
5752
5826
  if (fieldType.kind === "literal") continue;
5753
5827
  if (!(wireKey in config)) continue;
5754
5828
  const name = nameFor.get(wireKey) ?? wireKey;
5755
- lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, pyDialect)},`);
5829
+ lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, dialect)},`);
5756
5830
  }
5757
5831
  if (lines.length === 0) return `${callee}()`;
5758
5832
  return `${callee}(\n${lines.join("\n")}\n)`;
@@ -6896,7 +6970,8 @@ function emitMetadata(ctx, metaConst, cb) {
6896
6970
  cb.line(`name: ${JSON.stringify(name)},`);
6897
6971
  cb.line(`package: ${JSON.stringify(pkg)},`);
6898
6972
  if (ctx.app?.doc?.literature?.length) cb.line(`citations: ${JSON.stringify(ctx.app.doc.literature)},`);
6899
- if (ctx.app?.container?.image) cb.line(`container_image_tag: ${JSON.stringify(ctx.app.container.image)},`);
6973
+ const containerImage = ctx.package?.docker ?? ctx.app?.container?.image;
6974
+ if (containerImage) cb.line(`container_image_tag: ${JSON.stringify(containerImage)},`);
6900
6975
  });
6901
6976
  cb.line("};");
6902
6977
  }