@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/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { PEDELEC_EXTENSION_ID } from "./extension-id.js";
2
2
  import { SDK_VERSION } from "./version.generated.js";
3
3
  const SDK_EXTERNAL_PORT_NAME = "pedelec-sdk-external";
4
+ const PAGE_ACTIVITY_MESSAGE_TYPE = "page_activity";
5
+ const SDK_HELLO_MESSAGE_TYPE = "sdk_hello";
4
6
  const DEFAULT_BRIDGE_TIMEOUT_MS = 30_000;
5
7
  function normalizeListAssetsResponse(response) {
6
8
  if (!response || typeof response !== "object" || Array.isArray(response))
@@ -124,6 +126,8 @@ export class Pedelec {
124
126
  lastSeqBySession = new Map();
125
127
  nextRequestNumber = 1;
126
128
  port = null;
129
+ pageActivityListenersAttached = false;
130
+ lastReportedPageActive = null;
127
131
  constructor(options = {}) {
128
132
  this.pageWindow = typeof window === "undefined" ? null : window;
129
133
  this.channelId = createChannelId();
@@ -131,6 +135,7 @@ export class Pedelec {
131
135
  if (!this.pageWindow) {
132
136
  return;
133
137
  }
138
+ this.attachPageActivityListeners();
134
139
  this.connectExtension();
135
140
  }
136
141
  async createSession(input = {}) {
@@ -141,18 +146,18 @@ export class Pedelec {
141
146
  provider: resolvedInput.provider,
142
147
  effortLevel: resolvedInput.effortLevel,
143
148
  skills: resolvedInput.skills,
144
- sandbox: resolvedInput.sandbox,
149
+ workspace: resolvedInput.workspace,
145
150
  autoEndOnDisconnect: resolvedInput.autoEndOnDisconnect,
146
151
  },
147
152
  });
148
153
  if (!result.sessionId) {
149
154
  throw makeError("SDK_PROTOCOL_ERROR", "create_session response did not include sessionId");
150
155
  }
151
- return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.effortLevel, resolvedInput.inlineToolHandlers);
156
+ return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.effortLevel, resolvedInput.inlineToolHandlers, resolvedInput.autoEndOnDisconnect);
152
157
  }
