@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 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
- ## Listing uploaded assets
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 the first level of `input/`, ordered by filesystem modification time (newest first). Each item's `name` is the actual sandbox filename and may differ from the original `File.name`; `modifiedAt` is a filesystem modification timestamp, not an exact upload time. `path` is an agent-sandbox-relative `input/...` path and never an absolute local path. It may be called while the agent is running. Ended sessions reject with `SESSION_ENDED`. Recursive listing, pagination, download, deletion, rename, and move are not supported.
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 = `input/${string}`;
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" && !path.includes("\\") && path === `input/${name}` && !path.split("/").includes("..");
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 || this.uploadPromise) {
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.pendingSend || this.pendingPrepare || this.preparePromise || this.uploadPromise || this.status !== "idle") {
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 (typeof payload?.path !== "string" || !payload.path.startsWith("input/") || payload.path.includes(".."))
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;
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",