Skip to content

refactor: extract conversation-result normalizer with named status constants (#2803 slice 2)#2815

Merged
chubes4 merged 2 commits into
mainfrom
decompose-result-normalizer
Jun 28, 2026
Merged

refactor: extract conversation-result normalizer with named status constants (#2803 slice 2)#2815
chubes4 merged 2 commits into
mainfrom
decompose-result-normalizer

Conversation

@chubes4

@chubes4 chubes4 commented Jun 28, 2026

Copy link
Copy Markdown
Member

Summary

Second scoped slice of #2803. Decomposes the post-loop conversation-result "status surgery" out of datamachine_run_conversation() (in inc/Engine/AI/conversation-loop.php) into a cohesive normalizer with named status constants and a single decision path. The first slice (provider-turn adapter / DataMachineProviderTurnState) merged as #2808.

This is a readability/structure refactor, NOT a behavior change — the status strings on the wire are unchanged and the normalized result is byte-for-byte identical for every status path.

Status constants introduced

New final class DataMachineConversationStatus (inc/Engine/AI/DataMachineConversationStatus.php) — the single source of truth for the conversation status/finish vocabulary. Each constant holds the same string value as the literal it replaces (so the wire format is unchanged):

Constant Value
BUDGET_EXCEEDED budget_exceeded
RUNTIME_TOOL_PENDING runtime_tool_pending
COMPLETED completed
FAILED failed
INTERRUPTED interrupted
MAX_TURNS_REACHED max_turns_reached
COMPLETION_POLICY_STOP completion_policy_stop
COMPLETION_POLICY_CONTINUE completion_policy_continue
PROVIDER_ERROR provider_error

Plus incompleteStatuses() mirroring the inline array( 'budget_exceeded', 'interrupted', 'failed' ) used to seed the completed flag.

All bare conversation-status value literals in conversation-loop.php now route through these constants. (Metadata array keys like 'completed' => false, and the distinct job/persistence/tool-result vocabularies — complete_job('completed'/'failed'), persistence_status, 'success'/'failed' — are intentionally left alone; they belong to other layers and converting them would conflate domains.)

Normalizer extracted

  • ConversationResultNormalizer::normalize() (inc/Engine/AI/ConversationResultNormalizer.php) — a pure transform that takes the substrate-normalized loop result + surrounding turn context and returns the derived metadata.datamachine diagnostics (completed / max_turns_reached / runtime_tool_pending / interrupted / completion-nudge + completion-assertion evaluation / warning). It collapses the ~8 scattered if-blocks — which each re-read $result['status'] and recomputed completed in four different places — into one decision path keyed off the constants.
  • ConversationResultNormalization (inc/Engine/AI/ConversationResultNormalization.php) — small immutable value object carrying metadata, status, and a status_overridden flag. The caller writes the status back onto the result only when overridden (the runtime_tool_pending path is the only one that overrides), so status-less results don't gain a spurious empty status key — preserving byte-for-byte parity.

datamachine_run_conversation() now: run the loop → hand the result to the normalizer → apply the (only-when-overridden) status → attach metadata → return.

Line count

datamachine_run_conversation(): ~392 → 330 lines (the result-surgery block shrank from ~88 lines of scattered if-blocks to a single ~20-line normalizer call). Two now-dead outer accumulator vars ($runtime_tool_pending, $runtime_tool_requests) removed.

Wire-string parity

Confirmed unchanged: every constant value equals the prior literal byte-for-byte. The only status mutation ($result['status'] = 'runtime_tool_pending') is reproduced exactly via the status_overridden flag.

Per-status parity confirmation

Verified each path produces identical diagnostics (standalone harness + smoke tests):

  • completedcompleted=true, status unchanged
  • budget_exceeded (max turns)completed=false, max_turns_reached=true, warning, status unchanged
  • interruptedcompleted=false, interrupted=<payload>, status unchanged
  • failed / provider_errorcompleted=false, status unchanged (transcript persist still triggers via the FAILED check)
  • runtime_tool_pendingcompleted=false, runtime_tool_pending=true, runtime_tool_pending_requests=[...], status overridden to runtime_tool_pending
  • completion_policy_stop → suppresses the policy-not-satisfied max-turn warning exactly as before
  • silent max-turns-reached (tool calls present, turn_count ≥ ceiling, status ≠ budget_exceeded) → completed=false, max_turns_reached=true, warning, status unchanged

Test results

  • php -l clean on all four touched files.
  • homeboy lint data-machine / phpcs: clean (exit 0) on all four files, including the equals-alignment sniff (Generic.Formatting.MultipleStatementAlignment). phpstan: 0 findings. (The repo's eslint findings are pre-existing JS — no JS touched in this slice.)
  • Relevant smoke tests pass: agent-conversation-result, agents-chat-tool-continuation, ai-loop-event-sink, ai-completion-assertion-packet, runtime-tool-delegated-result, runtime-tool-async-broker, provider-turn-tool-extraction-contract, typed-artifact-output-contract, ai-enabled-tools, tool-result-consumer-contract.
  • Note: ai-error-status-propagation-smoke and runtime-tool-contract-store-smoke fail identically on clean main when run via standalone php (missing WP stubs: wp_json_encode, EngineData) — pre-existing/environmental, unrelated to this slice and outside the result normalizer.

Remaining #2803 scope (do NOT close)

This slice covers only the result-status normalization. Still open under #2803:

  • The 15-param / 6-by-ref datamachine_build_provider_turn_adapter() closure — the by-ref accumulator / turn-state smuggling (partially started by DataMachineProviderTurnState in refactor: route DM provider turn through default adapter dispatch seam (#2803) #2808).
  • The broader god-file split: the L1200–1900 runtime-tool fulfillment subsystem (RuntimeToolFulfillment) and the overall 64-free-function file decomposition.

Refs #2803.

…nstants

Decompose the post-loop "result surgery" out of datamachine_run_conversation()
into a cohesive normalizer with a single decision path, and replace the bare
status-string literals with named constants.

- Add DataMachineConversationStatus: single source of truth for the
  conversation status/finish vocabulary (budget_exceeded, runtime_tool_pending,
  completed, failed, interrupted, max_turns_reached, completion_policy_stop,
  completion_policy_continue, provider_error). Constants hold the SAME wire
  strings, so the status format on the wire is unchanged.
- Add ConversationResultNormalizer::normalize() + ConversationResultNormalization
  value object: derives the metadata.datamachine diagnostics (completed /
  max_turns_reached / runtime_tool_pending / interrupted / completion-assertion
  evaluation / warning) in one pass keyed off the constants, replacing the ~8
  scattered if-blocks that each re-read $result['status'] and recomputed the
  completed flag in four places.
- datamachine_run_conversation() now dispatches the loop, hands the result to
  the normalizer, applies the (only-when-overridden) status, and returns —
  dropping from ~392 to 330 lines.

Second scoped slice of #2803. Behavior/wire parity preserved for every status
path; the status override (runtime_tool_pending) is applied only when the
normalizer flags it, so status-less results gain no spurious empty status key.
@homeboy-ci

homeboy-ci Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Homeboy Results — data-machine

Lint

lint — passed

ℹ️ Full options: homeboy docs commands/lint
Deep dive: homeboy lint data-machine --changed-since 3cbfc1b

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-lint-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-lint-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/28336860393

Test

test — passed

  • 1294 passed
  • 4 skipped

ℹ️ Auto-fix lint issues: homeboy refactor data-machine --from lint --write
ℹ️ Collect coverage: homeboy test data-machine --coverage
ℹ️ Save test baseline: homeboy test data-machine --baseline
ℹ️ Pass args to test runner: homeboy test -- [args]
ℹ️ Full options: homeboy docs commands/test
Deep dive: homeboy test data-machine --changed-since 3cbfc1b

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-test-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-test-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/28336860393

Audit

audit — passed

  • audit — 10 finding(s)
  • Total: 10 finding(s)

Deep dive: homeboy audit data-machine --changed-since 3cbfc1b

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-audit-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-audit-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/28336860393
Tooling versions
  • Homeboy CLI: homeboy 0.269.2+f958423e4e6b+9d6bd2d3
  • Extension: wordpress from https://github.com/Extra-Chill/homeboy-extensions
  • Extension revision: 846eeef8
  • Action: unknown@unknown

Separate the two short scalar assignments ($status, $status_overridden)
from the wider array-key assignment block in the runtime-tool-pending
branch with a blank line, so they form their own alignment group instead
of being padded out to the array-block column. Resolves the CI
Generic.Formatting.MultipleStatementAlignment.NotSameWarning findings
("expected N spaces but found 48/37 spaces") on those lines. Verified
clean against the homeboy WordPress phpcs ruleset.
@chubes4 chubes4 merged commit aa98d24 into main Jun 28, 2026
5 checks passed
@chubes4 chubes4 deleted the decompose-result-normalizer branch June 28, 2026 22:52
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