Skip to content

feat: Add OutputBindings to TreeAction for declarative variable binding#84

Draft
ilang898 wants to merge 1 commit into
microsoft:masterfrom
ilang898:feature/output-bindings
Draft

feat: Add OutputBindings to TreeAction for declarative variable binding#84
ilang898 wants to merge 1 commit into
microsoft:masterfrom
ilang898:feature/output-bindings

Conversation

@ilang898
Copy link
Copy Markdown

Summary

Adds a new OutputBindings property to TreeAction that allows schema authors to declaratively bind ActionResponse properties to named variables. These variables are accessible via Vars.<name> in Roslyn expressions (ShouldSelect, action inputs, etc.), significantly reducing schema verbosity and improving readability.

Motivation

Today, ShouldSelect expressions in Forge schemas must use verbose Roslyn expressions to access action output properties:

Before (current):

{
  "ShouldSelect": "C#|Session.GetLastActionResponse().Status == \"Success\" && ((Newtonsoft.Json.Linq.JObject)Session.GetLastActionResponse().Output)[\"Metrics\"][\"Utilization\"].Value<double>() > 80.0"
}

This is hard to read, error-prone, and requires deep knowledge of the Roslyn expression API. With OutputBindings, the same logic becomes:

After (with OutputBindings):

{
  "Actions": {
    "CollectMetrics_Action": {
      "Action": "CollectMetricsAction",
      "OutputBindings": {
        "diagStatus": "Status",
        "utilization": "Output.Metrics.Utilization"
      }
    }
  },
  "ChildSelector": [
    {
      "ShouldSelect": "C#|Vars.diagStatus == \"Success\" && (double)Vars.utilization > 80.0",
      "Child": "HighUtilizationNode"
    }
  ]
}

How It Works

Schema Definition

OutputBindings is an optional Dictionary<string, string> on TreeAction:

  • Key: Variable name (accessible as Vars.<name> in expressions)
  • Value: Dot-path into the ActionResponse (e.g., Status, Output.Metrics.Score, Output.Items[0].Name)

Supported Dot-Path Expressions

Path Resolves To
Status ActionResponse.Status (string)
StatusCode ActionResponse.StatusCode (int)
Output ActionResponse.Output (object)
Output.PropertyName Nested property on Output
Output.Items[0] Array element access
Output.Items[0].Name Nested property on array element

Expression Access

Bound variables are available on the Vars dynamic object in all Roslyn expressions:

// Boolean routing
C#|Vars.diagStatus == "Success"

// Numeric comparison
C#|(double)Vars.utilization > 80.0

// Combine multiple variables from different actions
C#|Vars.isolationResult == "Completed" && Vars.verifyStatus == "Verified"

// Use in action inputs
C#|Vars.ticketId

// Access via Session.GetVar() in code
C#|Session.GetVar("diagStatus")?.ToString() == "Success"

Variable Scope & Lifetime

  • Variables are session-scoped they persist across nodes within the same tree walk
  • Later bindings with the same name overwrite earlier ones
  • Variables survive rehydration (persisted to ForgeState)
  • Variables are available in AfterVisitNode callbacks via TreeNodeContext.OutputVars

Complete Schema Example

{
  "Tree": {
    "Root": {
      "Type": "Action",
      "Actions": {
        "CollectDiagnostics_Action": {
          "Action": "CollectDiagnosticsAction",
          "Input": "C#|\"RunDiagnostics\"",
          "OutputBindings": {
            "diagStatus": "Status",
            "diagOutput": "Output"
          }
        }
      },
      "ChildSelector": [
        {
          "ShouldSelect": "C#|Vars.diagStatus == \"Success\"",
          "Child": "SuccessNode"
        },
        {
          "ShouldSelect": "C#|Vars.diagStatus != \"Success\"",
          "Child": "FailureNode"
        }
      ]
    },
    "SuccessNode": {
      "Type": "Action",
      "Actions": {
        "ApplyFix_Action": {
          "Action": "ApplyFixAction",
          "Input": "C#|Vars.diagOutput",
          "OutputBindings": {
            "fixResult": "Status"
          }
        }
      },
      "ChildSelector": [
        {
          "ShouldSelect": "C#|Vars.fixResult == \"Success\"",
          "Child": "VerifyNode"
        }
      ]
    },
    "FailureNode": { "Type": "Leaf" },
    "VerifyNode": { "Type": "Leaf" }
  }
}

Implementation Details

Files Changed

File Change
ForgeTree.cs Added OutputBindings property to TreeAction
ExpressionExecutor.cs Added Vars (ExpandoObject) to CodeGenInputParams, System.Dynamic assembly ref
TreeWalkerSession.cs Core logic: ResolveAllOutputBindingsForNode, ResolveOutputBindings, ResolveDotPath, CommitOutputVars, RehydrateOutputVars, GetVar
ITreeSession.cs Added GetVar(string name) method
TreeNodeContext.cs Added OutputVars dictionary for callback access
ForgeExceptions.cs Added OutputBindingException
ForgeSchemaValidationRules.json Added OutputBindings to ActionDefinition schema
Unit tests 14 test methods + 6 inline schemas + 1 file-based example schema

Design Decisions

  1. Thread safety: Bindings are resolved after all parallel actions on a node complete (not per-action), ensuring safe access to the shared Vars ExpandoObject.
  2. Continuation responses: Bindings work with timeout and retry-exhaustion synthetic responses ResolveAllOutputBindingsForNode reads the committed response regardless of how it was produced.
  3. JValue unwrapping: Rehydrated values are unwrapped from JValue to CLR types so string comparisons (Vars.status == "Success") work correctly.
  4. Dot-path validation: Negative array indexes and malformed bracket syntax (e.g., Items[0]foo) are rejected with descriptive OutputBindingException messages.

Breaking Change Note

ITreeSession.GetVar(string name) is a new method on the public interface. Existing implementations will need to add this method (can return null). This was chosen over an extension method because .NET Standard 2.0 doesn't support default interface methods.

Testing

  • 14 unit tests covering: basic status binding, output property access, cross-node variable persistence, variable overwriting, variable use in action inputs, GetVar accessor, schema validation
  • 6 inline test schemas + 1 file-based example schema

Adds a new OutputBindings property to TreeAction that allows schema authors
to declaratively bind ActionResponse properties to named variables. These
variables are accessible via Vars.<name> in Roslyn expressions (ShouldSelect,
action inputs, etc.), reducing schema verbosity and improving readability.

Key changes:
- TreeAction.OutputBindings: Dictionary<string, string> mapping variable
  names to dot-path expressions into the ActionResponse
- Vars ExpandoObject on CodeGenInputParams for Roslyn expression access
- Dot-path resolution supporting nested properties, array indexing, and
  JObject/CLR object traversal
- Session-scoped variable persistence via ForgeState for rehydration
- OutputVars exposed on TreeNodeContext for AfterVisitNode callbacks
- Thread-safe binding resolution (post all parallel actions)
- OutputBindingException for clear error reporting
- 14 unit tests covering basic, cross-node, overwrite, and accessor scenarios
- Schema validation rules updated for OutputBindings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ilang898
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

@microsoft-github-policy-service agree company="Microsoft"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant