AgentOrchestrator
Agentic tool-use loop: alternates between asking an {@see LlmService} for the next step and executing any tool calls it requests via {@see McpClientService}, until the LLM returns a final response or {@see self::$maxSteps} is reached.
AgentOrchestrator::setSystemPromptProjection()
public function setSystemPromptProjection(?callable $projection): selfProject this request's system instructions from the exact outgoing tool offer. The base prompt and conversation stay unchanged; null preserves the legacy path. Projection failures surface before contacting the model (greenhouse0867).
Parameters
| Name | Type | Description |
|---|---|---|
| $projection | (callable(string, list<array<string, mixed>>): string | null) |
AgentOrchestrator::setTrailingProjection()
public function setTrailingProjection(?callable $projection): selfProject one message AFTER the conversation, for this request only, from the exact outgoing tool offer. For what a caller must tell the model and GROWS during a run. Written into the system prompt, such text changes what comes BEFORE the conversation, so no request is a prefix of the next and the provider reads the whole prompt again on every step (greenhouse evidence/1109: 13 cold calls of 19). After the conversation, each request prolongs the one before. It rides as a user-role message — provider chat templates reject a system message that is not at the beginning — it is never accumulated into the history, and an empty string adds nothing. Null preserves the legacy path; a projection that fails surfaces before contacting the model.
Parameters
| Name | Type | Description |
|---|---|---|
| $projection | (callable(list<array<string, mixed>>): string | null) |
AgentOrchestrator::termination()
public function termination(): ?Milpa\AiGateway\RunTerminationThe latest base-loop exit; null before a run or while that run is in progress.
AgentOrchestrator::__construct()
public function __construct(Milpa\AiGateway\LlmService $llm, Milpa\ToolRuntime\Gate\GatedToolCalls $mcpClient, int $maxSteps = 20, ?Psr\Log\LoggerInterface $logger = null, ?Milpa\ToolRuntime\Rendering\RendererRegistry $rendererRegistry = null, ?Milpa\AiGateway\PlanBoard $planBoard = null, bool $lazyTools = false, ?Milpa\AiGateway\ProgressProbe $progressProbe = null, int $contextTokens = 0, ?Milpa\AiGateway\AnswerJudge $answerJudge = null, ?int $outputTokens = null):Parameters
| Name | Type | Description |
|---|---|---|
| $llm | Milpa\AiGateway\LlmService | |
| $mcpClient | Milpa\ToolRuntime\Gate\GatedToolCalls | |
| $maxSteps | int | |
| $logger | ?Psr\Log\LoggerInterface | |
| $rendererRegistry | ?Milpa\ToolRuntime\Rendering\RendererRegistry | |
| $planBoard | ?Milpa\AiGateway\PlanBoard | |
| $lazyTools | bool | |
| $progressProbe | ?Milpa\AiGateway\ProgressProbe | |
| $contextTokens | int | |
| $answerJudge | ?Milpa\AiGateway\AnswerJudge | |
| $outputTokens | ?int |
AgentOrchestrator::setOpeningMove()
public function setOpeningMove(?array $assistantMessage): selfOpen the next run with tool calls the caller already holds, instead of asking the model for them. For a call the caller knows deterministically: the one it recorded, that was refused, and whose refusal a person has since lifted. The loop takes the message as the reply to its first step WITHOUT contacting the provider; the calls then go through the same executor as any other — gate, recording, budget — the step counts against the ceiling, and the model reads the call and its result as its own conversation on the next step. It is played once. Null withdraws it. An opening move is tool calls and nothing else: words are never put in the model's mouth.
Parameters
| Name | Type | Description |
|---|---|---|
| $assistantMessage | (array<string, mixed> | null) | `role: assistant`, empty `content`, and `tool_calls` in the provider's shape (id, function.name, function.arguments as JSON text) |
Throws
\InvalidArgumentException when the message carries content, no call, or a malformed call
AgentOrchestrator::setContinuation()
public function setContinuation(?callable $continuation): selfLet the caller play the calls that follow deterministically from a tool call, instead of asking the model. After every tool call that ran, the continuation is asked with the tool, its arguments and what it answered. It answers null, or a list of `{name, arguments}` calls that follow from it — the promotion of a verified trial of an operation its house applies, say. The loop plays them as its NEXT STEP without contacting the provider: one assistant message with those calls, through the same executor as any other — gate, recording, budget. A continued call spends a step of the ceiling, may be refused like any call, and may itself be continued. One the run did not reach is dropped, never carried into another run. A continuation answers with tool calls or nothing: anything else fails the run.
Parameters
| Name | Type | Description |
|---|---|---|
| $continuation | (callable(string, array<string, mixed>, mixed): mixed | null) |
AgentOrchestrator::setUnofferedCall()
public function setUnofferedCall(?callable $told): selfTell the caller of a call the loop turned away because its tool was not offered in that step. A model can name a tool absent from the catalogue sent on a step — one its caller left out, or one that does not exist. The loop answers that call itself and never asks the registry, so nobody else heard of it: a house that keeps a session's log had no trace that the session tried (greenhouse decisions/0601, evidence/1163 §10). With this the caller is told, once per such call and in the order they came: the tool that was named, its arguments, and the very answer the model reads. Being told changes nothing: the answer is the same, the offer is the same, and the registry is still not asked. A teller that fails loses the telling, never the run. Null withdraws it.
Parameters
| Name | Type | Description |
|---|---|---|
| $told | (callable(string, array<string, mixed>, string): mixed | null) |
AgentOrchestrator::getLastToolResult()
public function getLastToolResult(): ?Milpa\ToolRuntime\ToolResultGet the last ToolResult from tool execution. Used by ProcessTelegramMessageJob to build keyboard from metadata.
AgentOrchestrator::setToolContext()
public function setToolContext(Milpa\ToolRuntime\Contracts\ToolContext $ctx): selfSet the tool context for channel-specific rendering.
Parameters
| Name | Type | Description |
|---|---|---|
| $ctx | Milpa\ToolRuntime\Contracts\ToolContext |
AgentOrchestrator::run()
public function run(string $prompt, string $systemPrompt = 'You are a helpful assistant.', array $history = [], ?callable $onStep = null): stringDrive the agent loop until it returns a final answer or pauses on a gate.
Parameters
| Name | Type | Description |
|---|---|---|
| $prompt | string | |
| $systemPrompt | string | |
| $history | list<array<string, mixed>> | prior turns, each a role/content message |
| $onStep | ?callable |