@kaoruisaac/pedelec 0.1.2 → 0.1.4

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
@@ -66,25 +66,26 @@ const session = await pedelec.createSession({
66
66
  properties: {},
67
67
  required: [],
68
68
  },
69
- handler: () => ({
69
+ handler: (_args, ctx) => ({
70
70
  url: location.href,
71
71
  title: document.title,
72
+ turnId: ctx.turnId,
72
73
  }),
73
74
  }),
74
75
  ],
75
76
  },
76
77
  });
77
78
 
78
- session.onChat((text) => {
79
- console.log(text);
79
+ session.onChat((text, ctx) => {
80
+ console.log(ctx.turnId, text);
80
81
  });
81
82
 
82
- session.onStatus((status) => {
83
- console.log("status", status);
83
+ session.onStatus((status, ctx) => {
84
+ console.log("status", status, "previous", ctx.previousStatus);
84
85
  });
85
86
 
86
- session.onError((error) => {
87
- console.error(error.code, error.message, error.details);
87
+ session.onError((error, ctx) => {
88
+ console.error(ctx.type, error.code, error.message, error.details);
88
89
  });
89
90
 
90
91
  await session.sendText("Analyze the current page state.");
@@ -277,7 +278,8 @@ type CreateSessionInput =
277
278
  ```ts
278
279
  let assistantText = "";
279
280
 
280
- session.onChat((text) => {
281
+ session.onChat((text, ctx) => {
282
+ console.debug("chat delta for turn", ctx.turnId, ctx.eventReceivedAt);
281
283
  assistantText += text;
282
284
  renderAssistantMessage(assistantText);
283
285
  });
@@ -286,8 +288,8 @@ session.onChat((text) => {
286
288
  The returned function unsubscribes the handler:
287
289
 
288
290
  ```ts
289
- const unsubscribe = session.onChat((text) => {
290
- console.log(text);
291
+ const unsubscribe = session.onChat((text, ctx) => {
292
+ console.log(ctx.turnId, text);
291
293
  });
292
294
 
293
295
  unsubscribe();
@@ -308,7 +310,8 @@ try {
308
310
  ## Session Status
309
311
 
310
312
  ```ts
311
- session.onStatus((status) => {
313
+ session.onStatus((status, ctx) => {
314
+ console.debug("status changed from", ctx.previousStatus, "to", ctx.status);
312
315
  switch (status) {
313
316
  case "idle":
314
317
  break;
@@ -363,10 +366,11 @@ const session = await pedelec.createSession({
363
366
  properties: {},
364
367
  required: [],
365
368
  },
366
- handler: () => ({
369
+ handler: (_args, ctx) => ({
367
370
  url: location.href,
368
371
  title: document.title,
369
372
  selectedText: window.getSelection()?.toString() ?? "",
373
+ turnId: ctx.turnId,
370
374
  }),
371
375
  }),
372
376
  ],
@@ -400,8 +404,9 @@ const session = await pedelec.createSession({
400
404
  },
401
405
  });
402
406
 
403
- session.onTool("update_counter", async (args) => {
407
+ session.onTool("update_counter", async (args, ctx) => {
404
408
  const { delta } = args as { delta: number };
409
+ console.debug("tool call", ctx.toolRequestId, ctx.turnId);
405
410
  counter.value += delta;
406
411
  return {
407
412
  counter: counter.value,
@@ -412,7 +417,37 @@ session.onTool("update_counter", async (args) => {
412
417
 
413
418
  Tool results must be JSON-serializable. If no handler is registered, the SDK returns a `TOOL_HANDLER_NOT_FOUND` error result. If the handler throws, the SDK returns a `TOOL_HANDLER_ERROR` error result.
414
419
 
415
- The legacy generic form `session.onTool((tool, args) => ...)` remains available as a fallback handler.
420
+ The generic form `session.onTool((tool, args, ctx) => ...)` remains available as a fallback handler.
421
+
422
+ ### Event Context and UI Lifecycles
423
+
424
+ All user-facing event callbacks receive context metadata. The SDK provides `ctx.sessionId`, `ctx.provider`, `ctx.model`, `ctx.turnId`, `ctx.turnStartedAt`, `ctx.eventReceivedAt`, `ctx.eventEmittedAt`, and `ctx.source` where they apply. `turnId` is SDK-local metadata for one accepted `sendText()` turn; it is not sent to Core and should not be parsed.
425
+
426
+ Apps should still use their own UI lifecycle state to decide whether a late callback is stale:
427
+
428
+ ```ts
429
+ let lifecycleId = 0;
430
+
431
+ function createWorldSession(session: PedelecSession) {
432
+ const generation = ++lifecycleId;
433
+
434
+ session.onTool("spawn_basic_shapes", async (args, ctx) => {
435
+ if (generation !== lifecycleId) {
436
+ return {
437
+ error: {
438
+ code: "STALE_TOOL_CALL",
439
+ message: "This tool call belongs to an older UI lifecycle.",
440
+ },
441
+ };
442
+ }
443
+
444
+ console.debug("handling tool", ctx.toolRequestId, ctx.turnId, ctx.turnStartedAt);
445
+ return spawnBasicShapes(args);
446
+ });
447
+ }
448
+ ```
449
+
450
+ The SDK context helps you inspect which session and turn produced an event. It does not automatically know whether your current canvas, page, render mode, or world instance is still valid.
416
451
 
417
452
  ### Tool Args Schema
418
453
 
@@ -448,8 +483,8 @@ If you have saved a `sessionId`, resume it later:
448
483
  ```ts
449
484
  const session = await pedelec.resumeSession("thread_abc123");
450
485
 
451
- session.onChat((text) => {
452
- console.log(text);
486
+ session.onChat((text, ctx) => {
487
+ console.log(ctx.turnId, text);
453
488
  });
454
489
 
455
490
  await session.sendText("Continue the previous task.");
@@ -468,8 +503,8 @@ After a session ends, future `sendText()` calls reject with `SESSION_ENDED`.
468
503
  Listen for runtime-ended sessions:
469
504
 
470
505
  ```ts
471
- session.onEnded(() => {
472
- console.log("session ended");
506
+ session.onEnded((ctx) => {
507
+ console.log("session ended", ctx.source);
473
508
  });
474
509
  ```
475
510
 
@@ -494,8 +529,8 @@ Low-level requests still use the SDK bridge timeout and reject with `PedelecErro
494
529
  Register `onError()` for session-level errors and use `try/catch` around async SDK calls.
495
530
 
496
531
  ```ts
497
- session.onError((error) => {
498
- console.error("session error", error.code, error.message, error.details);
532
+ session.onError((error, ctx) => {
533
+ console.error("session error", ctx.type, error.code, error.message, error.details);
499
534
  });
500
535
 
501
536
  try {
@@ -555,11 +590,11 @@ class PedelecSession {
555
590
  readonly model?: string;
556
591
 
557
592
  sendText(text: string): Promise<void>;
558
- onChat(handler: (text: string) => void): () => void;
559
- onTool(handler: (tool: string, args: unknown) => unknown | Promise<unknown>): () => void;
560
- onError(handler: (error: PedelecError) => void): () => void;
561
- onStatus(handler: (status: PedelecSessionStatus) => void): () => void;
562
- onEnded(handler: () => void): () => void;
593
+ onChat(handler: (text: string, ctx: ChatEventContext) => void): () => void;
594
+ onTool(handler: (tool: string, args: unknown, ctx: ToolCallContext) => unknown | Promise<unknown>): () => void;
595
+ onError(handler: (error: PedelecError, ctx: ErrorEventContext) => void): () => void;
596
+ onStatus(handler: (status: PedelecSessionStatus, ctx: StatusEventContext) => void): () => void;
597
+ onEnded(handler: (ctx: EndedEventContext) => void): () => void;
563
598
  getStatus(): PedelecSessionStatus;
564
599
  end(): Promise<void>;
565
600
  }
package/dist/index.d.ts CHANGED
@@ -51,18 +51,19 @@ export type ToolArgsOneOfSchema = ToolArgsSchemaMeta & {
51
51
  };
52
52
  export type ToolArgsSchemaNode = ToolArgsStringSchema | ToolArgsNumberSchema | ToolArgsIntegerSchema | ToolArgsBooleanSchema | ToolArgsArraySchema | ToolArgsObjectSchema | ToolArgsOneOfSchema;
53
53
  export type ToolArgsSchema = ToolArgsObjectSchema;
54
- export type ToolSpecificHandler<TArgs = unknown, TResult = unknown> = (args: TArgs) => TResult | Promise<TResult>;
55
- export type ToolDefinition<TArgs = unknown, TResult = unknown> = {
56
- name: string;
54
+ export type ToolSpecificHandler<TArgs = unknown, TResult = unknown> = (args: TArgs, ctx: ToolCallContext) => TResult | Promise<TResult>;
55
+ export type ToolDefinition<TArgs = unknown, TResult = unknown, TName extends string = string> = {
56
+ name: TName;
57
57
  description: string;
58
58
  argsSchema: ToolArgsSchema;
59
59
  timeoutMs?: number;
60
60
  handler?: ToolSpecificHandler<TArgs, TResult>;
61
61
  };
62
- export type SkillsInput = {
62
+ export type SkillsInput<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
63
63
  guidance: string;
64
- tools: ToolDefinition[];
64
+ tools: TTools;
65
65
  };
66
+ export type ToolNameOf<TTools extends readonly ToolDefinition[]> = Extract<TTools[number]["name"], string>;
66
67
  export type SerializableToolManifest = {
67
68
  name: string;
68
69
  description: string;
@@ -73,19 +74,19 @@ export type SerializableSkillsManifest = {
73
74
  guidance: string;
74
75
  tools: SerializableToolManifest[];
75
76
  };
76
- type CreateSessionInputWithProvider = {
77
+ type CreateSessionInputWithProvider<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
77
78
  provider: ProviderCode;
78
79
  model?: string;
79
- skills?: SkillsInput;
80
+ skills?: SkillsInput<TTools>;
80
81
  autoEndOnDisconnect?: boolean;
81
82
  };
82
- type CreateSessionInputWithDefaults = {
83
+ type CreateSessionInputWithDefaults<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
83
84
  provider?: undefined;
84
85
  model?: never;
85
- skills?: SkillsInput;
86
+ skills?: SkillsInput<TTools>;
86
87
  autoEndOnDisconnect?: boolean;
87
88
  };
88
- export type CreateSessionInput = CreateSessionInputWithProvider | CreateSessionInputWithDefaults;
89
+ export type CreateSessionInput<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = CreateSessionInputWithProvider<TTools> | CreateSessionInputWithDefaults<TTools>;
89
90
  export type ProviderInfo = {
90
91
  name: string;
91
92
  code: ProviderCode;
@@ -108,6 +109,43 @@ export type PedelecError = {
108
109
  details?: unknown;
109
110
  };
110
111
  export type PedelecSessionStatus = "idle" | "running" | "waiting_tool_result" | "ended" | "error";
112
+ export type PedelecEventContext = {
113
+ sessionId: string;
114
+ provider: string;
115
+ model?: string;
116
+ sessionCreatedAt: number;
117
+ eventReceivedAt?: number;
118
+ eventEmittedAt: number;
119
+ turnId?: string;
120
+ turnStartedAt?: number;
121
+ turnKind?: "user" | "prepare";
122
+ source: "core" | "sdk";
123
+ };
124
+ export type ChatEventContext = PedelecEventContext & {
125
+ type: "chat_delta";
126
+ turnId: string;
127
+ turnStartedAt: number;
128
+ eventReceivedAt: number;
129
+ };
130
+ export type ToolCallContext = PedelecEventContext & {
131
+ type: "tool_call";
132
+ toolRequestId: string;
133
+ tool: string;
134
+ turnId: string;
135
+ turnStartedAt: number;
136
+ eventReceivedAt: number;
137
+ };
138
+ export type StatusEventContext = PedelecEventContext & {
139
+ type: "status_changed" | "sdk_status_changed";
140
+ status: PedelecSessionStatus;
141
+ previousStatus: PedelecSessionStatus;
142
+ };
143
+ export type ErrorEventContext = PedelecEventContext & {
144
+ type: "error" | "sdk_error";
145
+ };
146
+ export type EndedEventContext = PedelecEventContext & {
147
+ type: "ended" | "sdk_ended";
148
+ };
111
149
  type SessionEvent = {
112
150
  type: "chat_delta";
113
151
  channelId: string;
@@ -145,12 +183,16 @@ type SessionEvent = {
145
183
  sessionId: string;
146
184
  seq?: number;
147
185
  };
148
- type ChatHandler = (text: string) => void;
149
- type GenericToolHandler = (tool: string, args: unknown) => unknown | Promise<unknown>;
150
- type ErrorHandler = (error: PedelecError) => void;
151
- type StatusHandler = (status: PedelecSessionStatus) => void;
152
- type EndedHandler = () => void;
153
- export declare function defineTool<TArgs = unknown, TResult = unknown>(tool: ToolDefinition<TArgs, TResult>): ToolDefinition<TArgs, TResult>;
186
+ type EventDispatchMeta = {
187
+ source: "core" | "sdk";
188
+ eventReceivedAt?: number;
189
+ };
190
+ type ChatHandler = (text: string, ctx: ChatEventContext) => void;
191
+ type GenericToolHandler<TToolName extends string = string> = (tool: TToolName, args: unknown, ctx: ToolCallContext) => unknown | Promise<unknown>;
192
+ type ErrorHandler = (error: PedelecError, ctx: ErrorEventContext) => void;
193
+ type StatusHandler = (status: PedelecSessionStatus, ctx: StatusEventContext) => void;
194
+ type EndedHandler = (ctx: EndedEventContext) => void;
195
+ export declare function defineTool<TArgs = unknown, TResult = unknown, const TName extends string = string>(tool: ToolDefinition<TArgs, TResult, TName>): ToolDefinition<TArgs, TResult, TName>;
154
196
  export declare class Pedelec {
155
197
  private readonly pageWindow;
156
198
  private readonly channelId;
@@ -162,11 +204,12 @@ export declare class Pedelec {
162
204
  private disconnectedError;
163
205
  private port;
164
206
  constructor(options?: PedelecOptions);
165
- createSession(input?: CreateSessionInput): Promise<PedelecSession>;
207
+ createSession(): Promise<PedelecSession<string>>;
208
+ createSession<const TTools extends readonly ToolDefinition[]>(input: CreateSessionInput<TTools>): Promise<PedelecSession<ToolNameOf<TTools>>>;
166
209
  listProviders(): Promise<ProviderInfo[]>;
167
210
  getSettings(): Promise<PedelecSettings>;
168
211
  getApprovalStatus(): Promise<ApprovalStatus>;
169
- resumeSession(sessionId: string): Promise<PedelecSession>;
212
+ resumeSession(sessionId: string): Promise<PedelecSession<string>>;
170
213
  request<T>(type: string, payload?: Record<string, unknown>): Promise<T>;
171
214
  unregisterSession(sessionId: string): void;
172
215
  private resolveCreateSessionInput;
@@ -181,13 +224,18 @@ export declare class Pedelec {
181
224
  private handleDisconnect;
182
225
  private broadcastError;
183
226
  }
184
- export declare class PedelecSession {
227
+ export declare class PedelecSession<TToolName extends string = string> {
185
228
  private readonly client;
186
229
  readonly sessionId: string;
187
230
  readonly provider: string;
188
231
  readonly model?: string;
232
+ readonly sessionCreatedAt: number;
189
233
  private status;
190
234
  private pendingSend;
235
+ private pendingPrepare;
236
+ private preparePromise;
237
+ private prepared;
238
+ private activeTurn;
191
239
  private genericToolHandler;
192
240
  private inlineToolHandlers;
193
241
  private readonly namedToolHandlers;
@@ -196,23 +244,37 @@ export declare class PedelecSession {
196
244
  private readonly statusHandlers;
197
245
  private readonly endedHandlers;
198
246
  constructor(client: Pedelec, sessionId: string, provider: string, model?: string, inlineToolHandlers?: Map<string, ToolSpecificHandler>);
247
+ prepare(): Promise<void>;
199
248
  sendText(text: string): Promise<void>;
200
249
  onChat(handler: ChatHandler): () => void;
201
- onTool(handler: GenericToolHandler): () => void;
202
- onTool<TArgs = unknown, TResult = unknown>(toolName: string, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
250
+ onTool(handler: GenericToolHandler<TToolName>): () => void;
251
+ onTool<TArgs = unknown, TResult = unknown>(toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
203
252
  onError(handler: ErrorHandler): () => void;
204
253
  onStatus(handler: StatusHandler): () => void;
205
254
  onEnded(handler: EndedHandler): () => void;
206
255
  getStatus(): PedelecSessionStatus;
207
256
  end(): Promise<void>;
208
- handleEvent(event: SessionEvent): void;
257
+ handleEvent(event: SessionEvent, meta?: EventDispatchMeta): void;
209
258
  replaceInlineToolHandlers(handlers: Map<string, ToolSpecificHandler>): void;
210
259
  private handleToolCall;
211
260
  private resolvePendingSend;
261
+ private resolvePendingPrepare;
262
+ private resolveActivePending;
212
263
  private rejectPendingSend;
264
+ private rejectPendingPrepare;
265
+ private rejectActivePending;
213
266
  private clearPendingSend;
267
+ private clearPendingPrepare;
214
268
  private markEnded;
215
269
  private emitError;
216
270
  private setStatus;
271
+ private requireActiveTurn;
272
+ private finishActiveTurn;
273
+ private createBaseContext;
274
+ private createChatContext;
275
+ private createToolCallContext;
276
+ private createStatusContext;
277
+ private createErrorContext;
278
+ private createEndedContext;
217
279
  }
218
280
  export {};
package/dist/index.js CHANGED
@@ -309,8 +309,12 @@ export class Pedelec {
309
309
  return;
310
310
  }
311
311
  if (isSessionEvent(message) && this.isNewEvent(message)) {
312
+ const eventReceivedAt = Date.now();
312
313
  if (message.sessionId) {
313
- this.sessions.get(message.sessionId)?.handleEvent(message);
314
+ this.sessions.get(message.sessionId)?.handleEvent(message, {
315
+ source: "core",
316
+ eventReceivedAt,
317
+ });
314
318
  }
315
319
  else if (message.type === "error") {
316
320
  this.broadcastError(normalizeError(message.error, "SDK_TRANSPORT_ERROR", "Pedelec transport error"));
@@ -354,7 +358,7 @@ export class Pedelec {
354
358
  }
355
359
  broadcastError(error) {
356
360
  for (const session of this.sessions.values()) {
357
- session.handleEvent({ type: "error", sessionId: session.sessionId, error });
361
+ session.handleEvent({ type: "error", sessionId: session.sessionId, error }, { source: "sdk" });
358
362
  }
359
363
  }
360
364
  }
@@ -363,8 +367,13 @@ export class PedelecSession {
363
367
  sessionId;
364
368
  provider;
365
369
  model;
370
+ sessionCreatedAt = Date.now();
366
371
  status = "idle";
367
372
  pendingSend = null;
373
+ pendingPrepare = null;
374
+ preparePromise = null;
375
+ prepared = false;
376
+ activeTurn = null;
368
377
  genericToolHandler = null;
369
378
  inlineToolHandlers = new Map();
370
379
  namedToolHandlers = new Map();
@@ -379,14 +388,63 @@ export class PedelecSession {
379
388
  this.model = model;
380
389
  this.inlineToolHandlers = new Map(inlineToolHandlers);
381
390
  }
382
- sendText(text) {
391
+ prepare() {
383
392
  if (this.status === "ended") {
384
393
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
385
394
  }
386
395
  if (this.pendingSend) {
387
396
  return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
388
397
  }
389
- this.setStatus("running");
398
+ if (this.prepared) {
399
+ return Promise.resolve();
400
+ }
401
+ if (this.preparePromise) {
402
+ return this.preparePromise;
403
+ }
404
+ const turn = createTurn("prepare");
405
+ this.activeTurn = turn;
406
+ this.setStatus("running", { source: "sdk" });
407
+ const donePromise = new Promise((resolve, reject) => {
408
+ this.pendingPrepare = { resolve, reject };
409
+ });
410
+ const requestPromise = this.client
411
+ .request("prepare_session", {
412
+ sessionId: this.sessionId,
413
+ })
414
+ .catch((err) => {
415
+ const error = normalizeError(err, "PREPARE_FAILED", "prepare failed");
416
+ this.emitError(error, { source: "sdk" });
417
+ this.clearPendingPrepare();
418
+ this.finishActiveTurn(turn);
419
+ throw error;
420
+ });
421
+ this.preparePromise = Promise.all([requestPromise, donePromise])
422
+ .then(() => {
423
+ this.prepared = true;
424
+ })
425
+ .finally(() => {
426
+ this.preparePromise = null;
427
+ });
428
+ return this.preparePromise;
429
+ }
430
+ async sendText(text) {
431
+ if (this.preparePromise) {
432
+ try {
433
+ await this.preparePromise;
434
+ }
435
+ catch {
436
+ // prepare is an optimization; sendText falls back to the original first-run path.
437
+ }
438
+ }
439
+ if (this.status === "ended") {
440
+ return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
441
+ }
442
+ if (this.pendingSend || this.pendingPrepare) {
443
+ return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
444
+ }
445
+ const turn = createTurn("user");
446
+ this.activeTurn = turn;
447
+ this.setStatus("running", { source: "sdk" });
390
448
  const donePromise = new Promise((resolve, reject) => {
391
449
  this.pendingSend = { resolve, reject };
392
450
  });
@@ -397,8 +455,9 @@ export class PedelecSession {
397
455
  })
398
456
  .catch((err) => {
399
457
  const error = normalizeError(err, "SEND_TEXT_FAILED", "sendText failed");
458
+ this.emitError(error, { source: "sdk" });
400
459
  this.clearPendingSend();
401
- this.emitError(error);
460
+ this.finishActiveTurn(turn);
402
461
  throw error;
403
462
  });
404
463
  return Promise.all([requestPromise, donePromise]).then(() => undefined);
@@ -456,65 +515,73 @@ export class PedelecSession {
456
515
  }
457
516
  catch (err) {
458
517
  const error = normalizeError(err, "END_SESSION_FAILED", "end failed");
459
- this.emitError(error);
518
+ this.emitError(error, { source: "sdk" });
460
519
  throw error;
461
520
  }
462
- this.markEnded();
521
+ this.markEnded({ source: "sdk" });
463
522
  this.client.unregisterSession(this.sessionId);
464
523
  }
465
- handleEvent(event) {
524
+ handleEvent(event, meta = { source: "sdk" }) {
466
525
  if (event.type === "chat_delta") {
526
+ const turn = this.requireActiveTurn("chat_delta", meta);
527
+ if (!turn)
528
+ return;
529
+ if (turn.kind === "prepare")
530
+ return;
467
531
  for (const handler of this.chatHandlers) {
468
- handler(event.text);
532
+ handler(event.text, this.createChatContext(meta, turn));
469
533
  }
470
534
  return;
471
535
  }
472
536
  if (event.type === "status_changed") {
473
- this.setStatus(event.status);
537
+ this.setStatus(event.status, meta);
474
538
  if (event.status === "idle") {
475
- this.resolvePendingSend();
539
+ this.resolveActivePending();
476
540
  }
477
541
  else if (event.status === "ended") {
478
- this.markEnded();
542
+ this.markEnded(meta);
479
543
  }
480
544
  else if (event.status === "error") {
481
545
  const error = makeError("SESSION_ERROR", "session entered error status", { sessionId: this.sessionId });
482
- this.rejectPendingSend(error);
483
- this.emitError(error);
546
+ this.emitError(error, meta);
547
+ this.rejectActivePending(error);
484
548
  }
485
549
  return;
486
550
  }
487
551
  if (event.type === "tool_call") {
488
- this.setStatus("waiting_tool_result");
489
- this.handleToolCall(event);
552
+ const turn = this.requireActiveTurn("tool_call", meta);
553
+ if (!turn)
554
+ return;
555
+ this.setStatus("waiting_tool_result", meta);
556
+ this.handleToolCall(event, meta, turn);
490
557
  return;
491
558
  }
492
559
  if (event.type === "done") {
493
- this.setStatus("idle");
494
- this.resolvePendingSend();
560
+ this.setStatus("idle", meta);
561
+ this.resolveActivePending();
495
562
  return;
496
563
  }
497
564
  if (event.type === "error") {
498
- this.setStatus("error");
565
+ this.setStatus("error", meta);
499
566
  const error = normalizeError(event.error, "SESSION_ERROR", "session error");
500
- this.rejectPendingSend(error);
501
- this.emitError(error);
567
+ this.emitError(error, meta);
568
+ this.rejectActivePending(error);
502
569
  return;
503
570
  }
504
571
  if (event.type === "ended") {
505
- this.markEnded();
572
+ this.markEnded(meta);
506
573
  }
507
574
  }
508
575
  replaceInlineToolHandlers(handlers) {
509
576
  this.inlineToolHandlers = new Map(handlers);
510
577
  }
511
- async handleToolCall(event) {
578
+ async handleToolCall(event, meta, turn) {
512
579
  let result;
513
580
  const namedHandler = this.namedToolHandlers.get(event.tool);
514
581
  const inlineHandler = this.inlineToolHandlers.get(event.tool);
515
582
  if (namedHandler) {
516
583
  try {
517
- result = await namedHandler(event.args);
584
+ result = await namedHandler(event.args, this.createToolCallContext(event, meta, turn));
518
585
  }
519
586
  catch (err) {
520
587
  result = {
@@ -524,7 +591,7 @@ export class PedelecSession {
524
591
  }
525
592
  else if (inlineHandler) {
526
593
  try {
527
- result = await inlineHandler(event.args);
594
+ result = await inlineHandler(event.args, this.createToolCallContext(event, meta, turn));
528
595
  }
529
596
  catch (err) {
530
597
  result = {
@@ -534,7 +601,7 @@ export class PedelecSession {
534
601
  }
535
602
  else if (this.genericToolHandler) {
536
603
  try {
537
- result = await this.genericToolHandler(event.tool, event.args);
604
+ result = await this.genericToolHandler(event.tool, event.args, this.createToolCallContext(event, meta, turn));
538
605
  }
539
606
  catch (err) {
540
607
  result = {
@@ -557,45 +624,152 @@ export class PedelecSession {
557
624
  });
558
625
  }
559
626
  catch (err) {
560
- this.emitError(normalizeError(err, "SUBMIT_TOOL_RESULT_FAILED", "submit_tool_result failed"));
627
+ this.emitError(normalizeError(err, "SUBMIT_TOOL_RESULT_FAILED", "submit_tool_result failed"), {
628
+ source: "sdk",
629
+ });
561
630
  }
562
631
  }
563
632
  resolvePendingSend() {
564
633
  const pending = this.pendingSend;
565
634
  this.pendingSend = null;
566
635
  pending?.resolve();
636
+ this.finishActiveTurn();
637
+ }
638
+ resolvePendingPrepare() {
639
+ const pending = this.pendingPrepare;
640
+ this.pendingPrepare = null;
641
+ pending?.resolve();
642
+ this.finishActiveTurn();
643
+ }
644
+ resolveActivePending() {
645
+ if (this.activeTurn?.kind === "prepare") {
646
+ this.resolvePendingPrepare();
647
+ }
648
+ else {
649
+ this.resolvePendingSend();
650
+ }
567
651
  }
568
652
  rejectPendingSend(error) {
569
653
  const pending = this.pendingSend;
570
654
  this.pendingSend = null;
571
655
  pending?.reject(error);
656
+ this.finishActiveTurn();
657
+ }
658
+ rejectPendingPrepare(error) {
659
+ const pending = this.pendingPrepare;
660
+ this.pendingPrepare = null;
661
+ pending?.reject(error);
662
+ this.finishActiveTurn();
663
+ }
664
+ rejectActivePending(error) {
665
+ if (this.activeTurn?.kind === "prepare") {
666
+ this.rejectPendingPrepare(error);
667
+ }
668
+ else {
669
+ this.rejectPendingSend(error);
670
+ }
572
671
  }
573
672
  clearPendingSend() {
574
673
  this.pendingSend = null;
575
674
  }
576
- markEnded() {
675
+ clearPendingPrepare() {
676
+ this.pendingPrepare = null;
677
+ }
678
+ markEnded(meta = { source: "sdk" }) {
577
679
  const wasEnded = this.status === "ended";
578
- this.setStatus("ended");
579
- this.rejectPendingSend(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
680
+ this.setStatus("ended", meta);
580
681
  if (!wasEnded) {
581
682
  for (const handler of this.endedHandlers) {
582
- handler();
683
+ handler(this.createEndedContext(meta));
583
684
  }
584
685
  }
686
+ this.rejectPendingSend(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
687
+ this.rejectPendingPrepare(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
688
+ this.finishActiveTurn();
585
689
  }
586
- emitError(error) {
690
+ emitError(error, meta = { source: "sdk" }) {
587
691
  for (const handler of this.errorHandlers) {
588
- handler(error);
692
+ handler(error, this.createErrorContext(meta));
589
693
  }
590
694
  }
591
- setStatus(status) {
695
+ setStatus(status, meta = { source: "sdk" }) {
592
696
  if (this.status === status)
593
697
  return;
698
+ const previousStatus = this.status;
594
699
  this.status = status;
595
700
  for (const handler of this.statusHandlers) {
596
- handler(status);
701
+ handler(status, this.createStatusContext(status, previousStatus, meta));
702
+ }
703
+ }
704
+ requireActiveTurn(type, meta) {
705
+ if (this.activeTurn)
706
+ return this.activeTurn;
707
+ this.emitError(makeError("SDK_PROTOCOL_ERROR", `${type} event was received without an active turn`, {
708
+ sessionId: this.sessionId,
709
+ }), { source: "sdk", eventReceivedAt: meta.eventReceivedAt });
710
+ return null;
711
+ }
712
+ finishActiveTurn(turn = this.activeTurn) {
713
+ if (!turn || !this.activeTurn)
714
+ return;
715
+ if (this.activeTurn.turnId === turn.turnId) {
716
+ this.activeTurn = null;
597
717
  }
598
718
  }
719
+ createBaseContext(meta, turn = this.activeTurn) {
720
+ return {
721
+ sessionId: this.sessionId,
722
+ provider: this.provider,
723
+ model: this.model,
724
+ sessionCreatedAt: this.sessionCreatedAt,
725
+ ...(meta.eventReceivedAt === undefined ? {} : { eventReceivedAt: meta.eventReceivedAt }),
726
+ eventEmittedAt: Date.now(),
727
+ ...(turn ? { turnId: turn.turnId, turnStartedAt: turn.turnStartedAt, turnKind: turn.kind } : {}),
728
+ source: meta.source,
729
+ };
730
+ }
731
+ createChatContext(meta, turn) {
732
+ return {
733
+ ...this.createBaseContext(meta, turn),
734
+ type: "chat_delta",
735
+ turnId: turn.turnId,
736
+ turnStartedAt: turn.turnStartedAt,
737
+ eventReceivedAt: meta.eventReceivedAt ?? Date.now(),
738
+ source: "core",
739
+ };
740
+ }
741
+ createToolCallContext(event, meta, turn) {
742
+ return {
743
+ ...this.createBaseContext(meta, turn),
744
+ type: "tool_call",
745
+ toolRequestId: event.toolRequestId,
746
+ tool: event.tool,
747
+ turnId: turn.turnId,
748
+ turnStartedAt: turn.turnStartedAt,
749
+ eventReceivedAt: meta.eventReceivedAt ?? Date.now(),
750
+ source: "core",
751
+ };
752
+ }
753
+ createStatusContext(status, previousStatus, meta) {
754
+ return {
755
+ ...this.createBaseContext(meta),
756
+ type: meta.source === "core" ? "status_changed" : "sdk_status_changed",
757
+ status,
758
+ previousStatus,
759
+ };
760
+ }
761
+ createErrorContext(meta) {
762
+ return {
763
+ ...this.createBaseContext(meta),
764
+ type: meta.source === "core" ? "error" : "sdk_error",
765
+ };
766
+ }
767
+ createEndedContext(meta) {
768
+ return {
769
+ ...this.createBaseContext(meta),
770
+ type: meta.source === "core" ? "ended" : "sdk_ended",
771
+ };
772
+ }
599
773
  }
600
774
  function isSessionEvent(message) {
601
775
  return (message.type === "chat_delta" ||
@@ -651,6 +825,16 @@ function createChannelId() {
651
825
  : Math.random().toString(36).slice(2);
652
826
  return `pedelec_${Date.now()}_${random}`;
653
827
  }
828
+ function createTurn(kind = "user") {
829
+ const random = typeof crypto !== "undefined" && "randomUUID" in crypto
830
+ ? crypto.randomUUID()
831
+ : Math.random().toString(36).slice(2);
832
+ return {
833
+ turnId: `turn_${Date.now()}_${random}`,
834
+ turnStartedAt: Date.now(),
835
+ kind,
836
+ };
837
+ }
654
838
  function getCurrentOrigin(pageWindow) {
655
839
  const origin = pageWindow?.location?.origin;
656
840
  return typeof origin === "string" && origin ? origin : null;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ import { Pedelec, defineTool, } from "./index";
2
+ async function typedOnToolNameFromCreateSession() {
3
+ const pedelec = new Pedelec();
4
+ const session = await pedelec.createSession({
5
+ provider: "codex",
6
+ skills: {
7
+ guidance: "Use available tools.",
8
+ tools: [
9
+ defineTool({
10
+ name: "get_selection",
11
+ description: "Get selected text.",
12
+ argsSchema: { type: "object", properties: {}, required: [] },
13
+ }),
14
+ defineTool({
15
+ name: "replace_text",
16
+ description: "Replace selected text.",
17
+ argsSchema: { type: "object", properties: {}, required: [] },
18
+ }),
19
+ ],
20
+ },
21
+ });
22
+ session.onTool((name, args, ctx) => {
23
+ const allowed = name;
24
+ const stillUnknown = args;
25
+ const toolCtx = ctx;
26
+ return { allowed, stillUnknown, toolCtx };
27
+ });
28
+ session.onTool("get_selection", async (args, ctx) => {
29
+ const stillUnknown = args;
30
+ const toolName = ctx.tool;
31
+ const turnId = ctx.turnId;
32
+ return { ok: true, stillUnknown, toolName, turnId };
33
+ });
34
+ session.onTool("replace_text", async () => {
35
+ return { ok: true };
36
+ });
37
+ // @ts-expect-error tool name must come from skills.tools[].name
38
+ session.onTool("not_exists", async () => {
39
+ return { ok: false };
40
+ });
41
+ session.onTool((name, _args, ctx) => {
42
+ // @ts-expect-error generic handler name should not be arbitrary string
43
+ const invalid = name;
44
+ // @ts-expect-error user-facing context must not expose core seq
45
+ const noSeq = ctx.seq;
46
+ return noSeq ?? invalid;
47
+ });
48
+ session.onChat((_text, ctx) => {
49
+ const chatCtx = ctx;
50
+ const receivedAt = ctx.eventReceivedAt;
51
+ void receivedAt;
52
+ void chatCtx;
53
+ });
54
+ session.onStatus((_status, ctx) => {
55
+ const statusCtx = ctx;
56
+ const previous = ctx.previousStatus;
57
+ void previous;
58
+ void statusCtx;
59
+ });
60
+ session.onError((_error, ctx) => {
61
+ const errorCtx = ctx;
62
+ return errorCtx;
63
+ });
64
+ session.onEnded((ctx) => {
65
+ const endedCtx = ctx;
66
+ return endedCtx;
67
+ });
68
+ }
69
+ async function resumedSessionFallsBackToString() {
70
+ const pedelec = new Pedelec();
71
+ const session = await pedelec.resumeSession("thread_1");
72
+ session.onTool((name) => {
73
+ const anyString = name;
74
+ return anyString;
75
+ });
76
+ session.onTool("runtime_tool_name", async () => {
77
+ return { ok: true };
78
+ });
79
+ }
80
+ async function noSkillsFallsBackToString() {
81
+ const pedelec = new Pedelec();
82
+ const session = await pedelec.createSession({ provider: "codex" });
83
+ session.onTool((name) => {
84
+ const anyString = name;
85
+ return anyString;
86
+ });
87
+ session.onTool("runtime_tool_name", async () => {
88
+ return { ok: true };
89
+ });
90
+ }
91
+ void typedOnToolNameFromCreateSession;
92
+ void resumedSessionFallsBackToString;
93
+ void noSkillsFallsBackToString;
94
+ const baseContext = {
95
+ sessionId: "thread_1",
96
+ provider: "codex",
97
+ sessionCreatedAt: Date.now(),
98
+ eventEmittedAt: Date.now(),
99
+ source: "sdk",
100
+ };
101
+ void baseContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,7 +26,8 @@
26
26
  "scripts": {
27
27
  "build": "tsc -p tsconfig.json",
28
28
  "test": "vitest run",
29
- "prepublishOnly": "npm run test && npm run build"
29
+ "type-test": "tsc -p tsconfig.type-test.json",
30
+ "prepublishOnly": "npm run test && npm run type-test && npm run build"
30
31
  },
31
32
  "devDependencies": {
32
33
  "typescript": "^5.8.3",