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.

session.onArtifact((artifact, ctx) => ...) receives metadata for a provider-generated file after Core has materialized it in the Workspace asset area. It is a Core-originated event scoped to the active user operation; its context has type: "provider_artifact", source: "core", and turnKind: "user". Registering a handler does not replay artifacts from completed turns.

session.onArtifact((artifact, ctx) => {
console.log(ctx.turnId, artifact.path, artifact.kind);
});

Artifacts can arrive while the session status is still running. The callback means the file is ready to read; it does not mean the turn is complete. sendText() resolves only after the matching operation_completed event, and by then all artifacts from that turn are already materialized. Use listAssets() to discover historical files instead of expecting onArtifact() to replay them.

Missed live artifact events during reconnect use the normal event sequence and recovery path. Sequence deduplication prevents an already delivered artifact event from being delivered again.

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.