@kaoruisaac/pedelec 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ A web application can use Pedelec to:
28
28
  - send user instructions and receive streamed assistant text;
29
29
  - expose narrowly scoped browser-side tools to the agent;
30
30
  - resume or end sessions; and
31
- - upload and list completed sandbox assets; and
31
+ - upload and list completed workspace assets; and
32
32
  - show connection, approval, provider, and lifecycle state in the UI.
33
33
 
34
34
  ## Why Pedelec exists
@@ -121,10 +121,15 @@ const session = await pedelec.createSession({
121
121
  });
122
122
 
123
123
  session.onChat((text) => {
124
- // Incremental text stream from the agent.
124
+ // One completed logical assistant message.
125
125
  console.log(text);
126
126
  });
127
127
 
128
+ session.onChatDelta((delta) => {
129
+ // Optional best-effort incremental text for live UI.
130
+ console.log(delta);
131
+ });
132
+
128
133
  session.onStatus((status) => {
129
134
  // idle | running | waiting_tool_result | ended | error
130
135
  console.log("status", status);
@@ -137,7 +142,19 @@ session.onError((error) => {
137
142
  await session.sendText("Please help me analyze the current page state");
138
143
  ```
139
144
 
140
- `sendText()` resolves after the current agent response completes. If the session is already handling a previous prompt, the new `sendText()` call is rejected to prevent multiple concurrent requests from running in the same session.
145
+ `sendText()` resolves after Core reports the matching semantic operation as completed. An `idle` status or bridge request response alone does not complete it. If the session is already handling a previous prompt, the new `sendText()` call is rejected to prevent multiple concurrent requests from running in the same session.
146
+
147
+ ## End and resume a session
148
+
149
+ ```ts
150
+ await session.end();
151
+ await session.resume();
152
+
153
+ console.log(session.getStatus()); // "idle"
154
+ await session.sendText("Continue");
155
+ ```
156
+
157
+ `session.resume()` reactivates the same non-detached handle and Core thread when Core still knows the thread and its recorded workspace still exists. It preserves handlers and session state, does not contact the provider runtime, and leaves provider startup/resume lazy until the next operation. A missing workspace returns `WORKSPACE_OPEN_FAILED`; a thread lost after a Desktop/Core restart returns `THREAD_NOT_FOUND`. `pedelec.resumeSession(sessionId)` is a separate reattachment API: it returns a new handle and does not implicitly revive an ended thread. A transport-detached old handle must use that API.
141
158
 
142
159
  ## Session assets
143
160
 
@@ -146,7 +163,7 @@ const path = await session.uploadAsset(file);
146
163
  const assets = await session.listAssets();
147
164
  ```
148
165
 
149
- The physical shared App/Agent directory is `.pedelec-sandbox/assets/`. In the SDK contract, `assets/` is its implicit root: `listAssets()` recursively lists regular files at every level as a flat array, ordered by filesystem modification time (newest first) and then by name; nested paths such as `/results/report.json` are returned in full. Directory entries and symlinks are excluded, as are `.pedelec-*` entries at every level; other dotfiles are included. It may run while the agent runs. One session can only upload one file at a time, but uploads can run alongside prepare or agent execution.
166
+ The physical shared App/Agent directory is `.pedelec-runtime/assets/`. In the SDK contract, `assets/` is its implicit root: `listAssets()` recursively lists regular files at every level as a flat array, ordered by filesystem modification time (newest first) and then by name; nested paths such as `/results/report.json` are returned in full. Directory entries and symlinks are excluded, as are `.pedelec-*` entries at every level; other dotfiles are included. It may run while the agent runs. One session can only upload one file at a time, but uploads can run alongside prepare or agent execution.
150
167
 
151
168
  ```ts
152
169
  const text = await session.readAsset("/report.txt", "text");
@@ -156,37 +173,39 @@ const result = await session.readAsset("/model.glb", "file");
156
173
 
157
174
  Public asset paths use `/...` with `assets/` as their implicit root; nested paths such as `/results/model.glb` are supported up to 100 MiB.
158
175
 
159
- ## Sandbox workspace
176
+ ## Workspace
177
+
178
+ Workspace is the filesystem root in which the Agent works. Pedelec stores its private runtime data under `<workspace>/.pedelec-runtime/`.
160
179
 
161
- By default, each session receives a temporary Desktop-managed sandbox. To use an application-owned workspace that can persist across sessions, pass an absolute path:
180
+ By default, each session receives a temporary Desktop-managed workspace. To use an application-owned workspace that can persist across sessions, pass an absolute path:
162
181
 
163
182
  ```ts
164
183
  const session = await pedelec.createSession({
165
184
  provider: "codex",
166
- sandbox: { path: "C:\\workspace\\project-a" },
185
+ workspace: { path: "C:\\workspace\\project-a" },
167
186
  });
168
187
  ```
169
188
 
170
- Pedelec creates `.pedelec-sandbox/` with missing `assets/`, `logs/`, `skills/`, and `tmp/` subdirectories, preserving existing project files and existing private data. It never deletes an explicit workspace, and multiple active sessions may share it. Filesystem write conflicts are the application's responsibility. An explicit path must not overlap Pedelec's managed sandbox root.
189
+ Pedelec creates `.pedelec-runtime/` with missing `assets/`, `logs/`, `skills/`, and `tmp/` subdirectories, preserving existing project files and existing private data. It never deletes an explicit workspace, and multiple active sessions may share it. Filesystem write conflicts are the application's responsibility. An explicit path must not overlap Pedelec's managed workspace root.
171
190
 
172
191
  ## Selecting an Application Workspace
173
192
 
174
- Use `sandboxFolderPicker()` when the application wants the user to choose a local workspace before creating a session:
193
+ Use `workspaceFolderPicker()` when the application wants the user to choose a local workspace before creating a session:
175
194
 
176
195
  ```ts
177
- const folder = await pedelec.sandboxFolderPicker();
196
+ const folder = await pedelec.workspaceFolderPicker();
178
197
  if (!folder) return; // The user cancelled the native picker.
179
198
 
180
- if (!folder.isEmptyFolder && !folder.hasSandboxConfig) {
199
+ if (!folder.isEmptyFolder && !folder.hasWorkspaceConfig) {
181
200
  // The application decides whether to warn the user.
182
201
  }
183
202
 
184
203
  const session = await pedelec.createSession({
185
- sandbox: { path: folder.path },
204
+ workspace: { path: folder.path },
186
205
  });
187
206
  ```
188
207
 
189
- `sandboxFolderPicker()` returns `Promise<SandboxFolderPickerResult | null>`. It is a read-only snapshot: it does not validate or initialize the selected folder, create workspace directories, or create the sandbox marker. `isEmptyFolder` only reports whether the selected folder root has any filesystem entry. `hasSandboxConfig` is true only when `.pedelec-lock.json` exists as a regular file; its JSON is not parsed. The picker requires the same origin approval as other sensitive Desktop APIs, and cancellation returns `null`. `createSession()` still applies the full sandbox rules and creates the marker after successful custom sandbox initialization. The marker currently records the SDK version and normalized caller origin but does not restrict reuse by origin or SDK version.
208
+ `workspaceFolderPicker()` returns `Promise<WorkspaceFolderPickerResult | null>`. It is a read-only snapshot: it does not validate or initialize the selected folder, create workspace directories, or create the workspace marker. `isEmptyFolder` only reports whether the selected folder root has any filesystem entry. `hasWorkspaceConfig` is true only when `.pedelec-workspace.json` exists as a regular file; its JSON is not parsed. The picker requires the same origin approval as other sensitive Desktop APIs, and cancellation returns `null`. `createSession()` still applies the full workspace rules and creates the marker after successful custom workspace initialization. The marker currently records the SDK version and normalized caller origin but does not restrict reuse by origin or SDK version.
190
209
 
191
210
  ---
192
211
 
package/dist/index.d.ts CHANGED
@@ -7,11 +7,11 @@ export type JsonPrimitive = string | number | boolean | null;
7
7
  export type JsonValue = JsonPrimitive | JsonValue[] | {
8
8
  [key: string]: JsonValue;
9
9
  };
10
- export type SandboxAssetPath = `/${string}`;
10
+ export type AssetPath = `/${string}`;
11
11
  export type ReadAssetType = "text" | "json" | "file";
12
- export type SandboxAsset = {
12
+ export type Asset = {
13
13
  name: string;
14
- path: SandboxAssetPath;
14
+ path: AssetPath;
15
15
  sizeBytes: number;
16
16
  modifiedAt: number;
17
17
  };
@@ -83,26 +83,26 @@ export type SerializableSkillsManifest = {
83
83
  guidance: string;
84
84
  tools: SerializableToolManifest[];
85
85
  };
86
- export type CreateSessionSandboxInput = {
86
+ export type CreateSessionWorkspaceInput = {
87
87
  path: string;
88
88
  };
89
- export interface SandboxFolderPickerResult {
89
+ export interface WorkspaceFolderPickerResult {
90
90
  path: string;
91
91
  isEmptyFolder: boolean;
92
- hasSandboxConfig: boolean;
92
+ hasWorkspaceConfig: boolean;
93
93
  }
94
94
  type CreateSessionInputWithProvider<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
95
95
  provider: ProviderCode;
96
96
  effortLevel?: EffortLevel;
97
97
  skills?: SkillsInput<TTools>;
98
- sandbox?: CreateSessionSandboxInput;
98
+ workspace?: CreateSessionWorkspaceInput;
99
99
  autoEndOnDisconnect?: boolean;
100
100
  };
101
101
  type CreateSessionInputWithDefaults<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
102
102
  provider?: undefined;
103
103
  effortLevel?: EffortLevel;
104
104
  skills?: SkillsInput<TTools>;
105
- sandbox?: CreateSessionSandboxInput;
105
+ workspace?: CreateSessionWorkspaceInput;
106
106
  autoEndOnDisconnect?: boolean;
107
107
  };
108
108
  export type CreateSessionInput<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = CreateSessionInputWithProvider<TTools> | CreateSessionInputWithDefaults<TTools>;
@@ -142,6 +142,9 @@ export type PedelecError = {
142
142
  details?: unknown;
143
143
  };
144
144
  export type PedelecSessionStatus = "idle" | "running" | "waiting_tool_result" | "ended" | "error";
145
+ export type PedelecSessionUsage = {
146
+ totalTokens?: number;
147
+ };
145
148
  export type PedelecEventContext = {
146
149
  sessionId: string;
147
150
  provider: string;
@@ -155,6 +158,12 @@ export type PedelecEventContext = {
155
158
  source: "core" | "sdk";
156
159
  };
157
160
  export type ChatEventContext = PedelecEventContext & {
161
+ type: "chat_message";
162
+ turnId: string;
163
+ turnStartedAt: number;
164
+ eventReceivedAt: number;
165
+ };
166
+ export type ChatDeltaEventContext = PedelecEventContext & {
158
167
  type: "chat_delta";
159
168
  turnId: string;
160
169
  turnStartedAt: number;
@@ -184,31 +193,52 @@ type SessionEvent = {
184
193
  channelId: string;
185
194
  sessionId: string;
186
195
  seq?: number;
196
+ operationId?: string;
197
+ text: string;
198
+ } | {
199
+ type: "chat_message";
200
+ channelId: string;
201
+ sessionId: string;
202
+ seq?: number;
203
+ operationId?: string;
187
204
  text: string;
188
205
  } | {
189
206
  type: "status_changed";
190
207
  channelId: string;
191
208
  sessionId: string;
192
209
  seq?: number;
210
+ operationId?: string;
193
211
  status: PedelecSessionStatus;
194
212
  } | {
195
213
  type: "tool_call";
196
214
  channelId: string;
197
215
  sessionId: string;
198
216
  seq?: number;
217
+ operationId: string;
199
218
  toolRequestId: string;
200
219
  tool: string;
201
220
  args: unknown;
202
221
  } | {
203
- type: "done";
222
+ type: "operation_completed";
223
+ channelId: string;
224
+ sessionId: string;
225
+ seq?: number;
226
+ operationId: string;
227
+ operationKind: "user" | "prepare";
228
+ success: boolean;
229
+ error?: PedelecError;
230
+ } | {
231
+ type: "usage_updated";
204
232
  channelId: string;
205
233
  sessionId: string;
206
234
  seq?: number;
235
+ totalTokens: number;
207
236
  } | {
208
237
  type: "error";
209
238
  channelId?: string;
210
239
  sessionId?: string;
211
240
  seq?: number;
241
+ operationId?: string;
212
242
  error?: PedelecError;
213
243
  } | {
214
244
  type: "ended";
@@ -216,11 +246,41 @@ type SessionEvent = {
216
246
  sessionId: string;
217
247
  seq?: number;
218
248
  };
249
+ type SessionSnapshot = {
250
+ threadId: string;
251
+ status: string;
252
+ latestSeq: number;
253
+ usage?: {
254
+ totalTokens: number;
255
+ };
256
+ activeOperation?: {
257
+ operationId: string;
258
+ operationKind: "user" | "prepare";
259
+ startedAt: string;
260
+ };
261
+ lastCompletedOperation?: {
262
+ operationId: string;
263
+ operationKind: "user" | "prepare";
264
+ success: boolean;
265
+ error?: PedelecError;
266
+ completedAt: string;
267
+ };
268
+ pendingToolRequest?: {
269
+ requestId: string;
270
+ threadId: string;
271
+ operationId: string;
272
+ toolName: string;
273
+ args: unknown;
274
+ createdAt: string;
275
+ timeoutMs: number;
276
+ };
277
+ };
219
278
  type EventDispatchMeta = {
220
279
  source: "core" | "sdk";
221
280
  eventReceivedAt?: number;
222
281
  };
223
282
  type ChatHandler = (text: string, ctx: ChatEventContext) => void;
283
+ type ChatDeltaHandler = (text: string, ctx: ChatDeltaEventContext) => void;
224
284
  type GenericToolHandler<TToolName extends string = string> = (tool: TToolName, args: unknown, ctx: ToolCallContext) => unknown | Promise<unknown>;
225
285
  type ErrorHandler = (error: PedelecError, ctx: ErrorEventContext) => void;
226
286
  type StatusHandler = (status: PedelecSessionStatus, ctx: StatusEventContext) => void;
@@ -235,10 +295,12 @@ export declare class Pedelec {
235
295
  private readonly lastSeqBySession;
236
296
  private nextRequestNumber;
237
297
  private port;
298
+ private pageActivityListenersAttached;
299
+ private lastReportedPageActive;
238
300
  constructor(options?: PedelecOptions);
239
301
  createSession(): Promise<PedelecSession<string>>;
240
302
  createSession<const TTools extends readonly ToolDefinition[]>(input: CreateSessionInput<TTools>): Promise<PedelecSession<ToolNameOf<TTools>>>;
241
- sandboxFolderPicker(): Promise<SandboxFolderPickerResult | null>;
303
+ workspaceFolderPicker(): Promise<WorkspaceFolderPickerResult | null>;
242
304
  listProviders(): Promise<ProviderInfo[]>;
243
305
  getSettings(): Promise<PedelecSettings>;
244
306
  getApprovalStatus(): Promise<ApprovalStatus>;
@@ -246,6 +308,16 @@ export declare class Pedelec {
246
308
  request<T>(type: string, payload?: Record<string, unknown>): Promise<T>;
247
309
  private requestWithOptions;
248
310
  unregisterSession(sessionId: string): void;
311
+ /** @internal */
312
+ registerExistingSession(session: PedelecSession<any>): void;
313
+ /** @internal */
314
+ isSessionRegistered(sessionId: string, session: PedelecSession<any>): boolean;
315
+ private attachPageActivityListeners;
316
+ private isDocumentHidden;
317
+ private isDocumentFocused;
318
+ private isPageForegroundActive;
319
+ private postSdkHello;
320
+ private reportPageActivity;
249
321
  private resolveCreateSessionInput;
250
322
  checkAvailability(): Promise<PedelecAvailability>;
251
323
  private resolveDefaultCreateSessionInput;
@@ -266,61 +338,79 @@ export declare class PedelecSession<TToolName extends string = string> {
266
338
  readonly provider: string;
267
339
  readonly effortLevel?: EffortLevel;
268
340
  readonly sessionCreatedAt: number;
341
+ readonly usage: PedelecSessionUsage;
269
342
  private status;
270
- private pendingSend;
271
- private pendingPrepare;
343
+ private pendingOperation;
272
344
  private preparePromise;
273
345
  private uploadPromise;
346
+ private reactivationPromise;
274
347
  private ending;
275
348
  private prepared;
349
+ private autoEndOnDisconnect;
276
350
  private activeTurn;
277
351
  private transportDetached;
278
352
  private genericToolHandler;
279
353
  private inlineToolHandlers;
280
354
  private readonly namedToolHandlers;
281
355
  private readonly chatHandlers;
356
+ private readonly chatDeltaHandlers;
282
357
  private readonly errorHandlers;
283
358
  private readonly statusHandlers;
284
359
  private readonly endedHandlers;
285
- constructor(client: Pedelec, sessionId: string, provider: string, effortLevel?: EffortLevel, inlineToolHandlers?: Map<string, ToolSpecificHandler>);
360
+ private readonly recoveredToolCalls;
361
+ private readonly handledToolRequestIds;
362
+ private authoritativePendingToolRequest;
363
+ constructor(client: Pedelec, sessionId: string, provider: string, effortLevel?: EffortLevel, inlineToolHandlers?: Map<string, ToolSpecificHandler>, autoEndOnDisconnect?: boolean);
286
364
  prepare(): Promise<void>;
287
365
  sendText(text: string): Promise<void>;
288
366
  onChat(handler: ChatHandler): () => void;
367
+ onChatDelta(handler: ChatDeltaHandler): () => void;
289
368
  onTool(handler: GenericToolHandler<TToolName>): () => void;
290
369
  onTool<TArgs = unknown, TResult = unknown>(toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
291
- uploadAsset(file: File): Promise<SandboxAssetPath>;
292
- uploadAsset(file: File, targetPath: SandboxAssetPath): Promise<SandboxAssetPath>;
293
- listAssets(): Promise<SandboxAsset[]>;
294
- readAsset(path: SandboxAssetPath, type: "text"): Promise<string>;
295
- readAsset<T = JsonValue>(path: SandboxAssetPath, type: "json"): Promise<T>;
296
- readAsset(path: SandboxAssetPath, type: "file"): Promise<File>;
370
+ uploadAsset(file: File): Promise<AssetPath>;
371
+ uploadAsset(file: File, targetPath: AssetPath): Promise<AssetPath>;
372
+ listAssets(): Promise<Asset[]>;
373
+ readAsset(path: AssetPath, type: "text"): Promise<string>;
374
+ readAsset<T = JsonValue>(path: AssetPath, type: "json"): Promise<T>;
375
+ readAsset(path: AssetPath, type: "file"): Promise<File>;
297
376
  onError(handler: ErrorHandler): () => void;
298
377
  onStatus(handler: StatusHandler): () => void;
299
378
  onEnded(handler: EndedHandler): () => void;
300
379
  getStatus(): PedelecSessionStatus;
301
380
  end(): Promise<void>;
381
+ resume(): Promise<void>;
382
+ /** @internal */
383
+ setAutoEndOnDisconnect(autoEndOnDisconnect: boolean): void;
302
384
  handleEvent(event: SessionEvent, meta?: EventDispatchMeta): void;
385
+ /** @internal */
386
+ handleSnapshot(snapshot: SessionSnapshot, meta?: EventDispatchMeta): void;
303
387
  replaceInlineToolHandlers(handlers: Map<string, ToolSpecificHandler>): void;
304
388
  /** @internal */
305
389
  isTransportDetached(): boolean;
306
390
  /** @internal */
307
391
  handleTransportDisconnect(error: PedelecError): void;
308
- private handleToolCall;
309
- private resolvePendingSend;
310
- private resolvePendingPrepare;
311
- private resolveActivePending;
312
- private rejectPendingSend;
313
- private rejectPendingPrepare;
392
+ private handleToolCallOnce;
314
393
  private rejectActivePending;
315
- private clearPendingSend;
316
- private clearPendingPrepare;
317
394
  private markEnded;
318
395
  private emitError;
319
396
  private setStatus;
320
- private requireActiveTurn;
397
+ private requireOperationTurn;
398
+ private updateUsage;
399
+ private matchesActiveOperation;
400
+ private markOperationRequestSettled;
401
+ private failOperationFromRequest;
402
+ private completeOperation;
403
+ private maybeSettleOperation;
404
+ private rejectOperation;
405
+ private queueSnapshotTool;
406
+ private reconcileRecoveredToolCalls;
407
+ private isAuthoritativeToolRequestCurrent;
408
+ private hasToolHandler;
409
+ private drainRecoveredToolCalls;
321
410
  private finishActiveTurn;
322
411
  private createBaseContext;
323
412
  private createChatContext;
413
+ private createChatDeltaContext;
324
414
  private createToolCallContext;
325
415
  private createStatusContext;
326
416
  private createErrorContext;