| name | portlang |
| description | portlang - the environment-first agent framework. Use when working with .field files, defining boundaries and verifiers, adding custom tools (shell, Python, MCP), debugging trajectories, measuring convergence, configuring structured JSON output with output_schema, running batch evals, viewing HTML trajectory dashboards, or analyzing agent behavior across runs. portlang manages environments not loops - you define the search space, the agent finds the path. |
| license | MIT |
| metadata | {"author":"portofcontext","version":"1.5.1"} |
portlang Skill
Core Concept
portlang treats agent behavior as search through a conditioned space. You don't script loops—you declare the search space:
- Boundaries: What the agent cannot do (enforced by sandbox)
- Verifiers: What success looks like (deterministic pass/fail signals, not walls of output)
- Context budget: Hard token ceiling
- Environment: What the agent can observe
The runtime executes the search. Every run produces a trajectory (complete event log).
The instruction budget problem: Models follow roughly 150–200 instructions reliably. A large system prompt, many tool definitions, growing conversation history, and re-observations all compete for that budget. portlang keeps budget usage low by design: each field is scoped narrowly, context is bounded by a hard ceiling, and re-observations inject only current state rather than accumulating history.
Prerequisites
portlang currently only runs on apple devices.
Before running portlang fields:
- Install portlang:
brew tap portofcontext/homebrew-tap
brew install portlang
- Set API key (choose one):
export ANTHROPIC_API_KEY=sk-ant-...
export OPENROUTER_API_KEY=sk-or-v1-...
- Verify installation:
portlang init
- Install the VS Code extension (recommended): Search for "portlang" in the VS Code extension marketplace and install it. Provides LSP support — syntax highlighting, validation, and autocompletion for
.field files.
Model naming by provider:
- Anthropic API:
anthropic/claude-sonnet-4.6, anthropic/claude-opus-4.5
- OpenRouter:
anthropic/claude-3.5-sonnet, anthropic/claude-3-opus, anything on openrouter that support tool calling
- Provider auto-detected from API key
Field File Structure
Fields use the .field extension (preferred) or .toml — both are supported. All sections are optional unless marked (required). Fields marked "inherit" pull their value from a parent field one directory up (auto-detected from ../*.field).
name = "my-task"
description = "..."
[vars]
[model]
name = "anthropic/claude-sonnet-4.6"
temperature = 0.5
[prompt]
goal = "..."
system = "..."
re_observation = ["echo '=== workspace ===' && ls -1", ...]
[environment]
root = "./workspace"
packages = ["nodejs"]
dockerfile = "./Dockerfile"
image = "custom:tag"
[boundary]
allow_write = ["*.py"]
collect = ["report.md"]
network = "deny"
max_tokens = 150000
max_cost = "$2.00"
max_steps = 30
bash = true
output_schema = """{ ... }"""
tools = "inherit"
[[skill]]
slug = "skill-name"
[[tool]]
[[verifier]]
type = "shell"
name = "..."
command = "..."
trigger = "on_stop"
description = "..."
eval_only = false
[[verifier]]
type = "levenshtein"
name = "..."
file = "output.txt"
expected = "..."
threshold = 0.9
[[verifier]]
type = "semantic"
name = "..."
file = "output.txt"
expected = "..."
threshold = 0.85
embedding_model = "bge-small-en-v1.5"
[[verifier]]
type = "tool_call"
name = "..."
tool = "bash"
field = "/input/path"
matches = "^[a-z]+"
not_matches = "^/etc"
Minimal Field File
name = "my-task"
[model]
name = "anthropic/claude-sonnet-4.6"
[prompt]
goal = "Create hello.py that prints 'Hello, World!'"
[environment]
root = "./workspace"
[boundary]
allow_write = ["hello.py"]
max_tokens = 80000
max_cost = "$1.00"
max_steps = 10
[[verifier]]
name = "works"
command = "python hello.py 2>&1 | grep -q 'Hello, World!'"
trigger = "on_stop"
description = "Must print 'Hello, World!'"
Essential Commands
portlang new task.field
portlang new -i
portlang run task.field
portlang run task.field --dry-run
portlang run task.field -n 10
portlang run task.field --runner claude-code
portlang run task.field --auto-reflect
portlang run task.field --output-dir ./out
portlang run task.field --json
portlang run task.field --var k=v
portlang run task.field --vars p.json
portlang run task.field --input ./data.csv
portlang run task.field --input '{"id":"123"}'
portlang list [field-name]
portlang eval run ./examples/
portlang eval run ./examples/ --runner claude-code
portlang eval run ./examples/ --resume <id>
portlang eval list [dir]
portlang eval view <id-or-dir>
portlang view trajectory <id>
portlang view trajectory <id> --format text
portlang view diff <id-a> <id-b>
portlang view field <field-name>
portlang reflect --field <field-name>
portlang reflect --field <field-name> -n 10
portlang reflect --trajectory-id <id>
portlang docs
Add --no-open to any view command to skip opening the browser. See reference/CLI.md for full flag details.
Key Patterns
1. Field Inheritance (shared model/boundary/tools across a suite)
If a *.field file exists one directory up, a child field can inherit from it automatically:
name = "parent"
[model]
name = "anthropic/claude-sonnet-4.6"
temperature = 0.5
[boundary]
network = "deny"
max_tokens = 100000
max_cost = "$1.00"
max_steps = 20
[[tool]]
type = "python"
file = "./tools/shared_utils.py"
name = "task-a"
model = "inherit"
boundary = "inherit"
tools = "inherit"
[prompt]
goal = "Do task A using the shared tools."
Inheritance eliminates duplication across eval suites. Override any section by defining it inline.
2. Template Variables (parameterize a field for reuse)
Declare variables in [vars], use {{ name }} anywhere in goal/system/re_observation/verifier commands, supply at runtime with --var:
[vars]
currency = { required = false, default = "usd", description = "Currency to report" }
[prompt]
goal = "Get the account balance and return amounts in {{ currency }}."
[[verifier]]
name = "correct-currency"
type = "tool_call"
tool = "bash"
trigger = "on_stop"
description = "Agent must have run bash"
portlang run task.field --var currency=gbp
portlang run task.field --vars params.json
portlang run task.field --input ./data.csv
--input with a file copies it to the workspace root. --input '{"key":"val"}' writes portlang_input.json. Use re_observation to surface the file contents to the agent each step.
3. Output: Files and Structured Data
portlang has two output mechanisms.
| Mechanism | How it works | Best for |
|---|
collect + file artifacts | Files the agent writes to the workspace, delivered after the run via --output-dir or --json | Documents, code, reports — anything that lives as a file |
output_schema + submit_output | Agent calls a built-in tool with a JSON payload matching the declared schema | Typed structured payloads the agent constructs — summaries, verdicts, scores, extracted data |
File artifacts — collect:
[boundary]
allow_write = ["report.md", "results/*.json"]
collect = ["report.md", "results/*.json"]
portlang run task.field --output-dir ./out
portlang run task.field --json
portlang run task.field --json | jq '.artifacts[0].content'
If collect is omitted, all allow_write files are delivered. Set collect = [] to deliver nothing.
Structured output — output_schema:
Define output_schema in [boundary] as a JSON Schema string. The agent calls submit_output with a matching payload; portlang validates it automatically — no verifier needed for schema conformance:
[boundary]
output_schema = '''
{
"type": "object",
"required": ["status", "count"],
"properties": {
"status": {"type": "string", "enum": ["success", "failure"]},
"count": {"type": "integer", "minimum": 0}
}
}
'''
To assert specific field values in structured output, use a tool_call verifier:
[[verifier]]
type = "tool_call"
name = "status-ok"
tool = "submit_output"
field = "/input/status"
matches = "^success$"
trigger = "on_stop"
description = "status must be success"
--json output combines both:
{
"run_id": "...",
"field": "my-task",
"outcome": "converged",
"structured_output": { "status": "success", "count": 3 },
"artifacts": [
{ "path": "report.md", "size": 4231, "content": "# Report\n..." }
],
"cost": "0.023",
"tokens": 18420,
"steps": 8,
"duration_ms": 34201,
"trajectory_id": "..."
}
4. Multi-Layer Verifiers (fail fast with precise feedback)
Layer verifiers from coarse to fine — each one assumes the previous passed:
[[verifier]]
name = "compiled"
command = "python script.py 2>/dev/null"
trigger = "on_stop"
description = "script.py must run without errors"
[[verifier]]
name = "correct-output"
type = "levenshtein"
file = "output.txt"
expected = "42"
threshold = 1.0
trigger = "on_stop"
description = "output.txt must contain exactly '42'"
Verifiers run in order, stop on first failure. Use output_schema when the agent produces structured JSON output — schema validation is built-in, no verifier needed.
5. Smart Verifier Types
Prefer typed verifiers. They run in the portlang runtime — no packages required, no container dependencies. Fall back to shell verifiers only for logic that can't be expressed with a typed verifier, and only use tools guaranteed in the container baseline (see section 8).
Trigger modes: on_stop (default) runs after the agent finishes. always runs after every step. on_tool:<tool_name> runs after each call to a specific tool — useful for incremental checks, e.g. trigger = "on_tool:write" to validate files as they're written.
eval_only: Set eval_only = true to exclude a verifier from regular portlang run but include it in portlang eval run. Use this for ground-truth comparisons (levenshtein, semantic) where the expected value is known but shouldn't affect development runs or steer agent behavior.
[[verifier]]
type = "levenshtein"
name = "close-enough"
file = "output.txt"
expected = "The answer is 42."
threshold = 0.9
trigger = "on_stop"
description = "Output must be at least 90% similar to expected"
[[verifier]]
type = "semantic"
name = "right-idea"
file = "summary.txt"
expected = "The model achieved high accuracy on the test set."
threshold = 0.85
trigger = "on_stop"
description = "Summary must convey the correct conclusion"
Local embedding model downloaded automatically (~67 MB, no API key required).
6. Scoped Boundaries
[boundary]
allow_write = ["output.json", "logs/*.txt"]
network = "deny"
max_tokens = 100000
max_cost = "$1.00"
max_steps = 20
7. Re-observation (prevent context rot)
[prompt]
goal = "..."
re_observation = [
"echo '=== workspace ===' && ls -1 *.py *.txt 2>/dev/null | cat",
"echo '=== tests ===' && python -m pytest --tb=no -q 2>&1 | tail -5",
]
Commands run before each agent step, injecting fresh state without accumulating history. This is the principled alternative to the Ralph loop — instead of periodically restarting the session to clear context rot, you push only what the agent needs to know right now. The context window stays bounded; the agent always knows current state.
8. Custom Environment
[environment]
root = "./workspace"
packages = ["nodejs", "npm"]
dockerfile = "./Dockerfile"
image = "myregistry/myimage:latest"
Default container baseline: The container is minimal. Available by default: standard POSIX shell builtins, bash, curl, wc, grep, cat, ls, find. Not available unless added to packages or a custom image: python3, node, jq, git, and most other tools.
Shell verifiers run inside the container and are subject to the same constraints. Prefer typed verifiers ("levenshtein", "semantic", "tool_call") or output_schema over shell verifiers whenever possible — they run natively in the portlang runtime and require nothing installed. Only use shell verifiers for checks that require container-side execution, and only invoke tools you've declared in packages.
9. Custom Tools
Default tools (always available, no [[tool]] entry needed):
bash — run shell commands in the container
glob — find files by pattern
write — write files to allowed paths
Define [[tool]] entries only to add capabilities beyond these three.
Python tools are the default choice. Use shell tools only for trivial single-command wrappers (e.g. wc, cp). Anything that parses output, handles errors, calls an API, or processes data belongs in a Python tool.
portlang auto-extracts everything the agent uses from the Python function — the quality of these annotations directly determines how well the agent uses the tool:
| Python source | Agent sees |
|---|
| Function name | Tool name |
| Docstring | Tool description (how agent decides when to call it) |
| Parameter type hints | Input schema (validated before call) |
Literal["a", "b"] | Enum constraints |
Pydantic BaseModel return | Output schema (agent knows exactly what it gets back) |
Use -> dict and the agent gets no output schema. Use -> MyModel(BaseModel) and the agent sees typed fields it can reason about.
Anti-pattern — don't do this:
[[tool]]
type = "shell"
name = "fetch_data"
command = "curl -s {url} | jq '.results' > /workspace/data.json && echo '{\"ok\": true}'"
input_schema = '{"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]}'
Do this instead — Python tool:
import json, pathlib
import requests
from pydantic import BaseModel
class FetchResult(BaseModel):
ok: bool
count: int
output_path: str
def fetch_data(url: str, output_path: str) -> FetchResult:
"""Fetch JSON from a URL and write results to output_path."""
resp = requests.get(url, timeout=10)
resp.raise_for_status()
data = resp.json().get("results", [])
pathlib.Path(output_path).write_text(json.dumps(data, indent=2))
return FetchResult(ok=True, count=len(data), output_path=output_path)
[[tool]]
type = "python"
file = "./tools/fetch_data.py"
function = "fetch_data"
Python tool rules:
- Use Pydantic models as return types. portlang auto-extracts the JSON schema — no manual schema needed. Raise exceptions on errors rather than returning error dicts.
- Each tool file runs in isolation — tool files cannot import each other. Put all related logic in one file.
- Declare third-party dependencies with a
# /// script PEP 723 block at the top; uv installs them automatically — no need to add packages to [environment].
- File paths in
file = are relative to the field file, not the workspace root.
Shell tool (for simple wrappers only):
[[tool]]
type = "shell"
name = "word_count"
description = "Count words in a file"
command = "wc -w {path}"
input_schema = '{"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}'
Tool-first design: Write Python tools before writing the field. The agent's goal should be: call the tool, write the output. This keeps steps under 5, cost under $0.05, and boundaries naturally minimal. Agents that try to do complex work through raw bash (curl pipes, temp files, awk scripts) burn budget and fail more often.
MCP server (stdio):
[[tool]]
type = "mcp"
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
transport = "stdio"
MCP server (HTTP/SSE):
[[tool]]
type = "mcp"
name = "stripe"
url = "https://mcp.stripe.com"
transport = "http"
headers = { Authorization = "Bearer ${STRIPE_KEY}" }
10. Agent Skills
Load Claude Code skills into the agent's context using [[skill]] entries. Skills inject specialized knowledge or instructions into the agent before it starts.
[[skill]]
slug = "rust-best-practices"
[[skill]]
slug = "portlang"
Skills are loaded in declaration order. Use --runner claude-code — skills are passed via npx skills inject and are available as system prompt context during the run.
11. Claude Code Runner
Use --runner claude-code to run the agent loop through Claude Code instead of the native runner. This gives the agent Edit, Glob, Grep, LSP, WebSearch, and WebFetch — the full Claude Code toolset — inside portlang's sandbox and verifier system.
portlang run example.field --runner claude-code
Auth: if Claude Code is already installed and authenticated, no setup is needed — portlang reads credentials from ~/.claude/.credentials.json automatically. Otherwise run claude setup-token, or set ANTHROPIC_API_KEY.
How field config maps to Claude Code:
| Field config | Behavior |
|---|
model.name | Passed to Claude Code |
[[tool]] MCP | Passed directly via --mcp-config |
[[tool]] shell/python | Wrapped as MCP stdio servers, run in container |
boundary.allow_write | Enforced via PostToolUse hook on Write/Edit |
boundary.max_steps/cost/tokens | Monitored from stream; process killed on breach |
[[verifier]] shell, on_stop | Run by portlang after agent exits |
[[verifier]] shell, always/on_tool | Run as Claude Code PostToolUse hooks |
boundary.network | Always enabled (Claude Code requires API access) |
boundary.output_schema | Agent receives a submit_output tool; structured output captured from tool call |
Limitations vs native runner: tool_call verifiers and boundary context tracing are not supported.
12. Batch Evaluation
portlang eval run ./examples/
portlang eval view ./examples/
Useful for regression testing after changes.
Debugging Workflow
- Run fails →
portlang view trajectory <id> --format text to replay step-by-step
- Find failure point → Check which verifier failed and at which step
- Non-determinism →
portlang view diff <id-a> <id-b> to find divergence
- Visual debugging →
portlang view trajectory <id> for HTML view
- Optimize →
portlang run task.field -n 10 to measure reliability
- Patterns →
portlang view field <field-name> for adaptation analysis
- Insights →
portlang reflect --field <field-name> to surface AI-powered insights from trajectories
Common Issues
Budget exhausted:
- Start conservative:
max_cost = "$0.25" for simple tasks, $1.00 for network-heavy tasks; increase after profiling
- Increase
max_tokens or reduce max_steps in [boundary]
- Simplify
re_observation commands — context rot (early history crowding out current state) often masquerades as budget exhaustion; inject minimal, current-state signals only
- Check for tool error loops
- Move complex logic into Python tools so the agent does orchestration, not implementation
Low convergence rate (<70%):
- Strengthen verifiers (make expectations explicit)
- Tighten boundaries (restrict file access)
- Clarify goal in
[prompt]
- Lower
temperature (e.g., temperature = 0.0)
Verifier always passes/fails:
- Weak signal (>95% or <10% pass rate) — adjust verifier command
Structured output not valid:
- Add an explicit
[[verifier]] to check output.json
- Ensure
[prompt].goal names the required fields explicitly
- Use
temperature = 0.0 for consistent JSON output
Reference Documentation
- reference/CLI.md - Full CLI reference (all commands and flags)
- reference/verifier_patterns.md - 20 real-world verifier examples
- reference/custom_tools.md - Shell, Python, MCP guides
- reference/trajectory_analysis.md - Advanced debugging
- reference/field_recipes.md - 8 complete field examples
Core Principles
- Boundaries are topology, not policy - Make bad actions impossible, not discouraged
- Verifiers give deterministic signals - Not post-hoc checks; they steer behavior by giving the agent precise feedback instead of raw output to interpret
- Context is finite and attention degrades - Smaller context windows produce better results; hard ceilings are quality controls, not just cost controls
- Trajectories are data - Replay, diff, analyze distributions; measure convergence rate before shipping
- Engineer the environment, not the prompt - Agent policy is opaque; the environment is yours. Use actual control flow for control flow, not prompt instructions
GitHub: https://github.com/portofcontext/portlang