Pod Events¶
An agent served by latent serve can handle events inside its own pod. A timer
the agent armed fires, or a caller posts an event to Studio. Studio's worker then
calls the pod that serves the conversation. The handler decides what happens.
Declare a handler¶
Mark an async agent method with @event:
from latent.agents import BaseAgent, agent
from latent.serve.events import EventContext, EventResult, event
@agent("support")
class SupportAgent(BaseAgent):
studio_capabilities = ("timebox.v1",)
@event("idle_nudge", source="scheduled")
async def idle_nudge(self, ctx: EventContext) -> EventResult:
"""The customer went quiet: nudge them once."""
return EventResult.send("Are you still there?")
source="scheduled"handles a timebox firing.source="external"handles an event a caller posted to Studio.- Names match
^[a-z][a-z0-9_-]{0,63}$. One(source, name)has one handler. - The manifest lists every declared event and adds the
events.v1capability. Addtimebox.v1andscratchpad.v1tostudio_capabilitieswhen the agent uses them.
Decide¶
Return an EventResult:
| Result | Effect |
|---|---|
EventResult.noop() |
Nothing happens. |
EventResult.send(text) |
Studio sends text to the conversation as the assistant. |
EventResult.finish(text) |
Studio sends text and finishes the conversation. |
EventResult.run_agent(prompt) |
Studio runs an agent turn with prompt as its input. |
EventContext carries conversation_id, thread_id, endpoint_id,
channel, event_name, source, and, when present, note, payload and
event_id.
Timeboxes and scratchpad¶
ctx.studio is a StudioEvents for the conversation. It calls Studio's pod
routes with the pod's project key (AGENT_STUDIO_API_KEY against
AGENT_STUDIO_URL); the pod never touches Studio's database.
nonce = await ctx.studio.arm("idle_nudge", seconds=600, note="first nudge")
await ctx.studio.cancel("idle_nudge")
state = await ctx.studio.peek("idle_nudge") # None if never armed
await ctx.studio.scratch_set("nudged", True)
nudged = await ctx.studio.scratch_get("nudged") # None if absent
await ctx.studio.scratch_remove("nudged")
A value stored as a secret comes back as a Secret (latent.serve.events):
it renders <redacted:key> in str, repr and f-strings, so returning it
from a tool never puts the token in the model's context or the trace.
reveal() returns the plaintext.
await ctx.studio.scratch_set("crm_token", token, secret=True)
token = await ctx.studio.scratch_get("crm_token") # Secret
headers = {"Authorization": f"Bearer {token.reveal()}"}
scratch_set without secret= keeps the flag the key already has; pass
secret=False to store a plain value over a secret one. arm takes an
idempotency_key: a retry with the same key returns the first arm's nonce
and starts no second timer.
The routes are POST /api/serving/timeboxes/arm, POST
/api/serving/timeboxes/cancel, GET /api/serving/timeboxes/{conversation}/{name}
and GET|PUT|DELETE /api/serving/scratchpad/{conversation}/{key}. Every
failure raises: a 401 or 403 raises StudioAuthError, other errors raise
httpx.HTTPStatusError. A handler that armed nothing never believes it did.
The key must be unscoped or scoped to this agent. An extension-scoped key is refused.
How Studio calls the pod¶
Studio's worker sends POST /v1/events/{source}/{name}, signed with the
version's HMAC key like every other pod call, with an EventRequest body and an
Idempotency-Key:
- The key is the timer's firing nonce, else the external
event_id. - Studio calls the pod only when the conversation's served version declares the event. A conversation with no served version keeps Studio's in-worker handler.
- Studio records each firing durably and retries a failed call with the same version and key. The pod answers a repeated key with its recorded answer.
The pod answers:
| Status | When |
|---|---|
| 200 | The handler's EventDecision. |
| 401 | Unsigned, wrongly signed or replayed. |
| 404 | No handler for that source and name. |
| 422 | The body is not a valid EventRequest, or it names another event. |
| 500 | The handler raised or did not return an EventResult. |
| 504 | The handler took longer than 20 s. |
latent conformance checks the 401, 404 and 422 answers of every declared
event.