Skip to content

Principal-Agent

Game ID: principal-agent

A principal delegates a task to a worker via an outcome-based contract. An independent oracle scores the worker's deliverable against the contract's success criteria. Payment is determined by the score.

Why This Game Matters for Multi-Agent Systems

The principal-agent problem is fundamental to any system where one agent delegates work to another under information asymmetry. The worker's effort is hidden: the principal cannot observe reasoning depth, tool usage, or retry count.

Orchestrator → Specialist Delegation — An orchestrator agent delegates a subtask (e.g., "fix these failing tests") to a specialist worker agent. The oracle independently verifies the result, removing the conflict of interest where the delegator evaluates their own worker.

Automated Bounty Systems — A principal posts a bounty with clear success criteria. Workers compete to deliver. An oracle scores deliverables, and payment is released based on outcome level.

SLA-Enforced Agent Pipelines — Each step in an agent pipeline is a principal-agent contract. The downstream consumer acts as oracle, scoring the upstream producer's output.

Nested Delegation Chains — Each level in a delegation hierarchy is its own principal-agent game, enabling recursive task decomposition with verifiable outcomes.

Abstract Scenario

A PRINCIPAL posts a task contract with success criteria and a payment schedule. A WORKER reviews the contract, asks clarifications, and decides whether to accept. If accepted, the worker produces a deliverable. An ORACLE independently scores the deliverable against the success criteria (0-100). The engine maps the score to an outcome level and computes payments.

Rules

  • 3 agents required: principal (index 0), worker (index 1), oracle (index 2).
  • 5 phases: offer → clarify → respond → execute → verify.
  • The principal posts a contract with task description, success criteria, and outcome levels.
  • The worker may ask up to max_clarify_rounds clarification questions (default: 2).
  • The worker can accept or reject the contract from either the clarify or respond phase. Rejection ends the game with payoffs 0.
  • If accepted, the worker submits a deliverable.
  • The oracle scores the deliverable 0-100. The engine maps the score to the highest outcome level whose threshold is satisfied.
  • Payoffs: worker = +payment, principal = -payment, oracle = 0.

Actions

Action Payload Phase Role Description
post_contract {task_description, success_criteria, outcome_levels?} offer principal Post the task contract. outcome_levels is optional (defaults used).
ask_clarification {question} clarify worker Ask a clarifying question.
answer_clarification {answer} clarify principal Answer the last unanswered question.
skip_clarify {} clarify principal/worker Skip remaining clarification rounds and advance to respond.
accept_contract {} clarify, respond worker Accept the contract and proceed to execute.
reject_contract {reason} clarify, respond worker Reject the contract. Game ends, all payoffs 0.
submit_deliverable {content} execute worker Submit the completed work.
record_outcome_score {score, notes} verify oracle Score 0-100 with evaluation notes.
message_only {} any any Send messages without advancing the turn.

Payload field aliases

The engine accepts common LLM field-name variants: text for question/answer/content, deliverable for content, rating for score.

Visible game state

All agents see the same state (the oracle needs both contract and deliverable to score):

{
    "contract": {
        "task_description": "Write unit tests",
        "success_criteria": "All tests pass with >80% coverage",
        "outcome_levels": [
            {"label": "fail", "threshold": 0, "payment": 0},
            {"label": "partial", "threshold": 50, "payment": 3},
            {"label": "success", "threshold": 80, "payment": 10},
        ],
    },
    "clarifications": [
        {"question": "Which language?", "answer": "Python"},
    ],
    "accepted": True,
    "deliverable": "Tests written with 95% coverage",
    "outcome_score": 90,
    "outcome_label": "success",
    "payment": 10,
    "action_history": [...],
}

Configuration

from neg_env import PrincipalAgentGame

# Defaults
game = PrincipalAgentGame()  # default outcome levels, max_clarify_rounds=2

# Custom outcome levels
game = PrincipalAgentGame(
    outcome_levels=[
        {"label": "bad", "threshold": 0, "payment": 0},
        {"label": "ok", "threshold": 40, "payment": 5},
        {"label": "great", "threshold": 90, "payment": 20},
    ],
    max_clarify_rounds=3,
)

Outcome format

# Successful resolution
{
    "payoffs": [
        {"agent_id": "principal", "utility": -10},
        {"agent_id": "worker", "utility": 10},
        {"agent_id": "oracle", "utility": 0.0},
    ],
    "reason": "task_resolved_success",  # or task_resolved_partial, task_resolved_fail
    "outcome_score": 90,
    "outcome_label": "success",
    "payment": 10,
}

# Contract rejected
{
    "payoffs": [
        {"agent_id": "principal", "utility": 0.0},
        {"agent_id": "worker", "utility": 0.0},
        {"agent_id": "oracle", "utility": 0.0},
    ],
    "reason": "contract_rejected",
}

Prompts

Three built-in role-specific prompts:

from neg_env.prompts import SYSTEM_PROMPT_PRINCIPAL, SYSTEM_PROMPT_WORKER, SYSTEM_PROMPT_ORACLE
  • SYSTEM_PROMPT_PRINCIPAL — post clear contracts, answer clarifications
  • SYSTEM_PROMPT_WORKER — review contract, ask clarifications, accept/reject, deliver quality work
  • SYSTEM_PROMPT_ORACLE — evaluate objectively, score 0-100

Example

from pathlib import Path
from dotenv import load_dotenv
from neg_env import ExperimentConfig, ExperimentRunner, LangChainNegotiationAgent
from neg_env.prompts import SYSTEM_PROMPT_PRINCIPAL, SYSTEM_PROMPT_WORKER, SYSTEM_PROMPT_ORACLE
from neg_env.games.principal_agent import PrincipalAgentGame

load_dotenv()

agents = [
    LangChainNegotiationAgent(
        agent_id="principal", provider="openai",
        model="gpt-4o-mini", system_prompt=SYSTEM_PROMPT_PRINCIPAL,
    ),
    LangChainNegotiationAgent(
        agent_id="worker", provider="openai",
        model="gpt-4o-mini", system_prompt=SYSTEM_PROMPT_WORKER,
    ),
    LangChainNegotiationAgent(
        agent_id="oracle", provider="openai",
        model="gpt-4o-mini", system_prompt=SYSTEM_PROMPT_ORACLE,
    ),
]

game = PrincipalAgentGame(max_clarify_rounds=2)
config = ExperimentConfig(
    game_id="principal-agent", num_matches=5,
    log_directory=Path("./logs/principal_agent"), open_dashboard=True,
)
result = ExperimentRunner(config).run(agents, game=game)