Lightweight agents on top of Pydantic AI with true soft tools and conversation history
Project description
GF AgentKit
Lightweight agents on top of Pydantic AI with:
- True soft tools (reasoning-first; tools are optional, not mandatory).
- A single “gateway” tool that delegates to a separate executor (to reduce tool gravity).
- Built-in conversation history (persistent transcript managed by the agent).
- A few ready-to-use tools (file I/O, safe process execution, agent delegation).
⚠️ This is a work-in-progress. The API may evolve.
Why?
Many agent frameworks push models to use tools aggressively (“hard tools”), which can be great for workflow graphs but not for reasoning-first tasks. GF AgentKit makes tool use opt-in:
- SoftToolAgent: the model decides when to ask for a tool using a schema-guided envelope (OPEN_RESPONSE / TOOL_CALL / TOOL_RESULT). The host runs tools and feeds results back as context.
- DelegatingToolsAgent: the model sees one gateway tool (
delegate_ops). That tool delegates to an internal executor which owns all the real tools. This reduces tool gravity and lets you keep a clean reasoning loop.
Both agents keep a persistent transcript so follow-ups naturally refer back to earlier turns.
Installation
pip install agentkit-gf
Requires: Python 3.11+.
You’ll also need an OpenAI key to use gpt-5-nano:
# macOS/Linux
export OPENAI_API_KEY=sk-...
# Windows PowerShell
$env:OPENAI_API_KEY = "sk-..."
At a Glance
agentkit_gf/
├─ _base_agent.py # shared transcript + constructor glue
├─ soft_tool_agent.py # true soft tools (Envelope schema)
├─ delegating_tools_agent.py # single gateway tool; delegates to executor
└─ tools/
├─ fs.py # FileTools: read/write/stat/hash/list (sandboxable)
├─ os.py # ProcessTools: run_process/run_shell (policy controlled)
├─ agent.py # create_agent_delegation_tool(...) factory
└─ builtin_tools_matrix.py # BuiltinTool enums + provider validation
Agents
1) SoftToolAgent (true soft tool)
-
The model returns a single Envelope JSON object each hop:
OPEN_RESPONSE { text, confidence? }TOOL_CALL { tool, args_json, reason }TOOL_RESULT { tool, args_json, result_json, success, note? }(normally emitted by the host)
-
You provide a registry of Python callables (
tool_name -> callable(**kwargs)). -
The agent executes tools in host code and feeds a
TOOL_RESULTback to the model. -
Maintains an internal transcript across turns.
Minimal example (read a file):
from agentkit_gf.soft_tool_agent import SoftToolAgent
from agentkit_gf.tools.fs import FileTools
file_tools = FileTools(root_dir=".") # restrict if you like
registry = {"read_text": file_tools.read_text}
agent = SoftToolAgent(model="openai:gpt-5-nano")
prompt = (
"Read ./notes.txt and tell me the first line.\n"
"If you need the file, return a TOOL_CALL Envelope for tool 'read_text' with args_json "
'{"path": "./notes.txt", "max_bytes": 10000}. Then, after TOOL_RESULT, respond with OPEN_RESPONSE.'
)
result = agent.run_soft_sync(prompt, registry, max_steps=5)
print(result.final_text)
Transcript helpers:
print(agent.export_history_text())
agent.reset_history()
2) DelegatingToolsAgent (single gateway tool)
- Presents one tool (
delegate_ops) to the model. - Internally spins up a private executor agent that owns all real tools (including optional provider built-ins like WebSearch).
- You pass in objects or callables; public methods are automatically exposed as tools (optionally prefixed).
Example:
from agentkit_gf.delegating_tools_agent import DelegatingToolsAgent
from agentkit_gf.tools.fs import FileTools
from agentkit_gf.tools.os import ProcessTools
from agentkit_gf.tools.builtin_tools_matrix import BuiltinTool
agent = DelegatingToolsAgent(
model="openai:gpt-5-nano",
builtin_enums=[BuiltinTool.WEB_SEARCH], # optional provider built-ins
tool_sources=[
FileTools(root_dir="."),
ProcessTools(root_cwd=".", allowed_basenames=["python", "bash", "ls"])
],
class_prefix="fs", # public tool names become "fs_read_text", etc.
system_prompt=(
"Answer-first. Use delegate_ops only if a specific missing fact requires it."
),
ops_system_prompt="Execute exactly one tool and return only its result.",
)
reply = agent.run_sync(
"Read ./notes.txt (use delegate_ops/tool 'fs_read_text' if needed) and summarize the first line."
).output
print(reply)
Included Tools
FileTools (agentkit_gf.tools.fs)
read_text(path, max_bytes=200_000, encoding="utf-8")read_bytes_base64(path, max_bytes=200_000)write_text(path, content, overwrite=False, encoding="utf-8")stat(path)/list_dir(path, include_hidden=False, max_entries=1000)hash_file(path, algorithm=HashAlgorithm.SHA256)
All enforce fail-fast validation and can be sandboxed with root_dir.
ProcessTools (agentkit_gf.tools.os)
run_process(argv: Sequence[str], timeout_sec=10, cwd=None)(no shell; recommended)run_shell(command: str, timeout_sec=10, cwd=None)(flexible; riskier)
Policy controls:
root_cwd(path sandbox)allowed_basenames(allowlist executables)max_output_bytes(clip stdout/stderr)
Agent Delegation Tool (agentkit_gf.tools.agent)
create_agent_delegation_tool(agent_factory: Callable[[str], Agent]) -> Callable[..., dict]- Produces a registry callable:
delegate_agent(agent_name: str, prompt: str) -> {"output": ...} - Handy if your soft tool needs to spin up or discover another agent on demand.
Soft vs. Hard Tools
-
Soft tools (this library):
- The model asks to call a tool via a schema; the host decides and executes.
- Great for reasoning-first flows where the model should prefer answering from context.
-
Hard tools:
- Registered with the provider; models are often biased to call them.
- Better for rigid flows or “do X with Y, then Z” pipelines.
You can mix: use SoftToolAgent for reasoning, and DelegatingToolsAgent when you need a single, auditable gateway to real tools (including provider built-ins).
Extending with Your Own Tools
You can pass objects or callables:
class MyDataOps:
def summarize_csv(self, path: str, top_n: int = 5) -> dict:
# ... return JSON-serializable result ...
return {"summary": "...", "top_n": top_n}
agent = DelegatingToolsAgent(
model="openai:gpt-5-nano",
builtin_enums=[],
tool_sources=[MyDataOps()],
class_prefix="data", # exposes "data_summarize_csv"
)
For SoftToolAgent, add to the registry:
registry = {"summarize_csv": MyDataOps().summarize_csv}
Testing
This repo ships pytest integration tests that hit gpt-5-nano.
Install the package in editable mode
python -m pip install -U pip setuptools wheel
python -m pip install -e .
Set your API key
# macOS/Linux
export OPENAI_API_KEY=sk-...
# Windows PowerShell
$env:OPENAI_API_KEY = "sk-..."
Run tests
pytest -vv
If you see ModuleNotFoundError: agentkit_gf, make sure you ran the editable install, or set PYTHONPATH to the repo root for the session.
Design Principles
- Reasoning-first: answer from context when possible; use tools only when necessary.
- Fail-fast: explicit validation, actionable error messages, no silent fallbacks.
- Strong typing: Pydantic models for all tool results; no raw dicts in public APIs.
- Separation of concerns: reasoning agent vs. executor agent; clean gateway (
delegate_ops). - Security knobs: tool sandboxes (
root_dir,root_cwd), allowlists, output clipping.
API Reference (quick)
SoftToolAgent
SoftToolAgent(
model: str,
system_prompt: str | None = None,
allow_llm_tool_result: bool = False,
)
run_soft_sync(
prompt: str,
tool_registry: Mapping[str, Callable[..., Any]],
max_steps: int = 4,
) -> SoftRunResult
# history helpers
export_history_text() -> str
export_history_blocks() -> Sequence[str]
reset_history() -> None
Envelope schema: the model returns exactly one JSON object with message.kind in:
OPEN_RESPONSE { text, confidence? }TOOL_CALL { tool, args_json, reason }(args_json must be an object string)TOOL_RESULT { tool, args_json, result_json, success, note? }
DelegatingToolsAgent
DelegatingToolsAgent(
model: str,
builtin_enums: Sequence[BuiltinTool],
tool_sources: Sequence[Callable | object | AbstractToolset] = (),
class_prefix: str | None = None,
system_prompt: str | None = None,
ops_system_prompt: str | None = None,
)
# single-step run (history-aware)
run_sync(prompt: str) -> RunResult
This agent exposes only one public tool to the model: delegate_ops(tool, args_json, why) (the executor runs exactly one real tool; results are recorded to the transcript).
License
See LICENSE - MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agentkit_gf-0.1.0.tar.gz.
File metadata
- Download URL: agentkit_gf-0.1.0.tar.gz
- Upload date:
- Size: 21.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5cc84e0bf30cb6a9fb408e4dac192d05e243ec2f822105ce48b3430da78733cc
|
|
| MD5 |
5f3799894a4864f1d23a4735c4aff3be
|
|
| BLAKE2b-256 |
0b2b657ba3dacc6f4870d6a8b972c91fa361424053306646a16cff0c8a686c61
|
File details
Details for the file agentkit_gf-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agentkit_gf-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c175797b3ededeca70a7b9730e5c401e047176e872280f3a85c2e0262a7c6b2a
|
|
| MD5 |
acc47056bf503a7bb1ee804e1cf2d544
|
|
| BLAKE2b-256 |
66930e4c46754ae036695673ab97a7169c94bc38ca649fd6ad2744e01ee6c60f
|