@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/README.md +594 -2
- package/dist/index.d.ts +159 -19
- package/dist/index.js +398 -66
- package/dist/index.type-test.d.ts +1 -0
- package/dist/index.type-test.js +101 -0
- package/package.json +4 -3
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
|
-
|
|
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(
|
|
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,
|
|
234
|
+
return this.resolveProviderOnlyCreateSessionInput(provider, normalizedSkills, autoEndOnDisconnect);
|
|
147
235
|
}
|
|
148
236
|
return {
|
|
149
237
|
provider: provider,
|
|
150
238
|
model,
|
|
151
|
-
|
|
239
|
+
skills: normalizedSkills.manifest,
|
|
240
|
+
inlineToolHandlers: normalizedSkills.handlers,
|
|
241
|
+
autoEndOnDisconnect,
|
|
152
242
|
};
|
|
153
243
|
}
|
|
154
|
-
async resolveDefaultCreateSessionInput(
|
|
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.
|
|
163
|
-
|
|
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,
|
|
258
|
+
async resolveProviderOnlyCreateSessionInput(provider, normalizedSkills, autoEndOnDisconnect) {
|
|
167
259
|
const settings = await this.getSettings();
|
|
168
260
|
return {
|
|
169
261
|
provider,
|
|
170
|
-
model: settings.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
314
|
-
|
|
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.
|
|
317
|
-
this.
|
|
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.
|
|
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.
|
|
370
|
-
this.
|
|
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.
|
|
376
|
-
|
|
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.
|
|
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.
|
|
388
|
-
this.
|
|
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
|
-
|
|
575
|
+
replaceInlineToolHandlers(handlers) {
|
|
576
|
+
this.inlineToolHandlers = new Map(handlers);
|
|
577
|
+
}
|
|
578
|
+
async handleToolCall(event, meta, turn) {
|
|
396
579
|
let result;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|