@kaoruisaac/pedelec 0.1.0 → 0.1.3

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,92 @@
1
1
  import { PEDELEC_EXTENSION_ID } from "./extension-id.js";
2
2
  const SDK_EXTERNAL_PORT_NAME = "pedelec-sdk-external";
3
3
  const DEFAULT_BRIDGE_TIMEOUT_MS = 30_000;
4
+ const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
5
+ export function defineTool(tool) {
6
+ return tool;
7
+ }
8
+ function normalizeSkillsInput(value) {
9
+ const handlers = new Map();
10
+ if (value === undefined)
11
+ return { handlers };
12
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13
+ throw makeError("INVALID_INPUT", "skills must be an object");
14
+ }
15
+ const skills = value;
16
+ if (typeof skills.guidance !== "string") {
17
+ throw makeError("INVALID_INPUT", "skills.guidance must be a string");
18
+ }
19
+ if (!Array.isArray(skills.tools)) {
20
+ throw makeError("INVALID_INPUT", "skills.tools must be an array");
21
+ }
22
+ const seen = new Set();
23
+ const tools = skills.tools.map((tool, index) => {
24
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
25
+ throw makeError("INVALID_INPUT", "skills.tools entries must be objects", { index });
26
+ }
27
+ const rawTool = tool;
28
+ if (typeof rawTool.name !== "string" || !TOOL_NAME_PATTERN.test(rawTool.name)) {
29
+ throw makeError("INVALID_INPUT", "tool name is invalid", { index, toolName: rawTool.name });
30
+ }
31
+ if (seen.has(rawTool.name)) {
32
+ throw makeError("INVALID_INPUT", "duplicate tool name", { toolName: rawTool.name });
33
+ }
34
+ seen.add(rawTool.name);
35
+ if (typeof rawTool.description !== "string" || rawTool.description.trim().length === 0) {
36
+ throw makeError("INVALID_INPUT", "tool description must be a non-empty string", {
37
+ toolName: rawTool.name,
38
+ });
39
+ }
40
+ if (rawTool.timeoutMs !== undefined &&
41
+ (!Number.isInteger(rawTool.timeoutMs) || rawTool.timeoutMs <= 0)) {
42
+ throw makeError("INVALID_INPUT", "tool timeoutMs must be a positive integer", {
43
+ toolName: rawTool.name,
44
+ });
45
+ }
46
+ if (rawTool.handler !== undefined && typeof rawTool.handler !== "function") {
47
+ throw makeError("INVALID_INPUT", "tool handler must be a function", { toolName: rawTool.name });
48
+ }
49
+ if (rawTool.input !== undefined) {
50
+ throw makeError("INVALID_INPUT", "tool input is no longer supported; use argsSchema", {
51
+ toolName: rawTool.name,
52
+ });
53
+ }
54
+ const argsSchema = normalizeToolArgsSchema(rawTool.argsSchema, rawTool.name);
55
+ if (rawTool.handler)
56
+ handlers.set(rawTool.name, rawTool.handler);
57
+ return {
58
+ name: rawTool.name,
59
+ description: rawTool.description,
60
+ argsSchema,
61
+ ...(rawTool.timeoutMs === undefined ? {} : { timeoutMs: rawTool.timeoutMs }),
62
+ };
63
+ });
64
+ return {
65
+ manifest: {
66
+ guidance: skills.guidance,
67
+ tools,
68
+ },
69
+ handlers,
70
+ };
71
+ }
72
+ function normalizeToolArgsSchema(argsSchema, toolName) {
73
+ if (!argsSchema || typeof argsSchema !== "object" || Array.isArray(argsSchema)) {
74
+ throw makeError("INVALID_INPUT", "tool argsSchema must be an object", { toolName });
75
+ }
76
+ const schema = argsSchema;
77
+ if (schema.type !== "object") {
78
+ throw makeError("INVALID_INPUT", "tool argsSchema must describe an object", { toolName });
79
+ }
80
+ try {
81
+ return JSON.parse(JSON.stringify(schema));
82
+ }
83
+ catch (err) {
84
+ throw makeError("INVALID_INPUT", "tool argsSchema must be serializable", {
85
+ toolName,
86
+ error: err instanceof Error ? err.message : String(err),
87
+ });
88
+ }
89
+ }
4
90
  export class Pedelec {
5
91
  pageWindow;
6
92
  channelId;
@@ -28,13 +114,14 @@ export class Pedelec {
28
114
  input: {
29
115
  provider: resolvedInput.provider,
30
116
  model: resolvedInput.model,
31
- skillsUrls: resolvedInput.skillsUrls,
117
+ skills: resolvedInput.skills,
118
+ autoEndOnDisconnect: resolvedInput.autoEndOnDisconnect,
32
119
  },
33
120
  });
