Skip to main content

Custom Python Tools

Write a Python function. The on-device model can call it — and can learn it.

# tools.py
from edgestudio.tools import edge_tool

@edge_tool
def hello_world() -> str:
return "hello world"

@edge_tool(description="Compute the service fee for an amount.", intent_tags=["billing"])
def calculate_fee(amount: float) -> dict:
return {"fee": amount * 0.003}
edge demo chat --model qwen3.5-9b-4bit \
--tools ./tools.py \
--prompt "Use the calculate_fee tool for amount 250. State the exact fee."

You do not write a server, a manifest, a port, or an HTTP endpoint. Edge discovers your functions, generates the model-visible schema from your type hints, runs your code in an isolated runner process, validates every call, and writes hash-first receipts.

Version

Custom Python tools require edge-studio 0.0.1rc21 or newer. The output_schema, execution_level, and explicit commit confirmation features on this page require 0.0.1rc23 or newer. On rc20 and earlier, chat supports the built-in local_facts_lookup executor only — see Local Facts Stores for the manifest-based path available since rc20.

How A Tool Call Actually Runs​

The model never executes code. Your code never runs inside the Edge CLI or the model process. The loop between them is deterministic and fail-closed:

StepWhoWhat happens
1EdgeFreezes the active tool set from your file before generation and injects the JSON tool-call contract
2ModelEmits plain text; a tool call is a single strict JSON object: {"tool_name": "...", "arguments": {...}}
3EdgeParses and validates: tool name in the frozen active set, arguments match the schema, call limit not exceeded
4RunnerEdge spawns its own fixed runner subprocess, verifies the tools file is byte-identical to the frozen state, imports it, and executes exactly one function
5EdgeValidates the result (JSON object, size cap, and the declared output_schema when present) before using or returning it
6ModelAnswers using the tool result, or issues another call (at most 4 per prompt)

Anything unexpected — an unknown tool, invalid arguments, a changed file, a timeout, an oversized result — fails closed: the tool is not executed, or its result is not used, and the receipt records why.

Writing Tool Functions​

@edge_tool only attaches metadata. Your file stays a normal Python file.

from typing import Literal, Optional
from edgestudio.tools import edge_tool

@edge_tool
def order_status(order_id: str) -> dict:
"""Look up the status of a local order record."""
return {"order_id": order_id, "status": "shipped"}

@edge_tool(
name="fee_quote", # defaults to the function name
description="Quote the fee for a tier.",
intent_tags=["billing", "quote"], # used by --tool-tag selection
)
def calculate_fee(
amount: float,
tier: Literal["basic", "pro"] = "basic",
note: Optional[str] = None,
) -> dict:
return {"fee": amount * (0.003 if tier == "basic" else 0.002)}

@edge_tool(
description="Prepare an order for review without submitting it.",
execution_level="prepare",
return_direct=True,
output_schema={
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {"type": "string", "enum": ["prepared"]},
},
"required": ["order_id", "status"],
"additionalProperties": False,
},
)
def prepare_order(order_id: str) -> dict:
return {"order_id": order_id, "status": "prepared"}

Rules:

  • Tool names match [A-Za-z_][A-Za-z0-9_]{0,63} and must be unique per file.
  • Every parameter needs a supported type hint. Missing or unsupported hints fail validation — nothing silently becomes Any.
  • The docstring's first line is used as the description when the decorator does not set one.
  • execution_level is read by default. Use prepare for reversible drafts and commit for real side effects. The level is part of the frozen tool schema.
  • permissions currently supports read_local only. It does not attest what developer-owned Python code does with the user's process permissions.

Supported Parameter Types​

Type hintJSON schema
str, int, float, boolstring, integer, number, boolean
Literal["a", "b"]enum of one primitive type
Optional[T] / T | Nonenullable T
list[T]array of a supported T

Anything else — dict, dataclasses, unions of two non-None types, **kwargs — is rejected by edge tools validate with an explicit error.

Return Values​

  • Return a JSON-serializable dict to control the exact shape the model sees.
  • Scalars and lists (str, int, float, bool, list, None) are wrapped as {"result": value}.
  • Results are capped at 64 KB of canonical JSON. Oversized results fail closed.
  • return_direct=True requires a closed output_schema. The runtime validates the actual result before direct delivery; wrong types, missing keys, and undeclared fields fail closed with tool_result_schema_mismatch.
  • Exceptions raised by your function end the tool loop fail-closed; the model falls back to a safe answer. Return a normal payload such as {"matches": []} for "not found" cases you want the model to reason about.

Execution Levels And Confirmation​

LevelIntended useRuntime behavior
readLookups and calculationsExecutes after normal schema validation
prepareReversible drafts or unsigned payloadsExecutes immediately; requires return_direct=True and output_schema
commitWrites, submissions, broadcasts, or other real side effectsChat creates a pending action; execution requires an explicit local confirmation command

A commit tool must also declare return_direct=True and a closed output_schema. Chat does not execute it. The returned JSON includes a private pending-action path, an expiring confirmation token, and the exact command:

edge tools confirm '<pending_action_path>' --token '<confirmation_token>' --json

Confirmation is bound to the frozen tools file, active tool set, tool schema, arguments, and execution level. A changed file, altered arguments, wrong or expired token, or a concurrent second confirmation fails closed. The pending file is local, mode 0600, and defaults to a 10-minute validity window.

Validate And Inspect​

edge tools validate ./tools.py --json
edge tools inspect ./tools.py --json

validate checks names, type hints, duplicates, and the active-set limit. inspect additionally prints each generated schema, a per-tool schema_sha256, the active_set_sha256, and the tools_file_sha256.

