Explicit I/O mapping between node and bound action #3

Open
opened 2026-04-16 11:06:26 +00:00 by timur · 0 comments
Owner

Motivation

Today a hero_logic node and its bound hero_proc action share input/output shape by name. If both sides agree (node input prompt → action input prompt), life is easy — and as of 7453ebd the node's input_schema auto-populates from the action on bind, so the common case needs zero configuration.

But users legitimately want more:

  • Rename: workflow surfaces user_prompt; action expects prompt. Today you have to make them match — a leaky abstraction.
  • Passthrough: take a node input straight to a node output without the action touching it.
  • Constant / literal: pass a fixed value as an action input (e.g. mode = "concise") so the workflow user doesn't have to enter it.
  • Derived output: compute a node output from an action output + a node input (concat, cast, rename).
  • Fan-out / selection: pick one field out of an action's object output and surface it as the node's primary output.

All of these are currently impossible without inserting an intermediate hero_proc action just to shuffle fields.

Proposal

Add two optional, ordered mapping tables to NodeConfig:

Mapping = {
  target: str   # the name this mapping produces
  source: str   # expression that resolves to a value
}

NodeConfig {
  ...existing fields...
  action_input_mappings: [Mapping]   # node.inputs + literals + outputs-of-earlier-nodes → action.inputs
  action_output_mappings: [Mapping]  # action.outputs + node.inputs + literals → node.outputs
}

Source expression grammar (v1, conservative):

  • inputs.<name> / inputs.<nested.path> — read from the node's own input object at run-time.
  • outputs.<name> — read from the action's parsed output (only valid in action_output_mappings).
  • literal:<value> — a constant string. For JSON values (numbers, objects), literal: prefix can be dropped in favour of a literal_json:<json> form if we need it.

These are the same tokens the engine's existing {{inputs.X}} / {{outputs.X}} interpolation grammar already understands, so we can reuse hero_proc_lib::template::resolve_path (or an equivalent in hero_logic).

Defaulting / back-compat

  • If action_input_mappings is empty, the engine passes inputs to job.create 1:1 (current behaviour).
  • If action_output_mappings is empty, the action's parsed output IS the node's output (current behaviour).
  • When the user picks an action via the UI, the mappings are pre-populated with identity rows for every declared input/output (one entry per property, target=name, source=inputs.name or outputs.name). Users can then rename/rewire without starting from a blank slate.

Engine changes

crates/hero_logic/src/engine/node_executors.rs::execute_action:

  1. Before job.create, if action_input_mappings is non-empty, build the inputs JSON by evaluating each source against the node's context (inputs.X, outputs.X, literal:…). Otherwise pass input_data as today.
  2. After the action finishes, if action_output_mappings is non-empty, build a new output object by evaluating each mapping against the combined (action_outputs, node_inputs) context; that becomes NodeRun.output_data. Otherwise, keep the raw action output as today.

UI changes (hero_logic_ui)

  • Node card gets a compact Mappings subsection inside the action column, collapsible, populated from action_input_mappings + action_output_mappings.
  • Each row: [target dropdown or input] ← [source expression]. Source can be edited as free text or via a picker that offers inputs.* from the node, outputs.* from the action, or literal:….
  • Identity rows are dimmed; non-identity rows are highlighted.
  • A Reset to identity button rebuilds mappings from the action's input_schema.

Non-goals (v1)

  • Computed expressions beyond path + literal (no concat / format / cast helpers yet — if needed, add a thin expression language later).
  • Validation that a mapping's target type matches the action's declared input type — nice-to-have, skip in v1.
  • Cross-node mappings via the mapping table (today that lives on edges as data_mappings); keep those separate for now.

Implementation checklist

  • Extend NodeConfig in crates/hero_logic/schemas/logic/logic.oschema: add action_input_mappings: [Mapping], action_output_mappings: [Mapping], plus the Mapping type.
  • Regenerate and patch template_loader.rs.
  • Engine: implement the two mapping passes in execute_action.
  • workflow_editor.js: on action bind, prefill both tables with identity rows from the action's declared schemas.
  • hero_logic_graph.js: render the Mappings subsection inside the action column.
  • Help text explaining the expression grammar (inputs.*, outputs.*, literal:…).

