KDCube

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.

ONE TOOL, TWO POLICY PLANES one model-callable tool name · description · parameters · traits Ordinary tool schema name · description · parameter schema how do I call this? · for the model Tool traits strategy · execution policy how may the runtime order & run it? · for the runtime The schema is prompt-facing; the traits are policy. The model can read the traits — but the runtime reads and enforces them.
Schema and traits — one for the model, one for the runtime.

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.

ORDERED STRATEGY COMPATIBILITY following candidate → earlier accepted → exploration exploitation neutral unknown exploration yes no yes no exploitation yes yes yes no neutral yes yes yes no unknown no no no no exploration → exploitation is refused: the write would consume a result that does not exist yet. unknown runs alone.
The ordered matrix — rows accepted earlier, columns the following candidate.

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.

DELEGATE + LONG WRITE: TIMING t1 · delegate complete t2 · write complete BEFORE · post-generation model generation continues schedule helper write the delegate finished at t1 — but nothing starts until the whole round is generated at t2 AFTER · early execution model generation continues helper working — scheduled at t1 write The detached call starts when its own block completes, not when the round ends — overlapping the long write instead of queuing behind it.
Delegate + long write — the detached helper overlaps generation instead of queuing behind it.

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
Each field states one part of the contract.
FieldMeaning
trigger: tool_call_completeDo nothing until one complete action block has produced a parseable tool id and parameter object.
concurrency: parallel_with_generationStart the validated call in a tracked task while later model output may still be streaming.
result_dependency: detachedNo sibling in this round consumes this result, and this call consumes no sibling result.
replay: at_most_once_per_roundA 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.

EARLY, BUT FULLY GOVERNED Complete action block <channel:action> parse one decision + parameter object Validation gates the same authorities that govern ordinary actions ordered action overseer accepted it decision shape valid known tool · protocol valid external parameter signature valid strategy is exactly neutral complete execution profile present Deterministic identity (turn_id, iteration, action_index) + semantic fingerprint Tracked execution ordinary tool dispatcher + handler, accounting, timeline, queue admission Model keeps streaming later actions in the same round continue generating end-of-round sees the call already consumed — no second validate, write, or execute The early path reuses the ordinary dispatcher; the ledger records the call once, so end-of-round never re-runs it.
Complete action → the ordinary gates → tracked execution, while generation continues.

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 toolsallowed 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.

ONE RESOLUTION PATH Python @tool_trait discovered intrinsic traits, travel with the tool surfaces.as_consumer config per-agent assignments & overrides · py · mcp · named agent_tool_config_from_bundle_props ToolSubsystem metadata Rendered tool catalog the model reads it — never the source of correctness Runtime governance the overseer & executor enforce it, independently Traits may begin in code or config, but they converge before the agent runs — one metadata plane, two independent readers.
Code and config converge into one metadata plane, read by both the catalog and the runtime.

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, or neutral.
  • 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.

KDCube Deep · 12.07.2026