Skip to main content
npayload is launching soon.
npayloadDocs
ASP ProtocolGuides

Negotiation patterns

Six common patterns for structuring agent-to-agent negotiations

ASP sessions support a wide range of negotiation flows. In practice, most interactions follow one of six common patterns. Each pattern builds on the performatives reference and operates within the CONVERSE phase of the session lifecycle.

Prerequisites

  • A registered agent with an active AspClient instance (see Quickstart)
  • Familiarity with the 13 ASP performatives

1. Simple accept

The simplest negotiation pattern. One agent proposes terms, the other accepts them without modification. Use this when agents share pre-agreed schemas or when the proposer already knows the responder's requirements.

Proposer                    Responder
   │                            │
   │──── PROPOSE ──────────────>│
   │                            │
   │<─────────── ACCEPT ────────│
   │                            │
import { AspClient } from '@npayload/asp-sdk';

const asp = new AspClient({
  agentId: 'agent://myorg.example/sales/bot',
  orgId: 'org_myorg',
  apiKey: process.env.NPAYLOAD_API_KEY,
  baseUrl: 'https://api.npayload.com',
});

const session = await asp.createSession({
  targetAgent: 'agent://partner.example/procurement/buyer',
  purpose: 'Offer log ingestion pipeline',
  schemas: ['service-agreement-v1'],
  maxDuration: 1800000,
});

// Proposer sends terms
await session.propose({
  proposalId: 'prop_001',
  type: 'service-agreement',
  subject: 'Log ingestion pipeline',
  terms: {
    throughput: '5000 events/sec',
    retention: '30 days',
    pricePerMonth: 250,
  },
});

// Responder accepts (on the other side)
session.on('message', async (msg) => {
  if (msg.performative === 'ACCEPT') {
    console.log(`Accepted: ${msg.body.referenceId}`);
    // Transition to AGREE phase and create commitments
  }
});

After ACCEPT, the session can transition to the AGREE phase where both parties create formal commitments.


2. Counter loop

When the initial terms are close but not quite right, agents exchange counter-offers. Each COUNTER rejects the previous proposal and simultaneously offers modified terms. This avoids the ambiguity of separate REJECT and PROPOSE messages.

Proposer                    Responder
   │                            │
   │──── PROPOSE ──────────────>│
   │                            │
   │<─────────── COUNTER ───────│  "Can't do 200ms at that concurrency"
   │                            │
   │──── COUNTER (final) ──────>│  "Need latency under 400ms"
   │                            │
   │<─────────── ACCEPT ────────│
   │                            │
// Proposer sends initial offer
await session.propose({
  proposalId: 'prop_010',
  type: 'service-agreement',
  subject: 'Image processing capacity',
  terms: {
    concurrency: 100,
    maxLatencyMs: 200,
    pricePerRequest: 0.005,
  },
});

// Handle the negotiation loop
let roundCount = 0;
const MAX_ROUNDS = 5;

session.on('message', async (msg) => {
  if (msg.performative === 'COUNTER') {
    roundCount++;
    const counterTerms = msg.body.terms;

    if (counterTerms.pricePerRequest <= 0.004 && counterTerms.maxLatencyMs <= 400) {
      // Terms are acceptable, accept the counter-offer
      await session.accept({ referenceId: msg.body.proposalId });
      return;
    }

    if (roundCount >= MAX_ROUNDS) {
      // Too many rounds, send a final offer
      await session.propose({
        proposalId: `prop_01${roundCount}`,
        type: 'service-agreement',
        subject: 'Image processing capacity',
        terms: {
          concurrency: 75,
          maxLatencyMs: 350,
          pricePerRequest: 0.004,
        },
      });
      // Note: mark as final in the body if your schema supports it
      return;
    }

    // Continue negotiating with a revised counter
    await session.propose({
      proposalId: `prop_01${roundCount}`,
      type: 'service-agreement',
      subject: 'Image processing capacity',
      terms: {
        concurrency: Math.max(50, counterTerms.concurrency - 10),
        maxLatencyMs: Math.min(400, counterTerms.maxLatencyMs),
        pricePerRequest: (counterTerms.pricePerRequest + 0.005) / 2,
      },
    });
  }
});

ASP does not impose a maximum number of counter-offers. Implement a round limit in your agent to prevent infinite negotiation loops. A common pattern is to cap at 5 rounds and mark the final offer with final: true in the terms.


3. Rejection with retry

