Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions docs/foundry-hosted-brokered.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ Canonical denied/error payload:
}
```

Both `approved` values describe final results. Neither value represents a
pending human review. Orka holds its original tool call while the review is
pending, then sends exactly one final result for that call. The hosted adapter
uses fixed messages for Orka's `approval_declined`, `approval_expired`,
`approval_cancelled`, `approval_stale`, `tool_execution_failed`, and
`tool_outcome_unknown` codes. A tool failure remains distinct from a declined
review. An unknown outcome finishes the continuation without another model
request, preventing an automatic retry of an uncertain action.

`function_call_output` is privileged continuation input. It is rejected unless a
known `previous_response_id` has a pending matching `call_id` **and** the request
uses the Orka-only continuation path. Configure
Expand Down Expand Up @@ -205,9 +214,11 @@ construction. The public continuation must still carry the same
`agent_session_id` so Foundry routes it to that sandbox.

An immediate initial/continuation pair proves routing affinity, not file recovery.
To prove persistence, stop the Foundry session between the two requests and show
that a new process reloads the pending state. Configure the state TTL and Orka
approval timeout longer than that test. Deployments without a session-persisted
To prove persistence, restart the AgentKit process between the two requests in
the same hosted session and show that it reloads the pending state. Preserve the
state file and keep the test inside the configured state TTL and Orka task/session
limits. This does not establish recovery of a lost Foundry runtime session.
Deployments without a session-persisted
file, shared file, or platform-managed store must pin one replica or use sticky
routing; otherwise a continuation that lands on a different/restarted container
fails safely with `unknown_previous_response_id`. Pending state expires after
Expand All @@ -221,6 +232,14 @@ they are persisted, replayed, embedded in deterministic responses, or sent back
through the model loop. A platform-managed state backend is still required
before treating multi-replica production as fully supported.

For Orka-managed human approval, configure
`AGENTKIT_FOUNDRY_RESPONSE_STATE_TTL_SECONDS=1800` and a hosted-session idle
timeout of at least 1800 seconds. The review can consume 600 seconds and tool
execution another 240 seconds, followed by result delivery and model continuation.
The default 900-second state lifetime leaves too little margin at that limit.
Task deadlines, active leases, and platform session limits still apply. See
[Human approval for Orka tools](orka-human-approval.md) for the full wait budget.

The file is sensitive runtime state, not harmless metadata. In model-loop mode
it includes model messages such as system instructions, conversation history,
and user prompts, in addition to brokered arguments/outputs and cached final
Expand Down Expand Up @@ -426,8 +445,10 @@ checking ownership, the active lease, and the expected response and call. It
also translates MCP results into AgentKit's approved/error envelope. Keep the
proof out of the ACP child configuration. The gateway must forward the proof
field; local tests cannot establish that a public Foundry deployment does so.
See the shared-proof limitations above. Human tool approvals for external v2
runtimes remain unsupported by Orka.
See the shared-proof limitations above. Human tool approvals require a qualified
Orka controller, supervisor, Foundry broker, and AgentKit image that support
`supportsBrokeredToolApprovals`. Older combinations remain unsupported. Review
policy, reviewer permissions, cancellation, and execution decisions stay in Orka.

## Hosted follow-up questions

Expand Down
144 changes: 144 additions & 0 deletions docs/orka-human-approval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Human approval for Orka tools

AgentKit can wait while a person reviews an Orka-managed tool call. Orka stores
the proposed action, checks reviewer permissions, and decides whether the tool
runs. AgentKit receives the final result for the original call and continues
without resubmitting the prompt or repeating completed tool steps.

This requires matching Orka, AgentKit, and, for hosted execution, Foundry broker
images qualified for `supportsBrokeredToolApprovals`. The direct path uses the
Microsoft Agent Framework ACP adapter and Orka's loopback MCP server. The hosted
path uses AgentKit's brokered Responses adapter through `agent-runtime-foundry`.
Do not advertise approval support for other runtime combinations without their
matching acceptance checks.

AgentKit's local `tools[].approval` setting is separate. Omission and `never`
remain valid; `auto` and `always` remain unsupported. `supportsPermissions`
stays false. Approval of shell commands or files inside the runtime is outside
this feature.

## Wait limits and setup

| Limit | Qualified setup |
|---|---|
| Human review | At most 600 seconds, subject to tighter task/session limits |
| Tool execution after approval | At most 240 seconds |
| Direct MCP `tools/call` | Orka injects `AGENTKIT_MCP_TIMEOUT=900` into the ACP child |
| Hosted AgentKit pending state | Set `AGENTKIT_FOUNDRY_RESPONSE_STATE_TTL_SECONDS=1800` |
| Foundry hosted-session idle timeout | Configure at least 1800 seconds |

The complete task needs time for initial model work, review, tool execution,
result delivery, and model continuation. A larger MCP or state timeout does not
extend a task deadline, an expired lease, or a platform session limit. A review
that reaches its deadline expires without executing the proposed action.

The direct MAF default remains 120 seconds outside the qualified Orka child.
Confirm the child receives the 900-second override, rather than setting it only
on the supervisor process. The MAF adapter applies that value to both its MCP
request timeout and HTTP client. It does not retry a tool call after connection
loss because the action might already have run.

Hosted AgentKit's `/readiness` response reports
`foundryResponses.stateTtlSeconds`; check that it is 1800 in the configured
deployment. Also verify the idle timeout on the deployed Foundry agent version.
[Foundry's hosted-session documentation](https://learn.microsoft.com/azure/foundry/agents/how-to/manage-hosted-sessions)
describes that platform setting. Keep the same `agent_session_id` and the
authenticated continuation proof on subsequent requests.

For AgentKit process recovery, configure `AGENTKIT_FOUNDRY_RESPONSE_STATE_FILE`
on access-controlled storage that survives the process restart. Use one writer
and preserve the same hosted session identity. Missing or expired state rejects
the continuation; it never authorizes restarting the original action. This
does not provide recovery of an entire lost Foundry runtime session.

## Outcomes

While review is pending, the tool execution count remains zero. The direct MCP
request stays open. Hosted AgentKit retains its pending `function_call`; Orka
sends no `function_call_output` until the result is final. `approved:false`
must never be sent merely because the person has not decided.

| Final outcome | AgentKit behavior |
|---|---|
| Approved and executed | Continue the original call with its actual output |
| `approval_declined` | Report that the person declined the call |
| `approval_expired` | Report that the review expired |
| `approval_cancelled` | Report cancellation when a final result can be delivered |
| `approval_stale` | Report that the approval no longer authorizes the call |
| `tool_execution_failed` | Report execution failure, distinct from a human decline |
| `tool_outcome_unknown` | Stop automatic continuation; never retry the uncertain action |

The direct adapter accepts these codes only from an MCP error result's
`structuredContent.code`. It replaces tool-controlled messages with fixed text.
An unknown outcome fails the direct prompt. Hosted AgentKit returns a fixed
unknown-outcome response and caches it, without asking the model for another
tool call. Identical hosted result delivery returns the cached response;
conflicting results are rejected.

Orka controls cancellation. Cancelling a task or losing its authority stops
the waiting runtime. A later approval must not revive it. The AgentKit hosted
endpoint does not independently grant permission to resume a cancelled task.

## Acceptance through Orka

Use Orka's
[`examples/human-approval-v2`](https://github.com/orka-agents/orka/tree/main/examples/human-approval-v2)
fixture with automatic `read-inventory` and approval-required
`create-work-order`. Both are simulated; the action records an execution count.
Follow its setup and Task fixture for each qualified runtime. Keep separate
runtime capacity available for an independent task while the first waits.

Inspect the pending review in Orka's task approval panel or through the normal
API. The following commands operate on an already-created disposable task.
`ORKA_CURL_CONFIG` names a private curl configuration containing the required
authentication. Keep its credential out of shell history and command arguments.

```sh
curl --fail --silent --show-error --config "$ORKA_CURL_CONFIG" \
"$ORKA_API_URL/api/v1/tasks/$TASK_NAME/approvals?namespace=$ORKA_NAMESPACE" |
jq '{taskName, approvals: [.approvals[] | {
id, action, targetTool, targetArgsPreview, targetArgsDigest,
status, expiresAt, executionOutcome
}]}'
```

Check the exact tool, safe argument preview, task, and expiry before selecting
`APPROVAL_ID`. Confirm the simulated action count is zero, then approve it:

```sh
curl --fail --silent --show-error --config "$ORKA_CURL_CONFIG" \
-H 'Content-Type: application/json' \
--data '{"decision":"approve","reason":"Simulated action acceptance"}' \
"$ORKA_API_URL/api/v1/tasks/$TASK_NAME/approvals/$APPROVAL_ID/decision?namespace=$ORKA_NAMESPACE"
```

The action must run once and its receipt must reach the original conversation.
Repeat with a fresh task and `"decision":"decline"`; the action count must stay
zero. Use fresh disposable tasks for cancellation, review expiry, a tool error
after approval, and duplicate or competing decisions. Check both execution
count and Orka's stored `executionOutcome`. Do not treat a successful API request
alone as evidence that a tool ran.

For direct execution, include a review longer than 120 seconds and within the
600-second limit. For hosted execution, restart only the AgentKit process with
saved pending state and prove the matching continuation works. Repeat without
saved state and confirm it fails without a new action. Verify an unrelated
conversation progresses while review is pending.

## Local compatibility tests

The deterministic tests cover waiting, named outcomes, cancellation, independent
work, saved-state recovery, and duplicate continuations. The HTTP test uses the
production MAF client and MCP SDK with a counted simulated broker. Run it with
a real wait longer than the old default:

```sh
AGENTKIT_TEST_APPROVAL_WAIT_SECONDS=121 \
uv run --directory runtimes/microsoft-agent-framework --extra dev \
pytest -q tests/test_orka_approvals.py -k http_mcp
uv run --directory runtimes/common --extra dev \
pytest -q tests/test_foundry_approvals.py
```

These tests establish runtime compatibility. Live Orka approval API execution
and public Foundry gateway behavior require the separate acceptance run above.
11 changes: 9 additions & 2 deletions docs/orka.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,18 @@ config. The registration's `adapterName` must be `agentkit-serve-acp`. Its
`adapterDigest` and the composition build's `AGENTKIT_ADAPTER_DIGEST` must both
equal the `sha256:` digest from `AGENTKIT_RUNTIME_IMAGE`. Set the profile's
`providerKind` to `agentkit` and advertise
`supportsAgentSessionConfiguration: false`. `approvalRequiredTools` must stay
empty because the AgentKit ACP child does not implement permission callbacks.
`supportsAgentSessionConfiguration: false` and `supportsPermissions: false`.
Orka-managed MCP tool approval uses the existing waiting call, without a local
ACP permission callback. A qualified Microsoft Agent Framework composition can
advertise `supportsBrokeredToolApprovals: true` and configure Orka's
`approvalRequiredTools`. The controller, supervisor, and AgentKit image must all
support that contract. Older images and unqualified adapters remain unsupported.
If the registration allows brokered tools, the Task must submit that exact
`allowedTools` list.

See [Human approval for Orka tools](orka-human-approval.md) for wait limits,
result handling, and acceptance checks through Orka's approval API.

Set `ORKA_ACP_CONTROLLER_EPOCH` from Orka's current `ControllerEpoch` record.
Select by `spec.name` because the resource name is hashed. This lookup requires
exactly one matching record with a positive integer epoch:
Expand Down
3 changes: 2 additions & 1 deletion docs/runtime-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ Startup-scoped model clients and long-lived MCP sessions still resolve their own
startup credentials at runtime initialization; they are not rebuilt for every turn.

- a missing or empty command fails before serving,
- `AGENTKIT_MCP_TIMEOUT` controls MCP initialization timeout,
- `AGENTKIT_MCP_TIMEOUT` controls MCP initialization timeout; MAF also uses it
for tool requests, including [Orka approval waits](orka-human-approval.md),
- each tool subprocess receives only env vars declared in that tool's `env`,
- undeclared `${VAR}` interpolation inside a declared env value is rejected, and
- tool sessions are entered once for the app lifespan and reused across requests,
Expand Down
6 changes: 6 additions & 0 deletions docs/runtime-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ and auth wiring. Orka-specific names here describe the protocol contract AgentKi
exposes; Orka remains responsible for policy, approval, idempotency, and
side-effect governance.

Orka v2's `supportsBrokeredToolApprovals` registration capability covers
controller-managed approvals over the ACP child's MCP connection. It is separate
from AgentKit's unsupported local `tool-approval` capability and the legacy v1
brokered hooks below. See [Human approval for Orka tools](orka-human-approval.md)
for the qualified direct MAF and hosted Foundry paths.

## Current support

| Runtime | Capabilities |
Expand Down
5 changes: 5 additions & 0 deletions runtimes/common/agentkit_serve_common/foundry.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from .conversation import FORWARDED_ROLES, ConversationTurn, RunRequest
from .runtime import AgentRunError, BrokeredToolDefinition, RunResult, RuntimeFactory
from .server import make_auth_dependency
from .tool_errors import orka_tool_error_details

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -2324,6 +2325,10 @@ def _advance_brokered_state(
def _final_text_from_tool_output(call: _PendingCall, output: dict[str, Any]) -> str:
if not output.get("approved"):
error = output.get("error") if isinstance(output.get("error"), dict) else {}
details = orka_tool_error_details(error)
if details is not None:
code, message = details
return f"Brokered tool {call.tool.name}: {code}: {message}"
code = str(error.get("code") or "brokered_tool_denied")
message = str(error.get("message") or "brokered tool was not performed")
return f"Brokered tool {call.tool.name} was not performed: {code}: {message}"
Expand Down
20 changes: 20 additions & 0 deletions runtimes/common/agentkit_serve_common/foundry_model_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .conversation import FORWARDED_ROLES, RunRequest
from .runtime import AgentRunError, BrokeredToolDefinition
from .skills import SkillCatalog
from .tool_errors import orka_tool_error_details

_MAX_ARGUMENT_DEPTH = 128
_MAX_RATE_LIMIT_RETRIES = 2
Expand Down Expand Up @@ -211,8 +212,27 @@ async def resume(
) from exc
if len(output_bytes) > self.max_output_bytes:
raise AgentRunError("brokered tool output is too large for model resume", status=413, code="brokered_output_too_large")
try:
payload = json.loads(output)
except (ValueError, RecursionError):
payload = None
details = (
orka_tool_error_details(payload.get("error"))
if isinstance(payload, dict) and payload.get("approved") is False
else None
)
if details is not None:
code, message = details
output = json.dumps({"approved": False, "error": {"code": code, "message": message}}, separators=(",", ":"))
if len(output.encode("utf-8")) > self.max_output_bytes:
raise AgentRunError("brokered tool output is too large for model resume", status=413, code="brokered_output_too_large")
resumed = [dict(message) for message in messages]
resumed.append({"role": "tool", "tool_call_id": call_id, "content": output})
if details is not None and details[0] == "tool_outcome_unknown":
# Another model round could repeat an action whose effect is unknown.
# Finish this continuation and let Orka retain the execution evidence.
text = f"{details[0]}: {details[1]}"
return ModelLoopFinal(text=text, messages=[*resumed, {"role": "assistant", "content": text}])
return await self._advance(resumed, call_id=next_call_id or f"call_{uuid.uuid4().hex}")

async def validate_credentials(self) -> None:
Expand Down
26 changes: 26 additions & 0 deletions runtimes/common/agentkit_serve_common/tool_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Fixed, non-sensitive outcomes from Orka's governed tool broker."""

from __future__ import annotations

from collections.abc import Mapping


_ORKA_TOOL_ERRORS = {
"approval_declined": "The tool call was declined.",
"approval_expired": "The tool approval expired.",
"approval_cancelled": "The tool call was cancelled.",
"approval_stale": "The tool approval is no longer valid.",
"tool_execution_failed": "MCP tool execution failed.",
"tool_outcome_unknown": "The tool execution outcome is unknown; do not retry.",
}


def orka_tool_error_details(value: object) -> tuple[str, str] | None:
"""Read an explicit broker code without forwarding tool-controlled text."""
if not isinstance(value, Mapping):
return None
code = value.get("code")
if not isinstance(code, str):
return None
message = _ORKA_TOOL_ERRORS.get(code)
return (code, message) if message is not None else None
Loading
Loading