References

  • Current auto-populate on bind: 7453ebd
  • Templating engine the expression grammar mirrors: hero_proc_lib::template (see hero_proc#47).
  • Edge-level data_mappings for cross-node field routing (existing, unaffected by this proposal).
## Motivation Today a hero_logic node and its bound hero_proc action share input/output shape by name. If both sides agree (node input `prompt` → action input `prompt`), life is easy — and as of [7453ebd](https://forge.ourworld.tf/lhumina_code/hero_logic/commit/7453ebd) the node's `input_schema` auto-populates from the action on bind, so the common case needs zero configuration. But users legitimately want more: - **Rename**: workflow surfaces `user_prompt`; action expects `prompt`. Today you have to make them match — a leaky abstraction. - **Passthrough**: take a node input straight to a node output without the action touching it. - **Constant / literal**: pass a fixed value as an action input (e.g. `mode = "concise"`) so the workflow user doesn't have to enter it. - **Derived output**: compute a node output from an action output + a node input (concat, cast, rename). - **Fan-out / selection**: pick one field out of an action's object output and surface it as the node's primary output. All of these are currently impossible without inserting an intermediate hero_proc action just to shuffle fields. ## Proposal Add two optional, ordered mapping tables to `NodeConfig`: ``` Mapping = { target: str # the name this mapping produces source: str # expression that resolves to a value } NodeConfig { ...existing fields... action_input_mappings: [Mapping] # node.inputs + literals + outputs-of-earlier-nodes → action.inputs action_output_mappings: [Mapping] # action.outputs + node.inputs + literals → node.outputs } ``` **Source expression grammar** (v1, conservative): - `inputs.<name>` / `inputs.<nested.path>` — read from the node's own input object at run-time. - `outputs.<name>` — read from the action's parsed output (only valid in `action_output_mappings`). - `literal:<value>` — a constant string. For JSON values (numbers, objects), `literal:` prefix can be dropped in favour of a `literal_json:<json>` form if we need it. These are the same tokens the engine's existing `{{inputs.X}}` / `{{outputs.X}}` interpolation grammar already understands, so we can reuse `hero_proc_lib::template::resolve_path` (or an equivalent in hero_logic). ## Defaulting / back-compat - If `action_input_mappings` is empty, the engine passes `inputs` to `job.create` 1:1 (current behaviour). - If `action_output_mappings` is empty, the action's parsed output IS the node's output (current behaviour). - When the user picks an action via the UI, the mappings are **pre-populated with identity rows** for every declared input/output (one entry per property, `target=name`, `source=inputs.name` or `outputs.name`). Users can then rename/rewire without starting from a blank slate. ## Engine changes `crates/hero_logic/src/engine/node_executors.rs::execute_action`: 1. Before `job.create`, if `action_input_mappings` is non-empty, build the `inputs` JSON by evaluating each `source` against the node's context (`inputs.X`, `outputs.X`, `literal:…`). Otherwise pass `input_data` as today. 2. After the action finishes, if `action_output_mappings` is non-empty, build a new output object by evaluating each mapping against the combined `(action_outputs, node_inputs)` context; that becomes `NodeRun.output_data`. Otherwise, keep the raw action output as today. ## UI changes (hero_logic_ui) - Node card gets a compact **Mappings** subsection inside the action column, collapsible, populated from `action_input_mappings` + `action_output_mappings`. - Each row: `[target dropdown or input] ← [source expression]`. Source can be edited as free text or via a picker that offers `inputs.*` from the node, `outputs.*` from the action, or `literal:…`. - Identity rows are dimmed; non-identity rows are highlighted. - A `Reset to identity` button rebuilds mappings from the action's `input_schema`. ## Non-goals (v1) - Computed expressions beyond path + literal (no concat / format / cast helpers yet — if needed, add a thin expression language later). - Validation that a mapping's target type matches the action's declared input type — nice-to-have, skip in v1. - Cross-node mappings via the mapping table (today that lives on edges as `data_mappings`); keep those separate for now. ## Implementation checklist - [ ] Extend `NodeConfig` in `crates/hero_logic/schemas/logic/logic.oschema`: add `action_input_mappings: [Mapping]`, `action_output_mappings: [Mapping]`, plus the `Mapping` type. - [ ] Regenerate and patch `template_loader.rs`. - [ ] Engine: implement the two mapping passes in `execute_action`. - [ ] `workflow_editor.js`: on action bind, prefill both tables with identity rows from the action's declared schemas. - [ ] `hero_logic_graph.js`: render the Mappings subsection inside the action column. - [ ] Help text explaining the expression grammar (`inputs.*`, `outputs.*`, `literal:…`). ## References - Current auto-populate on bind: https://forge.ourworld.tf/lhumina_code/hero_logic/commit/7453ebd - Templating engine the expression grammar mirrors: `hero_proc_lib::template` (see hero_proc#47). - Edge-level `data_mappings` for cross-node field routing (existing, unaffected by this proposal).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_logic#3
No description provided.