Version: 1.0.0
This guide helps AI agents integrate with the pt agent CLI interface. It covers the mental model, common workflows, output parsing, safety guarantees, and best practices.
pt agent is the canonical spelling used throughout this guide. pt robot remains a supported alias for the same subcommand surface, so older examples and scripts that use pt robot ... still map to the same commands.
This documentation describes both currently available features and planned features from the agent CLI contract specification. The following table summarizes what's implemented:
| Feature | Status | Notes |
|---|---|---|
pt agent plan |
✅ Implemented | pt robot plan is an alias; supports --deep, --min-age, --format, --only, --max-candidates, and token-efficient globals (--fields, --compact, --max-tokens, --estimate-tokens) |
pt agent apply |
✅ Implemented | pt robot apply is an alias; supports --recommended, --pids, --targets, --yes, --resume, and safety gates (--min-posterior, --max-kills, --max-blast-radius, --max-total-blast-radius, --require-known-signature) |
pt agent explain |
✅ Implemented | pt robot explain is an alias; supports --session plus --pids or --target |
Session management (--session) |
✅ Implemented | pt agent snapshot, pt agent sessions, pt agent plan --session, pt agent apply --session, pt agent verify --session, pt agent diff |
Safety gates (--min-posterior, --max-kills) |
✅ Implemented | Enforced at apply time; policy defaults still apply |
Pattern filtering (--patterns) |
🚧 Planned | Filter by process name patterns |
pt export |
✅ Implemented | Use pt bundle create or pt agent export |
pt report |
✅ Implemented | Top-level report delegates to the report backend when built with the report feature; pt agent report remains available for the fuller agent-oriented surface |
For immediate use: Focus on the "Currently Implemented" workflows in the Quickstart section. Commands that require optional features call that out explicitly; sections marked with 🚧 describe planned features.
- Mental Model
- Quickstart Workflows
- Output Formats and Parsing
- Exit Codes and Error Taxonomy
- Safety and Governance
- Best Practices
- Real Workflow Examples
Process Triage (pt) is a Bayesian-inspired tool that identifies and manages "zombie" or abandoned processes on a system. It classifies processes into four categories:
| Class | Description | Typical Action |
|---|---|---|
useful |
Active, doing real work | Leave alone |
useful_bad |
Active but misbehaving | Throttle, review |
abandoned |
Idle, likely forgotten | Kill (recoverable) |
zombie |
Dead but not reaped | Clean up |
All agent operations flow through a session—a stateful context that tracks a complete triage cycle:
snapshot → plan → explain → apply → verify → export
↓ ↓ ↑ ↓ ↑
└──── all share session_id ────┘
Key insight: The session model enables interruption and resumption. An agent can:
- Take a snapshot
- Generate a plan
- Get interrupted (timeout, reboot, etc.)
- Resume later with the same session ID
Phase 1: Plan (Read-Only)
- Scans processes, collects evidence, runs inference
- Produces a plan with recommendations
- Zero side effects—safe to run repeatedly
Phase 2: Apply (Mutating)
- Executes actions from an approved plan
- Validates identity before each action
- Records outcomes for verification
This separation allows agents to:
- Generate plans for human review
- Apply plans autonomously when confidence is high
- Retry failed applications without re-planning
pt-YYYYMMDD-HHMMSS-<random4>
Example: pt-20260115-143022-a7xq
These workflows work with the current implementation:
# Scan and report in JSON, no actions taken
pt agent plan --format json --max-candidates 10
# With deep inspection (more evidence, slower)
pt agent plan --deep --format json# Apply all KILL recommendations (requires explicit --yes)
pt agent apply --recommended --yes --format json
# Kill specific PIDs
pt agent apply --pids 1234,5678 --yes --format json# Get detailed analysis of a single process
SESSION=$(pt agent plan --format json | jq -r .session_id)
pt agent explain --session "$SESSION" --pids 1234 --format json# Stream progress events for a session (follow mode)
pt agent tail --session pt-20260115-143022-a7xq --followProgress events are persisted under the session directory:
~/.local/share/process_triage/sessions/<session_id>/logs/session.jsonl
# Generate plan and capture session ID
SESSION=$(pt agent plan --format json | jq -r .session_id)
pt agent apply --session "$SESSION" --recommended --yes
pt agent verify --session "$SESSION"# Only act on very confident classifications
SESSION=$(pt agent plan --format json | jq -r .session_id)
pt agent apply --session "$SESSION" \
--recommended --yes \
--min-posterior 0.99 \
--max-kills 5 \
--max-blast-radius 2048# Check status of existing session
pt agent sessions --session pt-20260115-143022-a7xq
# Resume from where it left off
pt agent apply --session pt-20260115-143022-a7xq --resume --recommended --yesEvery response includes these envelope fields:
{
"schema_version": "1.0.0",
"session_id": "pt-20260115-143022-a7xq",
"generated_at": "2026-01-15T14:30:22Z",
"host_id": "devbox1.example.com"
}The plan command returns candidates with mandatory fields:
{
"schema_version": "1.0.0",
"session_id": "pt-20260115-143022-a7xq",
"generated_at": "2026-01-15T14:30:22Z",
"host_id": "devbox1.example.com",
"summary": {
"total_scanned": 142,
"candidates_found": 3,
"kill_recommended": 2,
"review_recommended": 1,
"spare_count": 0,
"total_recoverable_mb": 2400,
"total_recoverable_cpu_pct": 15.2
},
"candidates": [
{
"pid": 1234,
"start_id": "9d2d4e20-8c2b-4a3a-a8a2-90bcb7a1d86f:123456789:1234",
"uid": 1000,
"ppid": 1,
"cmd_short": "node jest --worker",
"cmd_full": "node /path/to/jest/bin/jest.js --worker=12345",
"classification": "abandoned",
"posterior": {
"abandoned": 0.94,
"useful": 0.03,
"useful_bad": 0.02,
"zombie": 0.01
},
"confidence": "high",
"blast_radius": {
"memory_mb": 1200,
"cpu_pct": 98,
"child_count": 3,
"risk_level": "low",
"summary": "Killing frees 1.2GB RAM, terminates 3 children; no external impact"
},
"reversibility": {
"reversible": false,
"recovery_options": ["Restart via: npm test"],
"data_at_risk": false
},
"supervisor": {
"detected": false,
"type": null,
"recommended_action": "kill"
},
"uncertainty": {
"confidence_level": 0.94,
"uncertainty_drivers": [
{"factor": "io_activity", "impact": "medium", "note": "Last IO 45min ago"}
],
"decision_robustness": "high"
},
"recommended_action": "kill",
"action_rationale": "High-confidence abandoned process; blast radius contained"
}
],
"recommended": {
"preselected_pids": [1234, 5678],
"actions": [
{
"target": {"pid": 1234, "start_id": "9d2d4e20-8c2b-4a3a-a8a2-90bcb7a1d86f:123456789:1234"},
"action": "kill",
"stage": 1,
"gates": ["identity_valid", "not_protected"]
}
],
"total_actions": 2,
"estimated_recovery_mb": 2400
}
}{
"schema_version": "1.0.0",
"session_id": "pt-20260115-143022-a7xq",
"results": [
{
"target": {"pid": 1234, "start_id": "9d2d4e20..."},
"action": "kill",
"outcome": "success",
"duration_ms": 50,
"verification": {
"process_exited": true,
"exit_code": null,
"memory_freed_mb": 512
}
}
],
"summary": {
"total": 2,
"successful": 2,
"skipped": 0,
"failed": 0,
"memory_freed_mb": 1200
}
}{
"schema_version": "1.0.0",
"session_id": "pt-20260115-143022-a7xq",
"verification": {
"requested_at": "2026-01-15T14:31:00Z",
"completed_at": "2026-01-15T14:31:02Z",
"overall_status": "success"
},
"action_outcomes": [
{
"target": {"pid": 1234},
"action": "kill",
"outcome": "confirmed_dead",
"time_to_death_ms": 50,
"resources_freed": {"memory_mb": 1200}
}
],
"resource_summary": {
"memory_freed_mb": 2400,
"expected_mb": 2400
}
}- Always check
schema_versionbefore parsing—future versions may add fields - Required fields are guaranteed within a major version
- Unknown fields should be ignored, not cause errors
- Use jq for extraction:
jq -r '.candidates[] | select(.recommended_action == "kill")'
| Flag | Effect |
|---|---|
--compact |
Minimal output (default) |
--verbose |
Include all optional fields |
--include-prose |
Add natural language summaries |
--galaxy-brain |
Add full Bayesian math derivations |
--fields pid,classification |
Project specific fields only |
--limit 5 |
Top N candidates only |
--only kill |
Filter by action type |
| Code | Constant | Meaning | Agent Response |
|---|---|---|---|
| 0 | OK_CLEAN |
Success: nothing to do | No action needed |
| 1 | OK_CANDIDATES |
Candidates found, plan produced | Review/apply plan |
| 2 | OK_APPLIED |
Actions executed successfully | Verify outcomes |
| 3 | ERR_PARTIAL |
Some actions failed | Retry or escalate |
| 4 | ERR_BLOCKED |
Safety gates blocked action | Review constraints |
| 5 | ERR_GOAL_UNREACHABLE |
Goal not achievable | Report to user |
| 6 | ERR_INTERRUPTED |
Session interrupted (resumable) | Resume session |
| 10 | ERR_ARGS |
Invalid arguments | Fix command |
| 11 | ERR_CAPABILITY |
Required capability missing | Check prerequisites |
| 12 | ERR_PERMISSION |
Permission denied | Escalate (sudo) |
| 13 | ERR_VERSION |
Version mismatch | Update pt |
| 14 | ERR_LOCK |
Another pt instance running | Wait and retry |
| 15 | ERR_SESSION |
Session not found/invalid | Create new session |
| 20 | ERR_INTERNAL |
Internal error (bug) | Report bug |
| 21 | ERR_IO |
I/O error | Retry or escalate |
| 22 | ERR_TIMEOUT |
Operation timed out | Increase timeout |
When an error occurs, JSON output includes:
{
"schema_version": "1.0.0",
"error": {
"code": "IDENTITY_MISMATCH",
"message": "PID 1234 identity changed since plan was created",
"details": {
"expected_start_id": "9d2d4e20-8c2b-4a3a-a8a2-90bcb7a1d86f:123456789:1234",
"actual_start_id": "9d2d4e20-8c2b-4a3a-a8a2-90bcb7a1d86f:123456990:1234"
},
"recoverable": true,
"recovery_action": "Generate fresh plan with: pt agent plan"
}
}| Code | Meaning | Recoverable | Recovery Action |
|---|---|---|---|
IDENTITY_MISMATCH |
PID reused or process changed | Yes | Regenerate plan |
SESSION_NOT_FOUND |
Session ID doesn't exist | No | Create new session |
SESSION_EXPIRED |
Session past retention | No | Create new session |
PERMISSION_DENIED |
Insufficient privileges | Maybe | Try with sudo |
PROTECTED_PROCESS |
Target is protected | No | Skip this target |
BUDGET_EXHAUSTED |
FDR/alpha budget depleted | Yes | Wait for reset |
GATE_FAILED |
Safety gate blocked | Depends | Check gate details |
INTERNAL_ERROR |
Unexpected error | No | Report bug |
Every action passes through safety gates before execution:
| Gate | Check | Failure Behavior |
|---|---|---|
identity_valid |
PID + start_id + UID match | Abort, require fresh plan |
not_protected |
Not in protected list | Skip, log warning |
posterior_threshold |
P(target_class) > threshold | Skip with explanation |
blast_radius_limit |
Impact < configured max | Skip with explanation |
fdr_budget |
Within FDR/alpha-investing budget | Skip, log exhausted |
supervisor_check |
Supervisor action preferred | Warn if suboptimal |
Gates are evaluated in a specific order (fail-fast):
identity_valid— Ensures process hasn't been replacednot_protected— Respects protected process listsupervisor_check— Prefers supervisor actionsposterior_threshold— Enforces confidence requirementsblast_radius_limit— Limits impactfdr_budget— Statistical false discovery control
The start_id is critical. It's a composite key:
<boot_id>:<start_time_ticks>:<pid>
This ensures:
- PID reuse is detected (different start_time)
- Reboots invalidate stale plans (different boot_id)
- Wrong process is never killed
The following are always protected:
- PID 1 (init/systemd)
- Kernel threads
- Processes matching
protected_patternsin config - Processes owned by root (unless explicitly targeted)
Every candidate includes blast radius info:
{
"blast_radius": {
"memory_mb": 1200,
"cpu_pct": 98,
"child_count": 3,
"connection_count": 2,
"open_files": 47,
"dependent_processes": [
{"pid": 2345, "relationship": "child", "cmd_short": "node worker.js"}
],
"risk_level": "low",
"summary": "Killing frees 1.2GB RAM, terminates 3 children; no external impact"
}
}Risk levels: none, low, medium, high, critical
Process Triage uses False Discovery Rate control:
- Each kill consumes "alpha budget"
- Budget regenerates over time
- When exhausted, confident kills are still blocked
- Prevents runaway automation
# Good: Review plan before applying
pt agent plan --format json | jq '.candidates[] | {pid, cmd_short, recommendation}'
# Then apply based on what you saw
pt agent apply --recommended --yes --format jsonUse explicit session management when you want plan/apply to stay pinned to the same snapshot:
# Good: Explicit session management
SESSION=$(pt agent plan --format json | jq -r .session_id)
pt agent apply --session "$SESSION" --recommended --yes
# Why: Ensures plan and apply operate on the same snapshotdef parse_plan(output):
data = json.loads(output)
major = int(data["schema_version"].split(".")[0])
if major > 1:
raise ValueError(f"Unsupported schema version: {data['schema_version']}")
return datapt agent apply --recommended --yes --format json
case $? in
0) echo "Clean system, nothing to do" ;;
1) echo "Plan ready, no actions taken" ;;
2) echo "Actions applied successfully" ;;
3) echo "Partial failure—check results" ;;
4) echo "Safety blocked—review constraints" ;;
6) echo "Interrupted—can resume" ;;
*) echo "Error: check logs" ;;
esacFor autonomous operation, require high confidence and tight blast-radius limits:
# Conservative autonomous apply
pt agent apply --session "$SESSION" \
--recommended --yes \
--min-posterior 0.99 \
--max-kills 3 \
--max-blast-radius 1GBThe default policy already provides safety through protected process lists and posterior thresholds built into the recommendation logic, but explicit gates make automation easier to audit.
# Get just the fields you need
pt agent plan --format json \
--fields pid,classification,posterior,recommended_action \
--limit 20Always verify outcomes:
pt agent apply --session "$SESSION" --recommended --yes
pt agent verify --session "$SESSION"When supervisor.detected is true, prefer the supervisor command:
for candidate in plan["candidates"]:
if candidate["supervisor"]["detected"]:
# Use supervisor action instead of direct kill
cmd = candidate["supervisor"]["supervisor_command"]
# e.g., "systemctl --user stop my-app.service"Interrupted workflows can be resumed when the session is still valid:
# Resume the prior apply attempt
pt agent apply --session "$SESSION" --resumeThese examples demonstrate target workflow patterns. They intentionally include some not-yet-implemented filters such as --patterns, --exclude-patterns, and --min-idle-minutes, even though sessions and safety gates shown elsewhere in this guide are already implemented.
Scenario: An AI agent runs hourly on a developer's workstation to clean up abandoned build processes.
#!/bin/bash
# dev-cleanup-agent.sh
set -euo pipefail
LOG=/var/log/pt-agent/cleanup.log
log() { echo "$(date -Iseconds) $*" >> "$LOG"; }
# Only proceed if memory pressure is high
AVAIL_GB=$(awk '/MemAvailable/ {printf "%.1f", $2/1024/1024}' /proc/meminfo)
if (( $(echo "$AVAIL_GB > 4" | bc -l) )); then
log "INFO: Available memory ${AVAIL_GB}GB - skipping cleanup"
exit 0
fi
log "INFO: Low memory (${AVAIL_GB}GB available) - starting cleanup"
# Generate plan targeting typical dev process patterns
SESSION=$(pt agent plan --format json \
--patterns "node,python,cargo,rustc" \
--min-idle-minutes 30 | jq -r .session_id)
# Check what we found
PLAN=$(pt agent plan --session "$SESSION" --format json)
KILLS=$(echo "$PLAN" | jq '.summary.kill_recommended')
if [[ "$KILLS" -eq 0 ]]; then
log "INFO: No candidates found"
exit 0
fi
log "INFO: Found $KILLS kill candidates"
# Apply with conservative settings
pt agent apply --session "$SESSION" \
--recommended --yes \
--min-posterior 0.95 \
--max-kills 5 \
--max-blast-radius 2GB
# Verify and log results
VERIFY=$(pt agent verify --session "$SESSION" --format json)
FREED=$(echo "$VERIFY" | jq '.resource_summary.memory_freed_mb')
log "INFO: Cleanup complete - freed ${FREED}MB"Scenario: A monitoring agent checks for stuck CI jobs and reports via webhook.
#!/bin/bash
# ci-monitor-agent.sh
WEBHOOK_URL="${CI_WEBHOOK:-https://hooks.example.com/ci-alerts}"
# Scan for CI-related processes only
PLAN=$(pt agent plan --format json \
--patterns "jenkins,gitlab-runner,docker,buildkite" \
--min-idle-minutes 120 \
--include-prose)
SESSION=$(echo "$PLAN" | jq -r .session_id)
CANDIDATES=$(echo "$PLAN" | jq '.summary.candidates_found')
if [[ "$CANDIDATES" -eq 0 ]]; then
exit 0
fi
# Extract prose summary for human-readable alert
SUMMARY=$(echo "$PLAN" | jq -r '.prose_summary.executive // "Stuck CI processes detected"')
# Build alert payload
PAYLOAD=$(cat <<EOF
{
"alert": "stuck_ci_processes",
"session_id": "$SESSION",
"candidate_count": $CANDIDATES,
"summary": "$SUMMARY",
"candidates": $(echo "$PLAN" | jq '[.candidates[] | {pid, cmd_short, idle_minutes: .evidence.idle_minutes, memory_mb: .blast_radius.memory_mb}]')
}
EOF
)
# Send alert (don't auto-kill CI processes)
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
echo "Alert sent for session $SESSION"Scenario: An agent runs on K8s nodes to handle container processes that escape the kubelet.
#!/usr/bin/env python3
"""k8s-node-recovery-agent.py
Identifies and cleans up orphaned container processes on K8s nodes.
Integrates with node-problem-detector for reporting.
"""
import json
import subprocess
import sys
from datetime import datetime
def run_pt(*args):
"""Run pt agent command and return parsed JSON."""
result = subprocess.run(
["pt", "agent", *args, "--format", "json"],
capture_output=True,
text=True
)
if result.returncode >= 10:
raise RuntimeError(f"pt error: {result.stderr}")
return json.loads(result.stdout) if result.stdout else None
def main():
# Generate plan targeting container-related processes
plan = run_pt(
"plan",
"--patterns", "containerd-shim,runc,pause",
"--min-idle-minutes", "60",
"--exclude-patterns", "kubelet,dockerd,containerd"
)
session_id = plan["session_id"]
candidates = plan["candidates"]
if not candidates:
print(f"[{datetime.now().isoformat()}] No orphaned containers found")
return 0
print(f"[{datetime.now().isoformat()}] Found {len(candidates)} orphaned containers")
# Filter to high-confidence abandoned containers only
high_confidence = [
c for c in candidates
if c["posterior"]["abandoned"] > 0.98
and c["blast_radius"]["risk_level"] in ("none", "low")
]
if not high_confidence:
print("No high-confidence candidates - skipping cleanup")
# Report for investigation
report_to_npd(plan, "orphaned_containers_detected")
return 1
# Apply cleanup
result = run_pt(
"apply",
"--session", session_id,
"--recommended", "--yes",
"--min-posterior", "0.98",
"--max-kills", "10"
)
# Verify
verify = run_pt("verify", "--session", session_id)
freed_mb = verify.get("resource_summary", {}).get("memory_freed_mb", 0)
print(f"Cleanup complete: freed {freed_mb}MB across {len(high_confidence)} containers")
# Report success to node-problem-detector
report_to_npd(verify, "orphaned_containers_cleaned")
return 0
def report_to_npd(data, event_type):
"""Report to node-problem-detector via custom plugin."""
event = {
"timestamp": datetime.now().isoformat(),
"type": event_type,
"data": data
}
# Write to NPD custom plugin socket
try:
with open("/var/run/npd-custom/pt-agent.sock", "w") as f:
json.dump(event, f)
except IOError:
pass # NPD not available
if __name__ == "__main__":
sys.exit(main())Scenario: An AI assistant helps a user understand and clean up their system.
#!/usr/bin/env python3
"""interactive-pt-assistant.py
An AI assistant that uses pt to help users understand
process issues on their system.
"""
import json
import subprocess
def get_plan_summary():
"""Get a human-friendly summary of system state."""
result = subprocess.run(
["pt", "agent", "plan", "--format", "json", "--include-prose"],
capture_output=True,
text=True
)
if result.returncode == 0:
return "Your system looks clean - no problematic processes detected."
plan = json.loads(result.stdout)
prose = plan.get("prose_summary", {})
return {
"session_id": plan["session_id"],
"executive_summary": prose.get("executive", ""),
"recommended_actions": prose.get("actions", ""),
"rationale": prose.get("rationale", ""),
"candidate_count": plan["summary"]["candidates_found"],
"recoverable_memory_mb": plan["summary"]["total_recoverable_mb"]
}
def explain_candidate(session_id: str, pid: int):
"""Get detailed explanation for a specific process."""
result = subprocess.run(
["pt", "agent", "explain",
"--session", session_id,
"--pid", str(pid),
"--format", "json",
"--galaxy-brain"],
capture_output=True,
text=True
)
data = json.loads(result.stdout)
# Build human-friendly explanation
candidate = data["candidate"]
explanation = [
f"Process {pid}: {candidate['cmd_short']}",
f"",
f"Classification: {candidate['classification']} "
f"({candidate['posterior'][candidate['classification']]*100:.1f}% confidence)",
f"",
f"Why this classification:",
]
for driver in candidate["uncertainty"]["uncertainty_drivers"]:
explanation.append(f" - {driver['factor']}: {driver['note']}")
if candidate["supervisor"]["detected"]:
explanation.append(f"")
explanation.append(f"This process is managed by {candidate['supervisor']['type']}.")
explanation.append(f"Recommended: {candidate['supervisor']['supervisor_command']}")
return "\n".join(explanation)
def apply_with_confirmation(session_id: str, pids: list[int]):
"""Apply actions with user confirmation."""
# First show what would happen
preview = subprocess.run(
["pt", "agent", "apply",
"--session", session_id,
"--pids", ",".join(map(str, pids)),
"--dry-run",
"--format", "json"],
capture_output=True,
text=True
)
preview_data = json.loads(preview.stdout)
print("The following actions will be taken:")
for action in preview_data["actions"]:
print(f" - {action['action']} PID {action['target']['pid']}: "
f"{action['target'].get('cmd_short', 'unknown')}")
confirm = input("\nProceed? [y/N] ")
if confirm.lower() != 'y':
return "Cancelled"
# Execute
result = subprocess.run(
["pt", "agent", "apply",
"--session", session_id,
"--pids", ",".join(map(str, pids)),
"--yes",
"--format", "json"],
capture_output=True,
text=True
)
data = json.loads(result.stdout)
summary = data["summary"]
return (f"Complete: {summary['successful']} successful, "
f"{summary['failed']} failed, "
f"{summary['memory_freed_mb']}MB freed")
# Example usage in an agent loop:
if __name__ == "__main__":
print("Analyzing your system...")
summary = get_plan_summary()
if isinstance(summary, str):
print(summary)
else:
print(summary["executive_summary"])
print(f"\nFound {summary['candidate_count']} candidates "
f"({summary['recoverable_memory_mb']}MB recoverable)")
print(f"\nSession: {summary['session_id']}")For complete JSON schemas, see AGENT_CLI_CONTRACT.md.
Every candidate in plan output includes these fields (never omitted):
- Identity:
pid,start_id,uid,ppid,cmd_short,cmd_full - Classification:
classification,posterior,confidence - Safety:
blast_radius,reversibility,supervisor - Decision:
uncertainty,recommended_action,action_rationale
| Outcome | Meaning |
|---|---|
confirmed_dead |
Process terminated as expected |
confirmed_stopped |
Process stopped (SIGSTOP) |
still_running |
Process still exists |
respawned |
Process was killed but respawned |
pid_reused |
PID reassigned to different process |
cascaded |
Supervisor restarted the service |
timeout |
Verification timed out |
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2026-01-15 | Initial release |