@nhtio/adk 0.1.0-master-445a9ed0 → 1.20260529.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.
Files changed (170) hide show
  1. package/batteries/llm/openai_chat_completions/adapter.cjs +10 -9
  2. package/batteries/llm/openai_chat_completions/adapter.cjs.map +1 -1
  3. package/batteries/llm/openai_chat_completions/adapter.mjs +8 -8
  4. package/batteries/llm/openai_chat_completions/adapter.mjs.map +1 -1
  5. package/batteries/llm/openai_chat_completions/exceptions.cjs +1 -1
  6. package/batteries/llm/openai_chat_completions/helpers.cjs +16 -16
  7. package/batteries/llm/openai_chat_completions/helpers.cjs.map +1 -1
  8. package/batteries/llm/openai_chat_completions/helpers.d.ts +10 -10
  9. package/batteries/llm/openai_chat_completions/helpers.mjs +16 -16
  10. package/batteries/llm/openai_chat_completions/helpers.mjs.map +1 -1
  11. package/batteries/llm/openai_chat_completions/types.d.ts +6 -26
  12. package/batteries/llm/openai_chat_completions/validation.cjs +1 -3
  13. package/batteries/llm/openai_chat_completions/validation.cjs.map +1 -1
  14. package/batteries/llm/openai_chat_completions/validation.mjs +0 -2
  15. package/batteries/llm/openai_chat_completions/validation.mjs.map +1 -1
  16. package/batteries/llm/webllm_chat_completions/adapter.cjs +10 -9
  17. package/batteries/llm/webllm_chat_completions/adapter.cjs.map +1 -1
  18. package/batteries/llm/webllm_chat_completions/adapter.mjs +8 -8
  19. package/batteries/llm/webllm_chat_completions/adapter.mjs.map +1 -1
  20. package/batteries/llm/webllm_chat_completions/exceptions.cjs +1 -1
  21. package/batteries/llm/webllm_chat_completions/validation.cjs +1 -3
  22. package/batteries/llm/webllm_chat_completions/validation.cjs.map +1 -1
  23. package/batteries/llm/webllm_chat_completions/validation.mjs +0 -2
  24. package/batteries/llm/webllm_chat_completions/validation.mjs.map +1 -1
  25. package/batteries/storage/flydrive/index.d.ts +4 -10
  26. package/batteries/storage/flydrive.cjs +3 -12
  27. package/batteries/storage/flydrive.cjs.map +1 -1
  28. package/batteries/storage/flydrive.mjs +2 -11
  29. package/batteries/storage/flydrive.mjs.map +1 -1
  30. package/batteries/storage/in_memory/index.d.ts +17 -31
  31. package/batteries/storage/in_memory.cjs +30 -89
  32. package/batteries/storage/in_memory.cjs.map +1 -1
  33. package/batteries/storage/in_memory.mjs +30 -89
  34. package/batteries/storage/in_memory.mjs.map +1 -1
  35. package/batteries/storage/opfs/index.d.ts +4 -10
  36. package/batteries/storage/opfs.cjs +5 -55
  37. package/batteries/storage/opfs.cjs.map +1 -1
  38. package/batteries/storage/opfs.mjs +4 -54
  39. package/batteries/storage/opfs.mjs.map +1 -1
  40. package/batteries/tools/color.cjs +3 -3
  41. package/batteries/tools/color.mjs +2 -2
  42. package/batteries/tools/comparison.cjs +4 -3
  43. package/batteries/tools/comparison.cjs.map +1 -1
  44. package/batteries/tools/comparison.mjs +2 -2
  45. package/batteries/tools/data_structure.cjs +4 -3
  46. package/batteries/tools/data_structure.cjs.map +1 -1
  47. package/batteries/tools/data_structure.mjs +2 -2
  48. package/batteries/tools/datetime_extended.cjs +4 -4
  49. package/batteries/tools/datetime_extended.mjs +2 -2
  50. package/batteries/tools/datetime_math.cjs +3 -3
  51. package/batteries/tools/datetime_math.mjs +2 -2
  52. package/batteries/tools/encoding.cjs +4 -3
  53. package/batteries/tools/encoding.cjs.map +1 -1
  54. package/batteries/tools/encoding.mjs +2 -2
  55. package/batteries/tools/formatting.cjs +4 -3
  56. package/batteries/tools/formatting.cjs.map +1 -1
  57. package/batteries/tools/formatting.mjs +2 -2
  58. package/batteries/tools/geo_basics.cjs +3 -3
  59. package/batteries/tools/geo_basics.mjs +2 -2
  60. package/batteries/tools/math.cjs +4 -3
  61. package/batteries/tools/math.cjs.map +1 -1
  62. package/batteries/tools/math.mjs +2 -2
  63. package/batteries/tools/memory.cjs +7 -6
  64. package/batteries/tools/memory.cjs.map +1 -1
  65. package/batteries/tools/memory.mjs +5 -5
  66. package/batteries/tools/parsing.cjs +6 -5
  67. package/batteries/tools/parsing.cjs.map +1 -1
  68. package/batteries/tools/parsing.mjs +3 -3
  69. package/batteries/tools/retrievables.cjs +11 -11
  70. package/batteries/tools/retrievables.cjs.map +1 -1
  71. package/batteries/tools/retrievables.mjs +9 -10
  72. package/batteries/tools/retrievables.mjs.map +1 -1
  73. package/batteries/tools/standing_instructions.cjs +5 -4
  74. package/batteries/tools/standing_instructions.cjs.map +1 -1
  75. package/batteries/tools/standing_instructions.mjs +3 -3
  76. package/batteries/tools/statistics.cjs +5 -4
  77. package/batteries/tools/statistics.cjs.map +1 -1
  78. package/batteries/tools/statistics.mjs +3 -3
  79. package/batteries/tools/string_processing.cjs +4 -3
  80. package/batteries/tools/string_processing.cjs.map +1 -1
  81. package/batteries/tools/string_processing.mjs +2 -2
  82. package/batteries/tools/structured_data.cjs +4 -3
  83. package/batteries/tools/structured_data.cjs.map +1 -1
  84. package/batteries/tools/structured_data.mjs +2 -2
  85. package/batteries/tools/text_analysis.cjs +4 -4
  86. package/batteries/tools/text_analysis.mjs +3 -3
  87. package/batteries/tools/text_comparison.cjs +3 -3
  88. package/batteries/tools/text_comparison.mjs +2 -2
  89. package/batteries/tools/time.cjs +3 -3
  90. package/batteries/tools/time.mjs +2 -2
  91. package/batteries/tools/unit_conversion.cjs +3 -3
  92. package/batteries/tools/unit_conversion.mjs +2 -2
  93. package/batteries/tools.cjs +1 -1
  94. package/batteries/tools.mjs +1 -1
  95. package/batteries.cjs +1 -1
  96. package/batteries.mjs +1 -1
  97. package/chunk-KmRHZBOW.js +35 -0
  98. package/{common-aFmr9Oqs.mjs → common-DeZaonK1.mjs} +10 -76
  99. package/common-DeZaonK1.mjs.map +1 -0
  100. package/{common-BJ6V6dsH.js → common-Od8edUXU.js} +12 -89
  101. package/common-Od8edUXU.js.map +1 -0
  102. package/common.cjs +7 -9
  103. package/common.d.ts +0 -8
  104. package/common.mjs +7 -7
  105. package/{dispatch_runner-OimGCkk7.mjs → dispatch_runner-9j6bXHL3.mjs} +2 -34
  106. package/dispatch_runner-9j6bXHL3.mjs.map +1 -0
  107. package/{dispatch_runner-BWYNxmnp.js → dispatch_runner-CsoH0nld.js} +6 -37
  108. package/dispatch_runner-CsoH0nld.js.map +1 -0
  109. package/dispatch_runner.cjs +1 -1
  110. package/dispatch_runner.mjs +1 -1
  111. package/{exceptions-CSqzbL1N.js → exceptions-D5YrO9Vm.js} +2 -2
  112. package/{exceptions-CSqzbL1N.js.map → exceptions-D5YrO9Vm.js.map} +1 -1
  113. package/exceptions.cjs +2 -2
  114. package/factories.cjs +1 -1
  115. package/forge.cjs +4 -4
  116. package/forge.mjs +3 -3
  117. package/guards.cjs +9 -9
  118. package/guards.mjs +7 -7
  119. package/index.cjs +13 -13
  120. package/index.cjs.map +1 -1
  121. package/index.d.ts +1 -1
  122. package/index.mjs +10 -10
  123. package/index.mjs.map +1 -1
  124. package/lib/classes/retrievable.d.ts +4 -47
  125. package/lib/contracts/dispatch_context.d.ts +0 -44
  126. package/lib/contracts/turn_runner_config.d.ts +1 -5
  127. package/lib/contracts/turn_runner_context.d.ts +0 -25
  128. package/package.json +74 -74
  129. package/{runtime-BUDWyd-R.js → runtime-BJVkrGQe.js} +2 -2
  130. package/{runtime-BUDWyd-R.js.map → runtime-BJVkrGQe.js.map} +1 -1
  131. package/skills/adk-assembly/SKILL.md +2 -2
  132. package/{spooled_artifact-B_tVDDdB.mjs → spooled_artifact-C5ZtGxuJ.mjs} +2 -2
  133. package/{spooled_artifact-B_tVDDdB.mjs.map → spooled_artifact-C5ZtGxuJ.mjs.map} +1 -1
  134. package/{spooled_artifact-CFstzlqX.js → spooled_artifact-Cm9Te22K.js} +6 -5
  135. package/{spooled_artifact-CFstzlqX.js.map → spooled_artifact-Cm9Te22K.js.map} +1 -1
  136. package/spooled_artifact.cjs +2 -2
  137. package/spooled_artifact.mjs +2 -2
  138. package/{spooled_markdown_artifact-DWWak35I.mjs → spooled_markdown_artifact-BpUJol0W.mjs} +2 -2
  139. package/{spooled_markdown_artifact-DWWak35I.mjs.map → spooled_markdown_artifact-BpUJol0W.mjs.map} +1 -1
  140. package/{spooled_markdown_artifact-DK-T8Hy6.js → spooled_markdown_artifact-RRB113sy.js} +7 -6
  141. package/{spooled_markdown_artifact-DK-T8Hy6.js.map → spooled_markdown_artifact-RRB113sy.js.map} +1 -1
  142. package/{thought-DDqjQu3m.mjs → thought-CDb457b4.mjs} +2 -2
  143. package/{thought-DDqjQu3m.mjs.map → thought-CDb457b4.mjs.map} +1 -1
  144. package/{thought-DTsFRGdE.js → thought-DuN2PgdO.js} +6 -5
  145. package/{thought-DTsFRGdE.js.map → thought-DuN2PgdO.js.map} +1 -1
  146. package/{tool-cwJyEHI9.js → tool-COSeH8I6.js} +5 -4
  147. package/{tool-cwJyEHI9.js.map → tool-COSeH8I6.js.map} +1 -1
  148. package/{tool-q4LskG7K.mjs → tool-D2WB1EA1.mjs} +1 -1
  149. package/{tool-q4LskG7K.mjs.map → tool-D2WB1EA1.mjs.map} +1 -1
  150. package/{tool_call-BKIdAAoY.mjs → tool_call-BKyyxGaZ.mjs} +2 -2
  151. package/{tool_call-BKIdAAoY.mjs.map → tool_call-BKyyxGaZ.mjs.map} +1 -1
  152. package/{tool_call-3T0xTXlD.js → tool_call-DFgzcVcU.js} +6 -5
  153. package/{tool_call-3T0xTXlD.js.map → tool_call-DFgzcVcU.js.map} +1 -1
  154. package/{tool_registry-snPjF0zJ.js → tool_registry-Dkfprsck.js} +5 -39
  155. package/{tool_registry-snPjF0zJ.js.map → tool_registry-Dkfprsck.js.map} +1 -1
  156. package/{turn_runner-BScT8OgA.js → turn_runner-CMm2BHdX.js} +7 -10
  157. package/turn_runner-CMm2BHdX.js.map +1 -0
  158. package/{turn_runner-DRBLN2Y_.mjs → turn_runner-y7eyEcJH.mjs} +3 -7
  159. package/turn_runner-y7eyEcJH.mjs.map +1 -0
  160. package/turn_runner.cjs +1 -1
  161. package/turn_runner.mjs +1 -1
  162. package/types.d.ts +2 -2
  163. package/CHANGELOG.md +0 -49
  164. package/common-BJ6V6dsH.js.map +0 -1
  165. package/common-aFmr9Oqs.mjs.map +0 -1
  166. package/dispatch_runner-BWYNxmnp.js.map +0 -1
  167. package/dispatch_runner-OimGCkk7.mjs.map +0 -1
  168. package/lib/contracts/byte_store.d.ts +0 -93
  169. package/turn_runner-BScT8OgA.js.map +0 -1
  170. package/turn_runner-DRBLN2Y_.mjs.map +0 -1
