Agent Identity
← GUIDES · EVENTS

How does an AI agent wait for a reply without polling?

By Naol Ketema, founder · · Markdown

SHORT ANSWER

An AI agent waits for a reply without polling by blocking on a durable event log: it makes one long-poll request that the server holds open until a matching event arrives or a timeout passes, then re-arms. Because the log is durable and ordered, the agent passes a since cursor so nothing that happened while it was busy or offline is missed. In Agent Identity this is client.events.wait() (or GET /v1/events/wait), and client.events.subscribe() runs it as a loop for you.

What does waiting for a reply mean for an agent?

Most useful agent work has a gap in the middle. The agent sends an email and needs the customer's answer. It hands a task to another agent and needs the result. It cannot finish until something outside its control happens.

There are three ways to bridge that gap: poll on a timer, get called by a webhook, or block on an event stream until the thing you care about shows up. For an agent running as a loop, the third is usually the right one.

Why polling is the wrong default

Polling means calling a list endpoint every N seconds and diffing the result. It works, but it has three costs that compound as you add agents:

  • Wasted requests. Almost every poll returns nothing new, and each one counts against rate limits.
  • Added latency. On average you see a reply half a poll interval after it arrived. Shorten the interval and you pay more requests.
  • Bookkeeping. You have to remember what you already handled, compare lists, and cope with items that changed between two polls.

None of that is the agent's actual job. An LLM loop that spends its time diffing inbox listings is also harder to read and test.

What a durable event log and a long-poll wait give you

A durable event log is an append-only, ordered record of everything that happened: mail received, mail bounced, a task created, a task answered. A long-poll wait is a single HTTP request that the server holds open until an event matching your filter is appended, or until a timeout passes.

  • One request per event instead of one request per poll interval.
  • The reply is handed to the agent as soon as it is appended, not on the next tick.
  • A timeout is a normal result, not an error. The agent simply waits again.
  • Because the log is durable, an agent that was offline or busy can pass a cursor and pick up exactly where it left off.

In Agent Identity, GET /v1/events/wait returns at most one event per request and always answers 200: either { "event": {...}, "timed_out": false } or { "event": null, "timed_out": true }. The wait is capped at 55 seconds (timeout_ms from 0 to 55000, default 25000) so it lands inside common proxy idle timeouts. Longer waits are built by re-arming, not by raising the value.

The race you have to avoid

A wait matches only events created after it starts. If you send an email, then start waiting, and the reply arrives in between, a wait without a cursor will never see it. Fast autoresponders and agents on the same platform make this more likely than it sounds.

  • Cursor pattern: capture const since = new Date() before the call that triggers the reply, and pass it as since. For the next wait, advance since to the returned event's createdAt.
  • Concurrent pattern: start the wait promise first, then make the triggering call, then await the wait.
  • Never write a bare await send(); await wait(); with no since. That is the race.

How to do it with Agent Identity

Send from the agent's mailbox, then wait for mail.received on the same thread. The SDK is @agentidentity/sdk; the API key here is the agent's own key.

import { AgentClient } from "@agentidentity/sdk";

const client = new AgentClient({
  baseUrl: "https://api.agent-identity.dev",
  apiKey: process.env.AID_KEY!,
});
const identityId = process.env.AID_AGENT!;

const since = new Date();
const sent = await client.mail.send({
  identityId,
  to: "customer@example.com",
  subject: "Order 1142",
  text: "Can you confirm the delivery address?",
  idempotencyKey: "order-1142-address-check",
});

const { event, timedOut } = await client.events.wait({
  type: "mail.received",
  identityId,
  filter: { threadId: sent.threadId },
  since,
  timeoutMs: 55_000,
});

if (!timedOut && event) {
  console.log(event.payload.from, event.payload.subject);
}

For an agent that runs forever, use client.events.subscribe(). It advances its cursor from each event's own createdAt rather than your clock, re-polls when the server window elapses, and retries failures with exponential backoff.

for await (const event of client.events.subscribe({ type: "mail.received", identityId })) {
  const thread = await client.mail.getThread(identityId, event.payload.threadId);
  const reply = await compose(thread); // your LLM call

  await client.mail.send({
    identityId,
    to: [event.payload.from!],
    subject: "Re: " + (thread.subject ?? ""),
    text: reply,
    inReplyToMessageId: event.payload.messageId,
    idempotencyKey: "reply-" + event.payload.messageId,
  });
}

The same wait works for agent-to-agent work: after client.a2a.sendTask(), wait on a2a.sent_task.updated with aggregateId set to the task id. From a terminal, aid events tail --type mail.received --agent <id> re-arms the wait after each event so you can watch the log live. We run our own sales agent this way: it subscribes to the log and only wakes when something arrives.

When you might not need this

If your agent runs as a serverless function with no long-lived process, a webhook fits better: the platform calls you. If you only check for replies once a day in a batch job, a plain list call is fine. Long-poll waits shine when the agent is a process that would otherwise sit in a sleep loop.

FAQ

What happens if no reply arrives within 55 seconds?

The wait returns timedOut: true with no event, as a normal 200 response. Call wait again with the same since; nothing is lost because the log is durable.

Can one wait watch several event types?

No. Filters are exact matches and there is no wildcard or multi-type wait. Run one wait per type, or register a webhook for several types.

What if my agent crashes between receiving an event and replying?

Restart from the last createdAt you processed, and derive the reply's idempotencyKey from the inbound message id. A retried send with the same key returns the original result instead of sending twice.

Is this the same as wait_for_event?

Yes. wait_for_event is the concept; GET /v1/events/wait is the endpoint and client.events.wait() is the SDK method.

Related guides

Give your agent its own identity in a couple of minutes. Get started free → · Docs · All guides