34
121
  if (!result.sessionId) {
35
122
  throw makeError("SDK_PROTOCOL_ERROR", "create_session response did not include sessionId");
36
123
  }
37
- return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.model);
124
+ return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.model, resolvedInput.inlineToolHandlers);
38
125
  }
39
126
  async listProviders() {
40
127
  const result = await this.request("list_providers");
@@ -130,28 +217,31 @@ export class Pedelec {
130
217
  const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
131
218
  const hasProvider = provider.length > 0;
132
219
  const hasModel = raw.model !== undefined;
220
+ const autoEndOnDisconnect = raw.autoEndOnDisconnect !== false;
221
+ const normalizedSkills = normalizeSkillsInput(raw.skills);
133
222
  if (!hasProvider && hasModel) {
134
223
  throw makeError("INVALID_INPUT", "model cannot be provided without provider");
135
224
  }
136
- const skillsUrls = Array.isArray(raw.skillsUrls) ? raw.skillsUrls : [];
137
225
  const userModel = typeof raw.model === "string" ? raw.model : undefined;
138
226
  if (!hasProvider) {
139
- return this.resolveDefaultCreateSessionInput(skillsUrls);
227
+ return this.resolveDefaultCreateSessionInput(normalizedSkills, autoEndOnDisconnect);
140
228
  }
141
229
  if (!isProviderCode(provider)) {
142
230
  throw makeError("INVALID_INPUT", "provider is not supported", { provider });
143
231
  }
144
232
  let model = userModel;
145
233
  if (model === undefined) {
146
- return this.resolveProviderOnlyCreateSessionInput(provider, skillsUrls);
234
+ return this.resolveProviderOnlyCreateSessionInput(provider, normalizedSkills, autoEndOnDisconnect);
147
235
  }
148
236
  return {
149
237
  provider: provider,
150
238
  model,
151
- skillsUrls,
239
+ skills: normalizedSkills.manifest,
240
+ inlineToolHandlers: normalizedSkills.handlers,
241
+ autoEndOnDisconnect,
152
242
  };
153
243
  }
154
- async resolveDefaultCreateSessionInput(skillsUrls) {
244
+ async resolveDefaultCreateSessionInput(normalizedSkills, autoEndOnDisconnect) {
155
245
  const settings = await this.getSettings();
156
246
  if (!settings.defaultProvider) {
157
247
  throw makeError("DEFAULT_PROVIDER_NOT_SET", "Default provider is not set. Open the Pedelec desktop app Settings page and choose a default provider.");
@@ -159,18 +249,20 @@ export class Pedelec {
159
249
  await this.assertDefaultProviderAvailable(settings.defaultProvider);
160
250
  return {
161
251
  provider: settings.defaultProvider,
162
- model: settings.defaultModel ?? undefined,
163
- skillsUrls,
252
+ model: settings.defaultModels[settings.defaultProvider] ?? undefined,
253
+ skills: normalizedSkills.manifest,
254
+ inlineToolHandlers: normalizedSkills.handlers,
255
+ autoEndOnDisconnect,
164
256
  };
165
257
  }
166
- async resolveProviderOnlyCreateSessionInput(provider, skillsUrls) {
258
+ async resolveProviderOnlyCreateSessionInput(provider, normalizedSkills, autoEndOnDisconnect) {
167
259
  const settings = await this.getSettings();
168
260
  return {
169
261
  provider,
170
- model: settings.defaultProvider === provider && settings.defaultModel
171
- ? settings.defaultModel
172
- : undefined,
173
- skillsUrls,
262
+ model: settings.defaultModels[provider] ?? undefined,
263
+ skills: normalizedSkills.manifest,
264
+ inlineToolHandlers: normalizedSkills.handlers,
265
+ autoEndOnDisconnect,
174
266
  };
175
267
  }
176
268
  async assertDefaultProviderAvailable(provider) {
@@ -180,11 +272,13 @@ export class Pedelec {
180
272
  throw makeError("DEFAULT_PROVIDER_UNAVAILABLE", "Default provider is not currently available. Open the Pedelec desktop app Settings page and choose an available provider.", { provider });
181
273
  }
182
274
  }
183
- registerSession(sessionId, provider, model) {
275
+ registerSession(sessionId, provider, model, inlineToolHandlers = new Map()) {
184
276
  const existing = this.sessions.get(sessionId);
185
- if (existing)
277
+ if (existing) {
278
+ existing.replaceInlineToolHandlers(inlineToolHandlers);
186
279
  return existing;
187
- const session = new PedelecSession(this, sessionId, provider, model);
280
+ }
281
+ const session = new PedelecSession(this, sessionId, provider, model, inlineToolHandlers);
188
282
  this.sessions.set(sessionId, session);
189
283
  return session;
190
284
  }
@@ -215,8 +309,12 @@ export class Pedelec {
215
309
  return;
216
310
  }
217
311
  if (isSessionEvent(message) && this.isNewEvent(message)) {
312
+ const eventReceivedAt = Date.now();
218
313
  if (message.sessionId) {
219
- this.sessions.get(message.sessionId)?.handleEvent(message);
314
+ this.sessions.get(message.sessionId)?.handleEvent(message, {
315
+ source: "core",
316
+ eventReceivedAt,
317
+ });
220
318
  }
221
319
  else if (message.type === "error") {
222
320
  this.broadcastError(normalizeError(message.error, "SDK_TRANSPORT_ERROR", "Pedelec transport error"));
@@ -260,7 +358,7 @@ export class Pedelec {
260
358
  }
261
359
  broadcastError(error) {
262
360
  for (const session of this.sessions.values()) {
263
- session.handleEvent({ type: "error", sessionId: session.sessionId, error });
361
+ session.handleEvent({ type: "error", sessionId: session.sessionId, error }, { source: "sdk" });
264
362
  }
265
363
  }
266
364
  }
@@ -269,27 +367,84 @@ export class PedelecSession {
269
367
  sessionId;
270
368
  provider;
271
369
  model;
370
+ sessionCreatedAt = Date.now();
272
371
  status = "idle";
273
372
  pendingSend = null;
274
- toolHandler = null;
373
+ pendingPrepare = null;
374
+ preparePromise = null;
375
+ prepared = false;
376
+ activeTurn = null;
377
+ genericToolHandler = null;
378
+ inlineToolHandlers = new Map();
379
+ namedToolHandlers = new Map();
275
380
  chatHandlers = new Set();
276
381
  errorHandlers = new Set();
277
382
  statusHandlers = new Set();
278
383
  endedHandlers = new Set();
279
- constructor(client, sessionId, provider, model) {
384
+ constructor(client, sessionId, provider, model, inlineToolHandlers = new Map()) {
280
385
  this.client = client;
281
386
  this.sessionId = sessionId;
282
387
  this.provider = provider;
283
388
  this.model = model;
389
+ this.inlineToolHandlers = new Map(inlineToolHandlers);
284
390
  }
285
- sendText(text) {
391
+ prepare() {
286
392
  if (this.status === "ended") {
287
393
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
288
394
  }
289
395
  if (this.pendingSend) {
290
396
  return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
291
397
  }
292
- 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" });
293
448
  const donePromise = new Promise((resolve, reject) => {
294
449
  this.pendingSend = { resolve, reject };
295
450
  });
@@ -300,8 +455,9 @@ export class PedelecSession {
300
455
  })
301
456
  .catch((err) => {
302
457
  const error = normalizeError(err, "SEND_TEXT_FAILED", "sendText failed");
458
+ this.emitError(error, { source: "sdk" });
303
459
  this.clearPendingSend();
304
- this.emitError(error);
460
+ this.finishActiveTurn(turn);
305
461
  throw error;
306
462
  });
307
463
  return Promise.all([requestPromise, donePromise]).then(() => undefined);
@@ -310,11 +466,27 @@ export class PedelecSession {
310
466
  this.chatHandlers.add(handler);
311
467
  return () => this.chatHandlers.delete(handler);
312
468
  }
313
- onTool(handler) {
314
- this.toolHandler = handler;
469
+ onTool(toolNameOrHandler, maybeHandler) {
470
+ if (typeof toolNameOrHandler === "string") {
471
+ const toolName = toolNameOrHandler;
472
+ if (!TOOL_NAME_PATTERN.test(toolName)) {
473
+ throw makeError("INVALID_INPUT", "tool name is invalid", { toolName });
474
+ }
475
+ if (typeof maybeHandler !== "function") {
476
+ throw makeError("INVALID_INPUT", "tool handler must be a function", { toolName });
477
+ }
478
+ this.namedToolHandlers.set(toolName, maybeHandler);
479
+ return () => {
480
+ if (this.namedToolHandlers.get(toolName) === maybeHandler) {
481
+ this.namedToolHandlers.delete(toolName);
482
+ }
483
+ };
484
+ }
485
+ const handler = toolNameOrHandler;
486
+ this.genericToolHandler = handler;
315
487
  return () => {
316
- if (this.toolHandler === handler) {
317
- this.toolHandler = null;
488
+ if (this.genericToolHandler === handler) {
489
+ this.genericToolHandler = null;
318
490
  }
319
491
  };
320
492
  }
@@ -343,67 +515,83 @@ export class PedelecSession {
343
515
  }
344
516
  catch (err) {
345
517
  const error = normalizeError(err, "END_SESSION_FAILED", "end failed");
346
- this.emitError(error);
518
+ this.emitError(error, { source: "sdk" });
347
519
  throw error;
348
520
  }
349
- this.markEnded();
521
+ this.markEnded({ source: "sdk" });
350
522
  this.client.unregisterSession(this.sessionId);
351
523
  }
352
- handleEvent(event) {
524
+ handleEvent(event, meta = { source: "sdk" }) {
353
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;
354
531
  for (const handler of this.chatHandlers) {
355
- handler(event.text);
532
+ handler(event.text, this.createChatContext(meta, turn));
356
533
  }
357
534
  return;
358
535
  }
359
536
  if (event.type === "status_changed") {
360
- this.setStatus(event.status);
537
+ this.setStatus(event.status, meta);
361
538
  if (event.status === "idle") {
362
- this.resolvePendingSend();
539
+ this.resolveActivePending();
363
540
  }
364
541
  else if (event.status === "ended") {
365
- this.markEnded();
542
+ this.markEnded(meta);
366
543
  }
367
544
  else if (event.status === "error") {
368
545
  const error = makeError("SESSION_ERROR", "session entered error status", { sessionId: this.sessionId });
369
- this.rejectPendingSend(error);
370
- this.emitError(error);
546
+ this.emitError(error, meta);
547
+ this.rejectActivePending(error);
371
548
  }
372
549
  return;
373
550
  }
374
551
  if (event.type === "tool_call") {
375
- this.setStatus("waiting_tool_result");
376
- 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);
377
557
  return;
378
558
  }
379
559
  if (event.type === "done") {
380
- this.setStatus("idle");
381
- this.resolvePendingSend();
560
+ this.setStatus("idle", meta);
561
+ this.resolveActivePending();
382
562
  return;
383
563
  }
384
564
  if (event.type === "error") {
385
- this.setStatus("error");
565
+ this.setStatus("error", meta);
386
566
  const error = normalizeError(event.error, "SESSION_ERROR", "session error");
387
- this.rejectPendingSend(error);
388
- this.emitError(error);
567
+ this.emitError(error, meta);
568
+ this.rejectActivePending(error);
389
569
  return;
390
570
  }
391
571
  if (event.type === "ended") {
392
- this.markEnded();
572
+ this.markEnded(meta);
393
573
  }
394
574
  }
395
- async handleToolCall(event) {
575
+ replaceInlineToolHandlers(handlers) {
576
+ this.inlineToolHandlers = new Map(handlers);
577
+ }
578
+ async handleToolCall(event, meta, turn) {
396
579
  let result;
397
- if (!this.toolHandler) {
398
- result = {
399
- error: makeError("TOOL_HANDLER_NOT_FOUND", `No tool handler registered for ${event.tool}`, {
400
- tool: event.tool,
401
- }),
402
- };
580
+ const namedHandler = this.namedToolHandlers.get(event.tool);
581
+ const inlineHandler = this.inlineToolHandlers.get(event.tool);
582
+ if (namedHandler) {
583
+ try {
584
+ result = await namedHandler(event.args, this.createToolCallContext(event, meta, turn));
585
+ }
586
+ catch (err) {
587
+ result = {
588
+ error: normalizeError(err, "TOOL_HANDLER_ERROR", "Tool handler failed"),
589
+ };
590
+ }
403
591
  }
404
- else {
592
+ else if (inlineHandler) {
405
593
  try {
406
- result = await this.toolHandler(event.tool, event.args);
594
+ result = await inlineHandler(event.args, this.createToolCallContext(event, meta, turn));
407
595
  }
408
596
  catch (err) {
409
597
  result = {
@@ -411,6 +599,23 @@ export class PedelecSession {
411
599
  };
412
600
  }
413
601
  }
602
+ else if (this.genericToolHandler) {
603
+ try {
604
+ result = await this.genericToolHandler(event.tool, event.args, this.createToolCallContext(event, meta, turn));
605
+ }
606
+ catch (err) {
607
+ result = {
608
+ error: normalizeError(err, "TOOL_HANDLER_ERROR", "Tool handler failed"),
609
+ };
610
+ }
611
+ }
612
+ else {
613
+ result = {
614
+ error: makeError("TOOL_HANDLER_NOT_FOUND", `No tool handler registered for ${event.tool}`, {
615
+ tool: event.tool,
616
+ }),
617
+ };
618
+ }
414
619
  try {
415
620
  await this.client.request("submit_tool_result", {
416
621
  sessionId: this.sessionId,
@@ -419,45 +624,152 @@ export class PedelecSession {
419
624
  });
420
625
  }
421
626
  catch (err) {
422
- 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
+ });
423
630
  }
424
631
  }
425
632
  resolvePendingSend() {
426
633
  const pending = this.pendingSend;
427
634
  this.pendingSend = null;
428
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
+ }
429
651
  }
430
652
  rejectPendingSend(error) {
431
653
  const pending = this.pendingSend;
432
654
  this.pendingSend = null;
433
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
+ }
434
671
  }
435
672
  clearPendingSend() {
436
673
  this.pendingSend = null;
437
674
  }
438
- markEnded() {
675
+ clearPendingPrepare() {
676
+ this.pendingPrepare = null;
677
+ }
678
+ markEnded(meta = { source: "sdk" }) {
439
679
  const wasEnded = this.status === "ended";
440
- this.setStatus("ended");
441
- this.rejectPendingSend(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
680
+ this.setStatus("ended", meta);
442
681
  if (!wasEnded) {
443
682
  for (const handler of this.endedHandlers) {
444
- handler();
683
+ handler(this.createEndedContext(meta));
445
684
  }
446
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();
447
689
  }
448
- emitError(error) {
690
+ emitError(error, meta = { source: "sdk" }) {
449
691
  for (const handler of this.errorHandlers) {
450
- handler(error);
692
+ handler(error, this.createErrorContext(meta));
451
693
  }
452
694
  }
453
- setStatus(status) {
695
+ setStatus(status, meta = { source: "sdk" }) {
454
696
  if (this.status === status)
455
697
  return;
698
+ const previousStatus = this.status;
456
699
  this.status = status;
457
700
  for (const handler of this.statusHandlers) {
458
- 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;
459
717
  }
460
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
+ }
461
773
  }
462
774
  function isSessionEvent(message) {
463
775
  return (message.type === "chat_delta" ||
@@ -470,14 +782,23 @@ function isSessionEvent(message) {
470
782
  function isSettings(value) {
471
783
  if (!value || typeof value !== "object")
472
784
  return false;
785
+ if (Array.isArray(value))
786
+ return false;
473
787
  const settings = value;
788
+ const defaultModels = settings.defaultModels;
789
+ if (settings.defaultModel !== undefined)
790
+ return false;
791
+ if (!defaultModels || typeof defaultModels !== "object" || Array.isArray(defaultModels)) {
792
+ return false;
793
+ }
474
794
  return ((settings.defaultProvider === null ||
475
795
  settings.defaultProvider === "codex" ||
476
796
  settings.defaultProvider === "gemini" ||
477
797
  settings.defaultProvider === "opencode" ||
478
798
  settings.defaultProvider === "cursor" ||
479
- settings.defaultProvider === "claude") &&
480
- (settings.defaultModel === null || typeof settings.defaultModel === "string"));
799
+ settings.defaultProvider === "claude" ||
800
+ settings.defaultProvider === "ollama") &&
801
+ Object.entries(defaultModels).every(([provider, model]) => isProviderCode(provider) && typeof model === "string"));
481
802
  }
482
803
  function isApprovalStatus(value) {
483
804
  if (!value || typeof value !== "object")
@@ -492,7 +813,8 @@ function isProviderCode(value) {
492
813
  value === "gemini" ||
493
814
  value === "opencode" ||
494
815
  value === "cursor" ||
495
- value === "claude");
816
+ value === "claude" ||
817
+ value === "ollama");
496
818
  }
497
819
  function makeError(code, message, details) {
498
820
  return details === undefined ? { code, message } : { code, message, details };
@@ -503,6 +825,16 @@ function createChannelId() {
503
825
  : Math.random().toString(36).slice(2);
504
826
  return `pedelec_${Date.now()}_${random}`;
505
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
+ }
506
838
  function getCurrentOrigin(pageWindow) {
507
839
  const origin = pageWindow?.location?.origin;
508
840
  return typeof origin === "string" && origin ? origin : null;