Harness API
Harness wraps a Model and an explicit collection of Tool objects. Unlike the
legacy Agent, it does not print to the terminal, read CLI configuration, create a
runtime, or consult the global tool registry. Tool functions receive keyword
arguments validated against their JSON Schema.
Run lifecycle
- Copy the supplied history, or create a system message, then add the prompt.
- Check cancellation/deadline and call the model with the available tool schemas.
- If the model returns a final answer, validate
output_schemawhen supplied. - Otherwise, check the entire tool batch against the remaining budget.
- Validate each call, check approval when required, execute, and append its result.
- Continue until completion or a limit; return a
RunResult.
Tool call IDs must be nonempty and unique within a response. Each accepted model batch receives one tool response per call, including calls skipped for limits or cancellation. A tool failure becomes an error response that the model can act on. Malformed model messages, provider errors, approval errors and event-sink errors propagate to the caller. No external action is retried automatically.
Tools and approval
from pygent import Tool
lookup = Tool(
"lookup", "Look up an internal package by name.",
{"type": "object", "properties": {"name": {"type": "string"}},
"required": ["name"], "additionalProperties": False},
function=lambda name: {"name": name, "supported": True},
)
Mark mutations with requires_approval=True. They are denied unless
run(approve=callback) returns exactly True. The callback gets (tool_name,
arguments) with an independent copy of the validated arguments. The application
owns the approval UI and policy. This callback does not implement persisted
human-in-the-loop suspension; do not block a web request waiting for approval.
Each harness snapshots its tool metadata at construction. Registries are independent.
Functions themselves are not copied and must be safe for the intended concurrency.
Unknown tools never dispatch to the legacy registry. JSON Schema default values
are annotations, not automatically inserted values. Tool functions must return text
or JSON-serializable values; non-finite JSON values are rejected.
Limits and status
| Status | Meaning |
|---|---|
completed |
Model produced a final answer; optional output schema passed |
max_steps |
Model-turn budget exhausted |
max_tool_calls |
Next batch would exceed tool-call budget; batch skipped |
timeout |
Cooperative deadline observed between calls |
cancelled |
Cancellation event observed between calls |
invalid_output |
Final answer was not JSON conforming to the output schema |
RunLimits() defaults to 20 model calls, 100 tool attempts, and 16,000 characters
per tool result. max_time=None means no harness deadline. Denied, unknown and
invalid calls consume tool attempts. Budget-skipped calls do not. The truncation
marker is additional to the retained-character budget. This is a context capture
limit, not a limit on memory allocated inside an arbitrary tool function.
Cancellation uses threading.Event. Deadlines/cancellation do not preempt Python
functions, pending approvals, event callbacks or model calls. Configure provider
and tool I/O timeouts. Runtime.bash has a separate command timeout and bounded
capture, including output with no newline. Arbitrary custom tools need their own
resource limits.
Structured answers and telemetry
schema = {"type": "object", "properties": {"passed": {"type": "boolean"}},
"required": ["passed"], "additionalProperties": False}
# Ask for JSON explicitly in your prompt. This validates locally; it does not
# enable a provider-specific constrained decoding mode.
result = harness.run("Return JSON with a boolean passed field.", output_schema=schema)
if result.status == "completed":
print(result.data["passed"])
Pass on_event=callback to route RunEvent values into your own logs or traces.
Events include run ID, step, elapsed time, tool name/call ID and outcome. They do
not include prompts, arguments or outputs. Tool names and IDs can still contain
application-specific information. Events are also stored in the result.
result.to_dict() is JSON-serializable. Its messages and output may contain
sensitive data; choose redaction, access control and retention at the application
boundary. Pygent does not upload telemetry.
Continuation and orchestration
Continue with harness.run(next_prompt, history=result.messages). Each run copies
its transcript and has independent counters. Supply only trusted, complete
transcripts. A new run resets its budget. A model re-requesting a tool can repeat
its side effect; use application idempotency keys where necessary.
Saving a result is not a durable checkpoint: a crash between a side effect and
saving its result can leave uncertain execution state. Use a workflow engine and
idempotent tools when recovery is required. To use Pygent inside an existing
orchestrator, invoke run as an ordinary synchronous function and persist the
returned status and transcript. Native LangGraph/MCP adapters are not included.
For concurrent jobs, use separate model/tool instances or explicitly thread-safe
implementations. ScriptedModel is deliberately sequential. Native asyncio and
streaming model responses are future work.