Skip to content

Session Status and Events

A PedelecSession keeps a browser-side status and exposes lifecycle callbacks for status, errors, and end events.

type PedelecSessionStatus =
| "idle"
| "running"
| "waiting_tool_result"
| "ended"
| "error";
Status Meaning Can call sendText()?
idle No active turn; ready for another prompt Yes
running Preparation or a user turn is active No
waiting_tool_result The agent requested a frontend tool and is waiting No
ended The session has ended; the same attached handle may explicitly call resume() if Core and its recorded workspace are still recoverable No until resume() succeeds
error Runtime reported a session error No; recover by app policy, often a new/resumed session

New PedelecSession objects begin in idle locally. A handle returned by resumeSession() is synchronized from Core before the promise resolves, so it can initially report running, waiting_tool_result, error, or ended instead.

Pedelec.resumeSession(sessionId) only reattaches to an existing thread, so an ended thread stays ended. PedelecSession.resume() is the explicit same-handle ended → idle transition. It preserves the handle’s handlers and session state, and provider runtime work remains lazy until the next normal operation.

A turn without tools commonly follows:

idle → running → idle

A turn with a frontend tool commonly follows:

idle
→ running
→ waiting_tool_result
→ running
→ idle

The runtime may end or error from an active state:

running → error
running → ended
waiting_tool_result → error
waiting_tool_result → ended

Do not build UI logic that assumes every theoretical intermediate status must appear. Events can be normalized, de-duplicated, or arrive close together. idle is observable thread state; it is not by itself proof that a particular prepare() or sendText() promise has completed.

const offStatus = session.onStatus((status, ctx) => {
console.log(ctx.previousStatus, "", status);
console.log(ctx.type, ctx.source);
});

The callback runs only when the value changes. Registering does not immediately emit the initial status.

renderStatus(session.getStatus());
const offStatus = session.onStatus(renderStatus);

The SDK can emit a status before Core replies, for responsive UI:

  • sendText() and prepare() set running locally;
  • a tool event sets waiting_tool_result while being handled;
  • end() marks the session ended locally after its request succeeds.
  • session.resume() does not optimistically set idle; the authoritative Core snapshot and operation-less status_changed event set that state.

Context identifies origin:

ctx.source // "sdk" | "core"
ctx.type // "sdk_status_changed" | "status_changed"

A Core event uses source: "core". A state transition initiated inside the SDK uses source: "sdk".

Status handlers are de-duplicated. If the SDK already set running and Core later reports running, no second callback occurs because the value did not change.

const offError = session.onError((error, ctx) => {
console.error(error.code, error.message, error.details);
console.debug(ctx.type, ctx.source, ctx.sessionId);
});
type ErrorEventContext = PedelecEventContext & {
type: "error" | "sdk_error";
};

Use onError() for session-wide observation, logging, and UI. Also catch promises from the action that initiated work.

An error callback does not always mean the session status becomes error. For example, a failed submission of a tool result emits SUBMIT_TOOL_RESULT_FAILED from the SDK, while the runtime may later determine the final session state. Core operation errors are diagnostics; the matching operation completion carries semantic rejection.

const offEnded = session.onEnded((ctx) => {
console.log("ended by", ctx.source);
});
type EndedEventContext = PedelecEventContext & {
type: "ended" | "sdk_ended";
};

onEnded() runs once for each transition into ended, whether caused by a runtime event or a successful local end() call. Duplicate ended notifications while the handle is already ended do not run it again. After a successful same-handle ended -> idle resume(), a later transition back to ended runs the callback once for that new lifecycle transition.

Pending sendText() or prepare() work rejects with SESSION_ENDED when the session ends.

type PedelecEventContext = {
sessionId: string;
provider: string;
effortLevel?: "default" | "low" | "high";
sessionCreatedAt: number;
eventReceivedAt?: number;
eventEmittedAt: number;
turnId?: string;
turnStartedAt?: number;
turnKind?: "user" | "prepare";
source: "core" | "sdk";
};

Not every field exists for every event:

  • Core chat/tool callbacks include received time and active turn metadata.
  • Local status changes can occur before a Core event and may not have eventReceivedAt.
  • End/error events outside an active turn may not have turn fields.
  • A newly resumed session handle may have blank provider metadata in the current SDK if the application did not retain it separately.

Tool handlers receive extra fields:

ctx.type; // "tool_call"
ctx.toolRequestId; // Core tool request identifier
ctx.tool; // tool name
ctx.turnId;

Use toolRequestId for diagnostics and application bookkeeping, not for manually submitting results. The SDK submits the handler’s return value automatically.

Core events can include a per-session sequence number. The client ignores an event whose sequence is not newer than the last one seen for that session, and operation-scoped events are accepted only for the active matching operation ID. Application code should still be resilient to rapid transitions and late asynchronous work inside its own handlers.

When a subscription is recreated, the SDK receives an authoritative lifecycle snapshot before live events resume. A resumed or recovered handle uses that snapshot to adopt the Core status and active operation. If Core reports a pending frontend tool, register its handler after resumeSession(); the SDK keeps the recovered request queued and submits it once a matching handler is available. Same-handle session.resume() also reconciles its post-reactivation snapshot before the promise resolves, so getStatus() is idle at the await boundary.

A simple mapping:

session.onStatus((status) => {
setSendEnabled(status === "idle");
setBusy(status === "running" || status === "waiting_tool_result");
setToolWaiting(status === "waiting_tool_result");
setSessionClosed(status === "ended");
});

Do not map waiting_tool_result to a generic frozen state when an interactive tool is intentionally waiting for user input. Show the relevant modal or instruction.

Continue with Session lifecycle and resume.