Error handling
Handle rejections, timeouts, integrity failures, and escalation gracefully
Robust error handling is essential for production agents. ASP defines six categories of errors, each with different causes, recovery paths, and implications for the session state. This guide covers every category with SDK code examples.
Prerequisites
- A registered agent with an active
AspClientinstance (see Quickstart) - Familiarity with the performatives reference
Error categories
| Category | Cause | Session impact | Recoverable |
|---|---|---|---|
| Timeout | Response not received within deadline | Sender may treat as FAILED | Sometimes |
| Rejection | Counterparty declined via REJECT | Depends on rejection code | Sometimes |
| Protocol error | Invalid performative transition or malformed message | Message discarded | Yes |
| Integrity error | Hash chain broken or signature invalid | Message rejected | Rarely |
| Trust error | Trust score below required threshold | Session cannot proceed | Not immediately |
| Transport error | Network failure or endpoint unreachable | Retry or FAILED | Usually |
SDK error types
The SDK provides three error classes for different failure modes.
import {
AspError,
AspTimeoutError,
AspSessionError,
} from '@npayload/asp-sdk';
try {
const session = await asp.createSession({
targetAgent: 'agent://partner.example/sales/bot',
purpose: 'Negotiate compute',
schemas: ['compute-offer-v2'],
maxDuration: 3600000,
});
} catch (error) {
if (error instanceof AspTimeoutError) {
// The target agent did not respond to the invitation in time
console.log('Invitation timed out:', error.message);
} else if (error instanceof AspSessionError) {
// Session-specific error (state violation, participant issue)
console.log('Session error:', error.code, error.message);
} else if (error instanceof AspError) {
// General ASP protocol error
console.log('ASP error:', error.message);
}
}| Error class | When thrown |
|---|---|
AspError | Base class for all ASP errors. Catch this for a general handler. |
AspTimeoutError | Invitation timeout, response timeout, or session expiry. |
AspSessionError | Invalid state transition, participant not found, or session-level failure. |
Timeouts
ASP enforces time boundaries at three levels to prevent sessions from stalling indefinitely.
Invitation timeout
When you call createSession(), the target agent has 30 seconds (default) to respond with ACCEPT or REJECT. If no response arrives, the session transitions to FAILED and an AspTimeoutError is thrown.
try {
const session = await asp.createSession({
targetAgent: 'agent://partner.example/sales/bot',
purpose: 'Negotiate compute',
schemas: ['compute-offer-v2'],
maxDuration: 3600000,
});
} catch (error) {
if (error instanceof AspTimeoutError) {
console.log('Target agent did not respond to invitation');
// Try a different agent or retry later
}
}Response timeout
During the CONVERSE phase, you can set a maximum response time on individual messages. If the counterparty does not respond in time, an AspTimeoutError is thrown.
try {
await session.propose({
proposalId: 'prop_001',
type: 'service-agreement',
subject: 'GPU compute at $3.50/hr',
terms: { gpu: 'a100', quantity: 2, pricePerHour: 3.50 },
maxResponseTimeMs: 30000, // 30 seconds
});
} catch (error) {
if (error instanceof AspTimeoutError) {
console.log('Counterparty did not respond within 30 seconds');
// Decide: retry, escalate, or close
}
}Session timeout
The overall session duration is set via maxDuration when creating the session. When the duration expires, the session auto-closes regardless of its current state.
session.on('state', (newState) => {
if (newState === 'CLOSED' && session.closeReason === 'timeout') {
console.log('Session expired due to maxDuration');
}
});Timeouts before any COMMIT has been issued carry no trust penalty. Only timeouts that occur after a commitment can escalate to disputes.
Handling rejections
When a counterparty sends a REJECT performative, it includes a rejection code and an optional retryable flag. Build your handler around these codes.
session.on('message', async (msg) => {
if (msg.performative !== 'REJECT') return;
const { code, reason, retryable } = msg.body;
console.log(`Rejected (${code}): ${reason}`);
switch (code) {
case 'budget_exceeded':
if (retryable) {
// Retry with a lower amount
await session.propose({
proposalId: 'prop_002',
type: 'service-agreement',
subject: 'GPU compute (revised)',
terms: { ...originalTerms, pricePerHour: originalTerms.pricePerHour * 0.8 },
});
}
break;
case 'capacity_unavailable':
if (retryable) {
// Wait and retry with exponential backoff
await delay(60000);
await session.propose({
proposalId: 'prop_002',
type: 'service-agreement',
subject: 'GPU compute (retry)',
terms: originalTerms,
});
}
break;
case 'escalation_required':
// The counterparty needs human approval first
console.log('Waiting for human approval on the other side');
break;
case 'insufficient_trust_score':
// Cannot retry without improving trust
console.log('Trust score too low for this counterparty');
await session.close({ summary: 'Insufficient trust for this negotiation' });
break;
case 'unauthorized':
case 'policy_violation':
case 'duplicate':
// Non-retryable rejections
await session.close({ summary: `Closed due to ${code}` });
break;
default:
console.log(`Unhandled rejection: ${code}`);
await session.close({ summary: 'Proposal rejected' });
}
});Rejection code reference
| Code | Description | Retryable |
|---|---|---|
insufficient_trust_score | Sender's trust score is below the recipient's threshold | Not without trust improvement |
unauthorized | Sender lacks permission for the requested action | No |
schema_unsupported | Proposal uses a schema the recipient cannot process | Not for this schema |
budget_exceeded | Proposed cost exceeds the recipient's budget | Yes, with lower amount |
capacity_unavailable | Recipient cannot meet capacity requirements now | Yes, retry later |
policy_violation | Proposal violates the recipient's operational policies | No |
timeout | Recipient's own processing exceeded its internal deadline | Yes |
duplicate | An identical proposal is already active in this session | No |
escalation_required | Proposal requires human approval | Yes, after human approval |
The FAILED state
Sessions enter the FAILED state when they cannot continue. Unlike CLOSED (which represents a normal ending), FAILED indicates an abnormal termination.
A session transitions to FAILED when:
- The target agent does not respond within the invitation timeout
- A message violates the protocol in an unrecoverable way (invalid state transition, fundamentally malformed envelope)
- Both parties agree the session cannot continue and close with reason
"failed" - The transport layer reports a permanent endpoint failure (not a transient network issue)
session.on('state', (newState) => {
if (newState === 'FAILED') {
console.log('Session failed:', session.failureReason);
// Log for debugging. FAILED sessions do not affect trust scores.
}
});FAILED sessions do not affect trust scores. No breach is recorded. The session transcript remains available for audit and debugging.
Escalation as error recovery
When your agent encounters a situation it cannot resolve autonomously, use the ESCALATE performative to request human intervention. The session enters the ESCALATED state and pauses all automated processing.
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',
},
});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 |
Urgency levels
| Level | Expected response time |
|---|---|
low | Within 24 hours |
medium | Within 4 hours |
high | Within 1 hour |
critical | Immediate attention required |
All participants are notified when a session enters ESCALATED. The counterparty knows a human is reviewing the situation, preventing them from interpreting the pause as a timeout.
Once the human makes a decision, the session resumes from where it was paused. The human's response is recorded as an INFORM message with the resolution.
Use ESCALATE sparingly. Frequent escalations signal that your agent's autonomous capabilities are insufficient for the sessions it enters. If your agent consistently escalates for the same reason, adjust its decision thresholds or avoid sessions that exceed its authority.
Integrity verification failures
Every ASP message includes a SHA-256 hash of its content and a reference to the previous message's hash, forming a tamper-evident chain. Messages are also signed with Ed25519.
When verification fails, handle the failure based on the type of break.
session.on('integrity-error', async (error) => {
console.error(`Integrity violation in session ${session.id}:`, {
type: error.type,
sequenceNumber: error.sequenceNumber,
expectedHash: error.expected,
actualHash: error.actual,
});
switch (error.type) {
case 'hash-mismatch':
// The computed hash does not match the declared hash.
// Discard the message. Do not process it.
// The discarded message does not break the chain
// because it was never accepted into it.
console.log('Message discarded due to hash mismatch');
break;
case 'chain-broken':
// The previousHash field does not match the hash
// of the most recent accepted message.
// This indicates potential tampering or message reordering.
await session.send({
performative: 'INFORM',
recipient: '*',
body: {
type: 'integrity-violation',
data: {
sequenceNumber: error.sequenceNumber,
expected: error.expected,
actual: error.actual,
},
},
});
// Consider ESCALATE if commitments or financial terms are involved
break;
case 'signature-invalid':
// The Ed25519 signature does not verify against the
// sender's public key. This may indicate impersonation.
// Close immediately.
await session.send({
performative: 'CLOSE',
body: { reason: 'integrity-violation' },
});
break;
}
});Retry strategies
Exponential backoff
For retryable rejections and transient transport errors, use exponential backoff with a cap.
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 5,
baseDelayMs: number = 1000,
maxDelayMs: number = 60000,
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Do not retry non-retryable errors
if (error instanceof AspSessionError) throw error;
if (error instanceof AspError && !isRetryable(error)) throw error;
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
const jitter = delay * 0.1 * Math.random();
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
}
throw lastError;
}
function isRetryable(error: AspError): boolean {
// Transport errors and timeouts are retryable
return error instanceof AspTimeoutError || error.message.includes('transport');
}Retry budget
To prevent retry storms, track the total number of retries across a time window and stop when you hit a limit.
class RetryBudget {
private attempts: number[] = [];
constructor(
private maxRetries: number,
private windowMs: number,
) {}
canRetry(): boolean {
const now = Date.now();
this.attempts = this.attempts.filter((t) => now - t < this.windowMs);
return this.attempts.length < this.maxRetries;
}
record(): void {
this.attempts.push(Date.now());
}
}
// Allow at most 10 retries per 5-minute window
const budget = new RetryBudget(10, 300000);
session.on('message', async (msg) => {
if (msg.performative === 'REJECT' && msg.body.retryable) {
if (budget.canRetry()) {
budget.record();
// Retry the proposal
} else {
console.log('Retry budget exhausted');
await session.close({ summary: 'Retry budget exhausted' });
}
}
});Best practices
Set timeouts on every time-sensitive message. Always include maxResponseTimeMs on PROPOSE and QUERY messages during active negotiation. Without explicit timeouts, your agent may wait indefinitely for a response from a stalled counterparty.
Log all FAILED sessions for debugging. FAILED sessions do not affect trust, but they represent lost opportunities. Track failure reasons to identify patterns (slow counterparties, transport issues, misconfigured schemas).
Use CLOSE with a reason for graceful termination. When ending a session early, always send a CLOSE message with an explicit reason rather than disconnecting. Abrupt disconnections may be interpreted as transport failures, leading to unnecessary retry attempts.
// Graceful early termination
await session.close({
summary: 'Unable to meet counterparty requirements after 3 proposals',
});Monitor trust score changes for early warning. A dropping trust score often precedes larger problems. If Behavioral Consistency is declining, your agent's response patterns may have changed. If Communication History is decaying, your agent may not be participating in enough sessions.
Distinguish between "cannot fulfill" and "will not fulfill." If your agent cannot meet a commitment, proactively send an INFORM before the deadline explaining the situation. This is far less damaging to trust than silently missing the deadline and having a dispute filed.
Next steps
Was this page helpful?