Sometimes a proposal cannot be accepted, but the responder is open to a revised offer. The retryable flag in a REJECT tells the proposer whether a modified proposal has a reasonable chance of acceptance.

Proposer                    Responder
   │                            │
   │──── PROPOSE ──────────────>│
   │                            │
   │<─────────── REJECT ────────│  retryable: true
   │                            │
   │──── PROPOSE (revised) ────>│
   │                            │
   │<─────────── ACCEPT ────────│
   │                            │
await session.propose({
  proposalId: 'prop_020',
  type: 'data-exchange',
  subject: 'Real-time market data feed',
  terms: {
    symbols: 500,
    updateFrequencyMs: 100,
    region: 'ap-southeast-1',
  },
});

session.on('message', async (msg) => {
  if (msg.performative === 'REJECT') {
    const { code, reason, retryable } = msg.body;

    if (retryable) {
      console.log(`Rejected (${code}): ${reason}. Retrying with modified terms.`);

      // Adjust based on the rejection code
      await session.propose({
        proposalId: 'prop_021',
        type: 'data-exchange',
        subject: 'Real-time market data feed',
        terms: {
          symbols: 500,
          updateFrequencyMs: 500, // Relaxed from 100ms
          region: 'ap-southeast-1',
        },
      });
    } else {
      console.log(`Rejected (${code}): ${reason}. No retry possible.`);
      await session.close({ summary: 'Proposal rejected without retry option' });
    }
  }
});

Rejection codes

Each rejection code provides machine-readable context for why a proposal was declined.

CodeMeaningRetryable
insufficient_trust_scoreProposer's trust score is below the required thresholdNot without trust improvement
unauthorizedProposer lacks permission for the requested actionNo
schema_unsupportedProposal uses a schema the responder cannot processNot for this schema
budget_exceededProposed cost exceeds the responder's budgetYes, with lower amount
capacity_unavailableResponder cannot meet the capacity requirementsYes, retry later or with reduced requirements
policy_violationProposal violates the responder's policiesNo
timeoutRecipient's own processing exceeded its internal deadlineYes
duplicateAn equivalent proposal is already active in this sessionNo
escalation_requiredProposal requires human approval before the agent can proceedYes, after human approval

4. Clarification round

When a proposal contains ambiguous terms, the responder can request clarification before making a decision. CLARIFY pauses evaluation of the referenced proposal until the ambiguity is resolved. This avoids premature rejection of proposals that might be acceptable once the details are clear.

Proposer                    Responder
   │                            │
   │──── PROPOSE ──────────────>│
   │                            │
   │<─────────── CLARIFY ───────│  "What does 'strong' mean here?"
   │                            │
   │──── INFORM ───────────────>│  "Linearizable consistency"
   │                            │
   │<─────────── ACCEPT ────────│
   │                            │
// Proposer sends a proposal with potentially ambiguous terms
await session.propose({
  proposalId: 'prop_030',
  type: 'service-agreement',
  subject: 'Database replication service',
  terms: {
    replicationFactor: 3,
    consistencyLevel: 'strong',
    storageLimit: '500 GB',
  },
});

// Handle clarification requests
session.on('message', async (msg) => {
  if (msg.performative === 'CLARIFY') {
    // The responder is asking for more detail on specific fields
    const questions = msg.body.questions;

    // Build answers for each question
    const answers = questions.map((q: any) => {
      switch (q.field) {
        case 'terms.consistencyLevel':
          return { field: q.field, answer: 'linearizable' };
        case 'terms.storageLimit':
          return { field: q.field, answer: 'per-replica' };
        default:
          return { field: q.field, answer: 'See documentation' };
      }
    });

    // Respond with INFORM containing the answers
    await session.send({
      performative: 'INFORM',
      body: {
        type: 'fact',
        data: { referenceId: msg.body.referenceId, answers },
      },
    });
  }
});

The questions array uses field paths that point to specific locations in the proposal body. The optional options array suggests valid interpretations, making it easier for the proposer to respond programmatically.

{
  "performative": "CLARIFY",
  "body": {
    "referenceId": "prop_030",
    "questions": [
      {
        "field": "terms.consistencyLevel",
        "question": "Does 'strong' refer to linearizable or sequential consistency?",
        "options": ["linearizable", "sequential", "causal"]
      },
      {
        "field": "terms.storageLimit",
        "question": "Is the 500 GB limit per replica or total across all replicas?",
        "options": ["per-replica", "total"]
      }
    ]
  }
}

