PedelecSession API
PedelecSession<TToolName> represents one Core agent session.
import type { PedelecSession } from "@kaoruisaac/pedelec";Applications obtain sessions from Pedelec.createSession() or Pedelec.resumeSession() rather than constructing the class directly. An ended handle can then be explicitly reactivated with session.resume() when eligible.
Type parameter
Section titled “Type parameter”class PedelecSession<TToolName extends string = string>TToolName narrows the named onTool(name, handler) overload. createSession() can infer it from a readonly tools array.
Properties
Section titled “Properties”sessionId
Section titled “sessionId”readonly sessionId: stringCore session/thread identifier. Persist it when using autoEndOnDisconnect: false and planning to resume.
provider
Section titled “provider”readonly provider: stringProvider known when this SDK handle was registered. A fresh resumed handle may currently expose an empty string.
effortLevel
Section titled “effortLevel”readonly effortLevel?: "default" | "low" | "high"Normalized effort level selected for this session. A fresh resumed handle may leave it undefined because the bridge currently returns only the session ID.
sessionCreatedAt
Section titled “sessionCreatedAt”readonly sessionCreatedAt: numberBrowser Date.now() timestamp when this session object was constructed. It is not guaranteed to be the original Core thread creation time after resume.
readonly usage: PedelecSessionUsageNormalized cumulative token usage observed for this session. usage.totalTokens starts as undefined and is populated only when the provider reports a supported, valid total. It is monotonic for the lifetime of this SDK handle, and usage updates do not invoke chat, status, or error handlers.
The value is also included in a Core snapshot when known, so resumeSession() can hydrate it and same-handle resume() preserves it. Providers or payloads without a supported total leave it undefined; this currently includes Cursor ACP. The value is not context-window occupancy, monetary cost, or a guaranteed invoice-equivalent total, and no input/output/cache/reasoning breakdown is part of this API.
prepare()
Section titled “prepare()”prepare(): Promise<void>Optionally prepares provider/session startup before the first user turn.
Behavior:
- idempotent after success;
- concurrent prepare calls share one promise;
- rejects with
SESSION_BUSYduring an active user turn; - rejects with
SESSION_ENDEDafter end; - hides preparation assistant messages from
onChat()and deltas fromonChatDelta(); - can emit status/error events;
- resolves only after Desktop verifies the provider’s exact
PEDELEC_PREPAREDacknowledgment (surrounding whitespace is allowed); - is not required before
sendText().
await session.prepare().catch(() => { // Keep sendText available; preparation is an optimization.});sendText()
Section titled “sendText()”sendText(text: string): Promise<void>Starts one user turn.
The promise resolves after Core reports the matching semantic operation completion and rejects when that operation fails, the session ends, or another turn is already active. An idle event alone is not a completion signal.
Completed assistant messages are delivered through onChat(). Best-effort incremental fragments, when the provider exposes them, are delivered separately through onChatDelta().
await session.sendText("Analyze the current page.");The SDK does not trim or require non-empty text; applications should validate composer input.
Common errors: SESSION_BUSY, SESSION_ENDED, SEND_TEXT_FAILED, SESSION_ERROR, and transport/provider errors.
uploadAsset()
Section titled “uploadAsset()”uploadAsset(file: File): Promise<AssetPath>uploadAsset(file: File, targetPath: AssetPath): Promise<AssetPath>Uploads one browser File until session end. It can run alongside prepare, send, and provider execution, but only one upload may run per session. A file is limited to 100 MiB and is stored physically under .pedelec-runtime/assets/; the SDK returns a /... path rooted implicitly in assets/. Pass targetPath to write exactly to a nested path, creating missing parent directories and replacing an existing regular file.
The content is transferred through Desktop’s internal 127.0.0.1 loopback asset transfer server, rather than extension, Native Messaging, or Core IPC message bodies. See Error Codes for request, upload PUT, and ticket failures.
listAssets()
Section titled “listAssets()”listAssets(): Promise<Asset[]>Returns completed regular files from every level of the physical .pedelec-runtime/assets/ directory as a flat array. Files created by either the App or Agent can appear. Results are ordered by filesystem modification time newest first, then by name when timestamps are equal. Directory entries and file or directory symlinks are not returned or followed. At every level, entries whose basename starts with .pedelec- are excluded; other dotfiles and files inside dot-directories are included.
name is the basename, while path is the complete public path relative to assets/, such as /results/report.json. modifiedAt is the filesystem modification time rather than a guaranteed write-completion timestamp. Listing is allowed while the agent runs; an ending or ended session rejects with SESSION_ENDED. Pagination, deletion, rename, and move are not supported.
readAsset()
Section titled “readAsset()”readAsset(path: AssetPath, type: "text"): Promise<string>readAsset<T = JsonValue>(path: AssetPath, type: "json"): Promise<T>readAsset(path: AssetPath, type: "file"): Promise<File>Reads a known file from the physical shared session .pedelec-runtime/assets/ directory, using a public path such as /results/report.json. The path must begin with /, use forward slashes, and contain no empty, . or .. segments.
The read limit is 100 MiB. Reads are allowed while prepare, send, or provider execution is active, but an ending or ended session rejects with SESSION_ENDED. If the Agent may still be writing the same path, coordinate completion in your application or agent workflow before reading it.
Return behavior depends on type:
"text"strictly decodes UTF-8 and rejects invalid bytes withASSET_TEXT_DECODE_FAILED;"json"strictly decodes UTF-8 and parses JSON, rejecting invalid JSON withASSET_INVALID_JSON; the genericTis compile-time typing only and does not validate the parsed value;"file"returns a browserFileusing the asset name, MIME type, and modification time reported by Desktop.
const text = await session.readAsset("/report.txt", "text");const result = await session.readAsset<{ ok: boolean }>( "/results/result.json", "json",);const model = await session.readAsset("/model.glb", "file");The bytes are transferred through an internal loopback download ticket. Applications must not depend on the URL, token, port, or ticket lifetime. See Error Codes for path, file, size, download, decode, and parse failures.
onChat()
Section titled “onChat()”onChat( handler: (text: string, ctx: ChatEventContext) => void,): () => voidRegisters a completed assistant-message handler and returns an unsubscribe function. Each text value is one complete logical provider message.
const off = session.onChat((text, ctx) => { transcript.addCompleted(ctx.turnId, text);});Preparation output is not delivered to chat handlers.
onChatDelta()
Section titled “onChatDelta()”onChatDelta( handler: (text: string, ctx: ChatDeltaEventContext) => void,): () => voidRegisters a best-effort incremental assistant-text handler. Delta delivery depends on the provider, chunk boundaries have no semantic meaning, and completed messages do not imply synthetic missing deltas.
const off = session.onChatDelta((delta, ctx) => { transcript.appendLive(ctx.turnId, delta);});Preparation output is not delivered to delta handlers.
onTool()
Section titled “onTool()”Generic fallback overload
Section titled “Generic fallback overload”onTool( handler: ( tool: TToolName, args: unknown, ctx: ToolCallContext, ) => unknown | Promise<unknown>,): () => voidNamed overload
Section titled “Named overload”onTool<TArgs = unknown, TResult = unknown>( toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>,): () => voidNamed handlers override inline handlers; inline handlers override the generic fallback.
const off = session.onTool( "update_counter", (args: { delta: number }) => ({ value: counter.add(args.delta), }),);Handlers may be sync or async. Return JSON-compatible values. Throwing is converted into a TOOL_HANDLER_ERROR result for the agent.
onError()
Section titled “onError()”onError( handler: (error: PedelecError, ctx: ErrorEventContext) => void,): () => voidObserves Core and SDK session errors.
const off = session.onError((error, ctx) => { console.error(ctx.source, error.code, error.message, error.details);});Still catch promises from prepare(), sendText(), and end().
onStatus()
Section titled “onStatus()”onStatus( handler: ( status: PedelecSessionStatus, ctx: StatusEventContext, ) => void,): () => voidRuns when the local status value changes. It does not immediately emit the initial value.
renderStatus(session.getStatus());const off = session.onStatus(renderStatus);onEnded()
Section titled “onEnded()”onEnded(handler: (ctx: EndedEventContext) => void): () => voidRuns once for each transition into ended. Duplicate ended notifications while the handle is already ended do not run the callback again. After a successful same-handle ended -> idle resume(), a later transition back to ended runs it once again.
const off = session.onEnded(() => { disableComposer();});getStatus()
Section titled “getStatus()”getStatus(): PedelecSessionStatusReturns the current browser-side status snapshot.
if (session.getStatus() === "idle") { await session.sendText(text);}This is useful for UI gating but does not replace catching SESSION_BUSY, because state can change between the check and the request.
resume()
Section titled “resume()”resume(): Promise<void>Explicitly reactivates the same SDK handle after a successful end(). The handle object, Core thread ID, handlers, usage, provider/effort metadata, prepared state, and provider session identity are preserved. On success the authoritative status is idle, and the next provider operation starts or resumes provider work lazily.
The handle must not be transport-detached, Core must still contain the thread, the thread must be Ended (or already Idle for an idempotent retry), and the recorded workspace must still exist as a directory with loadable Pedelec skill/tool data. Missing or inaccessible recorded workspaces reject with WORKSPACE_OPEN_FAILED; missing Core threads reject with THREAD_NOT_FOUND. resume() does not recreate a thread from workspace contents or contact the provider runtime.
An already active non-detached handle resolves without another request. Concurrent calls share one in-flight request. A transport-detached handle rejects; use Pedelec.resumeSession(sessionId) to create a new reattachment handle instead.
await session.end();await session.resume();console.log(session.getStatus()); // "idle"await session.sendText("Continue the work");end(): Promise<void>Ends the Core session and marks the SDK handle ended.
On success:
onEnded()runs once for this transition intoended;- pending turn/preparation rejects with
SESSION_ENDED; - the session is removed from the client registry;
- later
sendText()/prepare()rejects until same-handleresume()succeeds.
Calling end() terminates the session but does not immediately delete its workspace. Without workspace.path, the session uses temporary Desktop-managed storage: Pedelec removes managed workspaces when the Desktop App exits normally and attempts to remove leftovers when it next starts. With an explicit absolute workspace.path, the workspace is application-managed and Pedelec never deletes it on session end, app exit, or stale cleanup; multiple active sessions may share it. Closing the main window only hides the app, so it does not trigger managed cleanup. An ended session still cannot use uploadAsset(), listAssets(), or readAsset(), even while its workspace remains present. Shared workspace filesystem conflicts remain the application’s responsibility, and explicit paths cannot overlap the managed workspace root.
Calling end() on an already-ended handle resolves immediately.
If the end request fails, the method emits an SDK error and rejects; it does not claim Core ended successfully.
Handler cleanup
Section titled “Handler cleanup”All event registrations return a () => void disposer. Call them when the owning component or application state is destroyed.
Unsubscribing does not cancel an already-running provider turn or handler promise.
Status values
Section titled “Status values”type PedelecSessionStatus = | "idle" | "running" | "waiting_tool_result" | "ended" | "error";See Session status and events for transitions and context fields.