Tool Traits: Tell the Runtime What a Tool Means
A tool schema tells an agent how to call a function. It does not tell the runtime whether that call reads, writes, or can safely begin before the model has finished generating the rest of its round. KDCube tool traits add that missing policy layer — metadata the runtime reads and enforces, not prompt advice.
A model-callable tool already has a name, a description, and a parameter schema. Those facts answer what can I call, and with which arguments? They do not answer two questions that become important the moment one ReAct round can contain more than one action.
- Strategy: what does this action do to the agent's knowledge or the outside world, and may it share an ordered round with another action?
- Execution: once this complete call has arrived in the stream, may it start while the model continues generating later actions?
KDCube currently defines exactly those two trait families. They are tool metadata, not prompt advice — the model can see them in its catalog, but the runtime also reads and enforces them.
Strategy is about ordered causality
strategy classifies what a tool does relative to the current answer
premise:
exploration obtains information: search, read, inspect, fetch, pull
exploitation mutates state or creates durable output: write, patch, upsert,
delete, render, host
neutral independent bookkeeping or detached scheduling that does not
change the answer premise
unknown no precise strategy reached the runtime
This is not a read/write permission label. It is an ordering signal
for the ReAct action overseer. Suppose a model emits two actions in one streamed round —
action 1: search the web, then action 2: write a report based on that
search. The second action cannot actually use the first action's result: both
calls were generated before the search ran. The overseer therefore rejects exploration
followed by exploitation in the same round. The model gets another round after the search
result exists, and can write from evidence then.
The compatibility check is ordered, and the asymmetry is deliberate. Writing a completed artifact and then starting an independent search for the next section can be valid; searching and immediately writing from an unavailable result is not.
unknown is the safe default. A tool with no resolved strategy runs alone.
That does not disable it; it simply prevents the runtime from claiming a same-round
ordering is safe without enough information.
Execution is about when a complete call may start
Ordinarily, tools execute after the whole model generation finishes. That is correct when later reasoning depends on their result. It wastes time for a detached call whose own arguments are complete and whose result no sibling in the same round consumes — consider delegation followed by a large write.
The execution trait makes the second timing possible without hardcoding
react.delegate in the runtime:
tool_traits:
strategy: [neutral]
execution:
trigger: tool_call_complete
concurrency: parallel_with_generation
result_dependency: detached
replay: at_most_once_per_round
| Field | Meaning |
|---|---|
trigger: tool_call_complete | Do nothing until one complete action block has produced a parseable tool id and parameter object. |
concurrency: parallel_with_generation | Start the validated call in a tracked task while later model output may still be streaming. |
result_dependency: detached | No sibling in this round consumes this result, and this call consumes no sibling result. |
replay: at_most_once_per_round | A retry of the same logical action reuses its execution record instead of repeating the side effect. |
Why four fields rather than run_early: true? Because "early" alone says
nothing about dependencies or replay. A boolean would make a timing preference look like a
correctness guarantee; the explicit profile makes every guarantee reviewable. The current
runtime activates early execution only when all four values match and strategy is
exactly neutral. An execution mapping on an exploration,
exploitation, mixed, or unknown tool remains on the ordinary post-generation path.
The call still passes the normal gates
Early does not mean ungoverned. The call begins only after the same authorities that govern ordinary actions accept it, and the early path calls the ordinary tool dispatcher — it does not copy the tool's implementation, timeline writing, accounting, queue admission, or error handling into a second executor.
When generation ends, the normal state machine still sees and counts the action. Its execution ledger says the call was already consumed, so the runtime does not validate, write, or execute it a second time.
At-most-once means one logical round
Starting an irreversible action before the whole round is known creates a specific retry problem. The model provider may retry, compaction may retry, or a later sibling may be rejected and cause another decision attempt. A detached call that already started must not be launched twice. KDCube derives a stable identity from:
(turn_id, iteration, original_action_index)
The execution ledger survives decision retries inside that turn. It also keeps a semantic fingerprint of tool id plus parameters, so the same call remains recognized if a provider retry reorders sibling actions; safe multi-action filtering preserves the original streamed index even when it drops another sibling.
This guarantee is intentionally named at_most_once_per_round.
It is not process-global exactly-once delivery. A tool that calls an
irreversible external API should still pass a provider-side idempotency key where that API
supports one.
Configure traits in tool code
Use the SDK decorator when a Python tool's traits are intrinsic and do not depend on which application or agent consumes it:
from typing import Annotated
from semantic_kernel.functions import kernel_function
from kdcube_ai_app.apps.chat.sdk.runtime.tool_traits import tool_trait
@tool_trait(strategy=["exploration"])
@kernel_function(name="search", description="Search application knowledge")
async def search(
query: Annotated[str, "Search query"],
) -> dict:
...
@tool_trait(
strategy=["neutral"],
execution={
"trigger": "tool_call_complete",
"concurrency": "parallel_with_generation",
"result_dependency": "detached",
"replay": "at_most_once_per_round",
},
)
@kernel_function(
name="launch_background_job",
description="Schedule an independent background job",
)
async def launch_background_job(
assignment: Annotated[str, "Self-contained assignment"],
) -> dict:
...
Decorator traits travel with discovered Python tool metadata. Built-in catalog tools
can place the same mapping directly in their static tool specification — that is how
react.delegate adopts the execution profile, as the first built-in user
rather than a hardcoded runtime exception:
TOOL_SPEC = {
"id": "react.delegate",
"tool_traits": {
"strategy": ["neutral"],
"execution": {
"trigger": "tool_call_complete",
"concurrency": "parallel_with_generation",
"result_dependency": "detached",
"replay": "at_most_once_per_round",
},
},
# purpose, params, and other catalog fields...
}
Configure traits per agent connection
Consumer configuration is the deployment policy. It can add or override traits for one
agent, including tools loaded from Python, MCP, and named services. Keys under
tool_traits are the unqualified callable names from that
connection; the runtime adds the connection alias to form the model-facing tool
id.
Python tools — this produces model-facing ids such as
workspace_jobs.search, while the configuration stays keyed by
search:
surfaces:
as_consumer:
agents:
main:
tools:
- id: workspace_jobs
kind: python
ref: tools/workspace_jobs.py
alias: workspace_jobs
allowed:
- search
- export_report
- launch_background_job
tool_traits:
search:
strategy: [exploration]
export_report:
strategy: [exploitation]
launch_background_job:
strategy: [neutral]
execution:
trigger: tool_call_complete
concurrency: parallel_with_generation
result_dependency: detached
replay: at_most_once_per_round
MCP tools cannot carry a Python decorator, so configure traits on the
MCP connection. "*" can apply one trait mapping to every MCP tool from a
connection, but only use it when every tool truly has the same behavior — a server
containing both reads and writes needs concrete entries:
- id: browser
kind: mcp
server_id: browser
alias: browser
allowed: [open_page, click, close]
tool_traits:
open_page:
strategy: [exploration]
click:
strategy: [exploration, exploitation]
close:
strategy: [neutral]
Named-service tools — allowed entries use provider
operation ids, but traits use the generic ReAct-facing callable names. That distinction
matters: object.search is the provider grammar operation;
search_objects is the callable the ReAct agent sees:
- id: task_service
kind: named_service
alias: named_services
namespaces:
task:
allowed:
- provider.about
- object.search
- object.schema
- object.upsert
tool_traits:
provider_about:
strategy: [exploration]
search_objects:
strategy: [exploration]
object_schema:
strategy: [exploration]
upsert_object:
strategy: [exploitation]
One resolution path
Traits may begin in code or configuration, but they converge before the agent runs. The catalog helps the model understand the tool, but correctness never depends on the model honoring prose — the stream overseer and executor resolve the same metadata independently.
The catalog might render a tool's scope like this — the same values the overseer and executor read directly:
Scope:
- strategy: neutral
- execution: trigger=tool_call_complete,
concurrency=parallel_with_generation,
result_dependency=detached,
replay=at_most_once_per_round
A practical review checklist
Before assigning traits to a tool, answer these questions:
- Does it obtain information, mutate durable state, or remain independent of the answer premise? Assign
exploration,exploitation, orneutral. - If it can do more than one, declare every true strategy. Mixed tools are not eligible for the current early-execution profile.
- If you want early execution, is its parameter object complete in one action block?
- Can it start without any sibling result, and can every sibling proceed without its result?
- Is starting it valid even if a later sibling is rejected?
- Can a retry be deduplicated within the round, and does the external provider also support idempotency where needed?
- Does the effective tool catalog show the intended traits for the exact agent and callable name?
If any early-execution answer is uncertain, configure only strategy and
keep the ordinary post-generation execution path.
What this buys the platform
Tool traits keep tool implementations focused on domain work while the shared runtime owns orchestration policy:
- Strategy prevents impossible same-round causality before a large streamed payload reaches the user.
- Unknown tools remain usable under conservative ordering.
- Execution lets genuinely detached work begin earlier without a list of special tool ids.
- One deterministic ledger keeps early and post-generation paths from producing duplicate calls, timeline records, or charges.
- Python, MCP, and named-service tools enter the same policy plane.
The result is a tool catalog that says more than what functions exist. It gives the runtime enough meaning to coordinate them safely.
Read more
- Tool subsystem: connection shapes and runtime wiring
- Custom Python, MCP, and named-service tools
- Strategy values and the ordered compatibility matrix
- Execution fields, replay identity, and the safety boundary
- Multi-action stream architecture and the action overseer
- Online ReAct stream governance
- Governed channel streaming