@kaoruisaac/pedelec 0.1.16 → 0.1.18
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 +10 -2
- package/dist/index.d.ts +7 -1
- package/dist/index.js +69 -11
- package/dist/index.type-test.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,14 +128,22 @@ await session.sendText("Please help me analyze the current page state");
|
|
|
128
128
|
|
|
129
129
|
`sendText()` resolves after the current agent response completes. 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.
|
|
130
130
|
|
|
131
|
-
##
|
|
131
|
+
## Session assets
|
|
132
132
|
|
|
133
133
|
```ts
|
|
134
134
|
const path = await session.uploadAsset(file);
|
|
135
135
|
const assets = await session.listAssets();
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
`listAssets()` lists only
|
|
138
|
+
`assets/` is shared by uploads and files written by the agent. `listAssets()` lists only its first level, ordered by filesystem modification time (newest first), and may run while the agent runs. One session can only upload one file at a time, but uploads can run alongside prepare or agent execution.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const text = await session.readAsset("/report.txt", "text");
|
|
142
|
+
const json = await session.readAsset<{ ok: boolean }>("/result.json", "json");
|
|
143
|
+
const result = await session.readAsset("/model.glb", "file");
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Public asset paths use `/...` with `assets/` as their implicit root; nested paths such as `/results/model.glb` are supported up to 100 MiB.
|
|
139
147
|
|
|
140
148
|
---
|
|
141
149
|
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export type JsonPrimitive = string | number | boolean | null;
|
|
|
6
6
|
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
7
7
|
[key: string]: JsonValue;
|
|
8
8
|
};
|
|
9
|
-
export type SandboxAssetPath =
|
|
9
|
+
export type SandboxAssetPath = `/${string}`;
|
|
10
|
+
export type ReadAssetType = "text" | "json" | "file";
|
|
10
11
|
export type SandboxAsset = {
|
|
11
12
|
name: string;
|
|
12
13
|
path: SandboxAssetPath;
|
|
@@ -258,6 +259,7 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
258
259
|
private pendingPrepare;
|
|
259
260
|
private preparePromise;
|
|
260
261
|
private uploadPromise;
|
|
262
|
+
private ending;
|
|
261
263
|
private prepared;
|
|
262
264
|
private activeTurn;
|
|
263
265
|
private genericToolHandler;
|
|
@@ -274,7 +276,11 @@ export declare class PedelecSession<TToolName extends string = string> {
|
|
|
274
276
|
onTool(handler: GenericToolHandler<TToolName>): () => void;
|
|
275
277
|
onTool<TArgs = unknown, TResult = unknown>(toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
|
|
276
278
|
uploadAsset(file: File): Promise<SandboxAssetPath>;
|
|
279
|
+
uploadAsset(file: File, targetPath: SandboxAssetPath): Promise<SandboxAssetPath>;
|
|
277
280
|
listAssets(): Promise<SandboxAsset[]>;
|
|
281
|
+
readAsset(path: SandboxAssetPath, type: "text"): Promise<string>;
|
|
282
|
+
readAsset<T = JsonValue>(path: SandboxAssetPath, type: "json"): Promise<T>;
|
|
283
|
+
readAsset(path: SandboxAssetPath, type: "file"): Promise<File>;
|
|
278
284
|
onError(handler: ErrorHandler): () => void;
|
|
279
285
|
onStatus(handler: StatusHandler): () => void;
|
|
280
286
|
onEnded(handler: EndedHandler): () => void;
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ function normalizeListAssetsResponse(response) {
|
|
|
12
12
|
return invalidListAssetsResponse({ index, asset });
|
|
13
13
|
const { name, path, sizeBytes, modifiedAt } = asset;
|
|
14
14
|
const validName = typeof name === "string" && name.length > 0 && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\");
|
|
15
|
-
const validPath = typeof path === "string" &&
|
|
15
|
+
const validPath = typeof path === "string" && path === `/${name}` && isValidAssetPath(path);
|
|
16
16
|
const validNumber = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
|
|
17
17
|
if (!validName || !validPath || !validNumber(sizeBytes) || !validNumber(modifiedAt)) {
|
|
18
18
|
return invalidListAssetsResponse({ index, asset });
|
|
@@ -23,6 +23,10 @@ function normalizeListAssetsResponse(response) {
|
|
|
23
23
|
function invalidListAssetsResponse(details) {
|
|
24
24
|
throw makeError("SDK_PROTOCOL_ERROR", "list_assets response had an invalid shape", { response: details });
|
|
25
25
|
}
|
|
26
|
+
function isValidAssetPath(path) {
|
|
27
|
+
return typeof path === "string" && /^\/(?:[^/\\\x00-\x1f]+)(?:\/[^/\\\x00-\x1f]+)*$/.test(path) &&
|
|
28
|
+
path.split("/").every((part, index) => index === 0 || (part !== "." && part !== ".."));
|
|
29
|
+
}
|
|
26
30
|
const MAX_ASSET_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
27
31
|
const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
|
|
28
32
|
export function defineTool(tool) {
|
|
@@ -436,6 +440,7 @@ export class PedelecSession {
|
|
|
436
440
|
pendingPrepare = null;
|
|
437
441
|
preparePromise = null;
|
|
438
442
|
uploadPromise = null;
|
|
443
|
+
ending = false;
|
|
439
444
|
prepared = false;
|
|
440
445
|
activeTurn = null;
|
|
441
446
|
genericToolHandler = null;
|
|
@@ -456,7 +461,7 @@ export class PedelecSession {
|
|
|
456
461
|
if (this.status === "ended") {
|
|
457
462
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
458
463
|
}
|
|
459
|
-
if (this.pendingSend
|
|
464
|
+
if (this.pendingSend) {
|
|
460
465
|
return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
|
|
461
466
|
}
|
|
462
467
|
if (this.prepared) {
|
|
@@ -492,9 +497,6 @@ export class PedelecSession {
|
|
|
492
497
|
return this.preparePromise;
|
|
493
498
|
}
|
|
494
499
|
async sendText(text) {
|
|
495
|
-
if (this.uploadPromise) {
|
|
496
|
-
return Promise.reject(makeError("SESSION_BUSY", "an asset upload is in progress", { sessionId: this.sessionId }));
|
|
497
|
-
}
|
|
498
500
|
if (this.preparePromise) {
|
|
499
501
|
try {
|
|
500
502
|
await this.preparePromise;
|
|
@@ -557,10 +559,10 @@ export class PedelecSession {
|
|
|
557
559
|
}
|
|
558
560
|
};
|
|
559
561
|
}
|
|
560
|
-
uploadAsset(file) {
|
|
561
|
-
if (this.status === "ended")
|
|
562
|
+
uploadAsset(file, targetPath) {
|
|
563
|
+
if (this.ending || this.status === "ended")
|
|
562
564
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
563
|
-
if (this.
|
|
565
|
+
if (this.uploadPromise) {
|
|
564
566
|
return Promise.reject(makeError("SESSION_BUSY", "session is busy", { sessionId: this.sessionId }));
|
|
565
567
|
}
|
|
566
568
|
if (typeof File === "undefined" || !(file instanceof File) || !file.name || file.size < 0) {
|
|
@@ -568,8 +570,11 @@ export class PedelecSession {
|
|
|
568
570
|
}
|
|
569
571
|
if (file.size > MAX_ASSET_UPLOAD_BYTES)
|
|
570
572
|
return Promise.reject(makeError("ASSET_TOO_LARGE", "asset exceeds the 100 MiB limit"));
|
|
573
|
+
if (targetPath !== undefined && !isValidAssetPath(targetPath))
|
|
574
|
+
return Promise.reject(makeError("ASSET_PATH_INVALID", "asset target path is invalid"));
|
|
571
575
|
this.uploadPromise = this.client.request("create_asset_upload", {
|
|
572
576
|
sessionId: this.sessionId, filename: file.name, sizeBytes: file.size, mimeType: file.type,
|
|
577
|
+
...(targetPath === undefined ? {} : { targetPath }),
|
|
573
578
|
}).then(async (ticket) => {
|
|
574
579
|
if (!ticket?.uploadUrl || !ticket.token)
|
|
575
580
|
throw makeError("SDK_PROTOCOL_ERROR", "asset upload ticket was invalid");
|
|
@@ -587,18 +592,69 @@ export class PedelecSession {
|
|
|
587
592
|
catch { /* normalized below */ }
|
|
588
593
|
if (!response.ok)
|
|
589
594
|
throw normalizeError(payload?.error, "ASSET_UPLOAD_FAILED", "asset upload failed");
|
|
590
|
-
if (
|
|
595
|
+
if (!isValidAssetPath(payload?.path))
|
|
591
596
|
throw makeError("SDK_PROTOCOL_ERROR", "asset upload response had an invalid path");
|
|
597
|
+
if (targetPath !== undefined && payload.path !== targetPath)
|
|
598
|
+
throw makeError("SDK_PROTOCOL_ERROR", "asset upload response path did not match target path");
|
|
592
599
|
return payload.path;
|
|
593
600
|
}).finally(() => { this.uploadPromise = null; });
|
|
594
601
|
return this.uploadPromise;
|
|
595
602
|
}
|
|
596
603
|
listAssets() {
|
|
597
|
-
if (this.status === "ended") {
|
|
604
|
+
if (this.ending || this.status === "ended") {
|
|
598
605
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
599
606
|
}
|
|
600
607
|
return this.client.request("list_assets", { sessionId: this.sessionId }).then(normalizeListAssetsResponse);
|
|
601
608
|
}
|
|
609
|
+
readAsset(path, type) {
|
|
610
|
+
if (this.ending || this.status === "ended")
|
|
611
|
+
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
612
|
+
if (!isValidAssetPath(path) || !["text", "json", "file"].includes(type)) {
|
|
613
|
+
return Promise.reject(makeError("ASSET_PATH_INVALID", "asset path or read type is invalid"));
|
|
614
|
+
}
|
|
615
|
+
return this.client.request("create_asset_download", { sessionId: this.sessionId, path }).then(async (ticket) => {
|
|
616
|
+
if (!ticket || typeof ticket !== "object")
|
|
617
|
+
throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
|
|
618
|
+
const value = ticket;
|
|
619
|
+
if (typeof value.downloadUrl !== "string" || typeof value.token !== "string" || typeof value.name !== "string" ||
|
|
620
|
+
typeof value.mimeType !== "string" || typeof value.modifiedAt !== "number" || !Number.isFinite(value.modifiedAt) || !isValidAssetPath(value.path)) {
|
|
621
|
+
throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
|
|
622
|
+
}
|
|
623
|
+
let response;
|
|
624
|
+
try {
|
|
625
|
+
response = await fetch(value.downloadUrl, { headers: { Authorization: `Bearer ${value.token}` }, credentials: "omit" });
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
throw normalizeError(error, "ASSET_READ_FAILED", "asset read failed");
|
|
629
|
+
}
|
|
630
|
+
if (!response.ok) {
|
|
631
|
+
let payload;
|
|
632
|
+
try {
|
|
633
|
+
payload = await response.json();
|
|
634
|
+
}
|
|
635
|
+
catch { /* normalize below */ }
|
|
636
|
+
throw normalizeError(payload?.error, "ASSET_READ_FAILED", "asset read failed");
|
|
637
|
+
}
|
|
638
|
+
const bytes = await response.arrayBuffer();
|
|
639
|
+
if (type === "file")
|
|
640
|
+
return new File([bytes], value.name, { type: value.mimeType || "application/octet-stream", lastModified: value.modifiedAt });
|
|
641
|
+
let text;
|
|
642
|
+
try {
|
|
643
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
throw makeError("ASSET_TEXT_DECODE_FAILED", "asset is not valid UTF-8");
|
|
647
|
+
}
|
|
648
|
+
if (type === "text")
|
|
649
|
+
return text;
|
|
650
|
+
try {
|
|
651
|
+
return JSON.parse(text);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
throw makeError("ASSET_INVALID_JSON", "asset does not contain valid JSON");
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
602
658
|
onError(handler) {
|
|
603
659
|
this.errorHandlers.add(handler);
|
|
604
660
|
return () => this.errorHandlers.delete(handler);
|
|
@@ -615,14 +671,16 @@ export class PedelecSession {
|
|
|
615
671
|
return this.status;
|
|
616
672
|
}
|
|
617
673
|
async end() {
|
|
618
|
-
if (this.status === "ended")
|
|
674
|
+
if (this.status === "ended" || this.ending)
|
|
619
675
|
return;
|
|
676
|
+
this.ending = true;
|
|
620
677
|
try {
|
|
621
678
|
await this.client.request("end_session", {
|
|
622
679
|
sessionId: this.sessionId,
|
|
623
680
|
});
|
|
624
681
|
}
|
|
625
682
|
catch (err) {
|
|
683
|
+
this.ending = false;
|
|
626
684
|
const error = normalizeError(err, "END_SESSION_FAILED", "end failed");
|
|
627
685
|
this.emitError(error, { source: "sdk" });
|
|
628
686
|
throw error;
|
package/dist/index.type-test.js
CHANGED
|
@@ -96,6 +96,20 @@ async function listAssetsHasPublicTypes() {
|
|
|
96
96
|
const path = assets[0].path;
|
|
97
97
|
path;
|
|
98
98
|
}
|
|
99
|
+
async function assetPathsUseAssetsAsAnImplicitRoot() {
|
|
100
|
+
const pedelec = new Pedelec();
|
|
101
|
+
const session = await pedelec.resumeSession("thread_1");
|
|
102
|
+
const file = new File(["asset"], "original.txt", { type: "text/plain" });
|
|
103
|
+
const generated = await session.uploadAsset(file);
|
|
104
|
+
generated;
|
|
105
|
+
const exact = await session.uploadAsset(file, "/img/image.txt");
|
|
106
|
+
exact;
|
|
107
|
+
const namedAssetsDirectory = "/assets/image.txt";
|
|
108
|
+
void namedAssetsDirectory;
|
|
109
|
+
// @ts-expect-error asset paths must begin with a slash
|
|
110
|
+
const missingSlash = "assets/image.txt";
|
|
111
|
+
void missingSlash;
|
|
112
|
+
}
|
|
99
113
|
async function availabilityHasPublicType() {
|
|
100
114
|
const pedelec = new Pedelec();
|
|
101
115
|
const availability = await pedelec.checkAvailability();
|