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
AspClientinstance (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.
| Code | Meaning | Retryable |
|---|---|---|
insufficient_trust_score | Proposer's trust score is below the required threshold | Not without trust improvement |
unauthorized | Proposer lacks permission for the requested action | No |
schema_unsupported | Proposal uses a schema the responder cannot process | Not for this schema |
budget_exceeded | Proposed cost exceeds the responder's budget | Yes, with lower amount |
capacity_unavailable | Responder cannot meet the capacity requirements | Yes, retry later or with reduced requirements |
policy_violation | Proposal violates the responder's policies | No |
timeout | Recipient's own processing exceeded its internal deadline | Yes |
duplicate | An equivalent proposal is already active in this session | No |
escalation_required | Proposal requires human approval before the agent can proceed | Yes, 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.
| Level | Permissions |
|---|---|
full | The delegate can accept, reject, or counter on behalf of the delegating agent |
limited | The delegate can send INFORM and QUERY but cannot make binding decisions |
advisory | The 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.
| Level | Expected response time |
|---|---|
low | Within 24 hours |
medium | Within 4 hours |
high | Within 1 hour |
critical | Immediate attention required |
Escalation reasons
| Reason | When to use |
|---|---|
authority-limit | The decision exceeds the agent's authorized scope |
confidence-low | The agent is not confident enough to proceed autonomously |
policy-ambiguous | The applicable policy is unclear or contradictory |
adversarial-detected | The 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
| Pattern | Messages | Best for |
|---|---|---|
| Simple accept | 2 | Pre-agreed terms, known counterparties |
| Counter loop | 4 or more | Price negotiation, capacity planning |
| Rejection with retry | 3 or more | Constraint-driven rejections with room for adjustment |
| Clarification round | 4 | Ambiguous terms that need human-readable explanation |
| Delegation | 3 or more | Specialist review (compliance, legal, pricing) |
| Escalation | 3 or more | Decisions that exceed autonomous authority |
Next steps
Was this page helpful?