@nylorun/agents 0.29.1-beta → 0.30.0-beta

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.30.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 2f3fc06: A skill file can be uploaded from a stream, so a client never holds a large one in memory (ADR-93).
8
+
9
+ - **`SkillFileSource.stream()`**, optional: each call opens a new stream of the file's bytes. When a source has it, `client.files.ensure` (and so `agentManifests.save`) sends the stream instead of `read()`'s bytes.
10
+ - **`client.files.uploadStream(stream, { sha256 })`** sends one file half-duplex, up to the Tenant's skill limit (2 GiB by default). The Runtime already streams the body to its hash.
11
+ - **A stream is never sent twice by the transport.** After an expired token or a protocol check, `ensure` opens a new stream and sends it once more.
12
+
13
+ ### Patch Changes
14
+
15
+ - Pin core to the tested release.
16
+ - Updated dependencies [2f3fc06]
17
+ - @nylorun/core@0.31.0-beta
18
+
3
19
  ## 0.29.1-beta
4
20
 
5
21
  ### Patch Changes
package/dist/files.d.ts CHANGED
@@ -17,14 +17,26 @@ export declare class FilesClient {
17
17
  signal?: AbortSignal;
18
18
  }): Promise<boolean>;
19
19
  /**
20
- * Uploads one file, at most 2 GiB. The Runtime checks its bytes hash to `sha256` (default:
21
- * their hash) and keeps a file it already holds as it is.
20
+ * Uploads one file, at most the Tenant's skill limit (2 GiB by default). The Runtime checks its
21
+ * bytes hash to `sha256` (default: their hash) and keeps a file it already holds as it is.
22
22
  */
23
23
  upload(bytes: Uint8Array, options?: {
24
24
  sha256?: string;
25
25
  signal?: AbortSignal;
26
26
  }): Promise<DefinitionFileView>;
27
- /** Uploads each of `files` the Runtime does not hold yet. */
27
+ /**
28
+ * Uploads one file from a stream, so a large file never sits in memory (ADR-93). `sha256` is
29
+ * required: the bytes aren't read before they are sent. The Runtime refuses a body with another
30
+ * hash and stores nothing.
31
+ */
32
+ uploadStream(stream: ReadableStream<Uint8Array>, options: {
33
+ sha256: string;
34
+ signal?: AbortSignal;
35
+ }): Promise<DefinitionFileView>;
36
+ /**
37
+ * Uploads each of `files` the Runtime does not hold yet: from its `stream()` when the source has
38
+ * one, else from `read()`.
39
+ */
28
40
  ensure(files: ReadonlyMap<string, SkillFileSource>, options?: {
29
41
  signal?: AbortSignal;
30
42
  }): Promise<void>;
package/dist/files.js CHANGED
@@ -37,8 +37,8 @@ export class FilesClient {
37
37
  }
38
38
  }
39
39
  /**
40
- * Uploads one file, at most 2 GiB. The Runtime checks its bytes hash to `sha256` (default:
41
- * their hash) and keeps a file it already holds as it is.
40
+ * Uploads one file, at most the Tenant's skill limit (2 GiB by default). The Runtime checks its
41
+ * bytes hash to `sha256` (default: their hash) and keeps a file it already holds as it is.
42
42
  */
43
43
  async upload(bytes, options = {}) {
44
44
  const response = await this.transport.request(pathOf(options.sha256 ?? (await definitionFileHash(bytes))), {
@@ -49,12 +49,44 @@ export class FilesClient {
49
49
  });
50
50
  return (await response.json());
51
51
  }
52
- /** Uploads each of `files` the Runtime does not hold yet. */
52
+ /**
53
+ * Uploads one file from a stream, so a large file never sits in memory (ADR-93). `sha256` is
54
+ * required: the bytes aren't read before they are sent. The Runtime refuses a body with another
55
+ * hash and stores nothing.
56
+ */
57
+ async uploadStream(stream, options) {
58
+ const response = await this.transport.request(pathOf(options.sha256), {
59
+ method: "PUT",
60
+ body: stream,
61
+ headers: { "content-type": "application/octet-stream" },
62
+ // Node's fetch sends a stream body only half-duplex.
63
+ duplex: "half",
64
+ ...(options.signal ? { signal: options.signal } : {}),
65
+ });
66
+ return (await response.json());
67
+ }
68
+ /**
69
+ * Uploads each of `files` the Runtime does not hold yet: from its `stream()` when the source has
70
+ * one, else from `read()`.
71
+ */
53
72
  async ensure(files, options = {}) {
54
73
  for (const [sha256, source] of files) {
55
74
  if (await this.has(sha256, options))
56
75
  continue;
57
- await this.upload(await source.read(), { sha256, ...options });
76
+ if (!source.stream) {
77
+ await this.upload(await source.read(), { sha256, ...options });
78
+ continue;
79
+ }
80
+ try {
81
+ await this.uploadStream(source.stream(), { sha256, ...options });
82
+ }
83
+ catch (error) {
84
+ // The transport doesn't resend a stream: once more with a new one after an expired token
85
+ // or a protocol check, which the transport has handled for the next request.
86
+ if (!(error instanceof RuntimeError) || (error.status !== 401 && error.status !== 426))
87
+ throw error;
88
+ await this.uploadStream(source.stream(), { sha256, ...options });
89
+ }
58
90
  }
59
91
  }
60
92
  }
package/dist/http.js CHANGED
@@ -138,18 +138,22 @@ export class Transport {
138
138
  headers: await this.authHeaders(init),
139
139
  redirect: "error",
140
140
  });
141
+ // A stream body can't be sent twice: its caller sends a new one (FilesClient.ensure).
142
+ const resendable = !(typeof ReadableStream !== "undefined" && init.body instanceof ReadableStream);
141
143
  // An expired token: get a new one and try once more.
142
144
  if (response.status === 401 && this.token && !options.retried401) {
143
145
  const body = (await response.clone().json().catch(() => undefined));
144
146
  if (body?.code === "token_expired") {
145
- await response.body?.cancel().catch(() => { });
146
147
  this.token.invalidate();
147
- return this.request(path, init, { ...options, retried401: true });
148
+ if (resendable) {
149
+ await response.body?.cancel().catch(() => { });
150
+ return this.request(path, init, { ...options, retried401: true });
151
+ }
148
152
  }
149
153
  }
150
154
  if (response.status === 426) {
151
155
  this.clearCompatibilityCache();
152
- if (!options.retried426) {
156
+ if (!options.retried426 && resendable) {
153
157
  await this.ensureCompatible(init.signal === null ? undefined : init.signal);
154
158
  return this.request(path, init, { retried426: true });
155
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.29.1-beta",
3
+ "version": "0.30.0-beta",
4
4
  "description": "Nylorun agent definitions and session client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,7 @@
43
43
  "test": "vitest run --passWithNoTests"
44
44
  },
45
45
  "dependencies": {
46
- "@nylorun/core": "0.30.0-beta",
46
+ "@nylorun/core": "0.31.0-beta",
47
47
  "gray-matter": "^4.0.3"
48
48
  },
49
49
  "peerDependencies": {