153
- async sandboxFolderPicker() {
154
- const result = await this.requestWithOptions("pick_sandbox_folder", {}, { timeoutMs: null });
155
- return normalizeSandboxFolderPickerResponse(result);
158
+ async workspaceFolderPicker() {
159
+ const result = await this.requestWithOptions("pick_workspace_folder", {}, { timeoutMs: null });
160
+ return normalizeWorkspaceFolderPickerResponse(result);
156
161
  }
157
162
  async listProviders() {
158
163
  return normalizeProviderInfoList(await this.request("list_providers"));
@@ -182,13 +187,27 @@ export class Pedelec {
182
187
  if (!sessionId?.trim()) {
183
188
  throw makeError("INVALID_INPUT", "sessionId is required");
184
189
  }
185
- const result = await this.request("resume_session", {
186
- sessionId,
187
- });
188
- if (!result.sessionId) {
189
- throw makeError("SDK_PROTOCOL_ERROR", "resume_session response did not include sessionId");
190
+ const existing = this.sessions.get(sessionId);
191
+ const session = existing && !existing.isTransportDetached()
192
+ ? existing
193
+ : this.registerSession(sessionId, "", undefined, new Map(), false);
194
+ try {
195
+ const result = await this.request("resume_session", {
196
+ sessionId,
197
+ });
198
+ if (!result.sessionId) {
199
+ throw makeError("SDK_PROTOCOL_ERROR", "resume_session response did not include sessionId");
200
+ }
201
+ if (typeof result.autoEndOnDisconnect === "boolean") {
202
+ session.setAutoEndOnDisconnect(result.autoEndOnDisconnect);
203
+ }
204
+ return session;
205
+ }
206
+ catch (error) {
207
+ if (session !== existing)
208
+ this.unregisterSession(sessionId);
209
+ throw error;
190
210
  }
191
- return this.registerSession(result.sessionId, "", undefined);
192
211
  }
193
212
  request(type, payload = {}) {
194
213
  return this.requestWithOptions(type, payload);
@@ -245,6 +264,76 @@ export class Pedelec {
245
264
  this.sessions.delete(sessionId);
246
265
  this.lastSeqBySession.delete(sessionId);
247
266
  }
267
+ /** @internal */
268
+ registerExistingSession(session) {
269
+ this.sessions.set(session.sessionId, session);
270
+ }
271
+ /** @internal */
272
+ isSessionRegistered(sessionId, session) {
273
+ return this.sessions.get(sessionId) === session;
274
+ }
275
+ attachPageActivityListeners() {
276
+ if (this.pageActivityListenersAttached || !this.pageWindow)
277
+ return;
278
+ this.pageActivityListenersAttached = true;
279
+ this.pageWindow.addEventListener("focus", () => {
280
+ if (!this.isDocumentHidden())
281
+ this.reportPageActivity(true, { force: true });
282
+ });
283
+ this.pageWindow.document?.addEventListener("visibilitychange", () => {
284
+ if (this.isDocumentHidden()) {
285
+ this.reportPageActivity(false);
286
+ return;
287
+ }
288
+ if (this.isDocumentFocused())
289
+ this.reportPageActivity(true);
290
+ });
291
+ }
292
+ isDocumentHidden() {
293
+ const doc = this.pageWindow?.document;
294
+ if (!doc)
295
+ return true;
296
+ if (typeof doc.hidden === "boolean")
297
+ return doc.hidden;
298
+ return doc.visibilityState === "hidden";
299
+ }
300
+ isDocumentFocused() {
301
+ const doc = this.pageWindow?.document;
302
+ if (!doc || typeof doc.hasFocus !== "function")
303
+ return false;
304
+ try {
305
+ return doc.hasFocus();
306
+ }
307
+ catch (_err) {
308
+ return false;
309
+ }
310
+ }
311
+ isPageForegroundActive() {
312
+ return !this.isDocumentHidden() && this.isDocumentFocused();
313
+ }
314
+ postSdkHello(port) {
315
+ try {
316
+ port.postMessage({ type: SDK_HELLO_MESSAGE_TYPE, sdkVersion: SDK_VERSION });
317
+ }
318
+ catch (err) {
319
+ const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
320
+ this.handleDisconnect(port, error);
321
+ }
322
+ }
323
+ reportPageActivity(active, options = {}) {
324
+ if (!this.port)
325
+ return;
326
+ if (!options.force && this.lastReportedPageActive === active)
327
+ return;
328
+ try {
329
+ this.port.postMessage({ type: PAGE_ACTIVITY_MESSAGE_TYPE, active });
330
+ this.lastReportedPageActive = active;
331
+ }
332
+ catch (err) {
333
+ const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
334
+ this.handleDisconnect(this.port, error);
335
+ }
336
+ }
248
337
  resolveCreateSessionInput(input) {
249
338
  const raw = (input ?? {});
250
339
  const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
@@ -258,15 +347,15 @@ export class Pedelec {
258
347
  }
259
348
  const autoEndOnDisconnect = raw.autoEndOnDisconnect !== false;
260
349
  const normalizedSkills = normalizeSkillsInput(raw.skills);
261
- const sandbox = normalizeCreateSessionSandboxInput(raw.sandbox);
350
+ const workspace = normalizeCreateSessionWorkspaceInput(raw.workspace);
262
351
  if (!hasProvider) {
263
- return this.resolveDefaultCreateSessionInput(effortLevel, normalizedSkills, sandbox, autoEndOnDisconnect);
352
+ return this.resolveDefaultCreateSessionInput(effortLevel, normalizedSkills, workspace, autoEndOnDisconnect);
264
353
  }
265
354
  return {
266
355
  provider: provider,
267
356
  effortLevel,
268
357
  skills: normalizedSkills.manifest,
269
- sandbox,
358
+ workspace,
270
359
  inlineToolHandlers: normalizedSkills.handlers,
271
360
  autoEndOnDisconnect,
272
361
  };
@@ -323,7 +412,7 @@ export class Pedelec {
323
412
  };
324
413
  }
325
414
  }
326
- async resolveDefaultCreateSessionInput(effortLevel, normalizedSkills, sandbox, autoEndOnDisconnect) {
415
+ async resolveDefaultCreateSessionInput(effortLevel, normalizedSkills, workspace, autoEndOnDisconnect) {
327
416
  const settings = await this.getSettings();
328
417
  if (!settings.defaultProvider) {
329
418
  throw makeError("DEFAULT_PROVIDER_NOT_SET", "Default provider is not set. Open the Pedelec desktop app Settings page and choose a default provider.");
@@ -333,7 +422,7 @@ export class Pedelec {
333
422
  provider: settings.defaultProvider,
334
423
  effortLevel,
335
424
  skills: normalizedSkills.manifest,
336
- sandbox,
425
+ workspace,
337
426
  inlineToolHandlers: normalizedSkills.handlers,
338
427
  autoEndOnDisconnect,
339
428
  };
@@ -345,13 +434,13 @@ export class Pedelec {
345
434
  throw makeError("DEFAULT_PROVIDER_UNAVAILABLE", "Default provider is not currently available. Open the Pedelec desktop app Settings page and choose an available provider.", { provider });
346
435
  }
347
436
  }
348
- registerSession(sessionId, provider, effortLevel, inlineToolHandlers = new Map()) {
437
+ registerSession(sessionId, provider, effortLevel, inlineToolHandlers = new Map(), autoEndOnDisconnect = false) {
349
438
  const existing = this.sessions.get(sessionId);
350
439
  if (existing && !existing.isTransportDetached()) {
351
440
  existing.replaceInlineToolHandlers(inlineToolHandlers);
352
441
  return existing;
353
442
  }
354
- const session = new PedelecSession(this, sessionId, provider, effortLevel, inlineToolHandlers);
443
+ const session = new PedelecSession(this, sessionId, provider, effortLevel, inlineToolHandlers, autoEndOnDisconnect);
355
444
  this.sessions.set(sessionId, session);
356
445
  return session;
357
446
  }
@@ -370,10 +459,13 @@ export class Pedelec {
370
459
  port.onMessage.addListener((message) => this.handlePortMessage(port, message));
371
460
  port.onDisconnect.addListener(() => this.handleDisconnect(port));
372
461
  this.port = port;
462
+ this.postSdkHello(port);
463
+ this.reportPageActivity(this.isPageForegroundActive(), { force: true });
373
464
  return port;
374
465
  }
375
466
  catch (err) {
376
467
  this.port = null;
468
+ this.lastReportedPageActive = null;
377
469
  throw normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
378
470
  }
379
471
  }
@@ -398,6 +490,22 @@ export class Pedelec {
398
490
  this.handleResponse(message);
399
491
  return;
400
492
  }
493
+ if (message.type === "session_snapshot") {
494
+ if (message.sessionId) {
495
+ const latestSeq = message.snapshot?.latestSeq;
496
+ if (typeof latestSeq === "number") {
497
+ const current = this.lastSeqBySession.get(message.sessionId);
498
+ if (current === undefined || latestSeq > current) {
499
+ this.lastSeqBySession.set(message.sessionId, latestSeq);
500
+ }
501
+ }
502
+ this.sessions.get(message.sessionId)?.handleSnapshot(message.snapshot, {
503
+ source: "core",
504
+ eventReceivedAt: Date.now(),
505
+ });
506
+ }
507
+ return;
508
+ }
401
509
  if (isSessionEvent(message) && this.isNewEvent(message)) {
402
510
  const eventReceivedAt = Date.now();
403
511
  if (message.sessionId) {
@@ -441,6 +549,7 @@ export class Pedelec {
441
549
  const runtime = globalThis.chrome?.runtime;
442
550
  const error = normalizeError(disconnectError ?? runtime?.lastError, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
443
551
  this.port = null;
552
+ this.lastReportedPageActive = null;
444
553
  this.invalidateSessions(error);
445
554
  for (const [requestId, pending] of this.pendingRequests) {
446
555
  if (pending.port !== port)
@@ -462,18 +571,18 @@ export class Pedelec {
462
571
  }
463
572
  }
464
573
  }
465
- function normalizeCreateSessionSandboxInput(value) {
574
+ function normalizeCreateSessionWorkspaceInput(value) {
466
575
  if (value === undefined)
467
576
  return undefined;
468
577
  if (!value || typeof value !== "object" || Array.isArray(value)) {
469
- throw makeError("INVALID_INPUT", "sandbox must be an object");
578
+ throw makeError("INVALID_INPUT", "workspace must be an object");
470
579
  }
471
580
  const path = value.path;
472
581
  if (typeof path !== "string") {
473
- throw makeError("INVALID_INPUT", "sandbox.path must be a string");
582
+ throw makeError("INVALID_INPUT", "workspace.path must be a string");
474
583
  }
475
584
  if (path.trim().length === 0) {
476
- throw makeError("INVALID_INPUT", "sandbox.path must not be empty");
585
+ throw makeError("INVALID_INPUT", "workspace.path must not be empty");
477
586
  }
478
587
  return { path };
479
588
  }
@@ -483,34 +592,43 @@ export class PedelecSession {
483
592
  provider;
484
593
  effortLevel;
485
594
  sessionCreatedAt = Date.now();
595
+ usage = { totalTokens: undefined };
486
596
  status = "idle";
487
- pendingSend = null;
488
- pendingPrepare = null;
597
+ pendingOperation = null;
489
598
  preparePromise = null;
490
599
  uploadPromise = null;
600
+ reactivationPromise = null;
491
601
  ending = false;
492
602
  prepared = false;
603
+ autoEndOnDisconnect;
493
604
  activeTurn = null;
494
605
  transportDetached = false;
495
606
  genericToolHandler = null;
496
607
  inlineToolHandlers = new Map();
497
608
  namedToolHandlers = new Map();
498
609
  chatHandlers = new Set();
610
+ chatDeltaHandlers = new Set();
499
611
  errorHandlers = new Set();
500
612
  statusHandlers = new Set();
501
613
  endedHandlers = new Set();
502
- constructor(client, sessionId, provider, effortLevel, inlineToolHandlers = new Map()) {
614
+ recoveredToolCalls = new Map();
615
+ handledToolRequestIds = new Set();
616
+ authoritativePendingToolRequest = undefined;
617
+ constructor(client, sessionId, provider, effortLevel, inlineToolHandlers = new Map(), autoEndOnDisconnect = false) {
503
618
  this.client = client;
504
619
  this.sessionId = sessionId;
505
620
  this.provider = provider;
506
621
  this.effortLevel = effortLevel;
507
622
  this.inlineToolHandlers = new Map(inlineToolHandlers);
623
+ this.autoEndOnDisconnect = autoEndOnDisconnect;
508
624
  }
509
625
  prepare() {
510
626
  if (this.transportDetached || this.status === "ended") {
511
627
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
512
628
  }
513
- if (this.pendingSend) {
629
+ if (this.pendingOperation?.turn.kind === "user" ||
630
+ (this.activeTurn && this.activeTurn.kind !== "prepare") ||
631
+ this.activeTurn?.external) {
514
632
  return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
515
633
  }
516
634
  if (this.prepared) {
@@ -520,24 +638,32 @@ export class PedelecSession {
520
638
  return this.preparePromise;
521
639
  }
522
640
  const turn = createTurn("prepare");
641
+ const priorStatus = this.status;
523
642
  this.activeTurn = turn;
524
643
  this.setStatus("running", { source: "sdk" });
525
- const donePromise = new Promise((resolve, reject) => {
526
- this.pendingPrepare = { resolve, reject };
644
+ const operationPromise = new Promise((resolve, reject) => {
645
+ this.pendingOperation = {
646
+ turn,
647
+ resolve,
648
+ reject,
649
+ priorStatus,
650
+ requestSettled: false,
651
+ };
527
652
  });
528
- const requestPromise = this.client
653
+ void this.client
529
654
  .request("prepare_session", {
530
655
  sessionId: this.sessionId,
656
+ operationId: turn.turnId,
531
657
  })
532
- .catch((err) => {
658
+ .then(() => {
659
+ this.markOperationRequestSettled(turn);
660
+ }, (err) => {
533
661
  const error = normalizeError(err, "PREPARE_FAILED", "prepare failed");
534
662
  if (!this.transportDetached)
535
663
  this.emitError(error, { source: "sdk" });
536
- this.clearPendingPrepare();
537
- this.finishActiveTurn(turn);
538
- throw error;
664
+ this.failOperationFromRequest(turn, error);
539
665
  });
540
- this.preparePromise = Promise.all([requestPromise, donePromise])
666
+ this.preparePromise = operationPromise
541
667
  .then(() => {
542
668
  this.prepared = true;
543
669
  })
@@ -558,34 +684,46 @@ export class PedelecSession {
558
684
  if (this.transportDetached || this.status === "ended") {
559
685
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
560
686
  }
561
- if (this.pendingSend || this.pendingPrepare) {
687
+ if (this.pendingOperation || this.activeTurn) {
562
688
  return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
563
689
  }
564
690
  const turn = createTurn("user");
691
+ const priorStatus = this.status;
565
692
  this.activeTurn = turn;
566
693
  this.setStatus("running", { source: "sdk" });
567
- const donePromise = new Promise((resolve, reject) => {
568
- this.pendingSend = { resolve, reject };
694
+ const operationPromise = new Promise((resolve, reject) => {
695
+ this.pendingOperation = {
696
+ turn,
697
+ resolve,
698
+ reject,
699
+ priorStatus,
700
+ requestSettled: false,
701
+ };
569
702
  });
570
- const requestPromise = this.client
703
+ void this.client
571
704
  .request("send_text", {
572
705
  sessionId: this.sessionId,
573
706
  text,
707
+ operationId: turn.turnId,
574
708
  })
575
- .catch((err) => {
709
+ .then(() => {
710
+ this.markOperationRequestSettled(turn);
711
+ }, (err) => {
576
712
  const error = normalizeError(err, "SEND_TEXT_FAILED", "sendText failed");
577
713
  if (!this.transportDetached)
578
714
  this.emitError(error, { source: "sdk" });
579
- this.clearPendingSend();
580
- this.finishActiveTurn(turn);
581
- throw error;
715
+ this.failOperationFromRequest(turn, error);
582
716
  });
583
- return Promise.all([requestPromise, donePromise]).then(() => undefined);
717
+ return operationPromise;
584
718
  }
585
719
  onChat(handler) {
586
720
  this.chatHandlers.add(handler);
587
721
  return () => this.chatHandlers.delete(handler);
588
722
  }
723
+ onChatDelta(handler) {
724
+ this.chatDeltaHandlers.add(handler);
725
+ return () => this.chatDeltaHandlers.delete(handler);
726
+ }
589
727
  onTool(toolNameOrHandler, maybeHandler) {
590
728
  if (typeof toolNameOrHandler === "string") {
591
729
  const toolName = toolNameOrHandler;
@@ -596,6 +734,7 @@ export class PedelecSession {
596
734
  throw makeError("INVALID_INPUT", "tool handler must be a function", { toolName });
597
735
  }
598
736
  this.namedToolHandlers.set(toolName, maybeHandler);
737
+ void this.drainRecoveredToolCalls();
599
738
  return () => {
600
739
  if (this.namedToolHandlers.get(toolName) === maybeHandler) {
601
740
  this.namedToolHandlers.delete(toolName);
@@ -604,6 +743,7 @@ export class PedelecSession {
604
743
  }
605
744
  const handler = toolNameOrHandler;
606
745
  this.genericToolHandler = handler;
746
+ void this.drainRecoveredToolCalls();
607
747
  return () => {
608
748
  if (this.genericToolHandler === handler) {
609
749
  this.genericToolHandler = null;
@@ -743,9 +883,73 @@ export class PedelecSession {
743
883
  this.markEnded({ source: "sdk" });
744
884
  this.client.unregisterSession(this.sessionId);
745
885
  }
886
+ resume() {
887
+ if (this.transportDetached) {
888
+ return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
889
+ }
890
+ if (this.reactivationPromise)
891
+ return this.reactivationPromise;
892
+ if (this.status !== "ended")
893
+ return Promise.resolve();
894
+ const wasRegistered = this.client.isSessionRegistered(this.sessionId, this);
895
+ const promise = (async () => {
896
+ if (!wasRegistered) {
897
+ // end() intentionally removed the handle from the registry. Put the
898
+ // exact same object back before the ended snapshot can arrive.
899
+ this.client.registerExistingSession(this);
900
+ }
901
+ try {
902
+ const result = await this.client.request("reactivate_session", {
903
+ sessionId: this.sessionId,
904
+ autoEndOnDisconnect: this.autoEndOnDisconnect,
905
+ });
906
+ if (result?.sessionId !== this.sessionId) {
907
+ throw makeError("SDK_PROTOCOL_ERROR", "reactivate_session response did not include sessionId");
908
+ }
909
+ if (this.status !== "idle") {
910
+ throw makeError("SDK_PROTOCOL_ERROR", "reactivate_session resolved before the authoritative idle snapshot", { sessionId: this.sessionId });
911
+ }
912
+ this.ending = false;
913
+ }
914
+ catch (err) {
915
+ const error = normalizeError(err, "SESSION_RESUME_FAILED", "session resume failed");
916
+ if (!this.transportDetached)
917
+ this.emitError(error, { source: "sdk" });
918
+ if (!wasRegistered && isDeterministicResumeError(error) && this.status !== "idle") {
919
+ this.client.unregisterSession(this.sessionId);
920
+ }
921
+ throw error;
922
+ }
923
+ })();
924
+ const guardedPromise = promise.finally(() => {
925
+ if (this.reactivationPromise === guardedPromise)
926
+ this.reactivationPromise = null;
927
+ });
928
+ this.reactivationPromise = guardedPromise;
929
+ return guardedPromise;
930
+ }
931
+ /** @internal */
932
+ setAutoEndOnDisconnect(autoEndOnDisconnect) {
933
+ this.autoEndOnDisconnect = autoEndOnDisconnect;
934
+ }
746
935
  handleEvent(event, meta = { source: "sdk" }) {
936
+ if (event.type === "usage_updated") {
937
+ this.updateUsage(event.totalTokens);
938
+ return;
939
+ }
747
940
  if (event.type === "chat_delta") {
748
- const turn = this.requireActiveTurn("chat_delta", meta);
941
+ const turn = this.requireOperationTurn(event.operationId, "chat_delta", meta);
942
+ if (!turn)
943
+ return;
944
+ if (turn.kind === "prepare")
945
+ return;
946
+ for (const handler of this.chatDeltaHandlers) {
947
+ handler(event.text, this.createChatDeltaContext(meta, turn));
948
+ }
949
+ return;
950
+ }
951
+ if (event.type === "chat_message") {
952
+ const turn = this.requireOperationTurn(event.operationId, "chat_message", meta);
749
953
  if (!turn)
750
954
  return;
751
955
  if (turn.kind === "prepare")
@@ -756,44 +960,109 @@ export class PedelecSession {
756
960
  return;
757
961
  }
758
962
  if (event.type === "status_changed") {
963
+ if (event.operationId && !this.matchesActiveOperation(event.operationId))
964
+ return;
965
+ if (!event.operationId && this.activeTurn)
966
+ return;
759
967
  this.setStatus(event.status, meta);
760
- if (event.status === "idle") {
761
- this.resolveActivePending();
762
- }
763
- else if (event.status === "ended") {
968
+ if (event.status === "idle")
969
+ this.ending = false;
970
+ if (event.status === "ended") {
764
971
  this.markEnded(meta);
765
972
  }
766
973
  else if (event.status === "error") {
767
974
  const error = makeError("SESSION_ERROR", "session entered error status", { sessionId: this.sessionId });
768
975
  this.emitError(error, meta);
769
- this.rejectActivePending(error);
770
976
  }
771
977
  return;
772
978
  }
773
979
  if (event.type === "tool_call") {
774
- const turn = this.requireActiveTurn("tool_call", meta);
980
+ const turn = this.requireOperationTurn(event.operationId, "tool_call", meta);
775
981
  if (!turn)
776
982
  return;
983
+ this.authoritativePendingToolRequest = {
984
+ operationId: event.operationId,
985
+ requestId: event.toolRequestId,
986
+ };
777
987
  this.setStatus("waiting_tool_result", meta);
778
- this.handleToolCall(event, meta, turn);
988
+ if (this.recoveredToolCalls.has(event.toolRequestId)) {
989
+ this.recoveredToolCalls.delete(event.toolRequestId);
990
+ }
991
+ if (turn.external && !this.hasToolHandler(event.tool)) {
992
+ this.recoveredToolCalls.set(event.toolRequestId, event);
993
+ return;
994
+ }
995
+ void this.handleToolCallOnce(event, meta, turn);
779
996
  return;
780
997
  }
781
- if (event.type === "done") {
782
- this.setStatus("idle", meta);
783
- this.resolveActivePending();
998
+ if (event.type === "operation_completed") {
999
+ if (!this.matchesActiveOperation(event.operationId))
1000
+ return;
1001
+ if (this.activeTurn?.kind !== event.operationKind)
1002
+ return;
1003
+ this.authoritativePendingToolRequest = null;
1004
+ this.setStatus(event.success ? "idle" : event.operationKind === "prepare" ? "idle" : "error", meta);
1005
+ this.completeOperation(this.activeTurn, event.success, event.error);
784
1006
  return;
785
1007
  }
786
1008
  if (event.type === "error") {
787
- this.setStatus("error", meta);
1009
+ if (event.operationId && !this.matchesActiveOperation(event.operationId))
1010
+ return;
788
1011
  const error = normalizeError(event.error, "SESSION_ERROR", "session error");
789
1012
  this.emitError(error, meta);
790
- this.rejectActivePending(error);
1013
+ if (!event.operationId)
1014
+ this.setStatus("error", meta);
791
1015
  return;
792
1016
  }
793
1017
  if (event.type === "ended") {
794
1018
  this.markEnded(meta);
795
1019
  }
796
1020
  }
1021
+ /** @internal */
1022
+ handleSnapshot(snapshot, meta = { source: "core" }) {
1023
+ if (!snapshot || snapshot.threadId !== this.sessionId)
1024
+ return;
1025
+ if (snapshot.usage)
1026
+ this.updateUsage(snapshot.usage.totalTokens);
1027
+ if (snapshot.status === "idle")
1028
+ this.ending = false;
1029
+ const active = snapshot.activeOperation;
1030
+ const pending = this.pendingOperation;
1031
+ this.reconcileRecoveredToolCalls(snapshot);
1032
+ if (!pending) {
1033
+ this.setStatus(coreStatusToSdkStatus(snapshot.status), meta);
1034
+ if (active) {
1035
+ this.activeTurn = {
1036
+ turnId: active.operationId,
1037
+ turnStartedAt: parseSnapshotTime(active.startedAt),
1038
+ kind: active.operationKind,
1039
+ external: true,
1040
+ };
1041
+ this.queueSnapshotTool(snapshot, this.activeTurn);
1042
+ }
1043
+ else if (this.activeTurn?.external) {
1044
+ this.activeTurn = null;
1045
+ }
1046
+ if (snapshot.status === "ended")
1047
+ this.markEnded(meta);
1048
+ return;
1049
+ }
1050
+ if (active?.operationId === pending.turn.turnId && active.operationKind === pending.turn.kind) {
1051
+ this.setStatus(coreStatusToSdkStatus(snapshot.status), meta);
1052
+ this.queueSnapshotTool(snapshot, pending.turn);
1053
+ return;
1054
+ }
1055
+ const completed = snapshot.lastCompletedOperation;
1056
+ if (completed?.operationId === pending.turn.turnId && completed.operationKind === pending.turn.kind) {
1057
+ this.setStatus(completed.success ? "idle" : completed.operationKind === "prepare" ? "idle" : "error", meta);
1058
+ this.completeOperation(pending.turn, completed.success, completed.error);
1059
+ return;
1060
+ }
1061
+ const error = makeError("SDK_LIFECYCLE_SYNC_ERROR", "authoritative session snapshot did not match the active SDK operation", { sessionId: this.sessionId, operationId: pending.turn.turnId });
1062
+ this.emitError(error, { source: "sdk" });
1063
+ this.rejectOperation(pending.turn, error);
1064
+ this.setStatus(coreStatusToSdkStatus(snapshot.status), meta);
1065
+ }
797
1066
  replaceInlineToolHandlers(handlers) {
798
1067
  this.inlineToolHandlers = new Map(handlers);
799
1068
  }
@@ -810,7 +1079,10 @@ export class PedelecSession {
810
1079
  this.emitError(error, { source: "sdk" });
811
1080
  this.rejectActivePending(error);
812
1081
  }
813
- async handleToolCall(event, meta, turn) {
1082
+ async handleToolCallOnce(event, meta, turn) {
1083
+ if (this.handledToolRequestIds.has(event.toolRequestId))
1084
+ return;
1085
+ this.handledToolRequestIds.add(event.toolRequestId);
814
1086
  let result;
815
1087
  const namedHandler = this.namedToolHandlers.get(event.tool);
816
1088
  const inlineHandler = this.inlineToolHandlers.get(event.tool);
@@ -851,7 +1123,7 @@ export class PedelecSession {
851
1123
  }),
852
1124
  };
853
1125
  }
854
- if (this.transportDetached)
1126
+ if (this.transportDetached || !this.isAuthoritativeToolRequestCurrent(event))
855
1127
  return;
856
1128
  try {
857
1129
  await this.client.request("submit_tool_result", {
@@ -866,51 +1138,10 @@ export class PedelecSession {
866
1138
  });
867
1139
  }
868
1140
  }
869
- resolvePendingSend() {
870
- const pending = this.pendingSend;
871
- this.pendingSend = null;
872
- pending?.resolve();
873
- this.finishActiveTurn();
874
- }
875
- resolvePendingPrepare() {
876
- const pending = this.pendingPrepare;
877
- this.pendingPrepare = null;
878
- pending?.resolve();
879
- this.finishActiveTurn();
880
- }
881
- resolveActivePending() {
882
- if (this.activeTurn?.kind === "prepare") {
883
- this.resolvePendingPrepare();
884
- }
885
- else {
886
- this.resolvePendingSend();
887
- }
888
- }
889
- rejectPendingSend(error) {
890
- const pending = this.pendingSend;
891
- this.pendingSend = null;
892
- pending?.reject(error);
893
- this.finishActiveTurn();
894
- }
895
- rejectPendingPrepare(error) {
896
- const pending = this.pendingPrepare;
897
- this.pendingPrepare = null;
898
- pending?.reject(error);
899
- this.finishActiveTurn();
900
- }
901
1141
  rejectActivePending(error) {
902
- if (this.activeTurn?.kind === "prepare") {
903
- this.rejectPendingPrepare(error);
904
- }
905
- else {
906
- this.rejectPendingSend(error);
907
- }
908
- }
909
- clearPendingSend() {
910
- this.pendingSend = null;
911
- }
912
- clearPendingPrepare() {
913
- this.pendingPrepare = null;
1142
+ const turn = this.activeTurn;
1143
+ if (turn)
1144
+ this.rejectOperation(turn, error);
914
1145
  }
915
1146
  markEnded(meta = { source: "sdk" }) {
916
1147
  const wasEnded = this.status === "ended";
@@ -920,9 +1151,9 @@ export class PedelecSession {
920
1151
  handler(this.createEndedContext(meta));
921
1152
  }
922
1153
  }
923
- this.rejectPendingSend(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
924
- this.rejectPendingPrepare(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
925
- this.finishActiveTurn();
1154
+ const error = makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId });
1155
+ if (this.activeTurn)
1156
+ this.rejectOperation(this.activeTurn, error);
926
1157
  }
927
1158
  emitError(error, meta = { source: "sdk" }) {
928
1159
  for (const handler of this.errorHandlers) {
@@ -938,14 +1169,140 @@ export class PedelecSession {
938
1169
  handler(status, this.createStatusContext(status, previousStatus, meta));
939
1170
  }
940
1171
  }
941
- requireActiveTurn(type, meta) {
942
- if (this.activeTurn)
1172
+ requireOperationTurn(operationId, type, meta) {
1173
+ if (operationId && this.activeTurn?.turnId === operationId) {
943
1174
  return this.activeTurn;
944
- this.emitError(makeError("SDK_PROTOCOL_ERROR", `${type} event was received without an active turn`, {
1175
+ }
1176
+ this.emitError(makeError("SDK_PROTOCOL_ERROR", `${type} event did not match the active operation`, {
945
1177
  sessionId: this.sessionId,
1178
+ operationId,
946
1179
  }), { source: "sdk", eventReceivedAt: meta.eventReceivedAt });
947
1180
  return null;
948
1181
  }
1182
+ updateUsage(totalTokens) {
1183
+ if (!isValidSessionUsageTotalTokens(totalTokens))
1184
+ return;
1185
+ if (this.usage.totalTokens === undefined || totalTokens >= this.usage.totalTokens) {
1186
+ this.usage.totalTokens = totalTokens;
1187
+ }
1188
+ }
1189
+ matchesActiveOperation(operationId) {
1190
+ return this.activeTurn?.turnId === operationId;
1191
+ }
1192
+ markOperationRequestSettled(turn) {
1193
+ const pending = this.pendingOperation;
1194
+ if (!pending || pending.turn.turnId !== turn.turnId)
1195
+ return;
1196
+ pending.requestSettled = true;
1197
+ this.maybeSettleOperation(pending);
1198
+ }
1199
+ failOperationFromRequest(turn, error) {
1200
+ const pending = this.pendingOperation;
1201
+ if (!pending || pending.turn.turnId !== turn.turnId)
1202
+ return;
1203
+ // A semantic terminal may have arrived before the bridge response. Keep the
1204
+ // operation reserved until that response boundary is observed, but never let
1205
+ // a delayed request failure overwrite its matching terminal result.
1206
+ if (pending.completed) {
1207
+ pending.requestSettled = true;
1208
+ this.maybeSettleOperation(pending);
1209
+ return;
1210
+ }
1211
+ this.setStatus(turn.kind === "prepare" || isAdmissionRejection(error) ? pending.priorStatus : "error", { source: "sdk" });
1212
+ this.rejectOperation(turn, error);
1213
+ }
1214
+ completeOperation(turn, success, error) {
1215
+ const pending = this.pendingOperation;
1216
+ if (!pending || pending.turn.turnId !== turn.turnId) {
1217
+ if (this.activeTurn?.turnId === turn.turnId && turn.external)
1218
+ this.finishActiveTurn(turn);
1219
+ return;
1220
+ }
1221
+ pending.completed = { success, error };
1222
+ this.maybeSettleOperation(pending);
1223
+ }
1224
+ maybeSettleOperation(pending) {
1225
+ if (!pending.requestSettled || !pending.completed)
1226
+ return;
1227
+ const turn = pending.turn;
1228
+ this.pendingOperation = null;
1229
+ this.finishActiveTurn(turn);
1230
+ if (pending.completed.success)
1231
+ pending.resolve();
1232
+ else
1233
+ pending.reject(pending.completed.error ?? makeError("SESSION_ERROR", "session operation failed"));
1234
+ }
1235
+ rejectOperation(turn, error) {
1236
+ const pending = this.pendingOperation;
1237
+ if (pending && pending.turn.turnId === turn.turnId) {
1238
+ this.pendingOperation = null;
1239
+ this.finishActiveTurn(turn);
1240
+ pending.reject(error);
1241
+ return;
1242
+ }
1243
+ this.finishActiveTurn(turn);
1244
+ }
1245
+ queueSnapshotTool(snapshot, turn) {
1246
+ const request = snapshot.pendingToolRequest;
1247
+ if (!request || request.operationId !== turn.turnId)
1248
+ return;
1249
+ if (this.handledToolRequestIds.has(request.requestId))
1250
+ return;
1251
+ this.recoveredToolCalls.set(request.requestId, {
1252
+ type: "tool_call",
1253
+ channelId: "",
1254
+ sessionId: this.sessionId,
1255
+ seq: snapshot.latestSeq,
1256
+ operationId: request.operationId,
1257
+ toolRequestId: request.requestId,
1258
+ tool: request.toolName,
1259
+ args: request.args,
1260
+ });
1261
+ void this.drainRecoveredToolCalls();
1262
+ }
1263
+ reconcileRecoveredToolCalls(snapshot) {
1264
+ const active = snapshot.activeOperation;
1265
+ const pending = active && snapshot.pendingToolRequest?.operationId === active.operationId
1266
+ ? snapshot.pendingToolRequest
1267
+ : undefined;
1268
+ this.authoritativePendingToolRequest = pending
1269
+ ? { operationId: pending.operationId, requestId: pending.requestId }
1270
+ : null;
1271
+ for (const [requestId, event] of this.recoveredToolCalls) {
1272
+ if (!active ||
1273
+ !pending ||
1274
+ event.operationId !== active.operationId ||
1275
+ event.toolRequestId !== pending.requestId) {
1276
+ this.recoveredToolCalls.delete(requestId);
1277
+ }
1278
+ }
1279
+ }
1280
+ isAuthoritativeToolRequestCurrent(event) {
1281
+ const pending = this.authoritativePendingToolRequest;
1282
+ if (pending === undefined)
1283
+ return true;
1284
+ if (pending === null)
1285
+ return false;
1286
+ return (pending.operationId === event.operationId &&
1287
+ pending.requestId === event.toolRequestId);
1288
+ }
1289
+ hasToolHandler(toolName) {
1290
+ return this.namedToolHandlers.has(toolName) ||
1291
+ this.inlineToolHandlers.has(toolName) ||
1292
+ this.genericToolHandler !== null;
1293
+ }
1294
+ async drainRecoveredToolCalls() {
1295
+ const turn = this.activeTurn;
1296
+ if (!turn)
1297
+ return;
1298
+ for (const [requestId, event] of Array.from(this.recoveredToolCalls.entries())) {
1299
+ if (event.operationId !== turn.turnId || !this.hasToolHandler(event.tool))
1300
+ continue;
1301
+ this.recoveredToolCalls.delete(requestId);
1302
+ this.setStatus("waiting_tool_result", { source: "core" });
1303
+ await this.handleToolCallOnce(event, { source: "core", eventReceivedAt: Date.now() }, turn);
1304
+ }
1305
+ }
949
1306
  finishActiveTurn(turn = this.activeTurn) {
950
1307
  if (!turn || !this.activeTurn)
951
1308
  return;
@@ -966,6 +1323,16 @@ export class PedelecSession {
966
1323
  };
967
1324
  }
968
1325
  createChatContext(meta, turn) {
1326
+ return {
1327
+ ...this.createBaseContext(meta, turn),
1328
+ type: "chat_message",
1329
+ turnId: turn.turnId,
1330
+ turnStartedAt: turn.turnStartedAt,
1331
+ eventReceivedAt: meta.eventReceivedAt ?? Date.now(),
1332
+ source: "core",
1333
+ };
1334
+ }
1335
+ createChatDeltaContext(meta, turn) {
969
1336
  return {
970
1337
  ...this.createBaseContext(meta, turn),
971
1338
  type: "chat_delta",
@@ -1010,9 +1377,11 @@ export class PedelecSession {
1010
1377
  }
1011
1378
  function isSessionEvent(message) {
1012
1379
  return (message.type === "chat_delta" ||
1380
+ message.type === "chat_message" ||
1013
1381
  message.type === "status_changed" ||
1014
1382
  message.type === "tool_call" ||
1015
- message.type === "done" ||
1383
+ message.type === "operation_completed" ||
1384
+ message.type === "usage_updated" ||
1016
1385
  message.type === "error" ||
1017
1386
  message.type === "ended");
1018
1387
  }
@@ -1033,21 +1402,21 @@ function normalizePedelecSettings(value) {
1033
1402
  }
1034
1403
  return { defaultProvider: value.defaultProvider };
1035
1404
  }
1036
- function normalizeSandboxFolderPickerResponse(value) {
1405
+ function normalizeWorkspaceFolderPickerResponse(value) {
1037
1406
  if (!isPlainObject(value) || !("path" in value)) {
1038
- throw makeError("SDK_PROTOCOL_ERROR", "pick_sandbox_folder response had an invalid shape");
1407
+ throw makeError("SDK_PROTOCOL_ERROR", "pick_workspace_folder response had an invalid shape");
1039
1408
  }
1040
1409
  const path = value.path;
1041
1410
  if (path === null)
1042
1411
  return null;
1043
1412
  if (typeof path !== "string" || path.length === 0 ||
1044
- typeof value.isEmptyFolder !== "boolean" || typeof value.hasSandboxConfig !== "boolean") {
1045
- throw makeError("SDK_PROTOCOL_ERROR", "pick_sandbox_folder response had an invalid shape");
1413
+ typeof value.isEmptyFolder !== "boolean" || typeof value.hasWorkspaceConfig !== "boolean") {
1414
+ throw makeError("SDK_PROTOCOL_ERROR", "pick_workspace_folder response had an invalid shape");
1046
1415
  }
1047
1416
  return {
1048
1417
  path,
1049
1418
  isEmptyFolder: value.isEmptyFolder,
1050
- hasSandboxConfig: value.hasSandboxConfig,
1419
+ hasWorkspaceConfig: value.hasWorkspaceConfig,
1051
1420
  };
1052
1421
  }
1053
1422
  function normalizeProviderInfoList(value) {
@@ -1081,6 +1450,23 @@ function createTurn(kind = "user") {
1081
1450
  kind,
1082
1451
  };
1083
1452
  }
1453
+ function isValidSessionUsageTotalTokens(value) {
1454
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
1455
+ }
1456
+ function coreStatusToSdkStatus(status) {
1457
+ if (status === "waitingToolResult")
1458
+ return "waiting_tool_result";
1459
+ if (status === "starting" || status === "stopping")
1460
+ return "running";
1461
+ if (status === "idle" || status === "running" || status === "ended" || status === "error") {
1462
+ return status;
1463
+ }
1464
+ return "error";
1465
+ }
1466
+ function parseSnapshotTime(value) {
1467
+ const parsed = Date.parse(value);
1468
+ return Number.isFinite(parsed) ? parsed : Date.now();
1469
+ }
1084
1470
  function getCurrentOrigin(pageWindow) {
1085
1471
  const origin = pageWindow?.location?.origin;
1086
1472
  return typeof origin === "string" && origin ? origin : null;
@@ -1088,6 +1474,22 @@ function getCurrentOrigin(pageWindow) {
1088
1474
  function isExtensionUnavailableError(error) {
1089
1475
  return error.code === "EXTENSION_UNAVAILABLE" || error.code === "EXTENSION_DISCONNECTED";
1090
1476
  }
1477
+ function isAdmissionRejection(error) {
1478
+ return error.code === "THREAD_BUSY" ||
1479
+ error.code === "THREAD_ENDED" ||
1480
+ error.code === "THREAD_ACCESS_DENIED" ||
1481
+ error.code === "THREAD_NOT_FOUND" ||
1482
+ error.code === "INVALID_INPUT";
1483
+ }
1484
+ function isDeterministicResumeError(error) {
1485
+ return error.code === "THREAD_BUSY" ||
1486
+ error.code === "THREAD_ENDED" ||
1487
+ error.code === "THREAD_ACCESS_DENIED" ||
1488
+ error.code === "THREAD_NOT_FOUND" ||
1489
+ error.code === "WORKSPACE_OPEN_FAILED" ||
1490
+ error.code === "PROVIDER_COMMAND_FAILED" ||
1491
+ error.code === "INVALID_INPUT";
1492
+ }
1091
1493
  function normalizeError(err, fallbackCode, fallbackMessage) {
1092
1494
  if (!err)
1093
1495
  return makeError(fallbackCode, fallbackMessage);