AI Conversation Loop
File: /inc/Engine/AI/conversation-loop.php
Since: 0.2.0
Data Machine runs multi-turn agent work through the Agents API conversation substrate. The canonical Data Machine entry point is DataMachineEngineAIdatamachine_run_conversation(); the former AIConversationLoop class has been removed.
Current Ownership
Data Machine caller
|
v
datamachine_run_conversation()
| owns Data Machine runtime policy and builds the turn runner
v
AgentsAPIAIWP_Agent_Conversation_Loop::run()
| owns generic turn sequencing, budgets, transcripts, locks, events
v
Data Machine turn runner closure
| owns request assembly, wp-ai-client dispatch, tool execution
v
RequestBuilder::build() -> wp_ai_client_prompt()WP_Agent_Conversation_Loop is the generic runtime loop. It should not know about Data Machine jobs, flow steps, handlers, tool policies, or completion assertions. Data Machine adapts those product concepts into the generic loop with options and a turn runner.
Canonical Entry Point
use function DataMachineEngineAIdatamachine_run_conversation;
$result = datamachine_run_conversation(
$messages, // Initial messages; normalized to Agents API envelopes.
$tools, // Data Machine tool definitions keyed by tool name.
$provider, // wp-ai-client provider identifier.
$model, // wp-ai-client model identifier.
$mode, // 'pipeline', 'chat', 'system', or extension mode.
$payload, // Data Machine runtime payload.
$max_turns, // Turn ceiling; resolved through IterationBudgetRegistry.
$single_turn // Stop after exactly one provider turn.
);Current callers, including AIStep and ChatOrchestrator, call this function directly. New code should not introduce AIConversationLoop::run() examples; references to that class are historical only.
What Data Machine Owns
datamachine_run_conversation() owns the Data Machine adapter layer around the generic loop:
- Message normalization through
AgentsAPIAIWP_Agent_Message::normalize_many(). - Runtime object resolution from
$payload, including event sinks, transcript persisters, transcript locks, completion assertions, and tool runtime rules. - Runtime object stripping before passing payload data into tools and requests.
- Turn budget construction through
IterationBudgetRegistry::create( 'conversation_turns', 0, $max_turns ). - Base log context for
mode,job_id,flow_step_id, andagent_slug. - Completion assertion preflight for required tools that are unavailable to the model.
- Data Machine turn runner creation through
datamachine_build_turn_runner(). - Mapping the Agents API
budget_exceededstatus back to Data Machine’smax_turns_reachedcompatibility flag. - Augmenting the normalized Agents API result with Data Machine-only fields such as
last_tool_calls, completion nudge diagnostics, and completion assertion diagnostics.
What Agents API Owns
AgentsAPIAIWP_Agent_Conversation_Loop::run() owns generic runtime mechanics:
- Turn sequencing and turn count.
- Budget enforcement through the
budgetsoption. - Final result normalization fields such as
messages,final_content,turn_count,status,usage, andrequest_metadata. - Transcript persistence via
transcript_persister. - Transcript locking via
transcript_lock,transcript_session_id, andtranscript_lock_ttl. - Runtime events through the
on_eventcallback. - Generic continuation decisions through the
should_continuecallback.
Data Machine passes a WP_Agent_Conversation_Request into the loop options. Its metadata includes the selected provider, model, and WordPressWorkspaceScope::metadata() so hosts can inspect the WordPress runtime associated with the run.
Turn Runner Responsibilities
The Data Machine turn runner handles one provider turn:
- Build and dispatch a provider request with
RequestBuilder::build(). - Emit a
request_builtevent with turn count, provider, model, success status, and request metadata. - Convert
WP_Errorrequest failures into a structured runtime error by throwingRuntimeException;datamachine_run_conversation()catches it and returns an error result. - Extract tool calls from the
GenerativeAiResult. - Extract text content with
RequestBuilder::resultText(). - Accumulate token usage for the substrate to total across turns.
- Append assistant text messages with
ConversationManager::buildConversationMessage(). - Validate duplicate tool calls with
ConversationManager::validateToolCall(). - Enforce Data Machine tool runtime rules before execution.
- Execute tools through
ToolExecutor::executeTool()with mode, agent, and client context. - Record Data Machine completion policy progress after each tool result.
- Append tool call and tool result envelope messages through
ConversationManager. - Add completion nudges when assertions are still missing and another turn is useful.
The turn runner returns per-turn messages, tool_execution_results, request_metadata, usage, conversation_complete, and continuation hints. The Agents API loop merges those into the final result.
Continuation Rules
Data Machine’s should_continue callback is intentionally small because Agents API owns the loop mechanics:
- Stop immediately when
$single_turnis true. - Stop when the turn runner marks
conversation_complete. - Continue when the turn executed tools, appended a completion nudge, rejected a duplicate call, or rejected a tool runtime rule.
Natural completion is still a Data Machine policy decision. If there are no tool calls, the turn runner asks the resolved completion policy whether natural completion is acceptable. Completion assertions can therefore keep the loop running even after a text-only model response.
Completion Assertions
Pipeline and chat payloads may include completion_assertions. Data Machine resolves them before entering the substrate loop.
Simple required-tool assertions require named tools to run successfully:
{
"completion_assertions": {
"required_tool_names": ["create_or_update_github_file", "create_github_pull_request"]
}
}Assertions can also require engine data keys, minimum successful tool counts, output fields, parameter matches, or any one of several named outcomes:
{
"completion_assertions": {
"complete_when_any": [
{
"name": "content_proposal",
"tools": [
{ "name": "create_or_update_github_file", "min_successful_calls": 2 },
{ "name": "create_github_pull_request", "required_output": ["html_url"] }
]
},
{
"name": "issue_reply",
"tools": [
{
"name": "manage_github_issue",
"required_parameters": { "action": "comment" },
"required_output": ["comment.html_url"]
}
]
}
]
}
}If a required tool is not available in the current tool set, datamachine_run_conversation() returns an error before any provider call:
[
'completed' => false,
'error_code' => 'completion_required_tool_unavailable',
'completion_assertions_required' => $assertions->required(),
'unavailable_required_tool_names' => $unavailable_required_tools,
'available_tool_names' => array_keys( $tools ),
'status' => 'error',
]When assertions are missing after a natural completion or partial progress, Data Machine appends a nudge as a user message and keeps the loop running when useful. Final results may include:
completion_nudge_countcompletion_nudgecompletion_assertions_requiredcompletion_assertions_missingcompletion_assertions_satisfiedcompletion_assertions_complete
Job engine data and loop events receive the same diagnostics for evidence and artifact reporting.
Result Shape
The returned array is normalized by AgentsAPIAIWP_Agent_Conversation_Result::normalize() and then augmented by Data Machine.
Common fields:
[
'messages' => [], // canonical Agents API envelopes
'final_content' => '',
'turn_count' => 1,
'completed' => true,
'status' => 'completed',
'last_tool_calls' => [],
'tool_execution_results' => [],
'usage' => [],
'request_metadata' => [],
]Error results use the same array style and include error; budget exhaustion also includes max_turns_reached and a warning.
Runtime Gates And Transport
Provider availability is checked in RequestBuilder::build(), not in the conversation loop. The turn runner treats a WP_Error from RequestBuilder as a failed turn and returns a structured error result.
Transport and provider behavior are documented in RequestBuilder Pattern. The conversation loop stores the per-turn request_metadata returned by RequestBuilder so callers and tests can inspect directives, request sizes, provider/model, and transport profile.
Test Hooks
Runtime tests can control or observe execution through stable hooks and payload collaborators:
datamachine_wp_ai_client_text_resultshort-circuits provider dispatch and may returnWP_Error,GenerativeAiResult, or compact array data for test doubles.datamachine_wp_ai_client_availabilityoverrides wp-ai-client availability checks.datamachine_wp_ai_client_request_timeoutanddatamachine_wp_ai_client_connect_timeoutcontrol timeout profiles.event_sinkin the payload receives loop events throughLoopEventSinkInterface.transcript_persister,transcript_lock,transcript_session_id, andtranscript_lock_ttlexercise transcript persistence and locking.
Representative smoke tests:
tests/agent-conversation-runner-request-smoke.phpverifies thatdatamachine_run_conversation()delegates through the Agents API substrate and returns normalized content, usage, tool results, and events.tests/agent-conversation-runtime-policy-smoke.phpcovers completion assertions, natural completion, nudges, duplicate-tool recovery, runtime rules, and completion diagnostics.tests/ai-message-envelope-smoke.phpverifies message envelope normalization and result normalization.
Historical Context
Older docs and changelog entries may mention AIConversationLoop::run() or AIConversationLoop::execute(). Those references describe the pre-substrate compatibility class. Current runtime docs and examples should use datamachine_run_conversation() and AgentsAPIAIWP_Agent_Conversation_Loop::run().