Skip to content
6 changes: 6 additions & 0 deletions docs/agentkitfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ use filesystem skills, stage them under `/agent/skills` in the runtime/deploymen
image or prefer MCP-backed skills. Memory providers require an explicit
`AGENTKIT_MEMORY_SCOPE` runtime env var; choose a per-user/session-safe scope.

Orka harness v2 and the hosted brokered model loop support the narrower
[bundled instruction-only skills](instruction-skills.md) mode. It exposes
`load_skill` over a startup snapshot of `SKILL.md` documents. Resources, scripts,
remote skill sources, search providers, and memory providers are not available
through that mode.

### `expose`

```yaml
Expand Down
105 changes: 83 additions & 22 deletions docs/foundry-hosted-brokered.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ payloads. Ordinary client requests must not be able to submit tool results.
Unknown response IDs, unknown call IDs, orphan tool outputs, duplicate conflicts,
malformed outputs, missing or wrong continuation auth, and multiple tool outputs
all return deterministic error envelopes. An identical duplicate continuation
from the broker path returns the same final response idempotently.
returns its original next response, even if later tool rounds have completed.

The body field is an AgentKit/Orka compatibility extension rather than a standard
OpenAI Responses field. Prove that the target Foundry gateway accepts and forwards
Expand Down Expand Up @@ -234,9 +234,20 @@ operations.

## Streaming

The current route is non-streaming. If clients send `stream: true`, AgentKit
returns the same normal JSON response rather than SSE. This keeps azd/direct curl
smokes deterministic while making streaming support an explicit future step.
`/responses` honors `stream: true` with server-sent events, both with and without
`brokeredTools`.
AgentKit sends `response.created` before starting model work, then sends
`response.completed` or `response.failed` with the same response ID. These events
include the effective `agent_session_id` when one is present. This lets Orka
record which hosted response it accepted before waiting for the model.

The stream contains acknowledgement and completion events, without token deltas.
Validation errors before acknowledgement keep their normal HTTP error status.
Disconnecting the stream cancels the active runtime or model request and releases
any in-progress brokered state. Orka still uses authenticated session stop and idle
checks to confirm hosted cleanup. Requests without `stream: true` keep the JSON
response format. Streaming does not add persistence or replay behavior to runtimes
without `brokeredTools`.

## Troubleshooting

Expand Down Expand Up @@ -367,22 +378,72 @@ deploy/foundry/scripts/local_brokered_conformance_container.sh \
--transcript-dir ./foundry-brokered-agentkit-transcript
```

## Lower-level model-loop fallback

Phase A4/A5 has an opt-in fallback when a high-level framework cannot prove
pause/resume: set `AGENTKIT_FOUNDRY_BROKERED_MODEL_LOOP=1` in Foundry brokered
mode. AgentKit then calls the configured OpenAI-compatible chat-completions
model directly with the static safe `brokeredTools` as function schemas. If the
model requests exactly one configured tool, AgentKit rewrites the model's tool
call id to a stable hosted Responses `call_<response-id>_<sequence>` id and
returns a `function_call` output item for Orka. On the Orka-authenticated
`function_call_output` continuation, AgentKit resumes the model with a `tool`
message and returns the final assistant message.

In this mode AgentKit-owned MCP/direct tools remain disabled; only the static
safe brokered schemas are model-visible. The first implementation intentionally
limits each turn to one brokered tool call and rejects unknown, multiple, or
repeated model tool calls deterministically.
## Model-driven tool workflows

Set `AGENTKIT_FOUNDRY_BROKERED_MODEL_LOOP=1` to let the model choose tools and
work through a task. AgentKit calls the configured OpenAI-compatible Chat
Completions model with the static `brokeredTools` schemas. For example, the
agent can inspect telemetry, use the result to look up an incident, and then
explain what it found.

Each operational tool call returns a `function_call` for Orka to execute. After
Orka sends the matching `function_call_output`, AgentKit resumes the model. It
can request another tool or return an answer. Every round gets a fresh response
ID and call ID; the next result must match both. AgentKit validates each new
call's name, arguments, and schema before returning it. Retrying the same result
returns the cached next response without another model request. File-backed
state preserves these completed rounds across restarts.

The model can make up to 16 sequential tool calls per user turn. At the limit,
AgentKit asks for a final answer without tools and rejects any further tool
call. The model endpoint must support `parallel_tool_calls: false`, which asks
for one tool call at a time. AgentKit also rejects parallel tool batches if a
model ignores that setting. AgentKit-owned MCP and direct operational tools
remain disabled.

