@kaoruisaac/pedelec 0.1.25 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -19
- package/dist/index.d.ts +18 -8
- package/dist/index.js +125 -79
- package/dist/index.type-test.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ const pedelec = new Pedelec();
|
|
|
88
88
|
|
|
89
89
|
const session = await pedelec.createSession({
|
|
90
90
|
provider: "codex",
|
|
91
|
-
|
|
91
|
+
effortLevel: "high",
|
|
92
92
|
skills: {
|
|
93
93
|
guidance: "Use get_current_page when you need browser page context.",
|
|
94
94
|
tools: [
|
|
@@ -145,6 +145,19 @@ const result = await session.readAsset("/model.glb", "file");
|
|
|
145
145
|
|
|
146
146
|
Public asset paths use `/...` with `assets/` as their implicit root; nested paths such as `/results/model.glb` are supported up to 100 MiB.
|
|
147
147
|
|
|
148
|
+
## Sandbox workspace
|
|
149
|
+
|
|
150
|
+
By default, each session receives a temporary Desktop-managed sandbox. To use an application-owned workspace that can persist across sessions, pass an absolute path:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const session = await pedelec.createSession({
|
|
154
|
+
provider: "codex",
|
|
155
|
+
sandbox: { path: "C:\\workspace\\project-a" },
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Pedelec creates missing `skills/`, `assets/`, `logs/`, and `tmp/` directories and preserves existing files. It never deletes an explicit workspace, and multiple active sessions may share it. Filesystem write conflicts are the application's responsibility. An explicit path must not overlap Pedelec's managed sandbox root.
|
|
160
|
+
|
|
148
161
|
---
|
|
149
162
|
|
|
150
163
|
## Creating a Session
|
|
@@ -174,31 +187,31 @@ if (availability.available) startUi();
|
|
|
174
187
|
|
|
175
188
|
An unavailable extension may be disconnected rather than absent. `desktop.launchAttempted` means the settings probe was sent, not that Desktop was confirmed to have launched. Invalid settings responses also count as Desktop unavailable in this probe.
|
|
176
189
|
|
|
177
|
-
###
|
|
190
|
+
### Selecting Provider and Effort
|
|
178
191
|
|
|
179
192
|
```ts
|
|
180
193
|
const session = await pedelec.createSession({
|
|
181
|
-
provider: "
|
|
182
|
-
|
|
194
|
+
provider: "codex",
|
|
195
|
+
effortLevel: "high",
|
|
183
196
|
});
|
|
197
|
+
|
|
198
|
+
// Or let Desktop Settings choose the provider.
|
|
199
|
+
const defaultSession = await pedelec.createSession({ effortLevel: "low" });
|
|
184
200
|
```
|
|
185
201
|
|
|
202
|
+
The SDK uses the provider-independent effort levels `default`, `low`, and `high`. An omitted level means `default`. Desktop users map each provider's profiles to concrete model and native effort arguments; that mapping is not exposed through the SDK.
|
|
203
|
+
|
|
186
204
|
Currently supported provider codes in the SDK:
|
|
187
205
|
|
|
188
|
-
| Provider | Code |
|
|
189
|
-
| --- | --- |
|
|
190
|
-
| Codex | `codex` |
|
|
191
|
-
| Antigravity | `antigravity` |
|
|
192
|
-
| OpenCode | `opencode` |
|
|
193
|
-
| Cursor | `cursor` |
|
|
194
|
-
| Claude Code | `claude` |
|
|
195
|
-
| Ollama | `ollama` |
|
|
206
|
+
| Provider | Code |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| Codex | `codex` |
|
|
209
|
+
| Antigravity | `antigravity` |
|
|
210
|
+
| OpenCode | `opencode` |
|
|
211
|
+
| Cursor | `cursor` |
|
|
212
|
+
| Claude Code | `claude` |
|
|
213
|
+
| Ollama | `ollama` |
|
|
196
214
|
|
|
197
|
-
Ollama sessions are executed by the `pedelec-agent
|
|
215
|
+
Ollama sessions are executed by the bundled `pedelec-agent`. The selected Ollama effort profile must contain a model in Desktop Settings; otherwise Core returns `MODEL_REQUIRED`. Low and high profiles are optional, but there is no fallback between profiles.
|
|
198
216
|
|
|
199
|
-
|
|
200
|
-
const session = await pedelec.createSession({
|
|
201
|
-
provider: "ollama",
|
|
202
|
-
model: "qwen3-14b-32k:latest",
|
|
203
|
-
});
|
|
204
|
-
```
|
|
217
|
+
`getSettings()` returns only `{ defaultProvider }`. It never exposes provider settings, effort arguments, credentials, or model names.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type PedelecOptions = {
|
|
|
2
2
|
bridgeTimeoutMs?: number;
|
|
3
3
|
};
|
|
4
4
|
export type ProviderCode = "codex" | "antigravity" | "opencode" | "cursor" | "claude" | "ollama";
|
|
5
|
+
export type EffortLevel = "default" | "low" | "high";
|
|
5
6
|
export type JsonPrimitive = string | number | boolean | null;
|
|
6
7
|
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
7
8
|
[key: string]: JsonValue;
|
|
@@ -82,16 +83,21 @@ export type SerializableSkillsManifest = {
|
|
|
82
83
|
guidance: string;
|
|
83
84
|
tools: SerializableToolManifest[];
|
|
84
85
|
};
|
|
86
|
+
export type CreateSessionSandboxInput = {
|
|
87
|
+
path: string;
|
|
88
|
+
};
|
|
85
89
|
type CreateSessionInputWithProvider<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
|
|
86
90
|
provider: ProviderCode;
|
|
87
|
-
|
|
91
|
+
effortLevel?: EffortLevel;
|
|
88
92
|
skills?: SkillsInput<TTools>;
|
|
93
|
+
sandbox?: CreateSessionSandboxInput;
|
|
89
94
|
autoEndOnDisconnect?: boolean;
|
|
90
95
|
};
|
|
91
96
|
type CreateSessionInputWithDefaults<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
|
|
92
97
|
provider?: undefined;
|
|
93
|
-
|
|
98
|
+
effortLevel?: EffortLevel;
|
|
94
99
|
skills?: SkillsInput<TTools>;
|
|
100
|
+
sandbox?: CreateSessionSandboxInput;
|
|
95
101
|
autoEndOnDisconnect?: boolean;
|
|
96
102
|
};
|
|
97
103
|
export type CreateSessionInput<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = CreateSessionInputWithProvider<TTools> | CreateSessionInputWithDefaults<TTools>;
|
|
@@ -103,7 +109,6 @@ export type ProviderInfo = {
|
|
|
103
109
|
};
|
|
104
110
|
export type PedelecSettings = {
|
|
105
111
|
defaultProvider: ProviderCode | null;
|
|
106
|
-
defaultModels: Partial<Record<ProviderCode, string>>;
|
|
107
112
|
};
|
|
108
113
|
export type ApprovalStatus = {
|
|
109
114
|
installed: boolean;
|
|
@@ -135,7 +140,7 @@ export type PedelecSessionStatus = "idle" | "running" | "waiting_tool_result" |
|
|
|
135
140
|
export type PedelecEventContext = {
|
|
136
141
|
sessionId: string;
|
|
137
142
|
provider: string;
|
|
138
|
-
|
|
143
|
+
effortLevel?: EffortLevel;
|
|
139
144
|
sessionCreatedAt: number;
|
|
140
145
|
eventReceivedAt?: number;
|
|
141
146
|
eventEmittedAt: number;
|
|
@@ -224,7 +229,6 @@ export declare class Pedelec {
|
|
|
224
229
|
private readonly sessions;
|
|
225
230
|
private readonly lastSeqBySession;
|
|
226
231
|
private nextRequestNumber;
|
|
227
|
-
private disconnectedError;
|
|
228
232
|
private port;
|
|
229
233
|
constructor(options?: PedelecOptions);
|
|
230
234
|
createSession(): Promise<PedelecSession<string>>;
|
|
@@ -238,21 +242,22 @@ export declare class Pedelec {
|
|
|
238
242
|
private resolveCreateSessionInput;
|
|
239
243
|
checkAvailability(): Promise<PedelecAvailability>;
|
|
240
244
|
private resolveDefaultCreateSessionInput;
|
|
241
|
-
private resolveProviderOnlyCreateSessionInput;
|
|
242
245
|
private assertDefaultProviderAvailable;
|
|
243
246
|
private registerSession;
|
|
247
|
+
private ensureExtensionPort;
|
|
244
248
|
private connectExtension;
|
|
245
249
|
private handlePortMessage;
|
|
246
250
|
private handleResponse;
|
|
247
251
|
private isNewEvent;
|
|
248
252
|
private handleDisconnect;
|
|
249
253
|
private broadcastError;
|
|
254
|
+
private invalidateSessions;
|
|
250
255
|
}
|
|
251
256
|
export declare class PedelecSession<TToolName extends string = string> {
|
|
252
257
|
private readonly client;
|
|
253
258
|
readonly sessionId: string;
|
|
254
259
|
readonly provider: string;
|
|
255
|
-
readonly
|
|
260
|
+
readonly effortLevel?: EffortLevel;
|
|
256
261
|
readonly sessionCreatedAt: number;
|
|
257
262
|
private status;
|
|
258
263
|
private pendingSend;
|
|
@@ -262,6 +267,7 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
262
267
|
private ending;
|
|
263
268
|
private prepared;
|
|
264
269
|
private activeTurn;
|
|
270
|
+
private transportDetached;
|
|
265
271
|
private genericToolHandler;
|
|
266
272
|
private inlineToolHandlers;
|
|
267
273
|
private readonly namedToolHandlers;
|
|
@@ -269,7 +275,7 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
269
275
|
private readonly errorHandlers;
|
|
270
276
|
private readonly statusHandlers;
|
|
271
277
|
private readonly endedHandlers;
|
|
272
|
-
constructor(client: Pedelec, sessionId: string, provider: string,
|
|
278
|
+
constructor(client: Pedelec, sessionId: string, provider: string, effortLevel?: EffortLevel, inlineToolHandlers?: Map<string, ToolSpecificHandler>);
|
|
273
279
|
prepare(): Promise<void>;
|
|
274
280
|
sendText(text: string): Promise<void>;
|
|
275
281
|
onChat(handler: ChatHandler): () => void;
|
|
@@ -288,6 +294,10 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
288
294
|
end(): Promise<void>;
|
|
289
295
|
handleEvent(event: SessionEvent, meta?: EventDispatchMeta): void;
|
|
290
296
|
replaceInlineToolHandlers(handlers: Map<string, ToolSpecificHandler>): void;
|
|
297
|
+
/** @internal */
|
|
298
|
+
isTransportDetached(): boolean;
|
|
299
|
+
/** @internal */
|
|
300
|
+
handleTransportDisconnect(error: PedelecError): void;
|
|
291
301
|
private handleToolCall;
|
|
292
302
|
private resolvePendingSend;
|
|
293
303
|
private resolvePendingPrepare;
|
package/dist/index.js
CHANGED
|
@@ -122,14 +122,12 @@ export class Pedelec {
|
|
|
122
122
|
sessions = new Map();
|
|
123
123
|
lastSeqBySession = new Map();
|
|
124
124
|
nextRequestNumber = 1;
|
|
125
|
-
disconnectedError = null;
|
|
126
125
|
port = null;
|
|
127
126
|
constructor(options = {}) {
|
|
128
127
|
this.pageWindow = typeof window === "undefined" ? null : window;
|
|
129
128
|
this.channelId = createChannelId();
|
|
130
129
|
this.bridgeTimeoutMs = Math.max(1, options.bridgeTimeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS);
|
|
131
130
|
if (!this.pageWindow) {
|
|
132
|
-
this.disconnectedError = makeError("EXTENSION_UNAVAILABLE", "Pedelec SDK must run in a browser page context.");
|
|
133
131
|
return;
|
|
134
132
|
}
|
|
135
133
|
this.connectExtension();
|
|
@@ -140,15 +138,16 @@ export class Pedelec {
|
|
|
140
138
|
const result = await this.request("create_session", {
|
|
141
139
|
input: {
|
|
142
140
|
provider: resolvedInput.provider,
|
|
143
|
-
|
|
141
|
+
effortLevel: resolvedInput.effortLevel,
|
|
144
142
|
skills: resolvedInput.skills,
|
|
143
|
+
sandbox: resolvedInput.sandbox,
|
|
145
144
|
autoEndOnDisconnect: resolvedInput.autoEndOnDisconnect,
|
|
146
145
|
},
|
|
147
146
|
});
|
|
148
147
|
if (!result.sessionId) {
|
|
149
148
|
throw makeError("SDK_PROTOCOL_ERROR", "create_session response did not include sessionId");
|
|
150
149
|
}
|
|
151
|
-
return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.
|
|
150
|
+
return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.effortLevel, resolvedInput.inlineToolHandlers);
|
|
152
151
|
}
|
|
153
152
|
async listProviders() {
|
|
154
153
|
return normalizeProviderInfoList(await this.request("list_providers"));
|
|
@@ -157,14 +156,6 @@ export class Pedelec {
|
|
|
157
156
|
return normalizePedelecSettings(await this.request("get_settings"));
|
|
158
157
|
}
|
|
159
158
|
async getApprovalStatus() {
|
|
160
|
-
if (this.disconnectedError) {
|
|
161
|
-
return {
|
|
162
|
-
installed: false,
|
|
163
|
-
approved: false,
|
|
164
|
-
origin: getCurrentOrigin(this.pageWindow),
|
|
165
|
-
appConnected: false,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
159
|
try {
|
|
169
160
|
const result = await this.request("get_approval_status");
|
|
170
161
|
return result;
|
|
@@ -195,11 +186,12 @@ export class Pedelec {
|
|
|
195
186
|
return this.registerSession(result.sessionId, "", undefined);
|
|
196
187
|
}
|
|
197
188
|
request(type, payload = {}) {
|
|
198
|
-
|
|
199
|
-
|
|
189
|
+
let port;
|
|
190
|
+
try {
|
|
191
|
+
port = this.ensureExtensionPort();
|
|
200
192
|
}
|
|
201
|
-
|
|
202
|
-
return Promise.reject(
|
|
193
|
+
catch (err) {
|
|
194
|
+
return Promise.reject(normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable."));
|
|
203
195
|
}
|
|
204
196
|
const requestId = `sdk_${Date.now()}_${this.nextRequestNumber++}`;
|
|
205
197
|
const message = { channelId: this.channelId, type, requestId, ...payload };
|
|
@@ -212,17 +204,22 @@ export class Pedelec {
|
|
|
212
204
|
}));
|
|
213
205
|
}, this.bridgeTimeoutMs);
|
|
214
206
|
this.pendingRequests.set(requestId, {
|
|
207
|
+
port,
|
|
215
208
|
resolve: (value) => resolve(value),
|
|
216
209
|
reject,
|
|
217
210
|
timeoutId,
|
|
218
211
|
});
|
|
219
212
|
try {
|
|
220
|
-
|
|
213
|
+
port.postMessage(message);
|
|
221
214
|
}
|
|
222
215
|
catch (err) {
|
|
223
216
|
this.pendingRequests.delete(requestId);
|
|
224
217
|
clearTimeout(timeoutId);
|
|
225
|
-
|
|
218
|
+
const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
|
|
219
|
+
if (this.port === port) {
|
|
220
|
+
this.handleDisconnect(port, error);
|
|
221
|
+
}
|
|
222
|
+
reject(error);
|
|
226
223
|
}
|
|
227
224
|
});
|
|
228
225
|
}
|
|
@@ -234,24 +231,24 @@ export class Pedelec {
|
|
|
234
231
|
const raw = (input ?? {});
|
|
235
232
|
const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
|
|
236
233
|
const hasProvider = provider.length > 0;
|
|
237
|
-
|
|
234
|
+
if (raw.model !== undefined) {
|
|
235
|
+
throw makeError("INVALID_INPUT", "model is no longer supported; configure provider effort profiles in Desktop Settings");
|
|
236
|
+
}
|
|
237
|
+
const effortLevel = raw.effortLevel === undefined ? "default" : raw.effortLevel;
|
|
238
|
+
if (!isEffortLevel(effortLevel)) {
|
|
239
|
+
throw makeError("INVALID_INPUT", "effortLevel must be one of default, low, or high");
|
|
240
|
+
}
|
|
238
241
|
const autoEndOnDisconnect = raw.autoEndOnDisconnect !== false;
|
|
239
242
|
const normalizedSkills = normalizeSkillsInput(raw.skills);
|
|
240
|
-
|
|
241
|
-
throw makeError("INVALID_INPUT", "model cannot be provided without provider");
|
|
242
|
-
}
|
|
243
|
-
const userModel = typeof raw.model === "string" ? raw.model : undefined;
|
|
243
|
+
const sandbox = normalizeCreateSessionSandboxInput(raw.sandbox);
|
|
244
244
|
if (!hasProvider) {
|
|
245
|
-
return this.resolveDefaultCreateSessionInput(normalizedSkills, autoEndOnDisconnect);
|
|
246
|
-
}
|
|
247
|
-
let model = userModel;
|
|
248
|
-
if (model === undefined) {
|
|
249
|
-
return this.resolveProviderOnlyCreateSessionInput(provider, normalizedSkills, autoEndOnDisconnect);
|
|
245
|
+
return this.resolveDefaultCreateSessionInput(effortLevel, normalizedSkills, sandbox, autoEndOnDisconnect);
|
|
250
246
|
}
|
|
251
247
|
return {
|
|
252
248
|
provider: provider,
|
|
253
|
-
|
|
249
|
+
effortLevel,
|
|
254
250
|
skills: normalizedSkills.manifest,
|
|
251
|
+
sandbox,
|
|
255
252
|
inlineToolHandlers: normalizedSkills.handlers,
|
|
256
253
|
autoEndOnDisconnect,
|
|
257
254
|
};
|
|
@@ -308,7 +305,7 @@ export class Pedelec {
|
|
|
308
305
|
};
|
|
309
306
|
}
|
|
310
307
|
}
|
|
311
|
-
async resolveDefaultCreateSessionInput(normalizedSkills, autoEndOnDisconnect) {
|
|
308
|
+
async resolveDefaultCreateSessionInput(effortLevel, normalizedSkills, sandbox, autoEndOnDisconnect) {
|
|
312
309
|
const settings = await this.getSettings();
|
|
313
310
|
if (!settings.defaultProvider) {
|
|
314
311
|
throw makeError("DEFAULT_PROVIDER_NOT_SET", "Default provider is not set. Open the Pedelec desktop app Settings page and choose a default provider.");
|
|
@@ -316,18 +313,9 @@ export class Pedelec {
|
|
|
316
313
|
await this.assertDefaultProviderAvailable(settings.defaultProvider);
|
|
317
314
|
return {
|
|
318
315
|
provider: settings.defaultProvider,
|
|
319
|
-
|
|
320
|
-
skills: normalizedSkills.manifest,
|
|
321
|
-
inlineToolHandlers: normalizedSkills.handlers,
|
|
322
|
-
autoEndOnDisconnect,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
async resolveProviderOnlyCreateSessionInput(provider, normalizedSkills, autoEndOnDisconnect) {
|
|
326
|
-
const settings = await this.getSettings();
|
|
327
|
-
return {
|
|
328
|
-
provider,
|
|
329
|
-
model: settings.defaultModels[provider] ?? undefined,
|
|
316
|
+
effortLevel,
|
|
330
317
|
skills: normalizedSkills.manifest,
|
|
318
|
+
sandbox,
|
|
331
319
|
inlineToolHandlers: normalizedSkills.handlers,
|
|
332
320
|
autoEndOnDisconnect,
|
|
333
321
|
};
|
|
@@ -339,33 +327,50 @@ export class Pedelec {
|
|
|
339
327
|
throw makeError("DEFAULT_PROVIDER_UNAVAILABLE", "Default provider is not currently available. Open the Pedelec desktop app Settings page and choose an available provider.", { provider });
|
|
340
328
|
}
|
|
341
329
|
}
|
|
342
|
-
registerSession(sessionId, provider,
|
|
330
|
+
registerSession(sessionId, provider, effortLevel, inlineToolHandlers = new Map()) {
|
|
343
331
|
const existing = this.sessions.get(sessionId);
|
|
344
|
-
if (existing) {
|
|
332
|
+
if (existing && !existing.isTransportDetached()) {
|
|
345
333
|
existing.replaceInlineToolHandlers(inlineToolHandlers);
|
|
346
334
|
return existing;
|
|
347
335
|
}
|
|
348
|
-
const session = new PedelecSession(this, sessionId, provider,
|
|
336
|
+
const session = new PedelecSession(this, sessionId, provider, effortLevel, inlineToolHandlers);
|
|
349
337
|
this.sessions.set(sessionId, session);
|
|
350
338
|
return session;
|
|
351
339
|
}
|
|
352
|
-
|
|
340
|
+
ensureExtensionPort() {
|
|
341
|
+
if (this.port)
|
|
342
|
+
return this.port;
|
|
343
|
+
if (!this.pageWindow) {
|
|
344
|
+
throw makeError("EXTENSION_UNAVAILABLE", "Pedelec SDK must run in a browser page context.");
|
|
345
|
+
}
|
|
353
346
|
const runtime = globalThis.chrome?.runtime;
|
|
354
347
|
if (!runtime?.connect) {
|
|
355
|
-
|
|
356
|
-
return;
|
|
348
|
+
throw makeError("EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
|
|
357
349
|
}
|
|
358
350
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
351
|
+
const port = runtime.connect(PEDELEC_EXTENSION_ID, { name: SDK_EXTERNAL_PORT_NAME });
|
|
352
|
+
port.onMessage.addListener((message) => this.handlePortMessage(port, message));
|
|
353
|
+
port.onDisconnect.addListener(() => this.handleDisconnect(port));
|
|
354
|
+
this.port = port;
|
|
355
|
+
return port;
|
|
362
356
|
}
|
|
363
357
|
catch (err) {
|
|
364
|
-
this.disconnectedError = normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
|
|
365
358
|
this.port = null;
|
|
359
|
+
throw normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
connectExtension() {
|
|
363
|
+
try {
|
|
364
|
+
this.ensureExtensionPort();
|
|
365
|
+
}
|
|
366
|
+
catch (_err) {
|
|
367
|
+
// The first connection is eager for normal browser startup, but a failed
|
|
368
|
+
// attempt remains retryable through the next request.
|
|
366
369
|
}
|
|
367
370
|
}
|
|
368
|
-
handlePortMessage(raw) {
|
|
371
|
+
handlePortMessage(port, raw) {
|
|
372
|
+
if (port !== this.port)
|
|
373
|
+
return;
|
|
369
374
|
const message = raw;
|
|
370
375
|
if (!message || typeof message !== "object")
|
|
371
376
|
return;
|
|
@@ -411,29 +416,52 @@ export class Pedelec {
|
|
|
411
416
|
this.lastSeqBySession.set(event.sessionId, event.seq);
|
|
412
417
|
return true;
|
|
413
418
|
}
|
|
414
|
-
handleDisconnect() {
|
|
419
|
+
handleDisconnect(port, disconnectError) {
|
|
420
|
+
if (port !== this.port)
|
|
421
|
+
return;
|
|
415
422
|
const runtime = globalThis.chrome?.runtime;
|
|
416
|
-
const error = normalizeError(runtime?.lastError, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
|
|
417
|
-
this.disconnectedError = error;
|
|
423
|
+
const error = normalizeError(disconnectError ?? runtime?.lastError, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
|
|
418
424
|
this.port = null;
|
|
419
|
-
|
|
425
|
+
this.invalidateSessions(error);
|
|
426
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
427
|
+
if (pending.port !== port)
|
|
428
|
+
continue;
|
|
429
|
+
this.pendingRequests.delete(requestId);
|
|
420
430
|
clearTimeout(pending.timeoutId);
|
|
421
431
|
pending.reject(error);
|
|
422
432
|
}
|
|
423
|
-
this.pendingRequests.clear();
|
|
424
|
-
this.broadcastError(error);
|
|
425
433
|
}
|
|
426
434
|
broadcastError(error) {
|
|
427
435
|
for (const session of this.sessions.values()) {
|
|
428
436
|
session.handleEvent({ type: "error", sessionId: session.sessionId, error }, { source: "sdk" });
|
|
429
437
|
}
|
|
430
438
|
}
|
|
439
|
+
invalidateSessions(error) {
|
|
440
|
+
for (const session of this.sessions.values()) {
|
|
441
|
+
session.handleTransportDisconnect(error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function normalizeCreateSessionSandboxInput(value) {
|
|
446
|
+
if (value === undefined)
|
|
447
|
+
return undefined;
|
|
448
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
449
|
+
throw makeError("INVALID_INPUT", "sandbox must be an object");
|
|
450
|
+
}
|
|
451
|
+
const path = value.path;
|
|
452
|
+
if (typeof path !== "string") {
|
|
453
|
+
throw makeError("INVALID_INPUT", "sandbox.path must be a string");
|
|
454
|
+
}
|
|
455
|
+
if (path.trim().length === 0) {
|
|
456
|
+
throw makeError("INVALID_INPUT", "sandbox.path must not be empty");
|
|
457
|
+
}
|
|
458
|
+
return { path };
|
|
431
459
|
}
|
|
432
460
|
export class PedelecSession {
|
|
433
461
|
client;
|
|
434
462
|
sessionId;
|
|
435
463
|
provider;
|
|
436
|
-
|
|
464
|
+
effortLevel;
|
|
437
465
|
sessionCreatedAt = Date.now();
|
|
438
466
|
status = "idle";
|
|
439
467
|
pendingSend = null;
|
|
@@ -443,6 +471,7 @@ export class PedelecSession {
|
|
|
443
471
|
ending = false;
|
|
444
472
|
prepared = false;
|
|
445
473
|
activeTurn = null;
|
|
474
|
+
transportDetached = false;
|
|
446
475
|
genericToolHandler = null;
|
|
447
476
|
inlineToolHandlers = new Map();
|
|
448
477
|
namedToolHandlers = new Map();
|
|
@@ -450,15 +479,15 @@ export class PedelecSession {
|
|
|
450
479
|
errorHandlers = new Set();
|
|
451
480
|
statusHandlers = new Set();
|
|
452
481
|
endedHandlers = new Set();
|
|
453
|
-
constructor(client, sessionId, provider,
|
|
482
|
+
constructor(client, sessionId, provider, effortLevel, inlineToolHandlers = new Map()) {
|
|
454
483
|
this.client = client;
|
|
455
484
|
this.sessionId = sessionId;
|
|
456
485
|
this.provider = provider;
|
|
457
|
-
this.
|
|
486
|
+
this.effortLevel = effortLevel;
|
|
458
487
|
this.inlineToolHandlers = new Map(inlineToolHandlers);
|
|
459
488
|
}
|
|
460
489
|
prepare() {
|
|
461
|
-
if (this.status === "ended") {
|
|
490
|
+
if (this.transportDetached || this.status === "ended") {
|
|
462
491
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
463
492
|
}
|
|
464
493
|
if (this.pendingSend) {
|
|
@@ -482,7 +511,8 @@ export class PedelecSession {
|
|
|
482
511
|
})
|
|
483
512
|
.catch((err) => {
|
|
484
513
|
const error = normalizeError(err, "PREPARE_FAILED", "prepare failed");
|
|
485
|
-
this.
|
|
514
|
+
if (!this.transportDetached)
|
|
515
|
+
this.emitError(error, { source: "sdk" });
|
|
486
516
|
this.clearPendingPrepare();
|
|
487
517
|
this.finishActiveTurn(turn);
|
|
488
518
|
throw error;
|
|
@@ -505,7 +535,7 @@ export class PedelecSession {
|
|
|
505
535
|
// prepare is an optimization; sendText falls back to the original first-run path.
|
|
506
536
|
}
|
|
507
537
|
}
|
|
508
|
-
if (this.status === "ended") {
|
|
538
|
+
if (this.transportDetached || this.status === "ended") {
|
|
509
539
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
510
540
|
}
|
|
511
541
|
if (this.pendingSend || this.pendingPrepare) {
|
|
@@ -524,7 +554,8 @@ export class PedelecSession {
|
|
|
524
554
|
})
|
|
525
555
|
.catch((err) => {
|
|
526
556
|
const error = normalizeError(err, "SEND_TEXT_FAILED", "sendText failed");
|
|
527
|
-
this.
|
|
557
|
+
if (!this.transportDetached)
|
|
558
|
+
this.emitError(error, { source: "sdk" });
|
|
528
559
|
this.clearPendingSend();
|
|
529
560
|
this.finishActiveTurn(turn);
|
|
530
561
|
throw error;
|
|
@@ -560,7 +591,7 @@ export class PedelecSession {
|
|
|
560
591
|
};
|
|
561
592
|
}
|
|
562
593
|
uploadAsset(file, targetPath) {
|
|
563
|
-
if (this.ending || this.status === "ended")
|
|
594
|
+
if (this.transportDetached || this.ending || this.status === "ended")
|
|
564
595
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
565
596
|
if (this.uploadPromise) {
|
|
566
597
|
return Promise.reject(makeError("SESSION_BUSY", "session is busy", { sessionId: this.sessionId }));
|
|
@@ -601,13 +632,13 @@ export class PedelecSession {
|
|
|
601
632
|
return this.uploadPromise;
|
|
602
633
|
}
|
|
603
634
|
listAssets() {
|
|
604
|
-
if (this.ending || this.status === "ended") {
|
|
635
|
+
if (this.transportDetached || this.ending || this.status === "ended") {
|
|
605
636
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
606
637
|
}
|
|
607
638
|
return this.client.request("list_assets", { sessionId: this.sessionId }).then(normalizeListAssetsResponse);
|
|
608
639
|
}
|
|
609
640
|
readAsset(path, type) {
|
|
610
|
-
if (this.ending || this.status === "ended")
|
|
641
|
+
if (this.transportDetached || this.ending || this.status === "ended")
|
|
611
642
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
612
643
|
if (!isValidAssetPath(path) || !["text", "json", "file"].includes(type)) {
|
|
613
644
|
return Promise.reject(makeError("ASSET_PATH_INVALID", "asset path or read type is invalid"));
|
|
@@ -671,8 +702,12 @@ export class PedelecSession {
|
|
|
671
702
|
return this.status;
|
|
672
703
|
}
|
|
673
704
|
async end() {
|
|
674
|
-
if (this.status === "ended" || this.ending)
|
|
705
|
+
if (this.transportDetached || this.status === "ended" || this.ending) {
|
|
706
|
+
if (this.transportDetached) {
|
|
707
|
+
throw makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId });
|
|
708
|
+
}
|
|
675
709
|
return;
|
|
710
|
+
}
|
|
676
711
|
this.ending = true;
|
|
677
712
|
try {
|
|
678
713
|
await this.client.request("end_session", {
|
|
@@ -742,6 +777,19 @@ export class PedelecSession {
|
|
|
742
777
|
replaceInlineToolHandlers(handlers) {
|
|
743
778
|
this.inlineToolHandlers = new Map(handlers);
|
|
744
779
|
}
|
|
780
|
+
/** @internal */
|
|
781
|
+
isTransportDetached() {
|
|
782
|
+
return this.transportDetached;
|
|
783
|
+
}
|
|
784
|
+
/** @internal */
|
|
785
|
+
handleTransportDisconnect(error) {
|
|
786
|
+
if (this.transportDetached)
|
|
787
|
+
return;
|
|
788
|
+
this.transportDetached = true;
|
|
789
|
+
this.setStatus("error", { source: "sdk" });
|
|
790
|
+
this.emitError(error, { source: "sdk" });
|
|
791
|
+
this.rejectActivePending(error);
|
|
792
|
+
}
|
|
745
793
|
async handleToolCall(event, meta, turn) {
|
|
746
794
|
let result;
|
|
747
795
|
const namedHandler = this.namedToolHandlers.get(event.tool);
|
|
@@ -783,6 +831,8 @@ export class PedelecSession {
|
|
|
783
831
|
}),
|
|
784
832
|
};
|
|
785
833
|
}
|
|
834
|
+
if (this.transportDetached)
|
|
835
|
+
return;
|
|
786
836
|
try {
|
|
787
837
|
await this.client.request("submit_tool_result", {
|
|
788
838
|
sessionId: this.sessionId,
|
|
@@ -887,7 +937,7 @@ export class PedelecSession {
|
|
|
887
937
|
return {
|
|
888
938
|
sessionId: this.sessionId,
|
|
889
939
|
provider: this.provider,
|
|
890
|
-
|
|
940
|
+
...(this.effortLevel ? { effortLevel: this.effortLevel } : {}),
|
|
891
941
|
sessionCreatedAt: this.sessionCreatedAt,
|
|
892
942
|
...(meta.eventReceivedAt === undefined ? {} : { eventReceivedAt: meta.eventReceivedAt }),
|
|
893
943
|
eventEmittedAt: Date.now(),
|
|
@@ -950,22 +1000,18 @@ function isProviderCode(value) {
|
|
|
950
1000
|
return value === "codex" || value === "antigravity" || value === "opencode" ||
|
|
951
1001
|
value === "cursor" || value === "claude" || value === "ollama";
|
|
952
1002
|
}
|
|
1003
|
+
function isEffortLevel(value) {
|
|
1004
|
+
return value === "default" || value === "low" || value === "high";
|
|
1005
|
+
}
|
|
953
1006
|
function isPlainObject(value) {
|
|
954
1007
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
955
1008
|
}
|
|
956
1009
|
function normalizePedelecSettings(value) {
|
|
957
1010
|
if (!isPlainObject(value) || (value.defaultProvider !== null && !isProviderCode(value.defaultProvider)) ||
|
|
958
|
-
|
|
1011
|
+
"defaultModels" in value || "providerSettings" in value) {
|
|
959
1012
|
throw makeError("SDK_PROTOCOL_ERROR", "get_settings response had invalid shape");
|
|
960
1013
|
}
|
|
961
|
-
|
|
962
|
-
for (const [code, model] of Object.entries(value.defaultModels)) {
|
|
963
|
-
if (!isProviderCode(code) || typeof model !== "string") {
|
|
964
|
-
throw makeError("SDK_PROTOCOL_ERROR", "get_settings response had invalid shape");
|
|
965
|
-
}
|
|
966
|
-
defaultModels[code] = model;
|
|
967
|
-
}
|
|
968
|
-
return { defaultProvider: value.defaultProvider, defaultModels };
|
|
1014
|
+
return { defaultProvider: value.defaultProvider };
|
|
969
1015
|
}
|
|
970
1016
|
function normalizeProviderInfoList(value) {
|
|
971
1017
|
if (!Array.isArray(value))
|
package/dist/index.type-test.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Pedelec, defineTool, } from "./index";
|
|
2
|
+
function sandboxInputHasPublicType() {
|
|
3
|
+
return { path: "C:\\workspace\\project" };
|
|
4
|
+
}
|
|
2
5
|
async function typedOnToolNameFromCreateSession() {
|
|
3
6
|
const pedelec = new Pedelec();
|
|
4
7
|
const session = await pedelec.createSession({
|
|
@@ -79,7 +82,12 @@ async function resumedSessionFallsBackToString() {
|
|
|
79
82
|
}
|
|
80
83
|
async function noSkillsFallsBackToString() {
|
|
81
84
|
const pedelec = new Pedelec();
|
|
82
|
-
const session = await pedelec.createSession({
|
|
85
|
+
const session = await pedelec.createSession({
|
|
86
|
+
provider: "codex",
|
|
87
|
+
sandbox: { path: "C:\\workspace\\project" },
|
|
88
|
+
});
|
|
89
|
+
const typedSandbox = sandboxInputHasPublicType();
|
|
90
|
+
void typedSandbox;
|
|
83
91
|
session.onTool((name) => {
|
|
84
92
|
const anyString = name;
|
|
85
93
|
return anyString;
|
|
@@ -88,6 +96,17 @@ async function noSkillsFallsBackToString() {
|
|
|
88
96
|
return { ok: true };
|
|
89
97
|
});
|
|
90
98
|
}
|
|
99
|
+
async function effortLevelPublicTypeContract() {
|
|
100
|
+
const pedelec = new Pedelec();
|
|
101
|
+
await pedelec.createSession({ effortLevel: "high" });
|
|
102
|
+
const session = await pedelec.createSession({ provider: "codex", effortLevel: "low" });
|
|
103
|
+
// @ts-expect-error model is no longer a createSession option
|
|
104
|
+
pedelec.createSession({ provider: "codex", model: "gpt-5" });
|
|
105
|
+
// @ts-expect-error session no longer exposes provider model
|
|
106
|
+
session.model;
|
|
107
|
+
// @ts-expect-error unsupported effort level
|
|
108
|
+
pedelec.createSession({ effortLevel: "medium" });
|
|
109
|
+
}
|
|
91
110
|
async function listAssetsHasPublicTypes() {
|
|
92
111
|
const pedelec = new Pedelec();
|
|
93
112
|
const session = await pedelec.resumeSession("thread_1");
|
|
@@ -132,6 +151,7 @@ function publicSecurityTypesAreRestricted() {
|
|
|
132
151
|
void typedOnToolNameFromCreateSession;
|
|
133
152
|
void resumedSessionFallsBackToString;
|
|
134
153
|
void noSkillsFallsBackToString;
|
|
154
|
+
void effortLevelPublicTypeContract;
|
|
135
155
|
void listAssetsHasPublicTypes;
|
|
136
156
|
void availabilityHasPublicType;
|
|
137
157
|
void publicSecurityTypesAreRestricted;
|