@@ -1,38 +1,11 @@
1
- import { s as isInstanceOf } from "../../tool_registry-DqLOyGyG.mjs";
2
- import "../../guards.mjs";
3
1
  //#region src/batteries/storage/in_memory/index.ts
4
2
  /**
5
- * In-memory spool readers and stores for tests, scripts, and non-durable prototypes.
6
- *
7
- * @module @nhtio/adk/batteries/storage/in_memory
3
+ * Sync in-memory {@link @nhtio/adk!SpoolReader} over a `string` body.
8
4
  *
9
5
  * @remarks
10
- * Opt-in in-memory persistence battery. Provides {@link InMemorySpoolReader} (a sync
11
- * {@link @nhtio/adk!SpoolReader} over a string) plus {@link InMemorySpoolStore} (a `Map<callId, bytes>`
12
- * with a `write()` method that returns a fresh reader bound to the stored bytes).
13
- *
14
- * Use this when:
15
- *
16
- * - Writing unit or functional tests that need a real `SpoolReader` over known bytes.
17
- * - Running a REPL or one-shot script where persistence beyond the process lifetime is not
18
- * needed.
19
- * - Prototyping an agent before deciding on a real disk/object-store-backed persistence layer.
20
- *
21
- * Do **not** use this for production agents that need durability across process restarts —
22
- * everything lives in process memory and is lost on exit.
23
- */
24
- /**
25
- * Sync in-memory {@link @nhtio/adk!SpoolReader} over a byte-faithful `Uint8Array` body.
26
- *
27
- * @remarks
28
- * Stores the raw bytes and decodes them as UTF-8 once at construction, then splits the decoded
29
- * string on `\n` and caches the resulting line array. All four `SpoolReader` methods resolve
30
- * synchronously from the cache — no I/O happens after construction. `byteLength()` reports the
31
- * true stored byte count (not the decoded character count), so it stays correct for multi-byte
32
- * content; `line()`/`readAll()` operate on the decoded text.
33
- *
34
- * The reader accepts a `string` or a `Uint8Array`. A `string` is encoded as UTF-8 for the byte
35
- * count; a `Uint8Array` is held byte-faithfully (no lossy re-encode) and decoded for text reads.
6
+ * Splits the supplied content on `\n` at construction time and caches the resulting line array
7
+ * plus the UTF-8 byte length. All three `SpoolReader` methods resolve synchronously from the
8
+ * cache no I/O happens after construction.
36
9
  *
37
10
  * Empty input yields a reader with `lineCount() === 0` and `byteLength() === 0`. A trailing
38
11
  * newline produces a final empty line: `"a\nb\n".split('\n') === ['a', 'b', '']`. This matches
@@ -44,14 +17,9 @@ var InMemorySpoolReader = class {
44
17
  #lines;
45
18
  #bytes;
46
19
  constructor(content) {
47
- if (typeof content === "string") {
48
- this.#content = content;
49
- this.#bytes = new TextEncoder().encode(content).length;
50
- } else {
51
- this.#content = new TextDecoder().decode(content);
52
- this.#bytes = content.byteLength;
53
- }
54
- this.#lines = this.#content === "" ? [] : this.#content.split("\n");
20
+ this.#content = content;
21
+ this.#lines = content === "" ? [] : content.split("\n");
22
+ this.#bytes = new TextEncoder().encode(content).length;
55
23
  }
56
24
  line(index) {
57
25
  return this.#lines[index];
@@ -67,72 +35,45 @@ var InMemorySpoolReader = class {
67
35
  }
68
36
  };
69
37
  /**
70
- * Drains a `ReadableStream<Uint8Array>` into a single concatenated `Uint8Array`.
71
- *
72
- * @remarks
73
- * In-memory storage cannot stream-to-disk, so a stream input is buffered fully — the documented
74
- * trade-off for {@link InMemorySpoolStore}. Use {@link @nhtio/adk/batteries/storage/opfs!OpfsSpoolStore}
75
- * or a Flydrive-backed store when true streaming persistence is required.
76
- */
77
- var drainStream = async (stream) => {
78
- const chunks = [];
79
- let total = 0;
80
- const reader = stream.getReader();
81
- try {
82
- for (;;) {
83
- const { done, value } = await reader.read();
84
- if (done) break;
85
- if (value) {
86
- chunks.push(value);
87
- total += value.byteLength;
88
- }
89
- }
90
- } finally {
91
- reader.releaseLock();
92
- }
93
- const out = new Uint8Array(total);
94
- let offset = 0;
95
- for (const chunk of chunks) {
96
- out.set(chunk, offset);
97
- offset += chunk.byteLength;
98
- }
99
- return out;
100
- };
101
- /**
102
38
  * In-memory "give bytes, get a reader" persistence layer keyed by `callId`.
103
39
  *
104
40
  * @remarks
105
- * Stores each value byte-faithfully as a `Uint8Array`. `string` inputs are encoded as UTF-8;
106
- * `Uint8Array` inputs are held verbatim (no lossy text round-trip, so binary payloads survive
107
- * intact); `ReadableStream<Uint8Array>` inputs are drained fully into a buffer — in-memory storage
108
- * cannot stream to disk, so the stream form resolves asynchronously and is the documented
109
- * trade-off for this battery.
41
+ * Stores the canonical UTF-8 string form of each value. `Uint8Array` inputs are decoded via
42
+ * `TextDecoder` once at write time — subsequent `read()` calls return a reader over the cached
43
+ * string with no further decoding.
110
44
  *
111
45
  * Each `write()` and each `read()` returns a *fresh* {@link InMemorySpoolReader} — the store
112
46
  * owns the bytes, the reader is a view. Mutating the store after handing out a reader does not
113
47
  * invalidate the reader.
114
48
  *
115
- * Implements {@link @nhtio/adk!SpoolStore} (i.e. `ByteStore<SpoolReader>`).
116
- *
117
49
  * @example
118
50
  * ```ts
119
51
  * const store = new InMemorySpoolStore()
120
52
  * const bytes = await tool.executor(ctx)(args)
121
- * const reader = await store.write(callId, bytes)
53
+ * const reader = store.write(callId, bytes)
122
54
  * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact
123
55
  * const artifact = new Ctor(reader)
124
56
  * ```
125
57
  */