An HTTP 429 from the model service is retried up to twice within the same
hosted response. AgentKit honors `retry-after-ms` or `Retry-After` delays of
up to 60 seconds each. A longer server delay ends the response with
`ModelUnavailable` and `upstream_status: 429`. Missing or malformed delay
headers use short exponential backoff with jitter. Disconnecting the hosted
stream cancels the wait. Other HTTP errors, transport failures, and invalid
model responses are not retried. These model retries do not repeat Orka tool
operations or submit a new hosted response.

Agents can also use [bundled instruction skills](instruction-skills.md). With
filesystem skills configured under `/agent/skills`, AgentKit advertises the
local `load_skill` tool alongside the brokered schemas. It returns the packaged
instructions from an immutable startup snapshot. Skill loads count toward the
same 16-call budget; they do not invoke Orka or execute scripts. All operational
calls described by those instructions still go through Orka.

For Orka harness v2, use the companion
[agent-runtime-foundry broker](https://github.com/orka-agents/agent-runtime-foundry/blob/main/docs/harness-v2.md).
Give its `ORKA_FOUNDRY_BROKER_AGENTKIT_CONTINUATION_PROOF` the same value as the
hosted agent's `AGENTKIT_FOUNDRY_BROKERED_CONTINUATION_PROOF`, using a secret of
at least 32 bytes without whitespace. The broker adds this value only after
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.

## Hosted follow-up questions

In model-loop mode, a follow-up user message can reference the final response ID
from the previous turn. It must use the same effective hosted session identity.
AgentKit retains recent user and assistant messages so the model can understand
questions such as "Which incident was that?" Raw tool messages and system
instructions are excluded from this retained dialogue. An assistant answer can
still contain information derived from a tool result.

History keeps complete recent exchanges within half the model-message byte
limit or one eighth of the response-state byte limit, whichever is smaller.
It shares the response store's TTL, capacity, and optional file persistence.
Missing, expired, evicted, or mismatched session history is rejected. Requests
without a session identity remain stateless. Pending tool calls must finish
before a user can continue their response.

## Implementation status and evidence

Expand All @@ -398,9 +459,9 @@ requires deployed Foundry/Orka/Fibey state.
| A2 static schemas and drift control | Implemented in Go writer/validator and Python runtime; export CLI added | `brokeredTools` ABI, `agentkit-brokered-tools`, `tests/test_config_validation.py`, `tests/test_brokered_schema.py`, Go config/ABI tests | Orka Tool CRDs must be exported during deployment and current digests verified before live runs. |
| A3 deterministic brokered runtime | Implemented for local/fake hosted protocol integration | deterministic `/responses` brokered path and tests | Live Orka deterministic read/write smoke still required. |
| A4 framework pause/resume decision | Lower-level OpenAI-compatible fallback implemented; high-level framework native hooks remain gated | `agentkit_serve_common.foundry_model_loop`, `AGENTKIT_FOUNDRY_BROKERED_MODEL_LOOP=1`, model-loop tests | Live model smoke for brokered read/write prompts. |
| A5 first real model adapter brokered mode | Fallback model loop can emit/resume brokered calls from static safe schemas | model-loop tests in `tests/test_foundry_brokered_protocol.py` | Deployed real model read and write prompts, including declined/policy/error outcomes. |
| A5 first real model adapter brokered mode | Model loop supports sequential brokered calls, local instruction skills, and bounded hosted dialogue | `tests/test_foundry_brokered_protocol.py`, `tests/test_foundry_tool_workflows.py` | Deployed real model read and write prompts, including policy/error outcomes. |
| A6 live Orka integration | Not proven in this repo state | Local AgentKit/Foundry side helpers exist | Deploy AgentKit and Orka hosted-Responses adapter; run brokered read/write approval smoke. |
| A7 Fibey | Not started; gates not satisfied | N/A | Requires A3/A5/A6 live gates first, then Fibey schemas/instructions/scenario. |
| A7 Fibey | Runtime supports packaged skills and sequential tool workflows | Catalog/ACP/MAF skill tests and hosted workflow tests | Package the Fibey skills and schemas, then validate the deployed scenario. |
| A8 hardening/docs/review | Local docs/tests/autoreview complete for current patch | This doc, `docs/agent-abi.md`, `docs/runtime-capabilities.md`; full tests/lint; `$autoreview` clean | Record live transcript and Orka/Fibey validation evidence before final completion. |

Local verification commands used for the current AgentKit patch:
Expand Down
80 changes: 80 additions & 0 deletions docs/instruction-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Bundled skills in governed mode

Skills let an agent load written guidance for a task before using its tools.
The Microsoft Agent Framework runtime can use the same bundled skills when
running under Orka harness v2 or the Foundry hosted brokered model loop.
Loading a skill reads instructions from the agent image; Orka still controls
operational tool calls and approvals.

Declare the skill directory in your AgentKitfile:

```yaml
runtime: microsoft-agent-framework
context:
providers:
- type: skills
source: filesystem
path: /agent/skills
```

Arrange each skill in a directory with a matching name:

```text
skills/
inspection/
SKILL.md
parts-lookup/
SKILL.md
```

For example, `skills/inspection/SKILL.md`:

```markdown
---
name: inspection
description: Prepare an equipment inspection using the current work order.
---
Retrieve the work order using the authorized lookup tool. Summarize the required
checks, identify missing information, and cite the returned record.
```

AgentKit resolves an `instructions.file` into the agent configuration, but does
not copy skill directories automatically. Add them to the built agent image
before composing its Orka runtime or deploying it to Foundry:

```dockerfile
FROM ghcr.io/acme/inspection-agent@sha256:<built-agent-image-digest>
COPY --chown=0:0 skills/ /agent/skills/
```

Make the directories and documents readable by the runtime user. Pin the image
that contains both the agent configuration and skills. For Orka ACP composition,
use that image's digest as `AGENTKIT_ADAPTER_DIGEST`; the existing
`agentConfigurationDigest` continues to cover the exact `agent.yaml` bytes.
Keep `/agent/skills` image-owned instead of mounting user or task workspace files
there. Changing a skill requires building and registering the updated image.

The runtime lists each skill's name and description for the model. The model
calls `load_skill` with `{"skill_name":"inspection"}` to receive the original
`SKILL.md` text, including its frontmatter. This works with upstream instruction
skills that already use `load_skill`.

Only `SKILL.md` documents are loaded. Files are snapshotted during startup and
tool calls use that immutable snapshot. Sibling files and scripts are not made
available, and the runtime does not expose `read_skill_resource` or
`run_skill_script`. A skill's text cannot grant tool access or bypass an
approval. Remote/search/memory context providers remain rejected in governed
mode.

Each skill needs a matching lowercase name of at most 64 characters, a
description of at most 1024 characters, and nonempty instructions. Discovery
covers the selected directory and two directory levels below it. Duplicate
names, symlinked paths, nonregular or hardlinked documents, invalid UTF-8, and
invalid frontmatter fail startup. Each document is limited to 128 KiB; a catalog
is limited to 64 skills and 1 MiB of document text. A directory may contain at
most 256 entries.

The hosted brokered loop must reserve `load_skill` for the local catalog when
skills are configured. Give operational tools their own names and schemas.
For the rest of the hosted setup, see
[Foundry hosted brokered tools](foundry-hosted-brokered.md).
6 changes: 4 additions & 2 deletions docs/orka.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ supervisor starts this child under a private UID/GID and session tree:

The v2 path is strict:

- `/agent/agent.yaml` must not contain direct `tools`, `brokeredTools`, or
context providers;
- `/agent/agent.yaml` must not contain direct `tools` or `brokeredTools`;
- the Microsoft Agent Framework runtime also supports bundled, instruction-only
filesystem skills; other context providers remain prohibited. See
[Bundled skills in governed mode](instruction-skills.md);
- the registered model must equal `model.name` in the baked config;
- `agentConfigurationDigest` is `sha256:` plus the SHA-256 of the exact
`/agent/agent.yaml` bytes;
Expand Down
4 changes: 3 additions & 1 deletion docs/runtime-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ acceptance, SSE replay, and cancel endpoints.
ACP mode opens no listener. It speaks newline-delimited ACP JSON-RPC on stdin
and stdout. The child verifies the configured model and SHA-256 digest of the
exact `/agent/agent.yaml` bytes before accepting a session. It rejects baked
direct tools, `brokeredTools`, and context providers. At session creation it
direct tools and `brokeredTools`. The Microsoft Agent Framework adapter can
load [packaged skill instructions](instruction-skills.md); other context
providers remain prohibited. At session creation it
accepts at most one loopback HTTP MCP server with bearer authentication, which
is the prompt-scoped broker created by the Orka supervisor.

Expand Down
6 changes: 4 additions & 2 deletions runtimes/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ The child accepts one ACP session, text and resource-link prompt blocks,
cancellation, and at most one loopback HTTP MCP server carrying a bearer
Authorization header. Resource links are added to the model prompt as labeled
text and are never fetched by the child. The runtime keeps successful user and
assistant turns for later prompts. It rejects baked `tools`, `brokeredTools`,
and context providers. Orka owns process and workspace isolation, prompt-scoped
assistant turns for later prompts. It rejects baked `tools` and `brokeredTools`.
The Microsoft Agent Framework adapter accepts
[packaged skill instructions](../../docs/instruction-skills.md); other context
providers remain prohibited. Orka owns process and workspace isolation, prompt-scoped
MCP authority, provider proxying, and cleanup proof.

## Adding a runtime adapter
Expand Down
45 changes: 40 additions & 5 deletions runtimes/common/agentkit_serve_common/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
from .config import AgentSpec, load_with_bytes
from .conversation import ConversationTurn, RunRequest, ToolCallEvent
from .runtime import AgentRunError, RuntimeFactory, RuntimeSession
from .skills import (
SkillCatalog,
SkillConfigurationError,
validate_packaged_skill_providers,
)

ACP_PROTOCOL_VERSION = 1
ACP_AGENT_CONFIGURATION_DIGEST_ENV = "AGENTKIT_ACP_AGENT_CONFIGURATION_DIGEST"
Expand Down Expand Up @@ -223,6 +228,19 @@ def _factory_supports_http_mcp(factory: RuntimeFactory) -> bool:
return bool(capability()) if callable(capability) else False


def _factory_supports_packaged_skills(factory: RuntimeFactory) -> bool:
capability = getattr(factory, "supports_acp_packaged_skills", None)
return bool(capability()) if callable(capability) else False


def _only_packaged_skill_providers(spec: AgentSpec) -> bool:
try:
validate_packaged_skill_providers(spec)
except SkillConfigurationError:
return False
return True


def _request_key(value: Any) -> str:
if isinstance(value, bool) or not isinstance(value, (int, str)):
raise ACPProtocolError(_INVALID_REQUEST, "JSON-RPC id must be a string or integer")
Expand Down Expand Up @@ -327,8 +345,10 @@ def validate_acp_runtime_binding(config_bytes: bytes, spec: AgentSpec) -> None:
raise ACPConfigurationError("ACP strict mode rejects baked direct tools")
if spec.brokered_tools:
raise ACPConfigurationError("ACP strict mode rejects baked brokeredTools")
if spec.context.providers:
raise ACPConfigurationError("ACP strict mode rejects baked context providers")
try:
validate_packaged_skill_providers(spec)
except SkillConfigurationError as exc:
raise ACPConfigurationError(str(exc)) from exc

expected_digest = _required_environment(ACP_AGENT_CONFIGURATION_DIGEST_ENV)
prefix = "sha256:"
Expand Down Expand Up @@ -364,6 +384,10 @@ def validate_acp_runtime_binding(config_bytes: bytes, spec: AgentSpec) -> None:
except ACPProtocolError as exc:
raise ACPConfigurationError(exc.message) from exc
_required_environment(ACP_PROVIDER_TOKEN_ENV)
try:
spec._packaged_skill_catalog = SkillCatalog.from_spec(spec)
except SkillConfigurationError as exc:
raise ACPConfigurationError(str(exc)) from exc


class ACPStdioServer:
Expand All @@ -378,7 +402,10 @@ def __init__(self, spec: AgentSpec, factory: RuntimeFactory, send: _MessageSende
_factory_supports_http_mcp(factory)
and not spec.tools
and not spec.brokered_tools
and not spec.context.providers
and (
not spec.context.providers
or (_factory_supports_packaged_skills(factory) and _only_packaged_skill_providers(spec))
)
)
self.sessions: dict[str, _SessionState] = {}
self.requests: dict[str, asyncio.Task[None]] = {}
Expand Down Expand Up @@ -596,8 +623,15 @@ async def _create_session(self, params: Any) -> dict[str, Any]:
raise ACPProtocolError(_INVALID_PARAMS, "ACP strict mode rejects baked direct tools")
if self.spec.brokered_tools:
raise ACPProtocolError(_INVALID_PARAMS, "ACP strict mode rejects baked brokeredTools")
if self.spec.context.providers:
raise ACPProtocolError(_INVALID_PARAMS, "ACP strict mode rejects baked context providers")
try:
validate_packaged_skill_providers(self.spec)
if self.spec.context.providers:
if not _factory_supports_packaged_skills(self.factory):
raise SkillConfigurationError("this runtime does not support ACP packaged skills")
if self.spec._packaged_skill_catalog is None:
self.spec._packaged_skill_catalog = SkillCatalog.from_spec(self.spec)
except SkillConfigurationError as exc:
raise ACPProtocolError(_INVALID_PARAMS, str(exc)) from exc

mcp_servers = request.get("mcpServers", [])
if not isinstance(mcp_servers, list):
Expand Down Expand Up @@ -720,6 +754,7 @@ def _project_spec(
projected = AgentSpec.model_validate(data)
except ValueError as exc:
raise ACPProtocolError(_INVALID_PARAMS, "ACP MCP server configuration is invalid") from exc
projected._packaged_skill_catalog = self.spec._packaged_skill_catalog
return projected, environment

async def _prompt(self, request_key: str, params: Any) -> dict[str, str]:
Expand Down
Loading
Loading