Skip to content

feat(flows): add decision node type for constrained LLM branching#210

Closed
JoshuaLelon wants to merge 2 commits into
openclaw:mainfrom
JoshuaLelon:feat/decision-node-type
Closed

feat(flows): add decision node type for constrained LLM branching#210
JoshuaLelon wants to merge 2 commits into
openclaw:mainfrom
JoshuaLelon:feat/decision-node-type

Conversation

@JoshuaLelon

@JoshuaLelon JoshuaLelon commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a first-class decision node type to ACPX flows for single constrained LLM calls that return validated { choice, reasoning? } results
  • Adds resolveDecision callback on FlowRunnerOptions — keeps ACPX provider-agnostic while enabling callers to inject structured output implementations (OpenAI response_format, Anthropic tool_use, etc.)
  • Falls back to isolated ACP agent session with structured prompt + extractJsonObject parse when no resolver is provided
  • Validates choice ∈ options at runtime; invalid choices fail the node
  • reasoning is optional — resolvers that don't need to explain themselves can omit it
  • Extracts shared runIsolatedAcpPromptWithTrace helper to deduplicate the isolated session path between executeAcpNode and executeDecisionNode
  • Output feeds naturally into existing switch edges via $.choice

Usage

import { decision, defineFlow, compute } from "acpx/flows";

const flow = defineFlow({
  name: "tone-review",
  startAt: "check_tone",
  nodes: {
    check_tone: decision({
      prompt: (ctx) => `Is the tone right for ${ctx.input.audience}?`,
      options: {
        good: "Tone is appropriate",
        too_formal: "Too formal, needs loosening",
        too_casual: "Too casual, needs tightening",
      },
    }),
    publish: compute({ run: () => ({ status: "published" }) }),
    revise: compute({ run: () => ({ status: "needs_revision" }) }),
  },
  edges: [
    {
      from: "check_tone",
      switch: {
        on: "$.choice",
        cases: {
          good: "publish",
          too_formal: "revise",
          too_casual: "revise",
        },
      },
    },
  ],
});

Files changed

File Change
src/flows/types.ts DecisionNodeDefinition, DecisionResolverInput, DecisionResult types; resolveDecision on FlowRunnerOptions; reasoning optional
src/flows/definition.ts decision() builder helper
src/flows/runtime.ts executeDecisionNode, validateDecisionResult, ACP fallback, runIsolatedAcpPromptWithTrace shared helper
src/flows/store.ts Snapshot serialization for decision nodes
src/flows.ts Public exports
test/flows.test.ts 7 test cases (routing, invalid choice, context propagation, optional reasoning, bad reasoning type, ACP fallback)

Test plan

  • pnpm run typecheck — clean
  • pnpm run build — clean
  • pnpm run test — 0 failures (7 new decision tests + all existing tests pass)
  • Pre-commit hooks (oxfmt, oxlint, build) pass

🤖 AI-assisted (Claude Code) — fully tested

Generated with Claude Code

JoshuaLelon and others added 2 commits April 1, 2026 19:08
Add a first-class `decision` node type to ACPX flows that makes a single
constrained LLM call and returns a validated `{ choice, reasoning }` result
for use with switch edge routing.

Key design:
- `DecisionNodeDefinition` takes `prompt`, `options: Record<string, string>`,
  and optional `model`/`profile` fields
- `resolveDecision` callback on `FlowRunnerOptions` keeps ACPX provider-agnostic —
  callers inject their own structured output implementation (e.g. OpenAI
  response_format, Anthropic tool_use)
- Falls back to isolated ACP agent session with structured prompt + JSON parse
  when no resolver is provided
- `validateDecisionResult` enforces choice ∈ options at runtime
- Output feeds naturally into existing `switch` edges via `$.choice`

🤖 AI-assisted (Claude Code) — fully tested

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ptional

- Make DecisionResult.reasoning optional — resolveDecision callbacks no
  longer need to provide reasoning if the decision doesn't require it
- Extract runIsolatedAcpPromptWithTrace helper to deduplicate the isolated
  session path shared by executeAcpNode and executeDecisionNode (~70 lines)
- Add ACP fallback test for decision nodes (no resolveDecision provided)
- Add test for optional reasoning and non-string reasoning validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@steipete

Copy link
Copy Markdown
Contributor

Thanks for the thoughtful PR. I did a Codex maintainer review against current main and I don't think this is safe to land in this shape.

Concrete blockers:

  1. The branch no longer rebases cleanly onto main; src/flows/runtime.ts conflicts with the current flow runtime.
  2. The new public decision node type is not wired into the current flow definition schema (src/flows/schema.ts). On current main, defineFlow({... decision(...) ...}) is validated before execution, so the new node type would be rejected before the runtime path added here can run.
  3. This adds a new public flow-authoring API but does not update the flow docs/changelog or the schema/validation tests that define the supported authoring contract.
  4. Design-wise, this is larger than a narrow fix: ACPX already supports constrained branching with acp + parse + switch. A first-class LLM decision node needs an explicit API decision around whether provider-specific structured output belongs in FlowRunnerOptions, in ACP node parsing, or in a separate helper/composition layer.

The useful bits here are the motivation and the tests around constrained choices, but this should come back as a smaller design-aligned PR: either a documented helper built on existing acp nodes, or a complete node-type proposal that updates schema, docs, snapshots, replay/viewer expectations, and compatibility tests together.

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.

2 participants