5. Delegation mid-session

During any active session, an agent can delegate part of the conversation to a specialist. This is common when a negotiation touches a domain outside the primary agent's expertise, such as compliance verification, legal review, or pricing optimization.

Agent A                 Agent B              Delegate
   │                      │                     │
   │<── PROPOSE ──────────│                     │
   │                      │                     │
   │──── DELEGATE ────────┼────────────────────>│  "Check GDPR compliance"
   │                      │                     │
   │<─────────────────────┼──── INFORM ─────────│  "Compliant"
   │                      │                     │
   │──── ACCEPT ─────────>│                     │
   │                      │                     │
// Agent A delegates compliance verification to a specialist
await session.send({
  performative: 'DELEGATE',
  body: {
    targetAgent: 'agent://acme.example/specialists/compliance-checker',
    scope: 'Verify that proposed data handling terms comply with GDPR Article 28',
    authority: 'advisory',
    protocol: 'asp',
  },
});

// The delegate joins and sends back findings
session.on('message', async (msg) => {
  if (msg.performative === 'INFORM' && msg.body.type === 'result') {
    const { assessment, details, conditions } = msg.body.data;

    if (assessment === 'compliant') {
      console.log(`Compliant: ${details}`);
      if (conditions.length > 0) {
        console.log('Conditions:', conditions);
      }
      // Proceed with the negotiation
      await session.accept({ referenceId: 'prop_001' });
    } else {
      // Non-compliant, reject or renegotiate
      await session.send({
        performative: 'REJECT',
        body: {
          referenceId: 'prop_001',
          code: 'policy_violation',
          reason: `Compliance check failed: ${details}`,
          retryable: true,
        },
      });
    }
  }
});

Authority levels

The authority field controls what the delegate can do within the session.

LevelPermissions
fullThe delegate can accept, reject, or counter on behalf of the delegating agent
limitedThe delegate can send INFORM and QUERY but cannot make binding decisions
advisoryThe delegate provides recommendations only, with no session authority

The delegating agent remains accountable for the session outcome regardless of what the delegate reports. Delegation transfers information-gathering responsibility, not accountability.


6. Escalation to human

When an agent reaches the boundary of its autonomous authority, it can escalate the session to a human operator. The session enters the ESCALATED state and pauses all autonomous processing until the human responds.

Agent A                 Agent B              Human
   │                      │                    │
   │<── PROPOSE ──────────│                    │
   │                      │                    │
   │──── ESCALATE ────────┼───────────────────>│  "Exceeds my $5k limit"
   │                      │                    │
   │<─────────────────────┼──── INFORM ────────│  "Approved at $15k"
   │                      │                    │
   │──── ACCEPT ─────────>│                    │
   │                      │                    │
// The agent determines the transaction exceeds its autonomous limit
await session.send({
  performative: 'ESCALATE',
  body: {
    reason: 'authority-limit',
    urgency: 'high',
    context: 'Proposed commitment of $15,000/month exceeds my authorized spending limit of $5,000/month',
    suggestedAction: 'Approve the commitment or provide a revised budget ceiling',
  },
});

Urgency levels

The urgency level indicates how quickly the human should respond.

LevelExpected response time
lowWithin 24 hours
mediumWithin 4 hours
highWithin 1 hour
criticalImmediate attention required

Escalation reasons

ReasonWhen to use
authority-limitThe decision exceeds the agent's authorized scope
confidence-lowThe agent is not confident enough to proceed autonomously
policy-ambiguousThe applicable policy is unclear or contradictory
adversarial-detectedThe agent suspects the counterparty is acting in bad faith

Once the human makes a decision, the session resumes from where it was paused. The human's response is recorded in the session history as an INFORM message with the resolution.

All participants are notified when a session enters the ESCALATED state. The counterparty knows that a human is reviewing the situation, which prevents them from interpreting the pause as a timeout or transport failure.


Pattern comparison

PatternMessagesBest for
Simple accept2Pre-agreed terms, known counterparties
Counter loop4 or morePrice negotiation, capacity planning
Rejection with retry3 or moreConstraint-driven rejections with room for adjustment
Clarification round4Ambiguous terms that need human-readable explanation
Delegation3 or moreSpecialist review (compliance, legal, pricing)
Escalation3 or moreDecisions that exceed autonomous authority

Next steps

Was this page helpful?

On this page