Admit tool actions inside an autonomous invocation¶
An autonomous participant invocation can call tools while it runs. RAES admits
each tool call as a separate participant action before the tool has effects.
Each tool action gets its own action identity, typed outcome and three-event
behavior history. The composition is participant-invocation-tool-actions/v1.
ADR-092
section 10 records the decision.
Claim the composition¶
Declare the claim in the participant-runtime capabilities. The claim requires
autonomous execution and positive tool_affordances support. Declare one
execution-binding offer for each tool action contract and implementation.
"invocation_tool_actions": {
"semantic_versions": ["participant-invocation-tool-actions/v1"],
"max_tool_actions_per_invocation": 16,
"max_concurrent_tool_actions_per_invocation": 4
}
Registration refuses the claim when the participant runtime has no
attach_invocation_tool_actions method. Without the claim, the runtime attaches
no port, and ordinary autonomous execution does not change.
Use the port¶
The runtime calls attach_invocation_tool_actions(port) before each matching
admit_action call. The port is inactive until that dispatch begins, so calls
made during attachment return invocation_inactive. Give the port only to the
harness that runs that invocation. The port never calls the runtime manager, so the harness can use it
from any thread while admit_action waits.
from raes_contracts.contracts import ParticipantActionResultModel
from raes_contracts.participant_tool_actions import ParticipantToolActionRequest
class HarnessRuntime(MyAutonomousParticipantRuntime):
def attach_invocation_tool_actions(self, port) -> None:
self._ports[port.invocation_action_instance_id] = port
def _model_action(self, request, snapshot, *, episode_id):
port = self._ports.pop(request.action_instance_id, None)
outcome = self._harness.run(request, tool_port=port)
return self._native_execution(request, snapshot, episode_id, outcome)
def call_tool(port, mcp_request_id: str, run_tool) -> None:
decision = port.admit(
ParticipantToolActionRequest(
tool_call_id=mcp_request_id,
invocation_action_instance_id=port.invocation_action_instance_id,
participant_address=port.participant_address,
episode_id=port.episode_id,
participant_implementation_ref=port.participant_implementation_ref,
tool_affordance_address="participant.behavior-specification.participant-behavior.tool-affordance.portal-client",
action_contract_address="participant.action-contract.probe-customer-portal-login",
observation_boundary_address="participant.observation-boundary.participant-view",
target_addresses=("provision.node.customer-portal.service.http",),
execution_scope_ref=port.execution_scope_ref,
execution_generation=port.execution_generation,
)
)
if not decision.admitted:
return # decision.refusal is a fixed code; do not run the tool.
if not port.dispatch(decision.action_instance_id).accepted:
return
try:
succeeded = run_tool()
except TimeoutError:
port.report_unknown(decision.action_instance_id, "timeout")
return
port.complete(
decision.action_instance_id,
ParticipantActionResultModel(
status="succeeded" if succeeded else "failed",
participant_address=port.participant_address,
episode_id=port.episode_id,
action_instance_id=decision.action_instance_id,
action_contract_address="participant.action-contract.probe-customer-portal-login",
observation_point=port.observation_point,
),
)
Start tool effects only after dispatch returns an accepted receipt. The
consumer keeps its own MCP handler, argument validation, transport
authentication, credentials, redaction and process cleanup. RAES does not
receive or store tool arguments or results.
Admission rules¶
The port admits a request only when all of these conditions are true:
The invocation is still open.
Participant, episode, implementation, invocation, scope and generation equal the port values.
Exactly one compiled affordance binding matches the affordance, action and participant.
The observation boundary and every target belong to that binding.
The action has no shared-time binding.
Exactly one manifest execution-binding offer covers the action, the implementation and the targets.
The policy is not
participant-autonomous-execution/v3, and no participant-crossing policy is installed.The control-plane store retains the claim. A failed write refuses with
ledger_unavailable.The declared limits are not exhausted.
A refusal returns a ParticipantToolActionRefusal code. It causes no native
call and adds no history.
Failure behavior¶
Event |
Result |
|---|---|
Same |
The port returns the original decision. |
Same |
|
|
The action settles as |
|
|
|
The action settles as |
An outcome with withheld or undeclared evidence, or a foreign observation point |
|
The invocation returns with undispatched actions |
They settle as |
The invocation returns with dispatched actions and no outcome |
They settle as |
A call after the invocation returns |
|
The invocation raises or returns an invalid result |
Tool history commits. The outer attempt is consumed. |
An indeterminate action records status unknown and observation status
orphaned_action. It also stops the participant scheduler. RAES never repeats
a tool effect to recover history.
Read the history¶
Each settled tool action appends action_attempted,
state_transition_recorded and observation_emitted. The triples follow
admission order, immediately before the triple of the outer invocation. Every
tool-action event carries invocation_tool_context. That context names the
outer action instance and contract, the affordance, the tool call identity, the
admission ordinal, the settlement, the scope and the generation. Participant
history views project the same field.
Tool history commits with the enclosing scheduler operation. Each tool action
is also a child operation with its own id. Read its state with
get_operation(action_instance_id). After a restart, an admitted action that
was never dispatched is cancelled. A dispatched action without an outcome is
indeterminate and needs explicit resolution.