@kaoruisaac/pedelec 0.1.16 → 0.1.17

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("assets/report.txt", "text");
142
+ const json = await session.readAsset<{ ok: boolean }>("assets/result.json", "json");
143
+ const result = await session.readAsset("assets/model.glb", "file");
144
+ ```
145
+
146
+ Reads support nested `assets/...` paths up to 100 MiB. `text` and `json` require valid UTF-8; binary content is transferred over a loopback download ticket rather than extension messages.
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 = `assets/${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;
@@ -275,6 +277,9 @@ export declare class PedelecSession<TToolName extends string = string> {
275
277
  onTool<TArgs = unknown, TResult = unknown>(toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
276
278
  uploadAsset(file: File): Promise<SandboxAssetPath>;
277
279
  listAssets(): Promise<SandboxAsset[]>;
280
+ readAsset(path: SandboxAssetPath, type: "text"): Promise<string>;
281
+ readAsset<T = JsonValue>(path: SandboxAssetPath, type: "json"): Promise<T>;
282
+ readAsset(path: SandboxAssetPath, type: "file"): Promise<File>;
278
283
  onError(handler: ErrorHandler): () => void;
279
284
  onStatus(handler: StatusHandler): () => void;
280
285
  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.includes("\\") && path === `assets/${name}` && !path.split("/").includes("..");
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" && path.startsWith("assets/") && path.length > "assets/".length && !path.includes("\\") &&
28
+ path.split("/").every((part, index) => index === 0 || (part.length > 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;
@@ -558,9 +560,9 @@ export class PedelecSession {
558
560
  };
559
561
  }
560
562
  uploadAsset(file) {
561
- if (this.status === "ended")
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) {
@@ -587,18 +589,67 @@ export class PedelecSession {
587
589
  catch { /* normalized below */ }
588
590
  if (!response.ok)
589
591
  throw normalizeError(payload?.error, "ASSET_UPLOAD_FAILED", "asset upload failed");
590
- if (typeof payload?.path !== "string" || !payload.path.startsWith("input/") || payload.path.includes(".."))
592
+ if (!isValidAssetPath(payload?.path))
591
593
  throw makeError("SDK_PROTOCOL_ERROR", "asset upload response had an invalid path");
592
594
  return payload.path;
593
595
  }).finally(() => { this.uploadPromise = null; });
594
596
  return this.uploadPromise;
595
597
  }
596
598
  listAssets() {
597
- if (this.status === "ended") {
599
+ if (this.ending || this.status === "ended") {
598
600
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
599
601
  }
600
602
  return this.client.request("list_assets", { sessionId: this.sessionId }).then(normalizeListAssetsResponse);
601
603
  }
604
+ readAsset(path, type) {
605
+ if (this.ending || this.status === "ended")
606
+ return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
607
+ if (!isValidAssetPath(path) || !["text", "json", "file"].includes(type)) {
608
+ return Promise.reject(makeError("ASSET_PATH_INVALID", "asset path or read type is invalid"));
609
+ }
610
+ return this.client.request("create_asset_download", { sessionId: this.sessionId, path }).then(async (ticket) => {
611
+ if (!ticket || typeof ticket !== "object")
612
+ throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
613
+ const value = ticket;
614
+ if (typeof value.downloadUrl !== "string" || typeof value.token !== "string" || typeof value.name !== "string" ||
615
+ typeof value.mimeType !== "string" || typeof value.modifiedAt !== "number" || !Number.isFinite(value.modifiedAt) || !isValidAssetPath(value.path)) {
616
+ throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
617
+ }
618
+ let response;
619
+ try {
620
+ response = await fetch(value.downloadUrl, { headers: { Authorization: `Bearer ${value.token}` }, credentials: "omit" });
621
+ }
622
+ catch (error) {
623
+ throw normalizeError(error, "ASSET_READ_FAILED", "asset read failed");
624
+ }
625
+ if (!response.ok) {
626
+ let payload;
627
+ try {
628
+ payload = await response.json();
629
+ }
630
+ catch { /* normalize below */ }
631
+ throw normalizeError(payload?.error, "ASSET_READ_FAILED", "asset read failed");
632
+ }
633
+ const bytes = await response.arrayBuffer();
634
+ if (type === "file")
635
+ return new File([bytes], value.name, { type: value.mimeType || "application/octet-stream", lastModified: value.modifiedAt });
636
+ let text;
637
+ try {
638
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
639
+ }
640
+ catch {
641
+ throw makeError("ASSET_TEXT_DECODE_FAILED", "asset is not valid UTF-8");
642
+ }
643
+ if (type === "text")
644
+ return text;
645
+ try {
646
+ return JSON.parse(text);
647
+ }
648
+ catch {
649
+ throw makeError("ASSET_INVALID_JSON", "asset does not contain valid JSON");
650
+ }
651
+ });
652
+ }
602
653
  onError(handler) {
603
654
  this.errorHandlers.add(handler);
604
655
  return () => this.errorHandlers.delete(handler);
@@ -615,14 +666,16 @@ export class PedelecSession {
615
666
  return this.status;
616
667
  }
617
668
  async end() {
618
- if (this.status === "ended")
669
+ if (this.status === "ended" || this.ending)
619
670
  return;
671
+ this.ending = true;
620
672
  try {
621
673
  await this.client.request("end_session", {
622
674
  sessionId: this.sessionId,
623
675
  });
624
676
  }
625
677
  catch (err) {
678
+ this.ending = false;
626
679
  const error = normalizeError(err, "END_SESSION_FAILED", "end failed");
627
680
  this.emitError(error, { source: "sdk" });
628
681
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",