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_roundsclarification 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:
SYSTEM_PROMPT_PRINCIPAL— post clear contracts, answer clarificationsSYSTEM_PROMPT_WORKER— review contract, ask clarifications, accept/reject, deliver quality workSYSTEM_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)