Validation executes your file's top level

Discovery imports the file (inside the isolated runner, never in the Edge CLI process). Top-level code runs, so validate is not a static scan and is not a safety check for untrusted files. Only point Edge at tool files you trust.

Chatting With Tools​

edge demo chat --model qwen3.5-9b-4bit \
--tools ./tools.py \
--prompt "Quote the fee for amount 400, tier pro." \
--json

Selection and limits:

  • Up to 8 tools are exposed per run. A file may define more, but then you must narrow the active set with --tool <name> (repeatable) or --tool-tag <tag>.
  • --tools, --tools-manifest, and --facts-store are mutually exclusive.
  • --tool / --tool-tag require --tools.

Choose the surface from what the run needs to do:

NeedChoose
Query local facts with no code, custom validation, or action--facts-store <store>
Add your own validation, structured evidence, argument binding, or follow-up action--tools ./tools.py

If you only need a stable tool name over the same built-in read-only facts lookup, use --tools-manifest instead. It does not execute your Python code.

Runtime behavior worth knowing:

  • Each tool call runs in a fresh runner process: your file's top level executes on every call, and no state persists between calls. Keep the top level light; load data inside the function or from disk.
  • print() output from your code goes to stderr. The runner's stdout carries only the JSON protocol.
  • Per-call timeout is 10 seconds; a timed-out runner is killed and the call fails closed.
  • If you edit the tools file while a chat session is running, the next call fails closed with tools_file_changed instead of silently executing code that was never frozen. Start a new session to pick up the edit.

Learning Tools: Neural Imprint​

Profile learning and tool learning ride the same artifact. edge demo learn can bake your tool contract into the Neural Imprint, so a restored Agent already knows your tools — the schemas are part of its computational state, not a prompt pasted at runtime.

edge demo learn run \
--sample-file ./learn_sample.json \
--model qwen3.5-9b-4bit \
--tools ./tools.py \
--json

Then chat with the baked artifact and the same tools file:

edge demo chat --model qwen3.5-9b-4bit \
--with-imprint <artifact-or-receipt-path> \
--tools ./tools.py \
--prompt "Quote the fee for amount 100." \
--json

The chat receipt reports tool_instruction_mode: imprint: the tool contract came from the restored prefix, and Edge injects no duplicate instruction.

What Invalidates A Learned Imprint​

The model learns your tools' schemas, never their implementation. The restore gate is therefore schema-level:

You changedRestore
Function bodies, comments, formatting — signatures unchanged✅ Restores; no relearn needed
A parameter, a type hint, a tool name, a description, output_schema, execution_level, or the active set❌ Fails closed — relearn with the current file
Chat without --tools while the imprint was learned with tools❌ Fails closed with imprint_requires_tools

Fail-closed means exactly that: Edge refuses to pair a stale contract with a different runtime instead of guessing. There is no compatibility mapping — relearning is the only upgrade path, by design.

Security And Audit Model​

What Edge guarantees:

  • The model only ever emits a JSON call; Edge validates and dispatches it.
  • Your code executes only in an Edge-owned runner subprocess — never in the Edge CLI or model process — and only for tools in the frozen active set.
  • The runner verifies the file it executes is byte-identical to the frozen state before importing it.
  • Tools load only when you explicitly pass --tools. Edge never scans directories or auto-loads tool files.
  • Receipts are hash-first: tools_file_sha256, active_set_sha256, schema_generator_version, and per-call args_sha256, result_sha256, tool_schema_sha256, runner_secret_verified, plus network_used_by_edge: false for every tool call.

What Edge does not claim:

  • Your tool code runs with your user permissions. Edge attests what Edge did — it does not prove whether your code used the network, read files, or spawned processes. Tool behavior is your code, your responsibility.
  • Expired, never-confirmed pending files are not automatically deleted yet; their tokens are unusable after expiry, but you should remove stale files that contain sensitive arguments.
  • A process crash after a confirmed tool completes its side effect but before its receipt is written can leave an audit gap. Edge keeps the action claimed and will not retry it automatically, preserving at-most-once execution.

Troubleshooting​

Error codeMeaningFix
unsupported_type_hint / missing_parameter_type_hintA parameter hint is unsupported or absentUse the supported types table above
duplicate_tool_nameTwo tools resolve to the same nameRename one
active_tool_limit_exceededMore than 8 tools discoveredSelect with --tool / --tool-tag
invalid_tool_argsModel sent unknown/missing/mistyped argumentsUsually retried automatically; recurring cases mean the schema is ambiguous — improve names and descriptions
tools_file_changedFile edited after the session froze itStart a new chat session
runner_timeoutA call exceeded 10 sKeep tools fast and local
tool_result_oversized / unsupported_tool_resultResult over 64 KB or not JSON-serializableReturn a bounded dict
return_direct_output_schema_requiredA direct-return tool has no declared result contractAdd a closed output_schema
tool_result_schema_mismatchThe real result does not match the declared contractFix the tool result; it will not be delivered
tool_confirmation_requiredA commit tool was reached without explicit confirmationUse the pending action and exact edge tools confirm command returned by chat
imprint_requires_toolsImprint was learned with tools; chat ran without --toolsPass the matching tools file
imprint_tool_active_set_mismatch / imprint_tool_schema_mismatchTool schemas differ from the learned contractRelearn, or restore the original signatures
conflicting_tool_options--tools combined with --tools-manifest or --facts-storePick one tool surface per run

When To Use The Manifest Path Instead​

--tools-manifest (see Local Facts Stores) remains the right surface when you only need stable developer-owned names over the built-in local facts lookup, or when you are on rc20. Custom Python tools are the path when the logic itself is yours.