@kaoruisaac/pedelec 0.1.0 → 0.1.2
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 +559 -2
- package/dist/index.d.ts +86 -8
- package/dist/index.js +182 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,566 @@
|
|
|
1
1
|
# Pedelec SDK
|
|
2
|
+
|
|
2
3
|
[](https://www.npmjs.com/package/@kaoruisaac/pedelec)
|
|
4
|
+
|
|
3
5
|
Browser SDK for connecting a web app to the Pedelec Chrome Extension and Desktop Runtime.
|
|
4
6
|
|
|
7
|
+
The SDK lets a browser page create local agent sessions, send user text, receive streamed assistant output, handle tool calls in the web app, and resume or end sessions.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
Pedelec SDK runs in a browser page context. It is not intended for Node.js, SSR server code, or background workers.
|
|
12
|
+
|
|
13
|
+
Before using it, the user needs:
|
|
14
|
+
|
|
15
|
+
- Pedelec Chrome Extension installed.
|
|
16
|
+
- Pedelec Desktop App running.
|
|
17
|
+
- Chrome Native Messaging host registered by the Desktop App.
|
|
18
|
+
- The target provider available locally. CLI-backed providers use commands such as `codex`, `gemini`, `opencode`, `cursor`, or `claude`; the Ollama provider uses Pedelec's bundled `pedelec-agent`.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install from npm:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @kaoruisaac/pedelec
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Import the SDK in browser-side TypeScript:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Pedelec, defineTool } from "@kaoruisaac/pedelec";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For local development before publishing:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install ../path/to/pedelec/sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To build the SDK from this repository:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd sdk
|
|
44
|
+
npm install
|
|
45
|
+
npm run build
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { Pedelec, defineTool } from "@kaoruisaac/pedelec";
|
|
52
|
+
|
|
53
|
+
const pedelec = new Pedelec();
|
|
54
|
+
|
|
55
|
+
const session = await pedelec.createSession({
|
|
56
|
+
provider: "codex",
|
|
57
|
+
model: "gpt-5",
|
|
58
|
+
skills: {
|
|
59
|
+
guidance: "Use get_current_page when you need browser page context.",
|
|
60
|
+
tools: [
|
|
61
|
+
defineTool({
|
|
62
|
+
name: "get_current_page",
|
|
63
|
+
description: "Read the current browser page title and URL.",
|
|
64
|
+
argsSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {},
|
|
67
|
+
required: [],
|
|
68
|
+
},
|
|
69
|
+
handler: () => ({
|
|
70
|
+
url: location.href,
|
|
71
|
+
title: document.title,
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
session.onChat((text) => {
|
|
79
|
+
console.log(text);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
session.onStatus((status) => {
|
|
83
|
+
console.log("status", status);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
session.onError((error) => {
|
|
87
|
+
console.error(error.code, error.message, error.details);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await session.sendText("Analyze the current page state.");
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`sendText()` resolves after the current agent turn is done. If the same session is already running a prompt, another `sendText()` rejects with `SESSION_BUSY`.
|
|
94
|
+
|
|
95
|
+
## Create a Client
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const pedelec = new Pedelec({
|
|
99
|
+
bridgeTimeoutMs: 30_000,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`bridgeTimeoutMs` controls how long SDK requests wait for an extension response. The default is 30 seconds. Values below 1 ms are clamped to 1 ms.
|
|
104
|
+
|
|
105
|
+
## Check Approval Status
|
|
106
|
+
|
|
107
|
+
The extension may ask the user to approve the current origin before a page can create or resume sessions. You can check status before showing a connect button.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const status = await pedelec.getApprovalStatus();
|
|
111
|
+
|
|
112
|
+
if (!status.installed) {
|
|
113
|
+
console.log("Pedelec extension is not available.");
|
|
114
|
+
} else if (!status.approved) {
|
|
115
|
+
console.log(`${status.origin} still needs approval.`);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Return shape:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
type ApprovalStatus = {
|
|
123
|
+
installed: boolean;
|
|
124
|
+
approved: boolean;
|
|
125
|
+
origin: string | null;
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If the extension cannot be reached, `getApprovalStatus()` returns `{ installed: false, approved: false, origin }` instead of throwing for extension unavailable cases.
|
|
130
|
+
|
|
131
|
+
## Providers
|
|
132
|
+
|
|
133
|
+
List available providers on the user's machine:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const providers = await pedelec.listProviders();
|
|
137
|
+
|
|
138
|
+
for (const provider of providers) {
|
|
139
|
+
console.log(provider.code, provider.available, provider.path, provider.error);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
type ProviderCode = "codex" | "gemini" | "opencode" | "cursor" | "claude" | "ollama";
|
|
145
|
+
|
|
146
|
+
type ProviderInfo = {
|
|
147
|
+
name: string;
|
|
148
|
+
code: ProviderCode;
|
|
149
|
+
path: string | null;
|
|
150
|
+
available: boolean;
|
|
151
|
+
error: string | null;
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Supported provider codes:
|
|
156
|
+
|
|
157
|
+
| Provider | Code | Example model |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| Codex | `codex` | `gpt-5` |
|
|
160
|
+
| Gemini | `gemini` | provider-supported model id |
|
|
161
|
+
| OpenCode | `opencode` | `ollama/qwen2.5-coder:14b` |
|
|
162
|
+
| Cursor | `cursor` | `gpt-5` |
|
|
163
|
+
| Claude Code | `claude` | `sonnet` |
|
|
164
|
+
| Ollama | `ollama` | `qwen3-14b-32k:latest` |
|
|
165
|
+
|
|
166
|
+
`available: false` usually means the provider CLI is not installed or is not available in `PATH`.
|
|
167
|
+
For Ollama, `available: true` only means Pedelec can find `pedelec-agent`; it does not mean the Ollama server is running or the selected model is installed.
|
|
168
|
+
|
|
169
|
+
## Settings
|
|
170
|
+
|
|
171
|
+
Read the Desktop App default provider and per-provider models:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const settings = await pedelec.getSettings();
|
|
175
|
+
|
|
176
|
+
console.log(settings.defaultProvider);
|
|
177
|
+
console.log(settings.defaultModels.codex);
|
|
178
|
+
console.log(settings.defaultModels.gemini);
|
|
179
|
+
console.log(settings.defaultModels.ollama);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
type PedelecSettings = {
|
|
184
|
+
defaultProvider: ProviderCode | null;
|
|
185
|
+
defaultModels: Partial<Record<ProviderCode, string>>;
|
|
186
|
+
};
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Create Sessions
|
|
190
|
+
|
|
191
|
+
Use the Desktop App default provider:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const session = await pedelec.createSession();
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use default provider with skills:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const session = await pedelec.createSession({
|
|
201
|
+
skills: {
|
|
202
|
+
guidance: "Use update_counter when the user asks to change the counter.",
|
|
203
|
+
tools: [
|
|
204
|
+
defineTool({
|
|
205
|
+
name: "update_counter",
|
|
206
|
+
description: "Update the visible counter by delta.",
|
|
207
|
+
argsSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
required: ["delta"],
|
|
210
|
+
properties: {
|
|
211
|
+
delta: {
|
|
212
|
+
type: "number",
|
|
213
|
+
description: "Counter delta.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If no default provider is configured, this rejects with `DEFAULT_PROVIDER_NOT_SET`. If the default provider is configured but unavailable, it rejects with `DEFAULT_PROVIDER_UNAVAILABLE`. If the default provider has its own default model, the SDK sends that model with the session request.
|
|
224
|
+
|
|
225
|
+
Use an explicit provider:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const session = await pedelec.createSession({
|
|
229
|
+
provider: "codex",
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
When only `provider` is passed, the SDK uses that provider's own Desktop App default model if one is configured. Otherwise, it sends the provider without a model and lets the provider CLI use its own default behavior.
|
|
234
|
+
Ollama is the exception: it requires a model, so provider-only Ollama sessions need `defaultModels.ollama` or they fail with `MODEL_REQUIRED`.
|
|
235
|
+
|
|
236
|
+
Use an explicit provider and model:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const session = await pedelec.createSession({
|
|
240
|
+
provider: "ollama",
|
|
241
|
+
model: "qwen3-14b-32k:latest",
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Ollama sessions are executed by the `pedelec-agent` binary bundled with the Desktop App, not by the `ollama` CLI. You still need to start the local Ollama server yourself. Ollama requires a model from the session input or `defaultModels.ollama`; otherwise the CoreRuntime returns `MODEL_REQUIRED`.
|
|
246
|
+
|
|
247
|
+
By default, SDK-created sessions are page-scoped. `autoEndOnDisconnect` defaults to `true`, so the Desktop thread is ended when the last SDK connection for that session disconnects, such as on page refresh or tab close. Keep the default for demo and page-scoped apps. Set `autoEndOnDisconnect: false` when you need to resume the same session after navigation or share it across pages:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const session = await pedelec.createSession({
|
|
251
|
+
provider: "codex",
|
|
252
|
+
autoEndOnDisconnect: false,
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`model` cannot be provided without `provider`.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
type CreateSessionInput =
|
|
260
|
+
| {
|
|
261
|
+
provider: ProviderCode;
|
|
262
|
+
model?: string;
|
|
263
|
+
skills?: SkillsInput;
|
|
264
|
+
autoEndOnDisconnect?: boolean;
|
|
265
|
+
}
|
|
266
|
+
| {
|
|
267
|
+
provider?: undefined;
|
|
268
|
+
skills?: SkillsInput;
|
|
269
|
+
autoEndOnDisconnect?: boolean;
|
|
270
|
+
};
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Stream Responses
|
|
274
|
+
|
|
275
|
+
`onChat()` receives text deltas. A delta is not guaranteed to be a sentence or paragraph, so UI code should append chunks.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
let assistantText = "";
|
|
279
|
+
|
|
280
|
+
session.onChat((text) => {
|
|
281
|
+
assistantText += text;
|
|
282
|
+
renderAssistantMessage(assistantText);
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The returned function unsubscribes the handler:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const unsubscribe = session.onChat((text) => {
|
|
290
|
+
console.log(text);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
unsubscribe();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Send Text
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
try {
|
|
300
|
+
await session.sendText("Summarize this workspace.");
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error("send failed", error);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`sendText(text)` sets the session status to `running`, sends the prompt to the runtime, and resolves after a `done` or idle status event. It rejects if the session ends, errors, disconnects, or is already busy.
|
|
307
|
+
|
|
308
|
+
## Session Status
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
session.onStatus((status) => {
|
|
312
|
+
switch (status) {
|
|
313
|
+
case "idle":
|
|
314
|
+
break;
|
|
315
|
+
case "running":
|
|
316
|
+
break;
|
|
317
|
+
case "waiting_tool_result":
|
|
318
|
+
break;
|
|
319
|
+
case "ended":
|
|
320
|
+
break;
|
|
321
|
+
case "error":
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
console.log(session.getStatus());
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
type PedelecSessionStatus =
|
|
331
|
+
| "idle"
|
|
332
|
+
| "running"
|
|
333
|
+
| "waiting_tool_result"
|
|
334
|
+
| "ended"
|
|
335
|
+
| "error";
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
| Status | Meaning |
|
|
339
|
+
| --- | --- |
|
|
340
|
+
| `idle` | The session can accept another prompt. |
|
|
341
|
+
| `running` | The agent is processing user text. |
|
|
342
|
+
| `waiting_tool_result` | The agent requested a tool result from the web app. |
|
|
343
|
+
| `ended` | The session has ended and cannot receive more text. |
|
|
344
|
+
| `error` | The session entered an error state. |
|
|
345
|
+
|
|
346
|
+
## Tool Calling
|
|
347
|
+
|
|
348
|
+
Use `skills` to tell the agent what tools your web app can handle. `guidance` is high-level instruction for the agent, and `tools` is the typed list of callable frontend capabilities. Core automatically generates `tools.md` and per-tool spec artifacts inside the sandbox; SDK users do not write or host those files.
|
|
349
|
+
|
|
350
|
+
Inline handlers are registered locally and are not sent to Core:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const session = await pedelec.createSession({
|
|
354
|
+
provider: "codex",
|
|
355
|
+
skills: {
|
|
356
|
+
guidance: "Use get_current_page for browser context.",
|
|
357
|
+
tools: [
|
|
358
|
+
defineTool({
|
|
359
|
+
name: "get_current_page",
|
|
360
|
+
description: "Read the current browser page title and URL.",
|
|
361
|
+
argsSchema: {
|
|
362
|
+
type: "object",
|
|
363
|
+
properties: {},
|
|
364
|
+
required: [],
|
|
365
|
+
},
|
|
366
|
+
handler: () => ({
|
|
367
|
+
url: location.href,
|
|
368
|
+
title: document.title,
|
|
369
|
+
selectedText: window.getSelection()?.toString() ?? "",
|
|
370
|
+
}),
|
|
371
|
+
}),
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
You can also register handlers after session creation. A named `onTool(name, handler)` handler overrides an inline handler for the same tool:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
const session = await pedelec.createSession({
|
|
381
|
+
provider: "codex",
|
|
382
|
+
skills: {
|
|
383
|
+
guidance: "Use update_counter when the user asks to change the counter.",
|
|
384
|
+
tools: [
|
|
385
|
+
defineTool({
|
|
386
|
+
name: "update_counter",
|
|
387
|
+
description: "Update the visible counter by delta.",
|
|
388
|
+
argsSchema: {
|
|
389
|
+
type: "object",
|
|
390
|
+
required: ["delta"],
|
|
391
|
+
properties: {
|
|
392
|
+
delta: {
|
|
393
|
+
type: "number",
|
|
394
|
+
description: "Counter delta.",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
}),
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
session.onTool("update_counter", async (args) => {
|
|
404
|
+
const { delta } = args as { delta: number };
|
|
405
|
+
counter.value += delta;
|
|
406
|
+
return {
|
|
407
|
+
counter: counter.value,
|
|
408
|
+
delta,
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Tool results must be JSON-serializable. If no handler is registered, the SDK returns a `TOOL_HANDLER_NOT_FOUND` error result. If the handler throws, the SDK returns a `TOOL_HANDLER_ERROR` error result.
|
|
414
|
+
|
|
415
|
+
The legacy generic form `session.onTool((tool, args) => ...)` remains available as a fallback handler.
|
|
416
|
+
|
|
417
|
+
### Tool Args Schema
|
|
418
|
+
|
|
419
|
+
`defineTool` uses `argsSchema` to describe the tool arguments sent to the provider and agent. The root schema must be an object:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
defineTool({
|
|
423
|
+
name: "update_counter",
|
|
424
|
+
description: "Update the visible counter by delta.",
|
|
425
|
+
argsSchema: {
|
|
426
|
+
type: "object",
|
|
427
|
+
required: ["delta"],
|
|
428
|
+
properties: {
|
|
429
|
+
delta: {
|
|
430
|
+
type: "number",
|
|
431
|
+
description: "Counter delta.",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`argsSchema` is the Pedelec Tool Args Schema subset, not full JSON Schema. It supports common `string`, `number`, `integer`, `boolean`, `array`, `object`, and `oneOf` nodes with fields such as `description`, `default`, `examples`, `enum`, `minimum`, `maximum`, `minItems`, `maxItems`, and `required`.
|
|
439
|
+
|
|
440
|
+
`default` is guidance for the agent; the SDK does not automatically fill missing argument values. Shorthand schemas such as `input: { delta: "number" }` are no longer supported.
|
|
441
|
+
|
|
442
|
+
The first version does not support `$defs`, `$ref`, `additionalProperties`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, or `format`. Reuse schema fragments with TypeScript constants instead of JSON Schema references.
|
|
443
|
+
|
|
444
|
+
## Resume Sessions
|
|
445
|
+
|
|
446
|
+
If you have saved a `sessionId`, resume it later:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
const session = await pedelec.resumeSession("thread_abc123");
|
|
450
|
+
|
|
451
|
+
session.onChat((text) => {
|
|
452
|
+
console.log(text);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
await session.sendText("Continue the previous task.");
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
`resumeSession(sessionId)` rejects with `INVALID_INPUT` when `sessionId` is empty.
|
|
459
|
+
|
|
460
|
+
## End Sessions
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
await session.end();
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
After a session ends, future `sendText()` calls reject with `SESSION_ENDED`.
|
|
467
|
+
|
|
468
|
+
Listen for runtime-ended sessions:
|
|
469
|
+
|
|
5
470
|
```ts
|
|
6
|
-
|
|
471
|
+
session.onEnded(() => {
|
|
472
|
+
console.log("session ended");
|
|
473
|
+
});
|
|
7
474
|
```
|
|
8
475
|
|
|
9
|
-
|
|
476
|
+
## Low-Level Requests
|
|
477
|
+
|
|
478
|
+
Most apps should use the typed methods above. `request<T>(type, payload)` is exposed for advanced integrations that need to call bridge operations directly.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
const result = await pedelec.request<{ sessionId: string }>("create_session", {
|
|
482
|
+
input: {
|
|
483
|
+
provider: "codex",
|
|
484
|
+
skills: undefined,
|
|
485
|
+
autoEndOnDisconnect: true,
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Low-level requests still use the SDK bridge timeout and reject with `PedelecError` objects.
|
|
491
|
+
|
|
492
|
+
## Error Handling
|
|
493
|
+
|
|
494
|
+
Register `onError()` for session-level errors and use `try/catch` around async SDK calls.
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
session.onError((error) => {
|
|
498
|
+
console.error("session error", error.code, error.message, error.details);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
await session.sendText("Update the selected content.");
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error("request failed", error);
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
type PedelecError = {
|
|
510
|
+
code: string;
|
|
511
|
+
message: string;
|
|
512
|
+
details?: unknown;
|
|
513
|
+
};
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Common error codes:
|
|
517
|
+
|
|
518
|
+
| Code | Meaning |
|
|
519
|
+
| --- | --- |
|
|
520
|
+
| `EXTENSION_UNAVAILABLE` | The SDK is not running in a supported browser page, or the extension cannot be reached. |
|
|
521
|
+
| `EXTENSION_DISCONNECTED` | The extension connection was interrupted. |
|
|
522
|
+
| `SDK_BRIDGE_TIMEOUT` | The extension did not respond before `bridgeTimeoutMs`. |
|
|
523
|
+
| `SDK_PROTOCOL_ERROR` | The extension response did not match the SDK protocol. |
|
|
524
|
+
| `SDK_TRANSPORT_ERROR` | A bridge request failed at the transport layer. |
|
|
525
|
+
| `APPROVAL_REJECTED` | The user rejected approval for the current origin. |
|
|
526
|
+
| `APPROVAL_TIMEOUT` | The user did not complete origin approval in time. |
|
|
527
|
+
| `OPEN_POPUP_FAILED` | The extension could not open the approval popup. |
|
|
528
|
+
| `NATIVE_HOST_UNAVAILABLE` | Chrome Native Messaging host cannot be reached. |
|
|
529
|
+
| `DEFAULT_PROVIDER_NOT_SET` | The Desktop App has no default provider configured. |
|
|
530
|
+
| `DEFAULT_PROVIDER_UNAVAILABLE` | The configured default provider is not currently available. |
|
|
531
|
+
| `INVALID_INPUT` | The request input is invalid, such as model without provider or empty session id. |
|
|
532
|
+
| `SESSION_BUSY` | The session already has a prompt in progress. |
|
|
533
|
+
| `SESSION_ENDED` | The session has ended. |
|
|
534
|
+
| `SESSION_ERROR` | The runtime reported a session error. |
|
|
535
|
+
| `TOOL_HANDLER_NOT_FOUND` | The agent called a tool but no web app handler is registered. |
|
|
536
|
+
| `TOOL_HANDLER_ERROR` | The web app tool handler threw an error. |
|
|
537
|
+
| `SUBMIT_TOOL_RESULT_FAILED` | The SDK could not submit a tool result back to the runtime. |
|
|
538
|
+
|
|
539
|
+
## Public API Summary
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
class Pedelec {
|
|
543
|
+
constructor(options?: { bridgeTimeoutMs?: number });
|
|
544
|
+
createSession(input?: CreateSessionInput): Promise<PedelecSession>;
|
|
545
|
+
listProviders(): Promise<ProviderInfo[]>;
|
|
546
|
+
getSettings(): Promise<PedelecSettings>;
|
|
547
|
+
getApprovalStatus(): Promise<ApprovalStatus>;
|
|
548
|
+
resumeSession(sessionId: string): Promise<PedelecSession>;
|
|
549
|
+
request<T>(type: string, payload?: Record<string, unknown>): Promise<T>;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
class PedelecSession {
|
|
553
|
+
readonly sessionId: string;
|
|
554
|
+
readonly provider: string;
|
|
555
|
+
readonly model?: string;
|
|
556
|
+
|
|
557
|
+
sendText(text: string): Promise<void>;
|
|
558
|
+
onChat(handler: (text: string) => void): () => void;
|
|
559
|
+
onTool(handler: (tool: string, args: unknown) => unknown | Promise<unknown>): () => void;
|
|
560
|
+
onError(handler: (error: PedelecError) => void): () => void;
|
|
561
|
+
onStatus(handler: (status: PedelecSessionStatus) => void): () => void;
|
|
562
|
+
onEnded(handler: () => void): () => void;
|
|
563
|
+
getStatus(): PedelecSessionStatus;
|
|
564
|
+
end(): Promise<void>;
|
|
565
|
+
}
|
|
566
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,89 @@
|
|
|
1
1
|
export type PedelecOptions = {
|
|
2
2
|
bridgeTimeoutMs?: number;
|
|
3
3
|
};
|
|
4
|
-
export type ProviderCode = "codex" | "gemini" | "opencode" | "cursor" | "claude";
|
|
4
|
+
export type ProviderCode = "codex" | "gemini" | "opencode" | "cursor" | "claude" | "ollama";
|
|
5
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
6
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
7
|
+
[key: string]: JsonValue;
|
|
8
|
+
};
|
|
9
|
+
export type ToolArgsSchemaMeta<TDefault extends JsonValue = JsonValue> = {
|
|
10
|
+
description?: string;
|
|
11
|
+
default?: TDefault;
|
|
12
|
+
examples?: TDefault[];
|
|
13
|
+
};
|
|
14
|
+
export type ToolArgsStringSchema = ToolArgsSchemaMeta<string> & {
|
|
15
|
+
type: "string";
|
|
16
|
+
enum?: string[];
|
|
17
|
+
minLength?: number;
|
|
18
|
+
maxLength?: number;
|
|
19
|
+
pattern?: string;
|
|
20
|
+
};
|
|
21
|
+
export type ToolArgsNumberSchema = ToolArgsSchemaMeta<number> & {
|
|
22
|
+
type: "number";
|
|
23
|
+
enum?: number[];
|
|
24
|
+
minimum?: number;
|
|
25
|
+
maximum?: number;
|
|
26
|
+
};
|
|
27
|
+
export type ToolArgsIntegerSchema = ToolArgsSchemaMeta<number> & {
|
|
28
|
+
type: "integer";
|
|
29
|
+
enum?: number[];
|
|
30
|
+
minimum?: number;
|
|
31
|
+
maximum?: number;
|
|
32
|
+
};
|
|
33
|
+
export type ToolArgsBooleanSchema = ToolArgsSchemaMeta<boolean> & {
|
|
34
|
+
type: "boolean";
|
|
35
|
+
enum?: boolean[];
|
|
36
|
+
};
|
|
37
|
+
export type ToolArgsArraySchema = ToolArgsSchemaMeta<JsonValue[]> & {
|
|
38
|
+
type: "array";
|
|
39
|
+
items: ToolArgsSchemaNode;
|
|
40
|
+
minItems?: number;
|
|
41
|
+
maxItems?: number;
|
|
42
|
+
uniqueItems?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export type ToolArgsObjectSchema = ToolArgsSchemaMeta<Record<string, JsonValue>> & {
|
|
45
|
+
type: "object";
|
|
46
|
+
properties?: Record<string, ToolArgsSchemaNode>;
|
|
47
|
+
required?: string[];
|
|
48
|
+
};
|
|
49
|
+
export type ToolArgsOneOfSchema = ToolArgsSchemaMeta & {
|
|
50
|
+
oneOf: ToolArgsSchemaNode[];
|
|
51
|
+
};
|
|
52
|
+
export type ToolArgsSchemaNode = ToolArgsStringSchema | ToolArgsNumberSchema | ToolArgsIntegerSchema | ToolArgsBooleanSchema | ToolArgsArraySchema | ToolArgsObjectSchema | ToolArgsOneOfSchema;
|
|
53
|
+
export type ToolArgsSchema = ToolArgsObjectSchema;
|
|
54
|
+
export type ToolSpecificHandler<TArgs = unknown, TResult = unknown> = (args: TArgs) => TResult | Promise<TResult>;
|
|
55
|
+
export type ToolDefinition<TArgs = unknown, TResult = unknown> = {
|
|
56
|
+
name: string;
|
|
57
|
+
description: string;
|
|
58
|
+
argsSchema: ToolArgsSchema;
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
handler?: ToolSpecificHandler<TArgs, TResult>;
|
|
61
|
+
};
|
|
62
|
+
export type SkillsInput = {
|
|
63
|
+
guidance: string;
|
|
64
|
+
tools: ToolDefinition[];
|
|
65
|
+
};
|
|
66
|
+
export type SerializableToolManifest = {
|
|
67
|
+
name: string;
|
|
68
|
+
description: string;
|
|
69
|
+
argsSchema: ToolArgsSchema;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
};
|
|
72
|
+
export type SerializableSkillsManifest = {
|
|
73
|
+
guidance: string;
|
|
74
|
+
tools: SerializableToolManifest[];
|
|
75
|
+
};
|
|
5
76
|
type CreateSessionInputWithProvider = {
|
|
6
77
|
provider: ProviderCode;
|
|
7
78
|
model?: string;
|
|
8
|
-
|
|
79
|
+
skills?: SkillsInput;
|
|
80
|
+
autoEndOnDisconnect?: boolean;
|
|
9
81
|
};
|
|
10
82
|
type CreateSessionInputWithDefaults = {
|
|
11
83
|
provider?: undefined;
|
|
12
84
|
model?: never;
|
|
13
|
-
|
|
85
|
+
skills?: SkillsInput;
|
|
86
|
+
autoEndOnDisconnect?: boolean;
|
|
14
87
|
};
|
|
15
88
|
export type CreateSessionInput = CreateSessionInputWithProvider | CreateSessionInputWithDefaults;
|
|
16
89
|
export type ProviderInfo = {
|
|
@@ -22,7 +95,7 @@ export type ProviderInfo = {
|
|
|
22
95
|
};
|
|
23
96
|
export type PedelecSettings = {
|
|
24
97
|
defaultProvider: ProviderCode | null;
|
|
25
|
-
|
|
98
|
+
defaultModels: Partial<Record<ProviderCode, string>>;
|
|
26
99
|
};
|
|
27
100
|
export type ApprovalStatus = {
|
|
28
101
|
installed: boolean;
|
|
@@ -73,10 +146,11 @@ type SessionEvent = {
|
|
|
73
146
|
seq?: number;
|
|
74
147
|
};
|
|
75
148
|
type ChatHandler = (text: string) => void;
|
|
76
|
-
type
|
|
149
|
+
type GenericToolHandler = (tool: string, args: unknown) => unknown | Promise<unknown>;
|
|
77
150
|
type ErrorHandler = (error: PedelecError) => void;
|
|
78
151
|
type StatusHandler = (status: PedelecSessionStatus) => void;
|
|
79
152
|
type EndedHandler = () => void;
|
|
153
|
+
export declare function defineTool<TArgs = unknown, TResult = unknown>(tool: ToolDefinition<TArgs, TResult>): ToolDefinition<TArgs, TResult>;
|
|
80
154
|
export declare class Pedelec {
|
|
81
155
|
private readonly pageWindow;
|
|
82
156
|
private readonly channelId;
|
|
@@ -114,21 +188,25 @@ export declare class PedelecSession {
|
|
|
114
188
|
readonly model?: string;
|
|
115
189
|
private status;
|
|
116
190
|
private pendingSend;
|
|
117
|
-
private
|
|
191
|
+
private genericToolHandler;
|
|
192
|
+
private inlineToolHandlers;
|
|
193
|
+
private readonly namedToolHandlers;
|
|
118
194
|
private readonly chatHandlers;
|
|
119
195
|
private readonly errorHandlers;
|
|
120
196
|
private readonly statusHandlers;
|
|
121
197
|
private readonly endedHandlers;
|
|
122
|
-
constructor(client: Pedelec, sessionId: string, provider: string, model?: string);
|
|
198
|
+
constructor(client: Pedelec, sessionId: string, provider: string, model?: string, inlineToolHandlers?: Map<string, ToolSpecificHandler>);
|
|
123
199
|
sendText(text: string): Promise<void>;
|
|
124
200
|
onChat(handler: ChatHandler): () => void;
|
|
125
|
-
onTool(handler:
|
|
201
|
+
onTool(handler: GenericToolHandler): () => void;
|
|
202
|
+
onTool<TArgs = unknown, TResult = unknown>(toolName: string, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
|
|
126
203
|
onError(handler: ErrorHandler): () => void;
|
|
127
204
|
onStatus(handler: StatusHandler): () => void;
|
|
128
205
|
onEnded(handler: EndedHandler): () => void;
|
|
129
206
|
getStatus(): PedelecSessionStatus;
|
|
130
207
|
end(): Promise<void>;
|
|
131
208
|
handleEvent(event: SessionEvent): void;
|
|
209
|
+
replaceInlineToolHandlers(handlers: Map<string, ToolSpecificHandler>): void;
|
|
132
210
|
private handleToolCall;
|
|
133
211
|
private resolvePendingSend;
|
|
134
212
|
private rejectPendingSend;
|
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
|
}
|
|
@@ -271,16 +365,19 @@ export class PedelecSession {
|
|
|
271
365
|
model;
|
|
272
366
|
status = "idle";
|
|
273
367
|
pendingSend = null;
|
|
274
|
-
|
|
368
|
+
genericToolHandler = null;
|
|
369
|
+
inlineToolHandlers = new Map();
|
|
370
|
+
namedToolHandlers = new Map();
|
|
275
371
|
chatHandlers = new Set();
|
|
276
372
|
errorHandlers = new Set();
|
|
277
373
|
statusHandlers = new Set();
|
|
278
374
|
endedHandlers = new Set();
|
|
279
|
-
constructor(client, sessionId, provider, model) {
|
|
375
|
+
constructor(client, sessionId, provider, model, inlineToolHandlers = new Map()) {
|
|
280
376
|
this.client = client;
|
|
281
377
|
this.sessionId = sessionId;
|
|
282
378
|
this.provider = provider;
|
|
283
379
|
this.model = model;
|
|
380
|
+
this.inlineToolHandlers = new Map(inlineToolHandlers);
|
|
284
381
|
}
|
|
285
382
|
sendText(text) {
|
|
286
383
|
if (this.status === "ended") {
|
|
@@ -310,11 +407,27 @@ export class PedelecSession {
|
|
|
310
407
|
this.chatHandlers.add(handler);
|
|
311
408
|
return () => this.chatHandlers.delete(handler);
|
|
312
409
|
}
|
|
313
|
-
onTool(
|
|
314
|
-
|
|
410
|
+
onTool(toolNameOrHandler, maybeHandler) {
|
|
411
|
+
if (typeof toolNameOrHandler === "string") {
|
|
412
|
+
const toolName = toolNameOrHandler;
|
|
413
|
+
if (!TOOL_NAME_PATTERN.test(toolName)) {
|
|
414
|
+
throw makeError("INVALID_INPUT", "tool name is invalid", { toolName });
|
|
415
|
+
}
|
|
416
|
+
if (typeof maybeHandler !== "function") {
|
|
417
|
+
throw makeError("INVALID_INPUT", "tool handler must be a function", { toolName });
|
|
418
|
+
}
|
|
419
|
+
this.namedToolHandlers.set(toolName, maybeHandler);
|
|
420
|
+
return () => {
|
|
421
|
+
if (this.namedToolHandlers.get(toolName) === maybeHandler) {
|
|
422
|
+
this.namedToolHandlers.delete(toolName);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const handler = toolNameOrHandler;
|
|
427
|
+
this.genericToolHandler = handler;
|
|
315
428
|
return () => {
|
|
316
|
-
if (this.
|
|
317
|
-
this.
|
|
429
|
+
if (this.genericToolHandler === handler) {
|
|
430
|
+
this.genericToolHandler = null;
|
|
318
431
|
}
|
|
319
432
|
};
|
|
320
433
|
}
|
|
@@ -392,18 +505,26 @@ export class PedelecSession {
|
|
|
392
505
|
this.markEnded();
|
|
393
506
|
}
|
|
394
507
|
}
|
|
508
|
+
replaceInlineToolHandlers(handlers) {
|
|
509
|
+
this.inlineToolHandlers = new Map(handlers);
|
|
510
|
+
}
|
|
395
511
|
async handleToolCall(event) {
|
|
396
512
|
let result;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
513
|
+
const namedHandler = this.namedToolHandlers.get(event.tool);
|
|
514
|
+
const inlineHandler = this.inlineToolHandlers.get(event.tool);
|
|
515
|
+
if (namedHandler) {
|
|
516
|
+
try {
|
|
517
|
+
result = await namedHandler(event.args);
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
result = {
|
|
521
|
+
error: normalizeError(err, "TOOL_HANDLER_ERROR", "Tool handler failed"),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
403
524
|
}
|
|
404
|
-
else {
|
|
525
|
+
else if (inlineHandler) {
|
|
405
526
|
try {
|
|
406
|
-
result = await
|
|
527
|
+
result = await inlineHandler(event.args);
|
|
407
528
|
}
|
|
408
529
|
catch (err) {
|
|
409
530
|
result = {
|
|
@@ -411,6 +532,23 @@ export class PedelecSession {
|
|
|
411
532
|
};
|
|
412
533
|
}
|
|
413
534
|
}
|
|
535
|
+
else if (this.genericToolHandler) {
|
|
536
|
+
try {
|
|
537
|
+
result = await this.genericToolHandler(event.tool, event.args);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
result = {
|
|
541
|
+
error: normalizeError(err, "TOOL_HANDLER_ERROR", "Tool handler failed"),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
result = {
|
|
547
|
+
error: makeError("TOOL_HANDLER_NOT_FOUND", `No tool handler registered for ${event.tool}`, {
|
|
548
|
+
tool: event.tool,
|
|
549
|
+
}),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
414
552
|
try {
|
|
415
553
|
await this.client.request("submit_tool_result", {
|
|
416
554
|
sessionId: this.sessionId,
|
|
@@ -470,14 +608,23 @@ function isSessionEvent(message) {
|
|
|
470
608
|
function isSettings(value) {
|
|
471
609
|
if (!value || typeof value !== "object")
|
|
472
610
|
return false;
|
|
611
|
+
if (Array.isArray(value))
|
|
612
|
+
return false;
|
|
473
613
|
const settings = value;
|
|
614
|
+
const defaultModels = settings.defaultModels;
|
|
615
|
+
if (settings.defaultModel !== undefined)
|
|
616
|
+
return false;
|
|
617
|
+
if (!defaultModels || typeof defaultModels !== "object" || Array.isArray(defaultModels)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
474
620
|
return ((settings.defaultProvider === null ||
|
|
475
621
|
settings.defaultProvider === "codex" ||
|
|
476
622
|
settings.defaultProvider === "gemini" ||
|
|
477
623
|
settings.defaultProvider === "opencode" ||
|
|
478
624
|
settings.defaultProvider === "cursor" ||
|
|
479
|
-
settings.defaultProvider === "claude"
|
|
480
|
-
|
|
625
|
+
settings.defaultProvider === "claude" ||
|
|
626
|
+
settings.defaultProvider === "ollama") &&
|
|
627
|
+
Object.entries(defaultModels).every(([provider, model]) => isProviderCode(provider) && typeof model === "string"));
|
|
481
628
|
}
|
|
482
629
|
function isApprovalStatus(value) {
|
|
483
630
|
if (!value || typeof value !== "object")
|
|
@@ -492,7 +639,8 @@ function isProviderCode(value) {
|
|
|
492
639
|
value === "gemini" ||
|
|
493
640
|
value === "opencode" ||
|
|
494
641
|
value === "cursor" ||
|
|
495
|
-
value === "claude"
|
|
642
|
+
value === "claude" ||
|
|
643
|
+
value === "ollama");
|
|
496
644
|
}
|
|
497
645
|
function makeError(code, message, details) {
|
|
498
646
|
return details === undefined ? { code, message } : { code, message, details };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaoruisaac/pedelec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/kaoruisaac/pedelec.git",
|
|
23
|
+
"url": "git+https://github.com/kaoruisaac/pedelec.git",
|
|
24
24
|
"directory": "sdk"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|