126
58
  var InMemorySpoolStore = class {
127
59
  #entries = /* @__PURE__ */ new Map();
60
+ #decoder = new TextDecoder();
61
+ /**
62
+ * Persists `bytes` under `callId` and returns a reader over them.
63
+ *
64
+ * @remarks
65
+ * `Uint8Array` inputs are decoded as UTF-8. Re-writing the same `callId` replaces the prior
66
+ * entry; readers handed out before the rewrite continue to view the old bytes (they hold their
67
+ * own snapshot via the `InMemorySpoolReader` constructor).
68
+ *
69
+ * @param callId - Identifier used to retrieve the bytes via {@link InMemorySpoolStore.read}.
70
+ * @param bytes - The bytes to store, as a `string` or `Uint8Array`.
71
+ * @returns A fresh {@link InMemorySpoolReader} bound to the stored bytes.
72
+ */
128
73
  write(callId, bytes) {
129
- if (isInstanceOf(bytes, "ReadableStream", ReadableStream)) return drainStream(bytes).then((buffer) => {
130
- this.#entries.set(callId, buffer);
131
- return new InMemorySpoolReader(buffer);
132
- });
133
- const buffer = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
134
- this.#entries.set(callId, buffer);
135
- return new InMemorySpoolReader(buffer);
74
+ const text = typeof bytes === "string" ? bytes : this.#decoder.decode(bytes);
75
+ this.#entries.set(callId, text);
76
+ return new InMemorySpoolReader(text);
136
77
  }
137
78
  /**
138
79
  * Returns a reader over the bytes previously written under `callId`, or `undefined` if the
@@ -142,9 +83,9 @@ var InMemorySpoolStore = class {
142
83
  * @returns A fresh {@link InMemorySpoolReader} bound to the stored bytes, or `undefined`.
143
84
  */
144
85
  read(callId) {
145
- const buffer = this.#entries.get(callId);
146
- if (buffer === void 0) return void 0;
147
- return new InMemorySpoolReader(buffer);
86
+ const text = this.#entries.get(callId);
87
+ if (text === void 0) return void 0;
88
+ return new InMemorySpoolReader(text);
148
89
  }
149
90
  /**
150
91
  * Removes the entry under `callId`.
@@ -1 +1 @@
1
- {"version":3,"file":"in_memory.mjs","names":["#content","#lines","#bytes","#entries"],"sources":["../../../src/batteries/storage/in_memory/index.ts"],"sourcesContent":["/**\n * In-memory spool readers and stores for tests, scripts, and non-durable prototypes.\n *\n * @module @nhtio/adk/batteries/storage/in_memory\n *\n * @remarks\n * Opt-in in-memory persistence battery. Provides {@link InMemorySpoolReader} (a sync\n * {@link @nhtio/adk!SpoolReader} over a string) plus {@link InMemorySpoolStore} (a `Map<callId, bytes>`\n * with a `write()` method that returns a fresh reader bound to the stored bytes).\n *\n * Use this when:\n *\n * - Writing unit or functional tests that need a real `SpoolReader` over known bytes.\n * - Running a REPL or one-shot script where persistence beyond the process lifetime is not\n * needed.\n * - Prototyping an agent before deciding on a real disk/object-store-backed persistence layer.\n *\n * Do **not** use this for production agents that need durability across process restarts —\n * everything lives in process memory and is lost on exit.\n */\n\nimport { isInstanceOf } from '@nhtio/adk/guards'\nimport type { SpoolReader, SpoolStore } from '@nhtio/adk/common'\n\n/**\n * Sync in-memory {@link @nhtio/adk!SpoolReader} over a byte-faithful `Uint8Array` body.\n *\n * @remarks\n * Stores the raw bytes and decodes them as UTF-8 once at construction, then splits the decoded\n * string on `\\n` and caches the resulting line array. All four `SpoolReader` methods resolve\n * synchronously from the cache — no I/O happens after construction. `byteLength()` reports the\n * true stored byte count (not the decoded character count), so it stays correct for multi-byte\n * content; `line()`/`readAll()` operate on the decoded text.\n *\n * The reader accepts a `string` or a `Uint8Array`. A `string` is encoded as UTF-8 for the byte\n * count; a `Uint8Array` is held byte-faithfully (no lossy re-encode) and decoded for text reads.\n *\n * Empty input yields a reader with `lineCount() === 0` and `byteLength() === 0`. A trailing\n * newline produces a final empty line: `\"a\\nb\\n\".split('\\n') === ['a', 'b', '']`. This matches\n * the JavaScript `String.prototype.split` contract and lets a `lineCount()` consumer\n * distinguish \"two lines, no trailing newline\" from \"two lines, trailing newline\".\n */\nexport class InMemorySpoolReader implements SpoolReader {\n readonly #content: string\n readonly #lines: string[]\n readonly #bytes: number\n\n constructor(content: string | Uint8Array) {\n if (typeof content === 'string') {\n this.#content = content\n this.#bytes = new TextEncoder().encode(content).length\n } else {\n this.#content = new TextDecoder().decode(content)\n this.#bytes = content.byteLength\n }\n this.#lines = this.#content === '' ? [] : this.#content.split('\\n')\n }\n\n line(index: number): string | undefined {\n return this.#lines[index]\n }\n\n byteLength(): number {\n return this.#bytes\n }\n\n lineCount(): number {\n return this.#lines.length\n }\n\n readAll(): string {\n return this.#content\n }\n}\n\n/**\n * Drains a `ReadableStream<Uint8Array>` into a single concatenated `Uint8Array`.\n *\n * @remarks\n * In-memory storage cannot stream-to-disk, so a stream input is buffered fully — the documented\n * trade-off for {@link InMemorySpoolStore}. Use {@link @nhtio/adk/batteries/storage/opfs!OpfsSpoolStore}\n * or a Flydrive-backed store when true streaming persistence is required.\n */\nconst drainStream = async (stream: ReadableStream<Uint8Array>): Promise<Uint8Array> => {\n const chunks: Uint8Array[] = []\n let total = 0\n const reader = stream.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value) {\n chunks.push(value)\n total += value.byteLength\n }\n }\n } finally {\n reader.releaseLock()\n }\n const out = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n out.set(chunk, offset)\n offset += chunk.byteLength\n }\n return out\n}\n\n/**\n * In-memory \"give bytes, get a reader\" persistence layer keyed by `callId`.\n *\n * @remarks\n * Stores each value byte-faithfully as a `Uint8Array`. `string` inputs are encoded as UTF-8;\n * `Uint8Array` inputs are held verbatim (no lossy text round-trip, so binary payloads survive\n * intact); `ReadableStream<Uint8Array>` inputs are drained fully into a buffer in-memory storage\n * cannot stream to disk, so the stream form resolves asynchronously and is the documented\n * trade-off for this battery.\n *\n * Each `write()` and each `read()` returns a *fresh* {@link InMemorySpoolReader} — the store\n * owns the bytes, the reader is a view. Mutating the store after handing out a reader does not\n * invalidate the reader.\n *\n * Implements {@link @nhtio/adk!SpoolStore} (i.e. `ByteStore<SpoolReader>`).\n *\n * @example\n * ```ts\n * const store = new InMemorySpoolStore()\n * const bytes = await tool.executor(ctx)(args)\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class InMemorySpoolStore implements SpoolStore {\n readonly #entries = new Map<string, Uint8Array>()\n\n /**\n * Persists `bytes` under `callId` and returns a reader over them.\n *\n * @remarks\n * `string` input is encoded as UTF-8; `Uint8Array` is stored byte-faithfully;\n * `ReadableStream<Uint8Array>` is drained fully (and `write` returns a `Promise`). Re-writing the\n * same `callId` replaces the prior entry; readers handed out before the rewrite continue to view\n * the old bytes (they hold their own snapshot via the `InMemorySpoolReader` constructor).\n *\n * @param callId - Identifier used to retrieve the bytes via {@link InMemorySpoolStore.read}.\n * @param bytes - The bytes to store, as a `string`, `Uint8Array`, or `ReadableStream<Uint8Array>`.\n * @returns A fresh {@link InMemorySpoolReader} bound to the stored bytes — a `Promise` for stream\n * input, synchronous otherwise.\n */\n write(callId: string, bytes: string): InMemorySpoolReader\n write(callId: string, bytes: Uint8Array): InMemorySpoolReader\n write(callId: string, bytes: ReadableStream<Uint8Array>): Promise<InMemorySpoolReader>\n write(\n callId: string,\n bytes: string | Uint8Array | ReadableStream<Uint8Array>\n ): InMemorySpoolReader | Promise<InMemorySpoolReader>\n write(\n callId: string,\n bytes: string | Uint8Array | ReadableStream<Uint8Array>\n ): InMemorySpoolReader | Promise<InMemorySpoolReader> {\n if (isInstanceOf(bytes, 'ReadableStream', ReadableStream)) {\n return drainStream(bytes).then((buffer) => {\n this.#entries.set(callId, buffer)\n return new InMemorySpoolReader(buffer)\n })\n }\n const buffer = typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes\n this.#entries.set(callId, buffer)\n return new InMemorySpoolReader(buffer)\n }\n\n /**\n * Returns a reader over the bytes previously written under `callId`, or `undefined` if the\n * entry has not been written or has been deleted.\n *\n * @param callId - Identifier supplied to a prior {@link InMemorySpoolStore.write} call.\n * @returns A fresh {@link InMemorySpoolReader} bound to the stored bytes, or `undefined`.\n */\n read(callId: string): InMemorySpoolReader | undefined {\n const buffer = this.#entries.get(callId)\n if (buffer === undefined) return undefined\n return new InMemorySpoolReader(buffer)\n }\n\n /**\n * Removes the entry under `callId`.\n *\n * @param callId - Identifier whose entry should be removed.\n * @returns `true` if an entry existed and was removed; `false` otherwise.\n */\n delete(callId: string): boolean {\n return this.#entries.delete(callId)\n }\n\n /**\n * Removes every entry from the store.\n *\n * @remarks\n * Existing readers handed out by prior `write()` / `read()` calls remain valid — they hold\n * their own snapshot.\n */\n clear(): void {\n this.#entries.clear()\n }\n\n /**\n * Returns the number of entries currently in the store.\n */\n get size(): number {\n return this.#entries.size\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAa,sBAAb,MAAwD;CACtD;CACA;CACA;CAEA,YAAY,SAA8B;EACxC,IAAI,OAAO,YAAY,UAAU;GAC/B,KAAKA,WAAW;GAChB,KAAKE,SAAS,IAAI,YAAY,EAAE,OAAO,OAAO,EAAE;EAClD,OAAO;GACL,KAAKF,WAAW,IAAI,YAAY,EAAE,OAAO,OAAO;GAChD,KAAKE,SAAS,QAAQ;EACxB;EACA,KAAKD,SAAS,KAAKD,aAAa,KAAK,CAAC,IAAI,KAAKA,SAAS,MAAM,IAAI;CACpE;CAEA,KAAK,OAAmC;EACtC,OAAO,KAAKC,OAAO;CACrB;CAEA,aAAqB;EACnB,OAAO,KAAKC;CACd;CAEA,YAAoB;EAClB,OAAO,KAAKD,OAAO;CACrB;CAEA,UAAkB;EAChB,OAAO,KAAKD;CACd;AACF;;;;;;;;;AAUA,IAAM,cAAc,OAAO,WAA4D;CACrF,MAAM,SAAuB,CAAC;CAC9B,IAAI,QAAQ;CACZ,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI;EACF,SAAS;GACP,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;GACV,IAAI,OAAO;IACT,OAAO,KAAK,KAAK;IACjB,SAAS,MAAM;GACjB;EACF;CACF,UAAU;EACR,OAAO,YAAY;CACrB;CACA,MAAM,MAAM,IAAI,WAAW,KAAK;CAChC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,IAAI,OAAO,MAAM;EACrB,UAAU,MAAM;CAClB;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,IAAa,qBAAb,MAAsD;CACpD,2BAAoB,IAAI,IAAwB;CAuBhD,MACE,QACA,OACoD;EACpD,IAAI,aAAa,OAAO,kBAAkB,cAAc,GACtD,OAAO,YAAY,KAAK,EAAE,MAAM,WAAW;GACzC,KAAKG,SAAS,IAAI,QAAQ,MAAM;GAChC,OAAO,IAAI,oBAAoB,MAAM;EACvC,CAAC;EAEH,MAAM,SAAS,OAAO,UAAU,WAAW,IAAI,YAAY,EAAE,OAAO,KAAK,IAAI;EAC7E,KAAKA,SAAS,IAAI,QAAQ,MAAM;EAChC,OAAO,IAAI,oBAAoB,MAAM;CACvC;;;;;;;;CASA,KAAK,QAAiD;EACpD,MAAM,SAAS,KAAKA,SAAS,IAAI,MAAM;EACvC,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;EACjC,OAAO,IAAI,oBAAoB,MAAM;CACvC;;;;;;;CAQA,OAAO,QAAyB;EAC9B,OAAO,KAAKA,SAAS,OAAO,MAAM;CACpC;;;;;;;;CASA,QAAc;EACZ,KAAKA,SAAS,MAAM;CACtB;;;;CAKA,IAAI,OAAe;EACjB,OAAO,KAAKA,SAAS;CACvB;AACF"}
1
+ {"version":3,"file":"in_memory.mjs","names":["#content","#lines","#bytes","#entries","#decoder"],"sources":["../../../src/batteries/storage/in_memory/index.ts"],"sourcesContent":["/**\n * In-memory spool readers and stores for tests, scripts, and non-durable prototypes.\n *\n * @module @nhtio/adk/batteries/storage/in_memory\n *\n * @remarks\n * Opt-in in-memory persistence battery. Provides {@link InMemorySpoolReader} (a sync\n * {@link @nhtio/adk!SpoolReader} over a string) plus {@link InMemorySpoolStore} (a `Map<callId, bytes>`\n * with a `write()` method that returns a fresh reader bound to the stored bytes).\n *\n * Use this when:\n *\n * - Writing unit or functional tests that need a real `SpoolReader` over known bytes.\n * - Running a REPL or one-shot script where persistence beyond the process lifetime is not\n * needed.\n * - Prototyping an agent before deciding on a real disk/object-store-backed persistence layer.\n *\n * Do **not** use this for production agents that need durability across process restarts —\n * everything lives in process memory and is lost on exit.\n */\n\nimport type { SpoolReader } from '@nhtio/adk/common'\n\n/**\n * Sync in-memory {@link @nhtio/adk!SpoolReader} over a `string` body.\n *\n * @remarks\n * Splits the supplied content on `\\n` at construction time and caches the resulting line array\n * plus the UTF-8 byte length. All three `SpoolReader` methods resolve synchronously from the\n * cache — no I/O happens after construction.\n *\n * Empty input yields a reader with `lineCount() === 0` and `byteLength() === 0`. A trailing\n * newline produces a final empty line: `\"a\\nb\\n\".split('\\n') === ['a', 'b', '']`. This matches\n * the JavaScript `String.prototype.split` contract and lets a `lineCount()` consumer\n * distinguish \"two lines, no trailing newline\" from \"two lines, trailing newline\".\n */\nexport class InMemorySpoolReader implements SpoolReader {\n readonly #content: string\n readonly #lines: string[]\n readonly #bytes: number\n\n constructor(content: string) {\n this.#content = content\n this.#lines = content === '' ? [] : content.split('\\n')\n this.#bytes = new TextEncoder().encode(content).length\n }\n\n line(index: number): string | undefined {\n return this.#lines[index]\n }\n\n byteLength(): number {\n return this.#bytes\n }\n\n lineCount(): number {\n return this.#lines.length\n }\n\n readAll(): string {\n return this.#content\n }\n}\n\n/**\n * In-memory \"give bytes, get a reader\" persistence layer keyed by `callId`.\n *\n * @remarks\n * Stores the canonical UTF-8 string form of each value. `Uint8Array` inputs are decoded via\n * `TextDecoder` once at write timesubsequent `read()` calls return a reader over the cached\n * string with no further decoding.\n *\n * Each `write()` and each `read()` returns a *fresh* {@link InMemorySpoolReader} — the store\n * owns the bytes, the reader is a view. Mutating the store after handing out a reader does not\n * invalidate the reader.\n *\n * @example\n * ```ts\n * const store = new InMemorySpoolStore()\n * const bytes = await tool.executor(ctx)(args)\n * const reader = store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class InMemorySpoolStore {\n readonly #entries = new Map<string, string>()\n readonly #decoder = new TextDecoder()\n\n /**\n * Persists `bytes` under `callId` and returns a reader over them.\n *\n * @remarks\n * `Uint8Array` inputs are decoded as UTF-8. Re-writing the same `callId` replaces the prior\n * entry; readers handed out before the rewrite continue to view the old bytes (they hold their\n * own snapshot via the `InMemorySpoolReader` constructor).\n *\n * @param callId - Identifier used to retrieve the bytes via {@link InMemorySpoolStore.read}.\n * @param bytes - The bytes to store, as a `string` or `Uint8Array`.\n * @returns A fresh {@link InMemorySpoolReader} bound to the stored bytes.\n */\n write(callId: string, bytes: string | Uint8Array): InMemorySpoolReader {\n const text = typeof bytes === 'string' ? bytes : this.#decoder.decode(bytes)\n this.#entries.set(callId, text)\n return new InMemorySpoolReader(text)\n }\n\n /**\n * Returns a reader over the bytes previously written under `callId`, or `undefined` if the\n * entry has not been written or has been deleted.\n *\n * @param callId - Identifier supplied to a prior {@link InMemorySpoolStore.write} call.\n * @returns A fresh {@link InMemorySpoolReader} bound to the stored bytes, or `undefined`.\n */\n read(callId: string): InMemorySpoolReader | undefined {\n const text = this.#entries.get(callId)\n if (text === undefined) return undefined\n return new InMemorySpoolReader(text)\n }\n\n /**\n * Removes the entry under `callId`.\n *\n * @param callId - Identifier whose entry should be removed.\n * @returns `true` if an entry existed and was removed; `false` otherwise.\n */\n delete(callId: string): boolean {\n return this.#entries.delete(callId)\n }\n\n /**\n * Removes every entry from the store.\n *\n * @remarks\n * Existing readers handed out by prior `write()` / `read()` calls remain valid — they hold\n * their own snapshot.\n */\n clear(): void {\n this.#entries.clear()\n }\n\n /**\n * Returns the number of entries currently in the store.\n */\n get size(): number {\n return this.#entries.size\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAoCA,IAAa,sBAAb,MAAwD;CACtD;CACA;CACA;CAEA,YAAY,SAAiB;EAC3B,KAAKA,WAAW;EAChB,KAAKC,SAAS,YAAY,KAAK,CAAC,IAAI,QAAQ,MAAM,IAAI;EACtD,KAAKC,SAAS,IAAI,YAAY,EAAE,OAAO,OAAO,EAAE;CAClD;CAEA,KAAK,OAAmC;EACtC,OAAO,KAAKD,OAAO;CACrB;CAEA,aAAqB;EACnB,OAAO,KAAKC;CACd;CAEA,YAAoB;EAClB,OAAO,KAAKD,OAAO;CACrB;CAEA,UAAkB;EAChB,OAAO,KAAKD;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,qBAAb,MAAgC;CAC9B,2BAAoB,IAAI,IAAoB;CAC5C,WAAoB,IAAI,YAAY;;;;;;;;;;;;;CAcpC,MAAM,QAAgB,OAAiD;EACrE,MAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAKI,SAAS,OAAO,KAAK;EAC3E,KAAKD,SAAS,IAAI,QAAQ,IAAI;EAC9B,OAAO,IAAI,oBAAoB,IAAI;CACrC;;;;;;;;CASA,KAAK,QAAiD;EACpD,MAAM,OAAO,KAAKA,SAAS,IAAI,MAAM;EACrC,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;EAC/B,OAAO,IAAI,oBAAoB,IAAI;CACrC;;;;;;;CAQA,OAAO,QAAyB;EAC9B,OAAO,KAAKA,SAAS,OAAO,MAAM;CACpC;;;;;;;;CASA,QAAc;EACZ,KAAKA,SAAS,MAAM;CACtB;;;;CAKA,IAAI,OAAe;EACjB,OAAO,KAAKA,SAAS;CACvB;AACF"}
@@ -45,7 +45,7 @@
45
45
  * const artifact = new Ctor(reader)
46
46
  * ```
47
47
  */
48
- import type { SpoolReader, SpoolStore } from "../../../common";
48
+ import type { SpoolReader } from "../../../common";
49
49
  /**
50
50
  * Minimal subset of the
51
51
  * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)
@@ -241,7 +241,7 @@ export interface OpfsSpoolStoreOptions {
241
241
  * const artifact = new Ctor(reader)
242
242
  * ```
243
243
  */
244
- export declare class OpfsSpoolStore implements SpoolStore {
244
+ export declare class OpfsSpoolStore {
245
245
  #private;
246
246
  constructor(opts?: OpfsSpoolStoreOptions);
247
247
  /**
@@ -257,18 +257,12 @@ export declare class OpfsSpoolStore implements SpoolStore {
257
257
  /**
258
258
  * Persists `bytes` under `callId` and returns a reader bound to the stored key.
259
259
  *
260
- * @remarks
261
- * `string` input is encoded as UTF-8; `Uint8Array` is stored byte-faithfully;
262
- * `ReadableStream<Uint8Array>` is written incrementally — the stream is consumed chunk-by-chunk
263
- * straight to OPFS without first materializing the whole payload in memory, which is the point
264
- * of accepting a stream for a durable store.
265
- *
266
260
  * @param callId - Identifier used to retrieve the bytes via {@link OpfsSpoolStore.read}.
267
- * @param bytes - The bytes to store, as a `string`, `Uint8Array`, or `ReadableStream<Uint8Array>`.
261
+ * @param bytes - The bytes to store, as a `string` or `Uint8Array`.
268
262
  * @param opts - Per-call override for `streamThresholdBytes`.
269
263
  * @returns An {@link OpfsSpoolReader} over the stored bytes.
270
264
  */
271
- write(callId: string, bytes: string | Uint8Array | ReadableStream<Uint8Array>, opts?: OpfsSpoolReaderOptions): Promise<OpfsSpoolReader>;
265
+ write(callId: string, bytes: string | Uint8Array, opts?: OpfsSpoolReaderOptions): Promise<OpfsSpoolReader>;
272
266
  /**
273
267
  * Returns a reader over the bytes previously written under `callId`.
274
268
  *
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_tool_registry = require("../../tool_registry-snPjF0zJ.js");
2
+ const require_tool_registry = require("../../tool_registry-Dkfprsck.js");
3
3
  require("../../guards.cjs");
4
4
  //#region src/batteries/storage/opfs/index.ts
5
5
  /**
@@ -252,27 +252,17 @@ var OpfsSpoolStore = class OpfsSpoolStore {
252
252
  /**
253
253
  * Persists `bytes` under `callId` and returns a reader bound to the stored key.
254
254
  *
255
- * @remarks
256
- * `string` input is encoded as UTF-8; `Uint8Array` is stored byte-faithfully;
257
- * `ReadableStream<Uint8Array>` is written incrementally — the stream is consumed chunk-by-chunk
258
- * straight to OPFS without first materializing the whole payload in memory, which is the point
259
- * of accepting a stream for a durable store.
260
- *
261
255
  * @param callId - Identifier used to retrieve the bytes via {@link OpfsSpoolStore.read}.
262
- * @param bytes - The bytes to store, as a `string`, `Uint8Array`, or `ReadableStream<Uint8Array>`.
256
+ * @param bytes - The bytes to store, as a `string` or `Uint8Array`.
263
257
  * @param opts - Per-call override for `streamThresholdBytes`.
264
258
  * @returns An {@link OpfsSpoolReader} over the stored bytes.
265
259
  */
266
260
  async write(callId, bytes, opts) {
267
261
  const name = this.#keyFor(callId);
268
262
  const handle = await (await this.#getRoot()).getFileHandle(name, { create: true });
269
- if (require_tool_registry.isInstanceOf(bytes, "ReadableStream", ReadableStream)) if (isWorkerScope()) await this.#writeStreamViaSyncHandle(handle, bytes);
270
- else await this.#writeStreamViaWritable(handle, bytes);
271
- else {
272
- const payload = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
273
- if (isWorkerScope()) await this.#writeViaSyncHandle(handle, payload);
274
- else await this.#writeViaWritable(handle, payload);
275
- }
263
+ const payload = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
264
+ if (isWorkerScope()) await this.#writeViaSyncHandle(handle, payload);
265
+ else await this.#writeViaWritable(handle, payload);
276
266
  return new OpfsSpoolReader(handle, { streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold });
277
267
  }
278
268
  /**
@@ -366,46 +356,6 @@ var OpfsSpoolStore = class OpfsSpoolStore {
366
356
  await writable.close();
367
357
  }
368
358
  }
369
- async #writeStreamViaWritable(handle, stream) {
370
- const writable = await handle.createWritable();
371
- try {
372
- const reader = stream.getReader();
373
- try {
374
- for (;;) {
375
- const { done, value } = await reader.read();
376
- if (done) break;
377
- if (value) await writable.write(value);
378
- }
379
- } finally {
380
- reader.releaseLock();
381
- }
382
- } finally {
383
- await writable.close();
384
- }
385
- }
386
- async #writeStreamViaSyncHandle(handle, stream) {
387
- const sync = await handle.createSyncAccessHandle();
388
- try {
389
- sync.truncate(0);
390
- let at = 0;
391
- const reader = stream.getReader();
392
- try {
393
- for (;;) {
394
- const { done, value } = await reader.read();
395
- if (done) break;
396
- if (value && value.byteLength > 0) {
397
- sync.write(value, { at });
398
- at += value.byteLength;
399
- }
400
- }
401
- } finally {
402
- reader.releaseLock();
403
- }
404
- sync.flush();
405
- } finally {
406
- sync.close();
407
- }
408
- }
409
359
  #isNotFoundError(err) {
410
360
  if (err === null || typeof err !== "object") return false;
411
361
  return err.name === "NotFoundError";
@@ -1 +1 @@
1
- {"version":3,"file":"opfs.cjs","names":["#handle","#threshold","#load","#readRange","#ready","#init","#buildStreamingIndex","#resolveRoot","#prefix","#defaultThreshold","#keyFor","#getRoot","#writeStreamViaSyncHandle","#writeStreamViaWritable","#writeViaSyncHandle","#writeViaWritable","#isNotFoundError","#root"],"sources":["../../../src/batteries/storage/opfs/index.ts"],"sourcesContent":["/**\n * Browser-only Origin Private File System storage for spooled artifacts.\n *\n * @module @nhtio/adk/batteries/storage/opfs\n *\n * @remarks\n * Opt-in **browser-only** storage battery backed by the\n * [Origin Private File System](https://developer.mozilla.org/docs/Web/API/File_System_API/Origin_private_file_system)\n * (OPFS). Provides {@link OpfsSpoolReader} (a {@link @nhtio/adk!SpoolReader} over a `OpfsFileHandle`)\n * and {@link OpfsSpoolStore} (a `write(callId, bytes) → reader` persistence layer that wraps an\n * OPFS directory).\n *\n * The reader has two modes selected lazily on first method invocation based on the size of the\n * underlying file:\n *\n * - **Eager mode** — when `file.size` is below `streamThresholdBytes` (default 10 MiB), the\n * reader calls `file.text()` once, splits the content on `\\n`, and caches lines + byte count.\n * All subsequent calls resolve from memory.\n * - **Streaming mode** — when `file.size` meets or exceeds the threshold, the reader streams the\n * file once via `file.stream().getReader()` to build a line-offset index (`number[]` of byte\n * offsets per line), then serves each `line(i)` request by slicing the underlying `Blob` —\n * `Blob.slice(start, end).text()` decodes only the requested range, no head-of-file scan.\n * Caps RAM at one index + one line buffer regardless of file size.\n *\n * The store auto-selects its write API by execution scope:\n *\n * - In **worker scopes** (`self instanceof WorkerGlobalScope`), it acquires a\n * `FileSystemSyncAccessHandle` and writes synchronously. Sync handles are the only API\n * available in workers and the fastest path for the spool-write hot path.\n * - On the **main thread**, it uses `OpfsFileHandle.createWritable()` and the async\n * stream API. Sync access handles are not exposed on the main thread.\n *\n * This module assumes a browser-equivalent runtime — `navigator.storage`,\n * `OpfsFileHandle`, `TextEncoder`/`TextDecoder`, and `Blob` must all exist. It must not\n * be imported from Node code; do so and you will fail at resolve time when `navigator` is\n * referenced.\n *\n * @example\n * ```ts\n * import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'\n *\n * const store = new OpfsSpoolStore({ keyPrefix: 'agent-runs/' })\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\n\nimport { isInstanceOf } from '@nhtio/adk/guards'\nimport type { SpoolReader, SpoolStore } from '@nhtio/adk/common'\n\n// The project's tsconfig limits `lib` to `ESNext`, so the DOM and File System Access types\n// referenced below are not in scope by default — neither `tsc --noEmit` nor the downstream dts\n// pipeline (api-extractor) can see them. Re-declare here the **minimum** surface this module\n// touches via a local handle-shape interface. Public API uses `OpfsFileHandle` /\n// `OpfsDirectoryHandle` instead of the DOM globals so the published `.d.ts` is self-contained\n// and consumers do not have to chase the lib graph.\n\n/**\n * Minimal subset of the\n * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)\n * `OpfsFileHandle` interface that this module touches at runtime. Structurally compatible\n * with the DOM-lib `OpfsFileHandle` — at call sites you pass real OPFS handles directly.\n */\nexport interface OpfsFileHandle {\n readonly kind: 'file'\n readonly name: string\n getFile(): Promise<OpfsFile>\n createWritable(): Promise<OpfsWritableFileStream>\n}\n\n/**\n * Minimal subset of the\n * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)\n * `OpfsDirectoryHandle` interface that this module touches at runtime. Structurally\n * compatible with the DOM-lib `OpfsDirectoryHandle` — at call sites you pass real OPFS\n * handles directly.\n */\nexport interface OpfsDirectoryHandle {\n readonly kind: 'directory'\n readonly name: string\n getFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileHandle>\n getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectoryHandle>\n removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>\n}\n\n/**\n * Minimal subset of the DOM `FileSystemWritableFileStream` interface used by the OPFS battery's\n * main-thread write path.\n */\nexport interface OpfsWritableFileStream {\n write(data: Uint8Array | ArrayBufferView | ArrayBuffer | string): Promise<void>\n close(): Promise<void>\n}\n\n/**\n * Minimal subset of the DOM `Blob` interface used by {@link OpfsSpoolReader} streaming-mode\n * random-access reads. Real OPFS handles return a `File` here; we narrow to the methods we\n * actually call.\n */\nexport interface OpfsBlob {\n readonly size: number\n slice(start?: number, end?: number, contentType?: string): OpfsBlob\n text(): Promise<string>\n stream(): OpfsReadableStream\n}\n\n/**\n * Minimal subset of the DOM `File` interface used by {@link OpfsSpoolReader}.\n */\nexport interface OpfsFile extends OpfsBlob {\n readonly name: string\n}\n\n/**\n * Minimal subset of the DOM `ReadableStream<Uint8Array>` interface used by streaming-mode\n * index construction.\n */\nexport interface OpfsReadableStream {\n getReader(): OpfsReadableStreamReader\n}\n\n/**\n * Minimal subset of the DOM `ReadableStreamDefaultReader<Uint8Array>` interface used by\n * streaming-mode index construction.\n */\nexport interface OpfsReadableStreamReader {\n read(): Promise<{ done: false; value: Uint8Array } | { done: true; value: undefined }>\n releaseLock(): void\n}\n\ndeclare const navigator: {\n storage: { getDirectory(): Promise<OpfsDirectoryHandle> }\n}\ndeclare class TextEncoder {\n encode(input?: string): Uint8Array\n}\ndeclare const self: unknown\ndeclare const WorkerGlobalScope: { new (): unknown } | undefined\n\ninterface FileSystemSyncAccessHandle {\n truncate(newSize: number): void\n write(buffer: Uint8Array | ArrayBuffer | ArrayBufferView, options?: { at?: number }): number\n flush(): void\n close(): void\n}\ninterface OpfsFileHandleWithSyncAccess extends OpfsFileHandle {\n createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>\n}\n\nconst DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024 // 10 MiB\n\nconst LF = 0x0a // '\\n'\n\nconst isNonNegativeFiniteNumber = (n: unknown): n is number =>\n typeof n === 'number' && Number.isFinite(n) && n >= 0\n\n/**\n * Constructor options for {@link OpfsSpoolReader}.\n */\nexport interface OpfsSpoolReaderOptions {\n /**\n * Byte-length threshold that switches between eager and streaming modes.\n *\n * @remarks\n * - Below the threshold → eager (whole-file in memory).\n * - At or above the threshold → streaming (line-offset index + per-line slice reads).\n *\n * Set to `0` to force streaming mode; set to `Number.POSITIVE_INFINITY` to force eager mode.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\ninterface EagerState {\n mode: 'eager'\n lines: string[]\n bytes: number\n content: string\n}\n\ninterface StreamingState {\n mode: 'streaming'\n file: OpfsFile\n /**\n * Byte offsets where each line *starts*. Length equals lineCount + 1; the final entry equals\n * the total byte length. So `offsets[i + 1] - offsets[i]` is the byte length of line `i`\n * including any trailing `\\n`.\n */\n offsets: number[]\n bytes: number\n}\n\ntype ReaderState = EagerState | StreamingState\n\n/**\n * Returns `true` when the current global scope is a Web Worker (`DedicatedWorkerGlobalScope`,\n * `SharedWorkerGlobalScope`, or `ServiceWorkerGlobalScope` all inherit from `WorkerGlobalScope`).\n *\n * @remarks\n * The check is needed at runtime because `FileSystemSyncAccessHandle` is only exposed in worker\n * scopes — calling it from the main thread throws. We pick the write strategy based on the\n * answer here.\n *\n * @internal\n */\nconst isWorkerScope = (): boolean => {\n if (typeof WorkerGlobalScope === 'undefined') return false\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on `self`; no cross-realm risk\n return self instanceof WorkerGlobalScope\n}\n\n/**\n * Reads an OPFS-backed file as a {@link @nhtio/adk!SpoolReader}.\n *\n * @remarks\n * Constructor is **not** async — but the first method call awaits a private readiness promise\n * that fetches the underlying `File` (and in eager mode, its contents). Subsequent calls reuse\n * the cached state. This keeps construction call sites synchronous while still doing real I/O\n * lazily.\n *\n * All four `SpoolReader` methods on this reader return promises. The `SpoolReader` contract\n * supports both sync and async return; consumers of `SpooledArtifact` handle either.\n */\nexport class OpfsSpoolReader implements SpoolReader {\n readonly #handle: OpfsFileHandle\n readonly #threshold: number\n #ready: Promise<ReaderState> | undefined\n\n constructor(handle: OpfsFileHandle, opts: OpfsSpoolReaderOptions = {}) {\n this.#handle = handle\n const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n // Allow `Infinity` (forces eager) but reject anything non-finite-negative.\n if (typeof raw !== 'number' || Number.isNaN(raw) || raw < 0) {\n throw new TypeError(\n `OpfsSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`\n )\n }\n this.#threshold = raw\n }\n\n /**\n * Returns `true` if `value` is an {@link OpfsSpoolReader} instance.\n *\n * @remarks\n * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety.\n *\n * @param value - The value to test.\n * @returns `true` when `value` is an {@link OpfsSpoolReader} instance.\n */\n public static isOpfsSpoolReader(value: unknown): value is OpfsSpoolReader {\n return isInstanceOf(value, 'OpfsSpoolReader', OpfsSpoolReader)\n }\n\n async line(index: number): Promise<string | undefined> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.lines[index]\n if (index < 0 || index >= state.offsets.length - 1) return undefined\n return this.#readRange(state.file, state.offsets[index], state.offsets[index + 1])\n }\n\n async byteLength(): Promise<number> {\n const state = await this.#load()\n return state.bytes\n }\n\n async lineCount(): Promise<number> {\n const state = await this.#load()\n return state.mode === 'eager' ? state.lines.length : state.offsets.length - 1\n }\n\n /**\n * Returns the full underlying content as a single decoded string, byte-faithful to the source.\n *\n * @remarks\n * In **eager mode** the content is already cached at first-call load and this method is\n * effectively a property access. In **streaming mode** there is no cache: the file is re-read\n * (as a single `File.text()` call) on every invocation. Use `SpooledArtifact.asString()`\n * judiciously on large streaming-mode artifacts.\n */\n async readAll(): Promise<string> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.content\n return state.file.text()\n }\n\n /**\n * Lazily initialise the reader's mode-specific state. Called by every public method; the\n * promise is cached so the work runs at most once.\n */\n #load(): Promise<ReaderState> {\n if (!this.#ready) this.#ready = this.#init()\n return this.#ready\n }\n\n async #init(): Promise<ReaderState> {\n const file = await this.#handle.getFile()\n const bytes = file.size\n if (!isNonNegativeFiniteNumber(bytes)) {\n throw new Error(`OpfsSpoolReader: file handle returned a non-finite size (${String(bytes)})`)\n }\n if (bytes < this.#threshold) {\n // Eager — pull the whole thing into memory.\n const content = await file.text()\n const lines = content === '' ? [] : content.split('\\n')\n return { mode: 'eager', lines, bytes, content }\n }\n // Streaming — build a line-offset index by scanning bytes once.\n return this.#buildStreamingIndex(file, bytes)\n }\n\n async #buildStreamingIndex(file: OpfsFile, bytes: number): Promise<StreamingState> {\n // Edge case first — an empty file is one offset (the EOF), zero lines.\n if (bytes === 0) return { mode: 'streaming', file, offsets: [0], bytes }\n\n // offsets[i] is the byte position where line `i` starts. offsets[lineCount] is one-past-end.\n // For \"a\\nb\\nc\" → offsets=[0, 2, 4, 5] (3 lines).\n // For \"a\\nb\\n\" → offsets=[0, 2, 4, 4] (3 lines, last is the trailing empty line). This\n // mirrors `String.prototype.split('\\n')` semantics so streaming and eager agree.\n const offsets: number[] = [0]\n let position = 0\n let lastByte = -1\n const reader = file.stream().getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n for (const byte of value) {\n position++\n if (byte === LF) offsets.push(position)\n lastByte = byte\n }\n }\n } finally {\n reader.releaseLock()\n }\n // If the file ends on a newline, the byte after the LF is the start of an empty trailing\n // line — record it. If it doesn't, the final line's end is the EOF and we need to push\n // it so line(N-1) can read up to bytes.\n if (lastByte === LF) offsets.push(position)\n else if (offsets[offsets.length - 1] !== position) offsets.push(position)\n return { mode: 'streaming', file, offsets, bytes }\n }\n\n /**\n * Slices the byte range `[start, end)` from the backing file and returns it as a UTF-8\n * string, stripping a trailing `\\n` if present.\n *\n * @remarks\n * `Blob.slice` is O(1) metadata; `Blob.text()` only decodes the slice. The line-offset index\n * brackets each line *with* its trailing LF (so `offsets[i+1]` points at the start of the\n * next line) and the `SpoolReader` contract returns lines *without* their trailing newline,\n * so we strip a single trailing LF if present.\n */\n async #readRange(file: OpfsFile, start: number, end: number): Promise<string> {\n if (start === end) return ''\n const slice = file.slice(start, end)\n const text = await slice.text()\n if (text.length > 0 && text.charCodeAt(text.length - 1) === LF) {\n return text.slice(0, -1)\n }\n return text\n }\n}\n\n/**\n * Constructor options for {@link OpfsSpoolStore}.\n */\nexport interface OpfsSpoolStoreOptions {\n /**\n * Optional thunk that resolves the {@link OpfsDirectoryHandle} used as the store root.\n *\n * @remarks\n * When omitted, the store resolves the root via `navigator.storage.getDirectory()` on its\n * first filesystem call. Override for tests (to point at a per-suite subdirectory) or to\n * scope the store to a nested directory inside OPFS.\n *\n * The thunk is invoked at most once per store; the returned handle is memoised.\n */\n directory?: () => Promise<OpfsDirectoryHandle>\n\n /**\n * Optional filename prefix prepended to every `callId`.\n *\n * @remarks\n * Prefix is a **filename prefix**, not a subdirectory — `keyPrefix: 'agent-runs/'` produces\n * a file literally named `agent-runs/<callId>` at the root, not a nested directory. (OPFS\n * filenames may not contain `/`, so use a non-`/` separator like `-` if you want a flat\n * namespace.) This mirrors the `keyPrefix` semantics in the flydrive and in-memory batteries.\n *\n * @defaultValue `\"\"`\n */\n keyPrefix?: string\n\n /**\n * Default `streamThresholdBytes` for readers produced by `write()` and `read()`. Individual\n * calls may override via their own `opts` argument.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\n/**\n * \"Give bytes, get a reader\" persistence layer over an OPFS directory.\n *\n * @remarks\n * `write(callId, bytes)` resolves the root directory (lazily, on first call), opens or creates\n * the file named `keyPrefix + callId`, then writes via the API matching the current scope:\n * a `FileSystemSyncAccessHandle` in worker scopes, `OpfsFileHandle.createWritable()` on\n * the main thread. A fresh {@link OpfsSpoolReader} pointed at the same file is returned.\n *\n * `read(callId)` returns a reader without re-writing; `delete(callId)` removes the entry.\n *\n * The store is otherwise stateless — it owns no in-memory cache of writes. Multiple\n * `OpfsSpoolStore` instances sharing the same root directory and key prefix see the same data.\n *\n * @example\n * ```ts\n * import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'\n *\n * const store = new OpfsSpoolStore({ keyPrefix: 'agent-runs/' })\n *\n * const bytes = await tool.executor(ctx)(args)\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class OpfsSpoolStore implements SpoolStore {\n readonly #resolveRoot: () => Promise<OpfsDirectoryHandle>\n readonly #prefix: string\n readonly #defaultThreshold: number\n #root: OpfsDirectoryHandle | undefined\n\n constructor(opts: OpfsSpoolStoreOptions = {}) {\n this.#resolveRoot = opts.directory ?? (() => navigator.storage.getDirectory())\n this.#prefix = opts.keyPrefix ?? ''\n this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n }\n\n /**\n * Returns `true` if `value` is an {@link OpfsSpoolStore} instance.\n *\n * @remarks\n * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety.\n *\n * @param value - The value to test.\n * @returns `true` when `value` is an {@link OpfsSpoolStore} instance.\n */\n public static isOpfsSpoolStore(value: unknown): value is OpfsSpoolStore {\n return isInstanceOf(value, 'OpfsSpoolStore', OpfsSpoolStore)\n }\n\n /**\n * Persists `bytes` under `callId` and returns a reader bound to the stored key.\n *\n * @remarks\n * `string` input is encoded as UTF-8; `Uint8Array` is stored byte-faithfully;\n * `ReadableStream<Uint8Array>` is written incrementally — the stream is consumed chunk-by-chunk\n * straight to OPFS without first materializing the whole payload in memory, which is the point\n * of accepting a stream for a durable store.\n *\n * @param callId - Identifier used to retrieve the bytes via {@link OpfsSpoolStore.read}.\n * @param bytes - The bytes to store, as a `string`, `Uint8Array`, or `ReadableStream<Uint8Array>`.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns An {@link OpfsSpoolReader} over the stored bytes.\n */\n async write(\n callId: string,\n bytes: string | Uint8Array | ReadableStream<Uint8Array>,\n opts?: OpfsSpoolReaderOptions\n ): Promise<OpfsSpoolReader> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n const handle = await root.getFileHandle(name, { create: true })\n if (isInstanceOf(bytes, 'ReadableStream', ReadableStream)) {\n if (isWorkerScope()) {\n await this.#writeStreamViaSyncHandle(handle, bytes)\n } else {\n await this.#writeStreamViaWritable(handle, bytes)\n }\n } else {\n const payload = typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes\n if (isWorkerScope()) {\n await this.#writeViaSyncHandle(handle, payload)\n } else {\n await this.#writeViaWritable(handle, payload)\n }\n }\n return new OpfsSpoolReader(handle, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Returns a reader over the bytes previously written under `callId`.\n *\n * @remarks\n * Returns `undefined` if the file does not exist.\n *\n * @param callId - Identifier supplied to a prior {@link OpfsSpoolStore.write} call.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns An {@link OpfsSpoolReader}, or `undefined` if the key is missing.\n */\n async read(callId: string, opts?: OpfsSpoolReaderOptions): Promise<OpfsSpoolReader | undefined> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n let handle: OpfsFileHandle\n try {\n handle = await root.getFileHandle(name)\n } catch (err) {\n if (this.#isNotFoundError(err)) return undefined\n throw err\n }\n return new OpfsSpoolReader(handle, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Removes the entry under `callId`.\n *\n * @param callId - Identifier whose entry should be removed.\n * @returns `true` if the entry existed and was removed; `false` if it didn't exist.\n */\n async delete(callId: string): Promise<boolean> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n try {\n await root.removeEntry(name)\n return true\n } catch (err) {\n if (this.#isNotFoundError(err)) return false\n throw err\n }\n }\n\n /**\n * Returns `true` if a file is present under `callId`.\n *\n * @param callId - Identifier to test.\n * @returns `true` when the file exists, `false` otherwise.\n */\n async has(callId: string): Promise<boolean> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n try {\n await root.getFileHandle(name)\n return true\n } catch (err) {\n if (this.#isNotFoundError(err)) return false\n throw err\n }\n }\n\n /**\n * Returns the full filename for a given `callId` (i.e. `keyPrefix + callId`).\n *\n * @remarks\n * Useful for tests or for callers that want to interact with the underlying OPFS directory\n * directly.\n */\n keyFor(callId: string): string {\n return this.#keyFor(callId)\n }\n\n #keyFor(callId: string): string {\n return this.#prefix + callId\n }\n\n async #getRoot(): Promise<OpfsDirectoryHandle> {\n if (!this.#root) this.#root = await this.#resolveRoot()\n return this.#root\n }\n\n async #writeViaSyncHandle(handle: OpfsFileHandle, payload: Uint8Array): Promise<void> {\n const sync = await (handle as OpfsFileHandleWithSyncAccess).createSyncAccessHandle()\n try {\n sync.truncate(0)\n sync.write(payload, { at: 0 })\n sync.flush()\n } finally {\n sync.close()\n }\n }\n\n async #writeViaWritable(handle: OpfsFileHandle, payload: Uint8Array): Promise<void> {\n const writable = await handle.createWritable()\n try {\n await writable.write(payload)\n } finally {\n await writable.close()\n }\n }\n\n async #writeStreamViaWritable(\n handle: OpfsFileHandle,\n stream: ReadableStream<Uint8Array>\n ): Promise<void> {\n const writable = await handle.createWritable()\n try {\n const reader = stream.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value) await writable.write(value)\n }\n } finally {\n reader.releaseLock()\n }\n } finally {\n await writable.close()\n }\n }\n\n async #writeStreamViaSyncHandle(\n handle: OpfsFileHandle,\n stream: ReadableStream<Uint8Array>\n ): Promise<void> {\n const sync = await (handle as OpfsFileHandleWithSyncAccess).createSyncAccessHandle()\n try {\n sync.truncate(0)\n let at = 0\n const reader = stream.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value && value.byteLength > 0) {\n sync.write(value, { at })\n at += value.byteLength\n }\n }\n } finally {\n reader.releaseLock()\n }\n sync.flush()\n } finally {\n sync.close()\n }\n }\n\n #isNotFoundError(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false\n const name = (err as { name?: unknown }).name\n return name === 'NotFoundError'\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"opfs.cjs","names":["#handle","#threshold","#load","#readRange","#ready","#init","#buildStreamingIndex","#resolveRoot","#prefix","#defaultThreshold","#keyFor","#getRoot","#writeViaSyncHandle","#writeViaWritable","#isNotFoundError","#root"],"sources":["../../../src/batteries/storage/opfs/index.ts"],"sourcesContent":["/**\n * Browser-only Origin Private File System storage for spooled artifacts.\n *\n * @module @nhtio/adk/batteries/storage/opfs\n *\n * @remarks\n * Opt-in **browser-only** storage battery backed by the\n * [Origin Private File System](https://developer.mozilla.org/docs/Web/API/File_System_API/Origin_private_file_system)\n * (OPFS). Provides {@link OpfsSpoolReader} (a {@link @nhtio/adk!SpoolReader} over a `OpfsFileHandle`)\n * and {@link OpfsSpoolStore} (a `write(callId, bytes) → reader` persistence layer that wraps an\n * OPFS directory).\n *\n * The reader has two modes selected lazily on first method invocation based on the size of the\n * underlying file:\n *\n * - **Eager mode** — when `file.size` is below `streamThresholdBytes` (default 10 MiB), the\n * reader calls `file.text()` once, splits the content on `\\n`, and caches lines + byte count.\n * All subsequent calls resolve from memory.\n * - **Streaming mode** — when `file.size` meets or exceeds the threshold, the reader streams the\n * file once via `file.stream().getReader()` to build a line-offset index (`number[]` of byte\n * offsets per line), then serves each `line(i)` request by slicing the underlying `Blob` —\n * `Blob.slice(start, end).text()` decodes only the requested range, no head-of-file scan.\n * Caps RAM at one index + one line buffer regardless of file size.\n *\n * The store auto-selects its write API by execution scope:\n *\n * - In **worker scopes** (`self instanceof WorkerGlobalScope`), it acquires a\n * `FileSystemSyncAccessHandle` and writes synchronously. Sync handles are the only API\n * available in workers and the fastest path for the spool-write hot path.\n * - On the **main thread**, it uses `OpfsFileHandle.createWritable()` and the async\n * stream API. Sync access handles are not exposed on the main thread.\n *\n * This module assumes a browser-equivalent runtime — `navigator.storage`,\n * `OpfsFileHandle`, `TextEncoder`/`TextDecoder`, and `Blob` must all exist. It must not\n * be imported from Node code; do so and you will fail at resolve time when `navigator` is\n * referenced.\n *\n * @example\n * ```ts\n * import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'\n *\n * const store = new OpfsSpoolStore({ keyPrefix: 'agent-runs/' })\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\n\nimport { isInstanceOf } from '@nhtio/adk/guards'\nimport type { SpoolReader } from '@nhtio/adk/common'\n\n// The project's tsconfig limits `lib` to `ESNext`, so the DOM and File System Access types\n// referenced below are not in scope by default — neither `tsc --noEmit` nor the downstream dts\n// pipeline (api-extractor) can see them. Re-declare here the **minimum** surface this module\n// touches via a local handle-shape interface. Public API uses `OpfsFileHandle` /\n// `OpfsDirectoryHandle` instead of the DOM globals so the published `.d.ts` is self-contained\n// and consumers do not have to chase the lib graph.\n\n/**\n * Minimal subset of the\n * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)\n * `OpfsFileHandle` interface that this module touches at runtime. Structurally compatible\n * with the DOM-lib `OpfsFileHandle` — at call sites you pass real OPFS handles directly.\n */\nexport interface OpfsFileHandle {\n readonly kind: 'file'\n readonly name: string\n getFile(): Promise<OpfsFile>\n createWritable(): Promise<OpfsWritableFileStream>\n}\n\n/**\n * Minimal subset of the\n * [File System Access](https://developer.mozilla.org/docs/Web/API/File_System_API)\n * `OpfsDirectoryHandle` interface that this module touches at runtime. Structurally\n * compatible with the DOM-lib `OpfsDirectoryHandle` — at call sites you pass real OPFS\n * handles directly.\n */\nexport interface OpfsDirectoryHandle {\n readonly kind: 'directory'\n readonly name: string\n getFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileHandle>\n getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectoryHandle>\n removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>\n}\n\n/**\n * Minimal subset of the DOM `FileSystemWritableFileStream` interface used by the OPFS battery's\n * main-thread write path.\n */\nexport interface OpfsWritableFileStream {\n write(data: Uint8Array | ArrayBufferView | ArrayBuffer | string): Promise<void>\n close(): Promise<void>\n}\n\n/**\n * Minimal subset of the DOM `Blob` interface used by {@link OpfsSpoolReader} streaming-mode\n * random-access reads. Real OPFS handles return a `File` here; we narrow to the methods we\n * actually call.\n */\nexport interface OpfsBlob {\n readonly size: number\n slice(start?: number, end?: number, contentType?: string): OpfsBlob\n text(): Promise<string>\n stream(): OpfsReadableStream\n}\n\n/**\n * Minimal subset of the DOM `File` interface used by {@link OpfsSpoolReader}.\n */\nexport interface OpfsFile extends OpfsBlob {\n readonly name: string\n}\n\n/**\n * Minimal subset of the DOM `ReadableStream<Uint8Array>` interface used by streaming-mode\n * index construction.\n */\nexport interface OpfsReadableStream {\n getReader(): OpfsReadableStreamReader\n}\n\n/**\n * Minimal subset of the DOM `ReadableStreamDefaultReader<Uint8Array>` interface used by\n * streaming-mode index construction.\n */\nexport interface OpfsReadableStreamReader {\n read(): Promise<{ done: false; value: Uint8Array } | { done: true; value: undefined }>\n releaseLock(): void\n}\n\ndeclare const navigator: {\n storage: { getDirectory(): Promise<OpfsDirectoryHandle> }\n}\ndeclare class TextEncoder {\n encode(input?: string): Uint8Array\n}\ndeclare const self: unknown\ndeclare const WorkerGlobalScope: { new (): unknown } | undefined\n\ninterface FileSystemSyncAccessHandle {\n truncate(newSize: number): void\n write(buffer: Uint8Array | ArrayBuffer | ArrayBufferView, options?: { at?: number }): number\n flush(): void\n close(): void\n}\ninterface OpfsFileHandleWithSyncAccess extends OpfsFileHandle {\n createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>\n}\n\nconst DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024 // 10 MiB\n\nconst LF = 0x0a // '\\n'\n\nconst isNonNegativeFiniteNumber = (n: unknown): n is number =>\n typeof n === 'number' && Number.isFinite(n) && n >= 0\n\n/**\n * Constructor options for {@link OpfsSpoolReader}.\n */\nexport interface OpfsSpoolReaderOptions {\n /**\n * Byte-length threshold that switches between eager and streaming modes.\n *\n * @remarks\n * - Below the threshold → eager (whole-file in memory).\n * - At or above the threshold → streaming (line-offset index + per-line slice reads).\n *\n * Set to `0` to force streaming mode; set to `Number.POSITIVE_INFINITY` to force eager mode.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\ninterface EagerState {\n mode: 'eager'\n lines: string[]\n bytes: number\n content: string\n}\n\ninterface StreamingState {\n mode: 'streaming'\n file: OpfsFile\n /**\n * Byte offsets where each line *starts*. Length equals lineCount + 1; the final entry equals\n * the total byte length. So `offsets[i + 1] - offsets[i]` is the byte length of line `i`\n * including any trailing `\\n`.\n */\n offsets: number[]\n bytes: number\n}\n\ntype ReaderState = EagerState | StreamingState\n\n/**\n * Returns `true` when the current global scope is a Web Worker (`DedicatedWorkerGlobalScope`,\n * `SharedWorkerGlobalScope`, or `ServiceWorkerGlobalScope` all inherit from `WorkerGlobalScope`).\n *\n * @remarks\n * The check is needed at runtime because `FileSystemSyncAccessHandle` is only exposed in worker\n * scopes — calling it from the main thread throws. We pick the write strategy based on the\n * answer here.\n *\n * @internal\n */\nconst isWorkerScope = (): boolean => {\n if (typeof WorkerGlobalScope === 'undefined') return false\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on `self`; no cross-realm risk\n return self instanceof WorkerGlobalScope\n}\n\n/**\n * Reads an OPFS-backed file as a {@link @nhtio/adk!SpoolReader}.\n *\n * @remarks\n * Constructor is **not** async — but the first method call awaits a private readiness promise\n * that fetches the underlying `File` (and in eager mode, its contents). Subsequent calls reuse\n * the cached state. This keeps construction call sites synchronous while still doing real I/O\n * lazily.\n *\n * All four `SpoolReader` methods on this reader return promises. The `SpoolReader` contract\n * supports both sync and async return; consumers of `SpooledArtifact` handle either.\n */\nexport class OpfsSpoolReader implements SpoolReader {\n readonly #handle: OpfsFileHandle\n readonly #threshold: number\n #ready: Promise<ReaderState> | undefined\n\n constructor(handle: OpfsFileHandle, opts: OpfsSpoolReaderOptions = {}) {\n this.#handle = handle\n const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n // Allow `Infinity` (forces eager) but reject anything non-finite-negative.\n if (typeof raw !== 'number' || Number.isNaN(raw) || raw < 0) {\n throw new TypeError(\n `OpfsSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`\n )\n }\n this.#threshold = raw\n }\n\n /**\n * Returns `true` if `value` is an {@link OpfsSpoolReader} instance.\n *\n * @remarks\n * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety.\n *\n * @param value - The value to test.\n * @returns `true` when `value` is an {@link OpfsSpoolReader} instance.\n */\n public static isOpfsSpoolReader(value: unknown): value is OpfsSpoolReader {\n return isInstanceOf(value, 'OpfsSpoolReader', OpfsSpoolReader)\n }\n\n async line(index: number): Promise<string | undefined> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.lines[index]\n if (index < 0 || index >= state.offsets.length - 1) return undefined\n return this.#readRange(state.file, state.offsets[index], state.offsets[index + 1])\n }\n\n async byteLength(): Promise<number> {\n const state = await this.#load()\n return state.bytes\n }\n\n async lineCount(): Promise<number> {\n const state = await this.#load()\n return state.mode === 'eager' ? state.lines.length : state.offsets.length - 1\n }\n\n /**\n * Returns the full underlying content as a single decoded string, byte-faithful to the source.\n *\n * @remarks\n * In **eager mode** the content is already cached at first-call load and this method is\n * effectively a property access. In **streaming mode** there is no cache: the file is re-read\n * (as a single `File.text()` call) on every invocation. Use `SpooledArtifact.asString()`\n * judiciously on large streaming-mode artifacts.\n */\n async readAll(): Promise<string> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.content\n return state.file.text()\n }\n\n /**\n * Lazily initialise the reader's mode-specific state. Called by every public method; the\n * promise is cached so the work runs at most once.\n */\n #load(): Promise<ReaderState> {\n if (!this.#ready) this.#ready = this.#init()\n return this.#ready\n }\n\n async #init(): Promise<ReaderState> {\n const file = await this.#handle.getFile()\n const bytes = file.size\n if (!isNonNegativeFiniteNumber(bytes)) {\n throw new Error(`OpfsSpoolReader: file handle returned a non-finite size (${String(bytes)})`)\n }\n if (bytes < this.#threshold) {\n // Eager — pull the whole thing into memory.\n const content = await file.text()\n const lines = content === '' ? [] : content.split('\\n')\n return { mode: 'eager', lines, bytes, content }\n }\n // Streaming — build a line-offset index by scanning bytes once.\n return this.#buildStreamingIndex(file, bytes)\n }\n\n async #buildStreamingIndex(file: OpfsFile, bytes: number): Promise<StreamingState> {\n // Edge case first — an empty file is one offset (the EOF), zero lines.\n if (bytes === 0) return { mode: 'streaming', file, offsets: [0], bytes }\n\n // offsets[i] is the byte position where line `i` starts. offsets[lineCount] is one-past-end.\n // For \"a\\nb\\nc\" → offsets=[0, 2, 4, 5] (3 lines).\n // For \"a\\nb\\n\" → offsets=[0, 2, 4, 4] (3 lines, last is the trailing empty line). This\n // mirrors `String.prototype.split('\\n')` semantics so streaming and eager agree.\n const offsets: number[] = [0]\n let position = 0\n let lastByte = -1\n const reader = file.stream().getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n for (const byte of value) {\n position++\n if (byte === LF) offsets.push(position)\n lastByte = byte\n }\n }\n } finally {\n reader.releaseLock()\n }\n // If the file ends on a newline, the byte after the LF is the start of an empty trailing\n // line — record it. If it doesn't, the final line's end is the EOF and we need to push\n // it so line(N-1) can read up to bytes.\n if (lastByte === LF) offsets.push(position)\n else if (offsets[offsets.length - 1] !== position) offsets.push(position)\n return { mode: 'streaming', file, offsets, bytes }\n }\n\n /**\n * Slices the byte range `[start, end)` from the backing file and returns it as a UTF-8\n * string, stripping a trailing `\\n` if present.\n *\n * @remarks\n * `Blob.slice` is O(1) metadata; `Blob.text()` only decodes the slice. The line-offset index\n * brackets each line *with* its trailing LF (so `offsets[i+1]` points at the start of the\n * next line) and the `SpoolReader` contract returns lines *without* their trailing newline,\n * so we strip a single trailing LF if present.\n */\n async #readRange(file: OpfsFile, start: number, end: number): Promise<string> {\n if (start === end) return ''\n const slice = file.slice(start, end)\n const text = await slice.text()\n if (text.length > 0 && text.charCodeAt(text.length - 1) === LF) {\n return text.slice(0, -1)\n }\n return text\n }\n}\n\n/**\n * Constructor options for {@link OpfsSpoolStore}.\n */\nexport interface OpfsSpoolStoreOptions {\n /**\n * Optional thunk that resolves the {@link OpfsDirectoryHandle} used as the store root.\n *\n * @remarks\n * When omitted, the store resolves the root via `navigator.storage.getDirectory()` on its\n * first filesystem call. Override for tests (to point at a per-suite subdirectory) or to\n * scope the store to a nested directory inside OPFS.\n *\n * The thunk is invoked at most once per store; the returned handle is memoised.\n */\n directory?: () => Promise<OpfsDirectoryHandle>\n\n /**\n * Optional filename prefix prepended to every `callId`.\n *\n * @remarks\n * Prefix is a **filename prefix**, not a subdirectory — `keyPrefix: 'agent-runs/'` produces\n * a file literally named `agent-runs/<callId>` at the root, not a nested directory. (OPFS\n * filenames may not contain `/`, so use a non-`/` separator like `-` if you want a flat\n * namespace.) This mirrors the `keyPrefix` semantics in the flydrive and in-memory batteries.\n *\n * @defaultValue `\"\"`\n */\n keyPrefix?: string\n\n /**\n * Default `streamThresholdBytes` for readers produced by `write()` and `read()`. Individual\n * calls may override via their own `opts` argument.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\n/**\n * \"Give bytes, get a reader\" persistence layer over an OPFS directory.\n *\n * @remarks\n * `write(callId, bytes)` resolves the root directory (lazily, on first call), opens or creates\n * the file named `keyPrefix + callId`, then writes via the API matching the current scope:\n * a `FileSystemSyncAccessHandle` in worker scopes, `OpfsFileHandle.createWritable()` on\n * the main thread. A fresh {@link OpfsSpoolReader} pointed at the same file is returned.\n *\n * `read(callId)` returns a reader without re-writing; `delete(callId)` removes the entry.\n *\n * The store is otherwise stateless — it owns no in-memory cache of writes. Multiple\n * `OpfsSpoolStore` instances sharing the same root directory and key prefix see the same data.\n *\n * @example\n * ```ts\n * import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'\n *\n * const store = new OpfsSpoolStore({ keyPrefix: 'agent-runs/' })\n *\n * const bytes = await tool.executor(ctx)(args)\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class OpfsSpoolStore {\n readonly #resolveRoot: () => Promise<OpfsDirectoryHandle>\n readonly #prefix: string\n readonly #defaultThreshold: number\n #root: OpfsDirectoryHandle | undefined\n\n constructor(opts: OpfsSpoolStoreOptions = {}) {\n this.#resolveRoot = opts.directory ?? (() => navigator.storage.getDirectory())\n this.#prefix = opts.keyPrefix ?? ''\n this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n }\n\n /**\n * Returns `true` if `value` is an {@link OpfsSpoolStore} instance.\n *\n * @remarks\n * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety.\n *\n * @param value - The value to test.\n * @returns `true` when `value` is an {@link OpfsSpoolStore} instance.\n */\n public static isOpfsSpoolStore(value: unknown): value is OpfsSpoolStore {\n return isInstanceOf(value, 'OpfsSpoolStore', OpfsSpoolStore)\n }\n\n /**\n * Persists `bytes` under `callId` and returns a reader bound to the stored key.\n *\n * @param callId - Identifier used to retrieve the bytes via {@link OpfsSpoolStore.read}.\n * @param bytes - The bytes to store, as a `string` or `Uint8Array`.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns An {@link OpfsSpoolReader} over the stored bytes.\n */\n async write(\n callId: string,\n bytes: string | Uint8Array,\n opts?: OpfsSpoolReaderOptions\n ): Promise<OpfsSpoolReader> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n const handle = await root.getFileHandle(name, { create: true })\n const payload = typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes\n if (isWorkerScope()) {\n await this.#writeViaSyncHandle(handle, payload)\n } else {\n await this.#writeViaWritable(handle, payload)\n }\n return new OpfsSpoolReader(handle, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Returns a reader over the bytes previously written under `callId`.\n *\n * @remarks\n * Returns `undefined` if the file does not exist.\n *\n * @param callId - Identifier supplied to a prior {@link OpfsSpoolStore.write} call.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns An {@link OpfsSpoolReader}, or `undefined` if the key is missing.\n */\n async read(callId: string, opts?: OpfsSpoolReaderOptions): Promise<OpfsSpoolReader | undefined> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n let handle: OpfsFileHandle\n try {\n handle = await root.getFileHandle(name)\n } catch (err) {\n if (this.#isNotFoundError(err)) return undefined\n throw err\n }\n return new OpfsSpoolReader(handle, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Removes the entry under `callId`.\n *\n * @param callId - Identifier whose entry should be removed.\n * @returns `true` if the entry existed and was removed; `false` if it didn't exist.\n */\n async delete(callId: string): Promise<boolean> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n try {\n await root.removeEntry(name)\n return true\n } catch (err) {\n if (this.#isNotFoundError(err)) return false\n throw err\n }\n }\n\n /**\n * Returns `true` if a file is present under `callId`.\n *\n * @param callId - Identifier to test.\n * @returns `true` when the file exists, `false` otherwise.\n */\n async has(callId: string): Promise<boolean> {\n const name = this.#keyFor(callId)\n const root = await this.#getRoot()\n try {\n await root.getFileHandle(name)\n return true\n } catch (err) {\n if (this.#isNotFoundError(err)) return false\n throw err\n }\n }\n\n /**\n * Returns the full filename for a given `callId` (i.e. `keyPrefix + callId`).\n *\n * @remarks\n * Useful for tests or for callers that want to interact with the underlying OPFS directory\n * directly.\n */\n keyFor(callId: string): string {\n return this.#keyFor(callId)\n }\n\n #keyFor(callId: string): string {\n return this.#prefix + callId\n }\n\n async #getRoot(): Promise<OpfsDirectoryHandle> {\n if (!this.#root) this.#root = await this.#resolveRoot()\n return this.#root\n }\n\n async #writeViaSyncHandle(handle: OpfsFileHandle, payload: Uint8Array): Promise<void> {\n const sync = await (handle as OpfsFileHandleWithSyncAccess).createSyncAccessHandle()\n try {\n sync.truncate(0)\n sync.write(payload, { at: 0 })\n sync.flush()\n } finally {\n sync.close()\n }\n }\n\n async #writeViaWritable(handle: OpfsFileHandle, payload: Uint8Array): Promise<void> {\n const writable = await handle.createWritable()\n try {\n await writable.write(payload)\n } finally {\n await writable.close()\n }\n }\n\n #isNotFoundError(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false\n const name = (err as { name?: unknown }).name\n return name === 'NotFoundError'\n }\n}\n"],"mappings":"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"}
@@ -251,27 +251,17 @@ var OpfsSpoolStore = class OpfsSpoolStore {
251
251
  /**
252
252
  * Persists `bytes` under `callId` and returns a reader bound to the stored key.
253
253
  *
254
- * @remarks
255
- * `string` input is encoded as UTF-8; `Uint8Array` is stored byte-faithfully;
256
- * `ReadableStream<Uint8Array>` is written incrementally — the stream is consumed chunk-by-chunk
257
- * straight to OPFS without first materializing the whole payload in memory, which is the point
258
- * of accepting a stream for a durable store.
259
- *
260
254
  * @param callId - Identifier used to retrieve the bytes via {@link OpfsSpoolStore.read}.
261
- * @param bytes - The bytes to store, as a `string`, `Uint8Array`, or `ReadableStream<Uint8Array>`.
255
+ * @param bytes - The bytes to store, as a `string` or `Uint8Array`.
262
256
  * @param opts - Per-call override for `streamThresholdBytes`.
263
257
  * @returns An {@link OpfsSpoolReader} over the stored bytes.
264
258
  */
265
259
  async write(callId, bytes, opts) {
266
260
  const name = this.#keyFor(callId);
267
261
  const handle = await (await this.#getRoot()).getFileHandle(name, { create: true });
268
- if (isInstanceOf(bytes, "ReadableStream", ReadableStream)) if (isWorkerScope()) await this.#writeStreamViaSyncHandle(handle, bytes);
269
- else await this.#writeStreamViaWritable(handle, bytes);
270
- else {
271
- const payload = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
272
- if (isWorkerScope()) await this.#writeViaSyncHandle(handle, payload);
273
- else await this.#writeViaWritable(handle, payload);
274
- }
262
+ const payload = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
263
+ if (isWorkerScope()) await this.#writeViaSyncHandle(handle, payload);
264
+ else await this.#writeViaWritable(handle, payload);
275
265
  return new OpfsSpoolReader(handle, { streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold });
276
266
  }
277
267
  /**
@@ -365,46 +355,6 @@ var OpfsSpoolStore = class OpfsSpoolStore {
365
355
  await writable.close();
366
356
  }
367
357
  }
368
- async #writeStreamViaWritable(handle, stream) {
369
- const writable = await handle.createWritable();
370
- try {
371
- const reader = stream.getReader();
372
- try {
373
- for (;;) {
374
- const { done, value } = await reader.read();
375
- if (done) break;
376
- if (value) await writable.write(value);
377
- }
378
- } finally {
379
- reader.releaseLock();
380
- }
381
- } finally {
382
- await writable.close();
383
- }
384
- }
385
- async #writeStreamViaSyncHandle(handle, stream) {
386
- const sync = await handle.createSyncAccessHandle();
387
- try {
388
- sync.truncate(0);
389
- let at = 0;
390
- const reader = stream.getReader();
391
- try {
392
- for (;;) {
393
- const { done, value } = await reader.read();
394
- if (done) break;
395
- if (value && value.byteLength > 0) {
396
- sync.write(value, { at });
397
- at += value.byteLength;
398
- }
399
- }
400
- } finally {
401
- reader.releaseLock();
402
- }
403
- sync.flush();
404
- } finally {
405
- sync.close();
406
- }
407
- }
408
358
  #isNotFoundError(err) {
409
359
  if (err === null || typeof err !== "object") return false;
410
360
  return err.name === "NotFoundError";