@kaoruisaac/pedelec 0.2.12 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/dist/index.d.ts +103 -13
- package/dist/index.js +498 -96
- package/dist/index.type-test.js +11 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,10 +121,15 @@ const session = await pedelec.createSession({
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
session.onChat((text) => {
|
|
124
|
-
//
|
|
124
|
+
// One completed logical assistant message.
|
|
125
125
|
console.log(text);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
session.onChatDelta((delta) => {
|
|
129
|
+
// Optional best-effort incremental text for live UI.
|
|
130
|
+
console.log(delta);
|
|
131
|
+
});
|
|
132
|
+
|
|
128
133
|
session.onStatus((status) => {
|
|
129
134
|
// idle | running | waiting_tool_result | ended | error
|
|
130
135
|
console.log("status", status);
|
|
@@ -137,7 +142,19 @@ session.onError((error) => {
|
|
|
137
142
|
await session.sendText("Please help me analyze the current page state");
|
|
138
143
|
```
|
|
139
144
|
|
|
140
|
-
`sendText()` resolves after the
|
|
145
|
+
`sendText()` resolves after Core reports the matching semantic operation as completed. An `idle` status or bridge request response alone does not complete it. If the session is already handling a previous prompt, the new `sendText()` call is rejected to prevent multiple concurrent requests from running in the same session.
|
|
146
|
+
|
|
147
|
+
## End and resume a session
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
await session.end();
|
|
151
|
+
await session.resume();
|
|
152
|
+
|
|
153
|
+
console.log(session.getStatus()); // "idle"
|
|
154
|
+
await session.sendText("Continue");
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`session.resume()` reactivates the same non-detached handle and Core thread when Core still knows the thread and its recorded workspace still exists. It preserves handlers and session state, does not contact the provider runtime, and leaves provider startup/resume lazy until the next operation. A missing workspace returns `WORKSPACE_OPEN_FAILED`; a thread lost after a Desktop/Core restart returns `THREAD_NOT_FOUND`. `pedelec.resumeSession(sessionId)` is a separate reattachment API: it returns a new handle and does not implicitly revive an ended thread. A transport-detached old handle must use that API.
|
|
141
158
|
|
|
142
159
|
## Session assets
|
|
143
160
|
|
package/dist/index.d.ts
CHANGED
|
@@ -142,6 +142,9 @@ export type PedelecError = {
|
|
|
142
142
|
details?: unknown;
|
|
143
143
|
};
|
|
144
144
|
export type PedelecSessionStatus = "idle" | "running" | "waiting_tool_result" | "ended" | "error";
|
|
145
|
+
export type PedelecSessionUsage = {
|
|
146
|
+
totalTokens?: number;
|
|
147
|
+
};
|
|
145
148
|
export type PedelecEventContext = {
|
|
146
149
|
sessionId: string;
|
|
147
150
|
provider: string;
|
|
@@ -155,6 +158,12 @@ export type PedelecEventContext = {
|
|
|
155
158
|
source: "core" | "sdk";
|
|
156
159
|
};
|
|
157
160
|
export type ChatEventContext = PedelecEventContext & {
|
|
161
|
+
type: "chat_message";
|
|
162
|
+
turnId: string;
|
|
163
|
+
turnStartedAt: number;
|
|
164
|
+
eventReceivedAt: number;
|
|
165
|
+
};
|
|
166
|
+
export type ChatDeltaEventContext = PedelecEventContext & {
|
|
158
167
|
type: "chat_delta";
|
|
159
168
|
turnId: string;
|
|
160
169
|
turnStartedAt: number;
|
|
@@ -184,31 +193,52 @@ type SessionEvent = {
|
|
|
184
193
|
channelId: string;
|
|
185
194
|
sessionId: string;
|
|
186
195
|
seq?: number;
|
|
196
|
+
operationId?: string;
|
|
197
|
+
text: string;
|
|
198
|
+
} | {
|
|
199
|
+
type: "chat_message";
|
|
200
|
+
channelId: string;
|
|
201
|
+
sessionId: string;
|
|
202
|
+
seq?: number;
|
|
203
|
+
operationId?: string;
|
|
187
204
|
text: string;
|
|
188
205
|
} | {
|
|
189
206
|
type: "status_changed";
|
|
190
207
|
channelId: string;
|
|
191
208
|
sessionId: string;
|
|
192
209
|
seq?: number;
|
|
210
|
+
operationId?: string;
|
|
193
211
|
status: PedelecSessionStatus;
|
|
194
212
|
} | {
|
|
195
213
|
type: "tool_call";
|
|
196
214
|
channelId: string;
|
|
197
215
|
sessionId: string;
|
|
198
216
|
seq?: number;
|
|
217
|
+
operationId: string;
|
|
199
218
|
toolRequestId: string;
|
|
200
219
|
tool: string;
|
|
201
220
|
args: unknown;
|
|
202
221
|
} | {
|
|
203
|
-
type: "
|
|
222
|
+
type: "operation_completed";
|
|
223
|
+
channelId: string;
|
|
224
|
+
sessionId: string;
|
|
225
|
+
seq?: number;
|
|
226
|
+
operationId: string;
|
|
227
|
+
operationKind: "user" | "prepare";
|
|
228
|
+
success: boolean;
|
|
229
|
+
error?: PedelecError;
|
|
230
|
+
} | {
|
|
231
|
+
type: "usage_updated";
|
|
204
232
|
channelId: string;
|
|
205
233
|
sessionId: string;
|
|
206
234
|
seq?: number;
|
|
235
|
+
totalTokens: number;
|
|
207
236
|
} | {
|
|
208
237
|
type: "error";
|
|
209
238
|
channelId?: string;
|
|
210
239
|
sessionId?: string;
|
|
211
240
|
seq?: number;
|
|
241
|
+
operationId?: string;
|
|
212
242
|
error?: PedelecError;
|
|
213
243
|
} | {
|
|
214
244
|
type: "ended";
|
|
@@ -216,11 +246,41 @@ type SessionEvent = {
|
|
|
216
246
|
sessionId: string;
|
|
217
247
|
seq?: number;
|
|
218
248
|
};
|
|
249
|
+
type SessionSnapshot = {
|
|
250
|
+
threadId: string;
|
|
251
|
+
status: string;
|
|
252
|
+
latestSeq: number;
|
|
253
|
+
usage?: {
|
|
254
|
+
totalTokens: number;
|
|
255
|
+
};
|
|
256
|
+
activeOperation?: {
|
|
257
|
+
operationId: string;
|
|
258
|
+
operationKind: "user" | "prepare";
|
|
259
|
+
startedAt: string;
|
|
260
|
+
};
|
|
261
|
+
lastCompletedOperation?: {
|
|
262
|
+
operationId: string;
|
|
263
|
+
operationKind: "user" | "prepare";
|
|
264
|
+
success: boolean;
|
|
265
|
+
error?: PedelecError;
|
|
266
|
+
completedAt: string;
|
|
267
|
+
};
|
|
268
|
+
pendingToolRequest?: {
|
|
269
|
+
requestId: string;
|
|
270
|
+
threadId: string;
|
|
271
|
+
operationId: string;
|
|
272
|
+
toolName: string;
|
|
273
|
+
args: unknown;
|
|
274
|
+
createdAt: string;
|
|
275
|
+
timeoutMs: number;
|
|
276
|
+
};
|
|
277
|
+
};
|
|
219
278
|
type EventDispatchMeta = {
|
|
220
279
|
source: "core" | "sdk";
|
|
221
280
|
eventReceivedAt?: number;
|
|
222
281
|
};
|
|
223
282
|
type ChatHandler = (text: string, ctx: ChatEventContext) => void;
|
|
283
|
+
type ChatDeltaHandler = (text: string, ctx: ChatDeltaEventContext) => void;
|
|
224
284
|
type GenericToolHandler<TToolName extends string = string> = (tool: TToolName, args: unknown, ctx: ToolCallContext) => unknown | Promise<unknown>;
|
|
225
285
|
type ErrorHandler = (error: PedelecError, ctx: ErrorEventContext) => void;
|
|
226
286
|
type StatusHandler = (status: PedelecSessionStatus, ctx: StatusEventContext) => void;
|
|
@@ -235,6 +295,8 @@ export declare class Pedelec {
|
|
|
235
295
|
private readonly lastSeqBySession;
|
|
236
296
|
private nextRequestNumber;
|
|
237
297
|
private port;
|
|
298
|
+
private pageActivityListenersAttached;
|
|
299
|
+
private lastReportedPageActive;
|
|
238
300
|
constructor(options?: PedelecOptions);
|
|
239
301
|
createSession(): Promise<PedelecSession<string>>;
|
|
240
302
|
createSession<const TTools extends readonly ToolDefinition[]>(input: CreateSessionInput<TTools>): Promise<PedelecSession<ToolNameOf<TTools>>>;
|
|
@@ -246,6 +308,16 @@ export declare class Pedelec {
|
|
|
246
308
|
request<T>(type: string, payload?: Record<string, unknown>): Promise<T>;
|
|
247
309
|
private requestWithOptions;
|
|
248
310
|
unregisterSession(sessionId: string): void;
|
|
311
|
+
/** @internal */
|
|
312
|
+
registerExistingSession(session: PedelecSession<any>): void;
|
|
313
|
+
/** @internal */
|
|
314
|
+
isSessionRegistered(sessionId: string, session: PedelecSession<any>): boolean;
|
|
315
|
+
private attachPageActivityListeners;
|
|
316
|
+
private isDocumentHidden;
|
|
317
|
+
private isDocumentFocused;
|
|
318
|
+
private isPageForegroundActive;
|
|
319
|
+
private postSdkHello;
|
|
320
|
+
private reportPageActivity;
|
|
249
321
|
private resolveCreateSessionInput;
|
|
250
322
|
checkAvailability(): Promise<PedelecAvailability>;
|
|
251
323
|
private resolveDefaultCreateSessionInput;
|
|
@@ -266,26 +338,33 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
266
338
|
readonly provider: string;
|
|
267
339
|
readonly effortLevel?: EffortLevel;
|
|
268
340
|
readonly sessionCreatedAt: number;
|
|
341
|
+
readonly usage: PedelecSessionUsage;
|
|
269
342
|
private status;
|
|
270
|
-
private
|
|
271
|
-
private pendingPrepare;
|
|
343
|
+
private pendingOperation;
|
|
272
344
|
private preparePromise;
|
|
273
345
|
private uploadPromise;
|
|
346
|
+
private reactivationPromise;
|
|
274
347
|
private ending;
|
|
275
348
|
private prepared;
|
|
349
|
+
private autoEndOnDisconnect;
|
|
276
350
|
private activeTurn;
|
|
277
351
|
private transportDetached;
|
|
278
352
|
private genericToolHandler;
|
|
279
353
|
private inlineToolHandlers;
|
|
280
354
|
private readonly namedToolHandlers;
|
|
281
355
|
private readonly chatHandlers;
|
|
356
|
+
private readonly chatDeltaHandlers;
|
|
282
357
|
private readonly errorHandlers;
|
|
283
358
|
private readonly statusHandlers;
|
|
284
359
|
private readonly endedHandlers;
|
|
285
|
-
|
|
360
|
+
private readonly recoveredToolCalls;
|
|
361
|
+
private readonly handledToolRequestIds;
|
|
362
|
+
private authoritativePendingToolRequest;
|
|
363
|
+
constructor(client: Pedelec, sessionId: string, provider: string, effortLevel?: EffortLevel, inlineToolHandlers?: Map<string, ToolSpecificHandler>, autoEndOnDisconnect?: boolean);
|
|
286
364
|
prepare(): Promise<void>;
|
|
287
365
|
sendText(text: string): Promise<void>;
|
|
288
366
|
onChat(handler: ChatHandler): () => void;
|
|
367
|
+
onChatDelta(handler: ChatDeltaHandler): () => void;
|
|
289
368
|
onTool(handler: GenericToolHandler<TToolName>): () => void;
|
|
290
369
|
onTool<TArgs = unknown, TResult = unknown>(toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
|
|
291
370
|
uploadAsset(file: File): Promise<AssetPath>;
|
|
@@ -299,28 +378,39 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
299
378
|
onEnded(handler: EndedHandler): () => void;
|
|
300
379
|
getStatus(): PedelecSessionStatus;
|
|
301
380
|
end(): Promise<void>;
|
|
381
|
+
resume(): Promise<void>;
|
|
382
|
+
/** @internal */
|
|
383
|
+
setAutoEndOnDisconnect(autoEndOnDisconnect: boolean): void;
|
|
302
384
|
handleEvent(event: SessionEvent, meta?: EventDispatchMeta): void;
|
|
385
|
+
/** @internal */
|
|
386
|
+
handleSnapshot(snapshot: SessionSnapshot, meta?: EventDispatchMeta): void;
|
|
303
387
|
replaceInlineToolHandlers(handlers: Map<string, ToolSpecificHandler>): void;
|
|
304
388
|
/** @internal */
|
|
305
389
|
isTransportDetached(): boolean;
|
|
306
390
|
/** @internal */
|
|
307
391
|
handleTransportDisconnect(error: PedelecError): void;
|
|
308
|
-
private
|
|
309
|
-
private resolvePendingSend;
|
|
310
|
-
private resolvePendingPrepare;
|
|
311
|
-
private resolveActivePending;
|
|
312
|
-
private rejectPendingSend;
|
|
313
|
-
private rejectPendingPrepare;
|
|
392
|
+
private handleToolCallOnce;
|
|
314
393
|
private rejectActivePending;
|
|
315
|
-
private clearPendingSend;
|
|
316
|
-
private clearPendingPrepare;
|
|
317
394
|
private markEnded;
|
|
318
395
|
private emitError;
|
|
319
396
|
private setStatus;
|
|
320
|
-
private
|
|
397
|
+
private requireOperationTurn;
|
|
398
|
+
private updateUsage;
|
|
399
|
+
private matchesActiveOperation;
|
|
400
|
+
private markOperationRequestSettled;
|
|
401
|
+
private failOperationFromRequest;
|
|
402
|
+
private completeOperation;
|
|
403
|
+
private maybeSettleOperation;
|
|
404
|
+
private rejectOperation;
|
|
405
|
+
private queueSnapshotTool;
|
|
406
|
+
private reconcileRecoveredToolCalls;
|
|
407
|
+
private isAuthoritativeToolRequestCurrent;
|
|
408
|
+
private hasToolHandler;
|
|
409
|
+
private drainRecoveredToolCalls;
|
|
321
410
|
private finishActiveTurn;
|
|
322
411
|
private createBaseContext;
|
|
323
412
|
private createChatContext;
|
|
413
|
+
private createChatDeltaContext;
|
|
324
414
|
private createToolCallContext;
|
|
325
415
|
private createStatusContext;
|
|
326
416
|
private createErrorContext;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PEDELEC_EXTENSION_ID } from "./extension-id.js";
|
|
2
2
|
import { SDK_VERSION } from "./version.generated.js";
|
|
3
3
|
const SDK_EXTERNAL_PORT_NAME = "pedelec-sdk-external";
|
|
4
|
+
const PAGE_ACTIVITY_MESSAGE_TYPE = "page_activity";
|
|
5
|
+
const SDK_HELLO_MESSAGE_TYPE = "sdk_hello";
|
|
4
6
|
const DEFAULT_BRIDGE_TIMEOUT_MS = 30_000;
|
|
5
7
|
function normalizeListAssetsResponse(response) {
|
|
6
8
|
if (!response || typeof response !== "object" || Array.isArray(response))
|
|
@@ -124,6 +126,8 @@ export class Pedelec {
|
|
|
124
126
|
lastSeqBySession = new Map();
|
|
125
127
|
nextRequestNumber = 1;
|
|
126
128
|
port = null;
|
|
129
|
+
pageActivityListenersAttached = false;
|
|
130
|
+
lastReportedPageActive = null;
|
|
127
131
|
constructor(options = {}) {
|
|
128
132
|
this.pageWindow = typeof window === "undefined" ? null : window;
|
|
129
133
|
this.channelId = createChannelId();
|
|
@@ -131,6 +135,7 @@ export class Pedelec {
|
|
|
131
135
|
if (!this.pageWindow) {
|
|
132
136
|
return;
|
|
133
137
|
}
|
|
138
|
+
this.attachPageActivityListeners();
|
|
134
139
|
this.connectExtension();
|
|
135
140
|
}
|
|
136
141
|
async createSession(input = {}) {
|
|
@@ -148,7 +153,7 @@ export class Pedelec {
|
|
|
148
153
|
if (!result.sessionId) {
|
|
149
154
|
throw makeError("SDK_PROTOCOL_ERROR", "create_session response did not include sessionId");
|
|
150
155
|
}
|
|
151
|
-
return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.effortLevel, resolvedInput.inlineToolHandlers);
|
|
156
|
+
return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.effortLevel, resolvedInput.inlineToolHandlers, resolvedInput.autoEndOnDisconnect);
|
|
152
157
|
}
|
|
153
158
|
async workspaceFolderPicker() {
|
|
154
159
|
const result = await this.requestWithOptions("pick_workspace_folder", {}, { timeoutMs: null });
|
|
@@ -182,13 +187,27 @@ export class Pedelec {
|
|
|
182
187
|
if (!sessionId?.trim()) {
|
|
183
188
|
throw makeError("INVALID_INPUT", "sessionId is required");
|
|
184
189
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
const existing = this.sessions.get(sessionId);
|
|
191
|
+
const session = existing && !existing.isTransportDetached()
|
|
192
|
+
? existing
|
|
193
|
+
: this.registerSession(sessionId, "", undefined, new Map(), false);
|
|
194
|
+
try {
|
|
195
|
+
const result = await this.request("resume_session", {
|
|
196
|
+
sessionId,
|
|
197
|
+
});
|
|
198
|
+
if (!result.sessionId) {
|
|
199
|
+
throw makeError("SDK_PROTOCOL_ERROR", "resume_session response did not include sessionId");
|
|
200
|
+
}
|
|
201
|
+
if (typeof result.autoEndOnDisconnect === "boolean") {
|
|
202
|
+
session.setAutoEndOnDisconnect(result.autoEndOnDisconnect);
|
|
203
|
+
}
|
|
204
|
+
return session;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (session !== existing)
|
|
208
|
+
this.unregisterSession(sessionId);
|
|
209
|
+
throw error;
|
|
190
210
|
}
|
|
191
|
-
return this.registerSession(result.sessionId, "", undefined);
|
|
192
211
|
}
|
|
193
212
|
request(type, payload = {}) {
|
|
194
213
|
return this.requestWithOptions(type, payload);
|
|
@@ -245,6 +264,76 @@ export class Pedelec {
|
|
|
245
264
|
this.sessions.delete(sessionId);
|
|
246
265
|
this.lastSeqBySession.delete(sessionId);
|
|
247
266
|
}
|
|
267
|
+
/** @internal */
|
|
268
|
+
registerExistingSession(session) {
|
|
269
|
+
this.sessions.set(session.sessionId, session);
|
|
270
|
+
}
|
|
271
|
+
/** @internal */
|
|
272
|
+
isSessionRegistered(sessionId, session) {
|
|
273
|
+
return this.sessions.get(sessionId) === session;
|
|
274
|
+
}
|
|
275
|
+
attachPageActivityListeners() {
|
|
276
|
+
if (this.pageActivityListenersAttached || !this.pageWindow)
|
|
277
|
+
return;
|
|
278
|
+
this.pageActivityListenersAttached = true;
|
|
279
|
+
this.pageWindow.addEventListener("focus", () => {
|
|
280
|
+
if (!this.isDocumentHidden())
|
|
281
|
+
this.reportPageActivity(true, { force: true });
|
|
282
|
+
});
|
|
283
|
+
this.pageWindow.document?.addEventListener("visibilitychange", () => {
|
|
284
|
+
if (this.isDocumentHidden()) {
|
|
285
|
+
this.reportPageActivity(false);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (this.isDocumentFocused())
|
|
289
|
+
this.reportPageActivity(true);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
isDocumentHidden() {
|
|
293
|
+
const doc = this.pageWindow?.document;
|
|
294
|
+
if (!doc)
|
|
295
|
+
return true;
|
|
296
|
+
if (typeof doc.hidden === "boolean")
|
|
297
|
+
return doc.hidden;
|
|
298
|
+
return doc.visibilityState === "hidden";
|
|
299
|
+
}
|
|
300
|
+
isDocumentFocused() {
|
|
301
|
+
const doc = this.pageWindow?.document;
|
|
302
|
+
if (!doc || typeof doc.hasFocus !== "function")
|
|
303
|
+
return false;
|
|
304
|
+
try {
|
|
305
|
+
return doc.hasFocus();
|
|
306
|
+
}
|
|
307
|
+
catch (_err) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
isPageForegroundActive() {
|
|
312
|
+
return !this.isDocumentHidden() && this.isDocumentFocused();
|
|
313
|
+
}
|
|
314
|
+
postSdkHello(port) {
|
|
315
|
+
try {
|
|
316
|
+
port.postMessage({ type: SDK_HELLO_MESSAGE_TYPE, sdkVersion: SDK_VERSION });
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
|
|
320
|
+
this.handleDisconnect(port, error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
reportPageActivity(active, options = {}) {
|
|
324
|
+
if (!this.port)
|
|
325
|
+
return;
|
|
326
|
+
if (!options.force && this.lastReportedPageActive === active)
|
|
327
|
+
return;
|
|
328
|
+
try {
|
|
329
|
+
this.port.postMessage({ type: PAGE_ACTIVITY_MESSAGE_TYPE, active });
|
|
330
|
+
this.lastReportedPageActive = active;
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
|
|
334
|
+
this.handleDisconnect(this.port, error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
248
337
|
resolveCreateSessionInput(input) {
|
|
249
338
|
const raw = (input ?? {});
|
|
250
339
|
const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
|
|
@@ -345,13 +434,13 @@ export class Pedelec {
|
|
|
345
434
|
throw makeError("DEFAULT_PROVIDER_UNAVAILABLE", "Default provider is not currently available. Open the Pedelec desktop app Settings page and choose an available provider.", { provider });
|
|
346
435
|
}
|
|
347
436
|
}
|
|
348
|
-
registerSession(sessionId, provider, effortLevel, inlineToolHandlers = new Map()) {
|
|
437
|
+
registerSession(sessionId, provider, effortLevel, inlineToolHandlers = new Map(), autoEndOnDisconnect = false) {
|
|
349
438
|
const existing = this.sessions.get(sessionId);
|
|
350
439
|
if (existing && !existing.isTransportDetached()) {
|
|
351
440
|
existing.replaceInlineToolHandlers(inlineToolHandlers);
|
|
352
441
|
return existing;
|
|
353
442
|
}
|
|
354
|
-
const session = new PedelecSession(this, sessionId, provider, effortLevel, inlineToolHandlers);
|
|
443
|
+
const session = new PedelecSession(this, sessionId, provider, effortLevel, inlineToolHandlers, autoEndOnDisconnect);
|
|
355
444
|
this.sessions.set(sessionId, session);
|
|
356
445
|
return session;
|
|
357
446
|
}
|
|
@@ -370,10 +459,13 @@ export class Pedelec {
|
|
|
370
459
|
port.onMessage.addListener((message) => this.handlePortMessage(port, message));
|
|
371
460
|
port.onDisconnect.addListener(() => this.handleDisconnect(port));
|
|
372
461
|
this.port = port;
|
|
462
|
+
this.postSdkHello(port);
|
|
463
|
+
this.reportPageActivity(this.isPageForegroundActive(), { force: true });
|
|
373
464
|
return port;
|
|
374
465
|
}
|
|
375
466
|
catch (err) {
|
|
376
467
|
this.port = null;
|
|
468
|
+
this.lastReportedPageActive = null;
|
|
377
469
|
throw normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
|
|
378
470
|
}
|
|
379
471
|
}
|
|
@@ -398,6 +490,22 @@ export class Pedelec {
|
|
|
398
490
|
this.handleResponse(message);
|
|
399
491
|
return;
|
|
400
492
|
}
|
|
493
|
+
if (message.type === "session_snapshot") {
|
|
494
|
+
if (message.sessionId) {
|
|
495
|
+
const latestSeq = message.snapshot?.latestSeq;
|
|
496
|
+
if (typeof latestSeq === "number") {
|
|
497
|
+
const current = this.lastSeqBySession.get(message.sessionId);
|
|
498
|
+
if (current === undefined || latestSeq > current) {
|
|
499
|
+
this.lastSeqBySession.set(message.sessionId, latestSeq);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.sessions.get(message.sessionId)?.handleSnapshot(message.snapshot, {
|
|
503
|
+
source: "core",
|
|
504
|
+
eventReceivedAt: Date.now(),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
401
509
|
if (isSessionEvent(message) && this.isNewEvent(message)) {
|
|
402
510
|
const eventReceivedAt = Date.now();
|
|
403
511
|
if (message.sessionId) {
|
|
@@ -441,6 +549,7 @@ export class Pedelec {
|
|
|
441
549
|
const runtime = globalThis.chrome?.runtime;
|
|
442
550
|
const error = normalizeError(disconnectError ?? runtime?.lastError, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
|
|
443
551
|
this.port = null;
|
|
552
|
+
this.lastReportedPageActive = null;
|
|
444
553
|
this.invalidateSessions(error);
|
|
445
554
|
for (const [requestId, pending] of this.pendingRequests) {
|
|
446
555
|
if (pending.port !== port)
|
|
@@ -483,34 +592,43 @@ export class PedelecSession {
|
|
|
483
592
|
provider;
|
|
484
593
|
effortLevel;
|
|
485
594
|
sessionCreatedAt = Date.now();
|
|
595
|
+
usage = { totalTokens: undefined };
|
|
486
596
|
status = "idle";
|
|
487
|
-
|
|
488
|
-
pendingPrepare = null;
|
|
597
|
+
pendingOperation = null;
|
|
489
598
|
preparePromise = null;
|
|
490
599
|
uploadPromise = null;
|
|
600
|
+
reactivationPromise = null;
|
|
491
601
|
ending = false;
|
|
492
602
|
prepared = false;
|
|
603
|
+
autoEndOnDisconnect;
|
|
493
604
|
activeTurn = null;
|
|
494
605
|
transportDetached = false;
|
|
495
606
|
genericToolHandler = null;
|
|
496
607
|
inlineToolHandlers = new Map();
|
|
497
608
|
namedToolHandlers = new Map();
|
|
498
609
|
chatHandlers = new Set();
|
|
610
|
+
chatDeltaHandlers = new Set();
|
|
499
611
|
errorHandlers = new Set();
|
|
500
612
|
statusHandlers = new Set();
|
|
501
613
|
endedHandlers = new Set();
|
|
502
|
-
|
|
614
|
+
recoveredToolCalls = new Map();
|
|
615
|
+
handledToolRequestIds = new Set();
|
|
616
|
+
authoritativePendingToolRequest = undefined;
|
|
617
|
+
constructor(client, sessionId, provider, effortLevel, inlineToolHandlers = new Map(), autoEndOnDisconnect = false) {
|
|
503
618
|
this.client = client;
|
|
504
619
|
this.sessionId = sessionId;
|
|
505
620
|
this.provider = provider;
|
|
506
621
|
this.effortLevel = effortLevel;
|
|
507
622
|
this.inlineToolHandlers = new Map(inlineToolHandlers);
|
|
623
|
+
this.autoEndOnDisconnect = autoEndOnDisconnect;
|
|
508
624
|
}
|
|
509
625
|
prepare() {
|
|
510
626
|
if (this.transportDetached || this.status === "ended") {
|
|
511
627
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
512
628
|
}
|
|
513
|
-
if (this.
|
|
629
|
+
if (this.pendingOperation?.turn.kind === "user" ||
|
|
630
|
+
(this.activeTurn && this.activeTurn.kind !== "prepare") ||
|
|
631
|
+
this.activeTurn?.external) {
|
|
514
632
|
return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
|
|
515
633
|
}
|
|
516
634
|
if (this.prepared) {
|
|
@@ -520,24 +638,32 @@ export class PedelecSession {
|
|
|
520
638
|
return this.preparePromise;
|
|
521
639
|
}
|
|
522
640
|
const turn = createTurn("prepare");
|
|
641
|
+
const priorStatus = this.status;
|
|
523
642
|
this.activeTurn = turn;
|
|
524
643
|
this.setStatus("running", { source: "sdk" });
|
|
525
|
-
const
|
|
526
|
-
this.
|
|
644
|
+
const operationPromise = new Promise((resolve, reject) => {
|
|
645
|
+
this.pendingOperation = {
|
|
646
|
+
turn,
|
|
647
|
+
resolve,
|
|
648
|
+
reject,
|
|
649
|
+
priorStatus,
|
|
650
|
+
requestSettled: false,
|
|
651
|
+
};
|
|
527
652
|
});
|
|
528
|
-
|
|
653
|
+
void this.client
|
|
529
654
|
.request("prepare_session", {
|
|
530
655
|
sessionId: this.sessionId,
|
|
656
|
+
operationId: turn.turnId,
|
|
531
657
|
})
|
|
532
|
-
.
|
|
658
|
+
.then(() => {
|
|
659
|
+
this.markOperationRequestSettled(turn);
|
|
660
|
+
}, (err) => {
|
|
533
661
|
const error = normalizeError(err, "PREPARE_FAILED", "prepare failed");
|
|
534
662
|
if (!this.transportDetached)
|
|
535
663
|
this.emitError(error, { source: "sdk" });
|
|
536
|
-
this.
|
|
537
|
-
this.finishActiveTurn(turn);
|
|
538
|
-
throw error;
|
|
664
|
+
this.failOperationFromRequest(turn, error);
|
|
539
665
|
});
|
|
540
|
-
this.preparePromise =
|
|
666
|
+
this.preparePromise = operationPromise
|
|
541
667
|
.then(() => {
|
|
542
668
|
this.prepared = true;
|
|
543
669
|
})
|
|
@@ -558,34 +684,46 @@ export class PedelecSession {
|
|
|
558
684
|
if (this.transportDetached || this.status === "ended") {
|
|
559
685
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
560
686
|
}
|
|
561
|
-
if (this.
|
|
687
|
+
if (this.pendingOperation || this.activeTurn) {
|
|
562
688
|
return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
|
|
563
689
|
}
|
|
564
690
|
const turn = createTurn("user");
|
|
691
|
+
const priorStatus = this.status;
|
|
565
692
|
this.activeTurn = turn;
|
|
566
693
|
this.setStatus("running", { source: "sdk" });
|
|
567
|
-
const
|
|
568
|
-
this.
|
|
694
|
+
const operationPromise = new Promise((resolve, reject) => {
|
|
695
|
+
this.pendingOperation = {
|
|
696
|
+
turn,
|
|
697
|
+
resolve,
|
|
698
|
+
reject,
|
|
699
|
+
priorStatus,
|
|
700
|
+
requestSettled: false,
|
|
701
|
+
};
|
|
569
702
|
});
|
|
570
|
-
|
|
703
|
+
void this.client
|
|
571
704
|
.request("send_text", {
|
|
572
705
|
sessionId: this.sessionId,
|
|
573
706
|
text,
|
|
707
|
+
operationId: turn.turnId,
|
|
574
708
|
})
|
|
575
|
-
.
|
|
709
|
+
.then(() => {
|
|
710
|
+
this.markOperationRequestSettled(turn);
|
|
711
|
+
}, (err) => {
|
|
576
712
|
const error = normalizeError(err, "SEND_TEXT_FAILED", "sendText failed");
|
|
577
713
|
if (!this.transportDetached)
|
|
578
714
|
this.emitError(error, { source: "sdk" });
|
|
579
|
-
this.
|
|
580
|
-
this.finishActiveTurn(turn);
|
|
581
|
-
throw error;
|
|
715
|
+
this.failOperationFromRequest(turn, error);
|
|
582
716
|
});
|
|
583
|
-
return
|
|
717
|
+
return operationPromise;
|
|
584
718
|
}
|
|
585
719
|
onChat(handler) {
|
|
586
720
|
this.chatHandlers.add(handler);
|
|
587
721
|
return () => this.chatHandlers.delete(handler);
|
|
588
722
|
}
|
|
723
|
+
onChatDelta(handler) {
|
|
724
|
+
this.chatDeltaHandlers.add(handler);
|
|
725
|
+
return () => this.chatDeltaHandlers.delete(handler);
|
|
726
|
+
}
|
|
589
727
|
onTool(toolNameOrHandler, maybeHandler) {
|
|
590
728
|
if (typeof toolNameOrHandler === "string") {
|
|
591
729
|
const toolName = toolNameOrHandler;
|
|
@@ -596,6 +734,7 @@ export class PedelecSession {
|
|
|
596
734
|
throw makeError("INVALID_INPUT", "tool handler must be a function", { toolName });
|
|
597
735
|
}
|
|
598
736
|
this.namedToolHandlers.set(toolName, maybeHandler);
|
|
737
|
+
void this.drainRecoveredToolCalls();
|
|
599
738
|
return () => {
|
|
600
739
|
if (this.namedToolHandlers.get(toolName) === maybeHandler) {
|
|
601
740
|
this.namedToolHandlers.delete(toolName);
|
|
@@ -604,6 +743,7 @@ export class PedelecSession {
|
|
|
604
743
|
}
|
|
605
744
|
const handler = toolNameOrHandler;
|
|
606
745
|
this.genericToolHandler = handler;
|
|
746
|
+
void this.drainRecoveredToolCalls();
|
|
607
747
|
return () => {
|
|
608
748
|
if (this.genericToolHandler === handler) {
|
|
609
749
|
this.genericToolHandler = null;
|
|
@@ -743,9 +883,73 @@ export class PedelecSession {
|
|
|
743
883
|
this.markEnded({ source: "sdk" });
|
|
744
884
|
this.client.unregisterSession(this.sessionId);
|
|
745
885
|
}
|
|
886
|
+
resume() {
|
|
887
|
+
if (this.transportDetached) {
|
|
888
|
+
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
889
|
+
}
|
|
890
|
+
if (this.reactivationPromise)
|
|
891
|
+
return this.reactivationPromise;
|
|
892
|
+
if (this.status !== "ended")
|
|
893
|
+
return Promise.resolve();
|
|
894
|
+
const wasRegistered = this.client.isSessionRegistered(this.sessionId, this);
|
|
895
|
+
const promise = (async () => {
|
|
896
|
+
if (!wasRegistered) {
|
|
897
|
+
// end() intentionally removed the handle from the registry. Put the
|
|
898
|
+
// exact same object back before the ended snapshot can arrive.
|
|
899
|
+
this.client.registerExistingSession(this);
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
const result = await this.client.request("reactivate_session", {
|
|
903
|
+
sessionId: this.sessionId,
|
|
904
|
+
autoEndOnDisconnect: this.autoEndOnDisconnect,
|
|
905
|
+
});
|
|
906
|
+
if (result?.sessionId !== this.sessionId) {
|
|
907
|
+
throw makeError("SDK_PROTOCOL_ERROR", "reactivate_session response did not include sessionId");
|
|
908
|
+
}
|
|
909
|
+
if (this.status !== "idle") {
|
|
910
|
+
throw makeError("SDK_PROTOCOL_ERROR", "reactivate_session resolved before the authoritative idle snapshot", { sessionId: this.sessionId });
|
|
911
|
+
}
|
|
912
|
+
this.ending = false;
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
const error = normalizeError(err, "SESSION_RESUME_FAILED", "session resume failed");
|
|
916
|
+
if (!this.transportDetached)
|
|
917
|
+
this.emitError(error, { source: "sdk" });
|
|
918
|
+
if (!wasRegistered && isDeterministicResumeError(error) && this.status !== "idle") {
|
|
919
|
+
this.client.unregisterSession(this.sessionId);
|
|
920
|
+
}
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
})();
|
|
924
|
+
const guardedPromise = promise.finally(() => {
|
|
925
|
+
if (this.reactivationPromise === guardedPromise)
|
|
926
|
+
this.reactivationPromise = null;
|
|
927
|
+
});
|
|
928
|
+
this.reactivationPromise = guardedPromise;
|
|
929
|
+
return guardedPromise;
|
|
930
|
+
}
|
|
931
|
+
/** @internal */
|
|
932
|
+
setAutoEndOnDisconnect(autoEndOnDisconnect) {
|
|
933
|
+
this.autoEndOnDisconnect = autoEndOnDisconnect;
|
|
934
|
+
}
|
|
746
935
|
handleEvent(event, meta = { source: "sdk" }) {
|
|
936
|
+
if (event.type === "usage_updated") {
|
|
937
|
+
this.updateUsage(event.totalTokens);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
747
940
|
if (event.type === "chat_delta") {
|
|
748
|
-
const turn = this.
|
|
941
|
+
const turn = this.requireOperationTurn(event.operationId, "chat_delta", meta);
|
|
942
|
+
if (!turn)
|
|
943
|
+
return;
|
|
944
|
+
if (turn.kind === "prepare")
|
|
945
|
+
return;
|
|
946
|
+
for (const handler of this.chatDeltaHandlers) {
|
|
947
|
+
handler(event.text, this.createChatDeltaContext(meta, turn));
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (event.type === "chat_message") {
|
|
952
|
+
const turn = this.requireOperationTurn(event.operationId, "chat_message", meta);
|
|
749
953
|
if (!turn)
|
|
750
954
|
return;
|
|
751
955
|
if (turn.kind === "prepare")
|
|
@@ -756,44 +960,109 @@ export class PedelecSession {
|
|
|
756
960
|
return;
|
|
757
961
|
}
|
|
758
962
|
if (event.type === "status_changed") {
|
|
963
|
+
if (event.operationId && !this.matchesActiveOperation(event.operationId))
|
|
964
|
+
return;
|
|
965
|
+
if (!event.operationId && this.activeTurn)
|
|
966
|
+
return;
|
|
759
967
|
this.setStatus(event.status, meta);
|
|
760
|
-
if (event.status === "idle")
|
|
761
|
-
this.
|
|
762
|
-
|
|
763
|
-
else if (event.status === "ended") {
|
|
968
|
+
if (event.status === "idle")
|
|
969
|
+
this.ending = false;
|
|
970
|
+
if (event.status === "ended") {
|
|
764
971
|
this.markEnded(meta);
|
|
765
972
|
}
|
|
766
973
|
else if (event.status === "error") {
|
|
767
974
|
const error = makeError("SESSION_ERROR", "session entered error status", { sessionId: this.sessionId });
|
|
768
975
|
this.emitError(error, meta);
|
|
769
|
-
this.rejectActivePending(error);
|
|
770
976
|
}
|
|
771
977
|
return;
|
|
772
978
|
}
|
|
773
979
|
if (event.type === "tool_call") {
|
|
774
|
-
const turn = this.
|
|
980
|
+
const turn = this.requireOperationTurn(event.operationId, "tool_call", meta);
|
|
775
981
|
if (!turn)
|
|
776
982
|
return;
|
|
983
|
+
this.authoritativePendingToolRequest = {
|
|
984
|
+
operationId: event.operationId,
|
|
985
|
+
requestId: event.toolRequestId,
|
|
986
|
+
};
|
|
777
987
|
this.setStatus("waiting_tool_result", meta);
|
|
778
|
-
this.
|
|
988
|
+
if (this.recoveredToolCalls.has(event.toolRequestId)) {
|
|
989
|
+
this.recoveredToolCalls.delete(event.toolRequestId);
|
|
990
|
+
}
|
|
991
|
+
if (turn.external && !this.hasToolHandler(event.tool)) {
|
|
992
|
+
this.recoveredToolCalls.set(event.toolRequestId, event);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
void this.handleToolCallOnce(event, meta, turn);
|
|
779
996
|
return;
|
|
780
997
|
}
|
|
781
|
-
if (event.type === "
|
|
782
|
-
this.
|
|
783
|
-
|
|
998
|
+
if (event.type === "operation_completed") {
|
|
999
|
+
if (!this.matchesActiveOperation(event.operationId))
|
|
1000
|
+
return;
|
|
1001
|
+
if (this.activeTurn?.kind !== event.operationKind)
|
|
1002
|
+
return;
|
|
1003
|
+
this.authoritativePendingToolRequest = null;
|
|
1004
|
+
this.setStatus(event.success ? "idle" : event.operationKind === "prepare" ? "idle" : "error", meta);
|
|
1005
|
+
this.completeOperation(this.activeTurn, event.success, event.error);
|
|
784
1006
|
return;
|
|
785
1007
|
}
|
|
786
1008
|
if (event.type === "error") {
|
|
787
|
-
this.
|
|
1009
|
+
if (event.operationId && !this.matchesActiveOperation(event.operationId))
|
|
1010
|
+
return;
|
|
788
1011
|
const error = normalizeError(event.error, "SESSION_ERROR", "session error");
|
|
789
1012
|
this.emitError(error, meta);
|
|
790
|
-
|
|
1013
|
+
if (!event.operationId)
|
|
1014
|
+
this.setStatus("error", meta);
|
|
791
1015
|
return;
|
|
792
1016
|
}
|
|
793
1017
|
if (event.type === "ended") {
|
|
794
1018
|
this.markEnded(meta);
|
|
795
1019
|
}
|
|
796
1020
|
}
|
|
1021
|
+
/** @internal */
|
|
1022
|
+
handleSnapshot(snapshot, meta = { source: "core" }) {
|
|
1023
|
+
if (!snapshot || snapshot.threadId !== this.sessionId)
|
|
1024
|
+
return;
|
|
1025
|
+
if (snapshot.usage)
|
|
1026
|
+
this.updateUsage(snapshot.usage.totalTokens);
|
|
1027
|
+
if (snapshot.status === "idle")
|
|
1028
|
+
this.ending = false;
|
|
1029
|
+
const active = snapshot.activeOperation;
|
|
1030
|
+
const pending = this.pendingOperation;
|
|
1031
|
+
this.reconcileRecoveredToolCalls(snapshot);
|
|
1032
|
+
if (!pending) {
|
|
1033
|
+
this.setStatus(coreStatusToSdkStatus(snapshot.status), meta);
|
|
1034
|
+
if (active) {
|
|
1035
|
+
this.activeTurn = {
|
|
1036
|
+
turnId: active.operationId,
|
|
1037
|
+
turnStartedAt: parseSnapshotTime(active.startedAt),
|
|
1038
|
+
kind: active.operationKind,
|
|
1039
|
+
external: true,
|
|
1040
|
+
};
|
|
1041
|
+
this.queueSnapshotTool(snapshot, this.activeTurn);
|
|
1042
|
+
}
|
|
1043
|
+
else if (this.activeTurn?.external) {
|
|
1044
|
+
this.activeTurn = null;
|
|
1045
|
+
}
|
|
1046
|
+
if (snapshot.status === "ended")
|
|
1047
|
+
this.markEnded(meta);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (active?.operationId === pending.turn.turnId && active.operationKind === pending.turn.kind) {
|
|
1051
|
+
this.setStatus(coreStatusToSdkStatus(snapshot.status), meta);
|
|
1052
|
+
this.queueSnapshotTool(snapshot, pending.turn);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const completed = snapshot.lastCompletedOperation;
|
|
1056
|
+
if (completed?.operationId === pending.turn.turnId && completed.operationKind === pending.turn.kind) {
|
|
1057
|
+
this.setStatus(completed.success ? "idle" : completed.operationKind === "prepare" ? "idle" : "error", meta);
|
|
1058
|
+
this.completeOperation(pending.turn, completed.success, completed.error);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const error = makeError("SDK_LIFECYCLE_SYNC_ERROR", "authoritative session snapshot did not match the active SDK operation", { sessionId: this.sessionId, operationId: pending.turn.turnId });
|
|
1062
|
+
this.emitError(error, { source: "sdk" });
|
|
1063
|
+
this.rejectOperation(pending.turn, error);
|
|
1064
|
+
this.setStatus(coreStatusToSdkStatus(snapshot.status), meta);
|
|
1065
|
+
}
|
|
797
1066
|
replaceInlineToolHandlers(handlers) {
|
|
798
1067
|
this.inlineToolHandlers = new Map(handlers);
|
|
799
1068
|
}
|
|
@@ -810,7 +1079,10 @@ export class PedelecSession {
|
|
|
810
1079
|
this.emitError(error, { source: "sdk" });
|
|
811
1080
|
this.rejectActivePending(error);
|
|
812
1081
|
}
|
|
813
|
-
async
|
|
1082
|
+
async handleToolCallOnce(event, meta, turn) {
|
|
1083
|
+
if (this.handledToolRequestIds.has(event.toolRequestId))
|
|
1084
|
+
return;
|
|
1085
|
+
this.handledToolRequestIds.add(event.toolRequestId);
|
|
814
1086
|
let result;
|
|
815
1087
|
const namedHandler = this.namedToolHandlers.get(event.tool);
|
|
816
1088
|
const inlineHandler = this.inlineToolHandlers.get(event.tool);
|
|
@@ -851,7 +1123,7 @@ export class PedelecSession {
|
|
|
851
1123
|
}),
|
|
852
1124
|
};
|
|
853
1125
|
}
|
|
854
|
-
if (this.transportDetached)
|
|
1126
|
+
if (this.transportDetached || !this.isAuthoritativeToolRequestCurrent(event))
|
|
855
1127
|
return;
|
|
856
1128
|
try {
|
|
857
1129
|
await this.client.request("submit_tool_result", {
|
|
@@ -866,51 +1138,10 @@ export class PedelecSession {
|
|
|
866
1138
|
});
|
|
867
1139
|
}
|
|
868
1140
|
}
|
|
869
|
-
resolvePendingSend() {
|
|
870
|
-
const pending = this.pendingSend;
|
|
871
|
-
this.pendingSend = null;
|
|
872
|
-
pending?.resolve();
|
|
873
|
-
this.finishActiveTurn();
|
|
874
|
-
}
|
|
875
|
-
resolvePendingPrepare() {
|
|
876
|
-
const pending = this.pendingPrepare;
|
|
877
|
-
this.pendingPrepare = null;
|
|
878
|
-
pending?.resolve();
|
|
879
|
-
this.finishActiveTurn();
|
|
880
|
-
}
|
|
881
|
-
resolveActivePending() {
|
|
882
|
-
if (this.activeTurn?.kind === "prepare") {
|
|
883
|
-
this.resolvePendingPrepare();
|
|
884
|
-
}
|
|
885
|
-
else {
|
|
886
|
-
this.resolvePendingSend();
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
rejectPendingSend(error) {
|
|
890
|
-
const pending = this.pendingSend;
|
|
891
|
-
this.pendingSend = null;
|
|
892
|
-
pending?.reject(error);
|
|
893
|
-
this.finishActiveTurn();
|
|
894
|
-
}
|
|
895
|
-
rejectPendingPrepare(error) {
|
|
896
|
-
const pending = this.pendingPrepare;
|
|
897
|
-
this.pendingPrepare = null;
|
|
898
|
-
pending?.reject(error);
|
|
899
|
-
this.finishActiveTurn();
|
|
900
|
-
}
|
|
901
1141
|
rejectActivePending(error) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
else {
|
|
906
|
-
this.rejectPendingSend(error);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
clearPendingSend() {
|
|
910
|
-
this.pendingSend = null;
|
|
911
|
-
}
|
|
912
|
-
clearPendingPrepare() {
|
|
913
|
-
this.pendingPrepare = null;
|
|
1142
|
+
const turn = this.activeTurn;
|
|
1143
|
+
if (turn)
|
|
1144
|
+
this.rejectOperation(turn, error);
|
|
914
1145
|
}
|
|
915
1146
|
markEnded(meta = { source: "sdk" }) {
|
|
916
1147
|
const wasEnded = this.status === "ended";
|
|
@@ -920,9 +1151,9 @@ export class PedelecSession {
|
|
|
920
1151
|
handler(this.createEndedContext(meta));
|
|
921
1152
|
}
|
|
922
1153
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1154
|
+
const error = makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId });
|
|
1155
|
+
if (this.activeTurn)
|
|
1156
|
+
this.rejectOperation(this.activeTurn, error);
|
|
926
1157
|
}
|
|
927
1158
|
emitError(error, meta = { source: "sdk" }) {
|
|
928
1159
|
for (const handler of this.errorHandlers) {
|
|
@@ -938,14 +1169,140 @@ export class PedelecSession {
|
|
|
938
1169
|
handler(status, this.createStatusContext(status, previousStatus, meta));
|
|
939
1170
|
}
|
|
940
1171
|
}
|
|
941
|
-
|
|
942
|
-
if (this.activeTurn)
|
|
1172
|
+
requireOperationTurn(operationId, type, meta) {
|
|
1173
|
+
if (operationId && this.activeTurn?.turnId === operationId) {
|
|
943
1174
|
return this.activeTurn;
|
|
944
|
-
|
|
1175
|
+
}
|
|
1176
|
+
this.emitError(makeError("SDK_PROTOCOL_ERROR", `${type} event did not match the active operation`, {
|
|
945
1177
|
sessionId: this.sessionId,
|
|
1178
|
+
operationId,
|
|
946
1179
|
}), { source: "sdk", eventReceivedAt: meta.eventReceivedAt });
|
|
947
1180
|
return null;
|
|
948
1181
|
}
|
|
1182
|
+
updateUsage(totalTokens) {
|
|
1183
|
+
if (!isValidSessionUsageTotalTokens(totalTokens))
|
|
1184
|
+
return;
|
|
1185
|
+
if (this.usage.totalTokens === undefined || totalTokens >= this.usage.totalTokens) {
|
|
1186
|
+
this.usage.totalTokens = totalTokens;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
matchesActiveOperation(operationId) {
|
|
1190
|
+
return this.activeTurn?.turnId === operationId;
|
|
1191
|
+
}
|
|
1192
|
+
markOperationRequestSettled(turn) {
|
|
1193
|
+
const pending = this.pendingOperation;
|
|
1194
|
+
if (!pending || pending.turn.turnId !== turn.turnId)
|
|
1195
|
+
return;
|
|
1196
|
+
pending.requestSettled = true;
|
|
1197
|
+
this.maybeSettleOperation(pending);
|
|
1198
|
+
}
|
|
1199
|
+
failOperationFromRequest(turn, error) {
|
|
1200
|
+
const pending = this.pendingOperation;
|
|
1201
|
+
if (!pending || pending.turn.turnId !== turn.turnId)
|
|
1202
|
+
return;
|
|
1203
|
+
// A semantic terminal may have arrived before the bridge response. Keep the
|
|
1204
|
+
// operation reserved until that response boundary is observed, but never let
|
|
1205
|
+
// a delayed request failure overwrite its matching terminal result.
|
|
1206
|
+
if (pending.completed) {
|
|
1207
|
+
pending.requestSettled = true;
|
|
1208
|
+
this.maybeSettleOperation(pending);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
this.setStatus(turn.kind === "prepare" || isAdmissionRejection(error) ? pending.priorStatus : "error", { source: "sdk" });
|
|
1212
|
+
this.rejectOperation(turn, error);
|
|
1213
|
+
}
|
|
1214
|
+
completeOperation(turn, success, error) {
|
|
1215
|
+
const pending = this.pendingOperation;
|
|
1216
|
+
if (!pending || pending.turn.turnId !== turn.turnId) {
|
|
1217
|
+
if (this.activeTurn?.turnId === turn.turnId && turn.external)
|
|
1218
|
+
this.finishActiveTurn(turn);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
pending.completed = { success, error };
|
|
1222
|
+
this.maybeSettleOperation(pending);
|
|
1223
|
+
}
|
|
1224
|
+
maybeSettleOperation(pending) {
|
|
1225
|
+
if (!pending.requestSettled || !pending.completed)
|
|
1226
|
+
return;
|
|
1227
|
+
const turn = pending.turn;
|
|
1228
|
+
this.pendingOperation = null;
|
|
1229
|
+
this.finishActiveTurn(turn);
|
|
1230
|
+
if (pending.completed.success)
|
|
1231
|
+
pending.resolve();
|
|
1232
|
+
else
|
|
1233
|
+
pending.reject(pending.completed.error ?? makeError("SESSION_ERROR", "session operation failed"));
|
|
1234
|
+
}
|
|
1235
|
+
rejectOperation(turn, error) {
|
|
1236
|
+
const pending = this.pendingOperation;
|
|
1237
|
+
if (pending && pending.turn.turnId === turn.turnId) {
|
|
1238
|
+
this.pendingOperation = null;
|
|
1239
|
+
this.finishActiveTurn(turn);
|
|
1240
|
+
pending.reject(error);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
this.finishActiveTurn(turn);
|
|
1244
|
+
}
|
|
1245
|
+
queueSnapshotTool(snapshot, turn) {
|
|
1246
|
+
const request = snapshot.pendingToolRequest;
|
|
1247
|
+
if (!request || request.operationId !== turn.turnId)
|
|
1248
|
+
return;
|
|
1249
|
+
if (this.handledToolRequestIds.has(request.requestId))
|
|
1250
|
+
return;
|
|
1251
|
+
this.recoveredToolCalls.set(request.requestId, {
|
|
1252
|
+
type: "tool_call",
|
|
1253
|
+
channelId: "",
|
|
1254
|
+
sessionId: this.sessionId,
|
|
1255
|
+
seq: snapshot.latestSeq,
|
|
1256
|
+
operationId: request.operationId,
|
|
1257
|
+
toolRequestId: request.requestId,
|
|
1258
|
+
tool: request.toolName,
|
|
1259
|
+
args: request.args,
|
|
1260
|
+
});
|
|
1261
|
+
void this.drainRecoveredToolCalls();
|
|
1262
|
+
}
|
|
1263
|
+
reconcileRecoveredToolCalls(snapshot) {
|
|
1264
|
+
const active = snapshot.activeOperation;
|
|
1265
|
+
const pending = active && snapshot.pendingToolRequest?.operationId === active.operationId
|
|
1266
|
+
? snapshot.pendingToolRequest
|
|
1267
|
+
: undefined;
|
|
1268
|
+
this.authoritativePendingToolRequest = pending
|
|
1269
|
+
? { operationId: pending.operationId, requestId: pending.requestId }
|
|
1270
|
+
: null;
|
|
1271
|
+
for (const [requestId, event] of this.recoveredToolCalls) {
|
|
1272
|
+
if (!active ||
|
|
1273
|
+
!pending ||
|
|
1274
|
+
event.operationId !== active.operationId ||
|
|
1275
|
+
event.toolRequestId !== pending.requestId) {
|
|
1276
|
+
this.recoveredToolCalls.delete(requestId);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
isAuthoritativeToolRequestCurrent(event) {
|
|
1281
|
+
const pending = this.authoritativePendingToolRequest;
|
|
1282
|
+
if (pending === undefined)
|
|
1283
|
+
return true;
|
|
1284
|
+
if (pending === null)
|
|
1285
|
+
return false;
|
|
1286
|
+
return (pending.operationId === event.operationId &&
|
|
1287
|
+
pending.requestId === event.toolRequestId);
|
|
1288
|
+
}
|
|
1289
|
+
hasToolHandler(toolName) {
|
|
1290
|
+
return this.namedToolHandlers.has(toolName) ||
|
|
1291
|
+
this.inlineToolHandlers.has(toolName) ||
|
|
1292
|
+
this.genericToolHandler !== null;
|
|
1293
|
+
}
|
|
1294
|
+
async drainRecoveredToolCalls() {
|
|
1295
|
+
const turn = this.activeTurn;
|
|
1296
|
+
if (!turn)
|
|
1297
|
+
return;
|
|
1298
|
+
for (const [requestId, event] of Array.from(this.recoveredToolCalls.entries())) {
|
|
1299
|
+
if (event.operationId !== turn.turnId || !this.hasToolHandler(event.tool))
|
|
1300
|
+
continue;
|
|
1301
|
+
this.recoveredToolCalls.delete(requestId);
|
|
1302
|
+
this.setStatus("waiting_tool_result", { source: "core" });
|
|
1303
|
+
await this.handleToolCallOnce(event, { source: "core", eventReceivedAt: Date.now() }, turn);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
949
1306
|
finishActiveTurn(turn = this.activeTurn) {
|
|
950
1307
|
if (!turn || !this.activeTurn)
|
|
951
1308
|
return;
|
|
@@ -966,6 +1323,16 @@ export class PedelecSession {
|
|
|
966
1323
|
};
|
|
967
1324
|
}
|
|
968
1325
|
createChatContext(meta, turn) {
|
|
1326
|
+
return {
|
|
1327
|
+
...this.createBaseContext(meta, turn),
|
|
1328
|
+
type: "chat_message",
|
|
1329
|
+
turnId: turn.turnId,
|
|
1330
|
+
turnStartedAt: turn.turnStartedAt,
|
|
1331
|
+
eventReceivedAt: meta.eventReceivedAt ?? Date.now(),
|
|
1332
|
+
source: "core",
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
createChatDeltaContext(meta, turn) {
|
|
969
1336
|
return {
|
|
970
1337
|
...this.createBaseContext(meta, turn),
|
|
971
1338
|
type: "chat_delta",
|
|
@@ -1010,9 +1377,11 @@ export class PedelecSession {
|
|
|
1010
1377
|
}
|
|
1011
1378
|
function isSessionEvent(message) {
|
|
1012
1379
|
return (message.type === "chat_delta" ||
|
|
1380
|
+
message.type === "chat_message" ||
|
|
1013
1381
|
message.type === "status_changed" ||
|
|
1014
1382
|
message.type === "tool_call" ||
|
|
1015
|
-
message.type === "
|
|
1383
|
+
message.type === "operation_completed" ||
|
|
1384
|
+
message.type === "usage_updated" ||
|
|
1016
1385
|
message.type === "error" ||
|
|
1017
1386
|
message.type === "ended");
|
|
1018
1387
|
}
|
|
@@ -1081,6 +1450,23 @@ function createTurn(kind = "user") {
|
|
|
1081
1450
|
kind,
|
|
1082
1451
|
};
|
|
1083
1452
|
}
|
|
1453
|
+
function isValidSessionUsageTotalTokens(value) {
|
|
1454
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
|
|
1455
|
+
}
|
|
1456
|
+
function coreStatusToSdkStatus(status) {
|
|
1457
|
+
if (status === "waitingToolResult")
|
|
1458
|
+
return "waiting_tool_result";
|
|
1459
|
+
if (status === "starting" || status === "stopping")
|
|
1460
|
+
return "running";
|
|
1461
|
+
if (status === "idle" || status === "running" || status === "ended" || status === "error") {
|
|
1462
|
+
return status;
|
|
1463
|
+
}
|
|
1464
|
+
return "error";
|
|
1465
|
+
}
|
|
1466
|
+
function parseSnapshotTime(value) {
|
|
1467
|
+
const parsed = Date.parse(value);
|
|
1468
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
1469
|
+
}
|
|
1084
1470
|
function getCurrentOrigin(pageWindow) {
|
|
1085
1471
|
const origin = pageWindow?.location?.origin;
|
|
1086
1472
|
return typeof origin === "string" && origin ? origin : null;
|
|
@@ -1088,6 +1474,22 @@ function getCurrentOrigin(pageWindow) {
|
|
|
1088
1474
|
function isExtensionUnavailableError(error) {
|
|
1089
1475
|
return error.code === "EXTENSION_UNAVAILABLE" || error.code === "EXTENSION_DISCONNECTED";
|
|
1090
1476
|
}
|
|
1477
|
+
function isAdmissionRejection(error) {
|
|
1478
|
+
return error.code === "THREAD_BUSY" ||
|
|
1479
|
+
error.code === "THREAD_ENDED" ||
|
|
1480
|
+
error.code === "THREAD_ACCESS_DENIED" ||
|
|
1481
|
+
error.code === "THREAD_NOT_FOUND" ||
|
|
1482
|
+
error.code === "INVALID_INPUT";
|
|
1483
|
+
}
|
|
1484
|
+
function isDeterministicResumeError(error) {
|
|
1485
|
+
return error.code === "THREAD_BUSY" ||
|
|
1486
|
+
error.code === "THREAD_ENDED" ||
|
|
1487
|
+
error.code === "THREAD_ACCESS_DENIED" ||
|
|
1488
|
+
error.code === "THREAD_NOT_FOUND" ||
|
|
1489
|
+
error.code === "WORKSPACE_OPEN_FAILED" ||
|
|
1490
|
+
error.code === "PROVIDER_COMMAND_FAILED" ||
|
|
1491
|
+
error.code === "INVALID_INPUT";
|
|
1492
|
+
}
|
|
1091
1493
|
function normalizeError(err, fallbackCode, fallbackMessage) {
|
|
1092
1494
|
if (!err)
|
|
1093
1495
|
return makeError(fallbackCode, fallbackMessage);
|
package/dist/index.type-test.js
CHANGED
|
@@ -50,10 +50,20 @@ async function typedOnToolNameFromCreateSession() {
|
|
|
50
50
|
});
|
|
51
51
|
session.onChat((_text, ctx) => {
|
|
52
52
|
const chatCtx = ctx;
|
|
53
|
+
const messageType = ctx.type;
|
|
53
54
|
const receivedAt = ctx.eventReceivedAt;
|
|
54
55
|
void receivedAt;
|
|
56
|
+
void messageType;
|
|
55
57
|
void chatCtx;
|
|
56
58
|
});
|
|
59
|
+
session.onChatDelta((_text, ctx) => {
|
|
60
|
+
const chatDeltaCtx = ctx;
|
|
61
|
+
const deltaType = ctx.type;
|
|
62
|
+
const receivedAt = ctx.eventReceivedAt;
|
|
63
|
+
void receivedAt;
|
|
64
|
+
void deltaType;
|
|
65
|
+
void chatDeltaCtx;
|
|
66
|
+
});
|
|
57
67
|
session.onStatus((_status, ctx) => {
|
|
58
68
|
const statusCtx = ctx;
|
|
59
69
|
const previous = ctx.previousStatus;
|
|
@@ -68,6 +78,7 @@ async function typedOnToolNameFromCreateSession() {
|
|
|
68
78
|
const endedCtx = ctx;
|
|
69
79
|
return endedCtx;
|
|
70
80
|
});
|
|
81
|
+
await session.resume();
|
|
71
82
|
}
|
|
72
83
|
async function resumedSessionFallsBackToString() {
|
|
73
84
|
const pedelec = new Pedelec();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.3.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from sdk/package.json. Do not edit manually.
|
|
2
|
-
export const SDK_VERSION = "0.
|
|
2
|
+
export const SDK_VERSION = "0.3.0";
|