expo-local-llm 0.4.1 → 0.5.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.
package/README.md CHANGED
@@ -71,8 +71,8 @@ function Chat() {
71
71
  }
72
72
 
73
73
  const handleSend = async (prompt: string) => {
74
- // Streaming — updates streamedText live
75
- await streamResponse(prompt);
74
+ // Streaming — updates streamedText live, resolves with the final text
75
+ const text = await streamResponse(prompt);
76
76
 
77
77
  // Or non-streaming:
78
78
  // const response = await respond(prompt);
@@ -158,6 +158,19 @@ const { respond } = useLocalLLM({
158
158
  const recipe = JSON.parse(await respond('Give me a pasta recipe'));
159
159
  ```
160
160
 
161
+ For one-shot structured calls (classification, extraction) where you don't want conversation
162
+ history accumulating between calls, use `generate()` instead of a session:
163
+
164
+ ```ts
165
+ import { generate } from 'expo-local-llm';
166
+
167
+ const json = await generate('Classify this message: "how was my sleep?"', {
168
+ instructions: 'Classify what the message asks for.',
169
+ responseFormat: 'json',
170
+ schema: { topic: { type: 'string', enum: ['sleep', 'heart', 'none'] } },
171
+ });
172
+ ```
173
+
161
174
  Streaming with a schema yields progressively-filled snapshots via `streamedJSON` (the current
162
175
  partial as a JSON string) and `streamedObject` (the parsed value):
163
176
 
@@ -198,8 +211,8 @@ if (!result.ok) {
198
211
  |--------|------|-------------|
199
212
  | `instructions` | `string` | System instructions for the session |
200
213
  | `options.temperature` | `number` | Sampling temperature |
201
- | `options.maxTokens` | `number` | Max output tokens (capped at 256 on Android) |
202
- | `options.topK` | `number` | Top-K sampling |
214
+ | `options.maxTokens` | `number` | Max output tokens (capped at 256 on Android; maps to `maximumResponseTokens` on iOS) |
215
+ | `options.topK` | `number` | Top-K sampling (maps to `.random(top:)` on iOS) |
203
216
  | `tools` | `ToolDefinition[]` | Tools the model can invoke (iOS 26+ only) |
204
217
  | `toolTimeout` | `number` | Seconds before an unresolved tool call times out (default 30) |
205
218
  | `responseFormat` | `"text" \| "json"` | Set to `"json"` for structured JSON output |
@@ -220,10 +233,18 @@ if (!result.ok) {
220
233
  | `activeToolCalls` | `ActiveToolCall[]` | In-flight tool calls (`{ callId, toolName }`) |
221
234
  | `respond` | `(prompt: string) => Promise<string>` | Non-streaming generation. `undefined` when unavailable. |
222
235
  | `session` | `LLMSession \| null` | The native session object. `null` when unavailable. |
223
- | `streamResponse` | `(prompt: string) => Promise<void>` | Streaming generation (updates `streamedText`). `undefined` when model not `available`. |
224
- | `cancelStream` | `() => Promise<void>` | Cancel active stream. `undefined` when unavailable. |
236
+ | `streamResponse` | `(prompt: string) => Promise<string>` | Streaming generation (updates `streamedText`). Resolves with the final text at stream end, or the partial text if cancelled; rejects on stream failure. `undefined` when model not `available`. |
237
+ | `cancelStream` | `() => Promise<void>` | Cancel the active stream. Actually stops inference (not just token delivery); the pending `streamResponse` promise resolves with the partial text. `undefined` when unavailable. |
238
+ | `reset` | `() => void` | Clear the conversation transcript, keeping instructions/tools/schema/options. Also cancels any in-flight stream. `undefined` only when there is no session. |
225
239
  | `downloadModel` | `() => Promise<void>` | Trigger model download (Android). `undefined` when unavailable. |
226
240
 
241
+ ### `generate(prompt, config?)`
242
+
243
+ One-shot stateless generation — creates a session, responds once, and releases it. Takes the
244
+ same config as `useLocalLLM`/`createLLMSession`. Use it for classification or extraction
245
+ calls where conversation history between calls is unwanted. Throws when the module or model
246
+ is unavailable (check `ExpoLocalLlmModule.getAvailability()` first if you need to gate).
247
+
227
248
  ### `ModelAvailability`
228
249
 
229
250
  `'available' | 'notEnabled' | 'notReady' | 'notEligible' | 'downloadRequired' | 'downloading' | 'unknown'`
@@ -16,6 +16,10 @@ class ConversationHistory(private val systemInstructions: String?) {
16
16
  trimHistory()
17
17
  }
18
18
 
19
+ fun clear() {
20
+ messages.clear()
21
+ }
22
+
19
23
  fun buildPrompt(): String {
20
24
  val sb = StringBuilder()
21
25
  if (!systemInstructions.isNullOrBlank()) {
@@ -42,14 +42,18 @@ class ExpoLocalLlmModule : Module() {
42
42
  session.respond(prompt)
43
43
  }
44
44
 
45
- AsyncFunction("streamResponse") { session: LLMSession, prompt: String ->
46
- session.startStream(prompt)
45
+ AsyncFunction("streamResponse") Coroutine { session: LLMSession, prompt: String ->
46
+ session.streamResponse(prompt)
47
47
  }
48
48
 
49
49
  AsyncFunction("cancelStream") { session: LLMSession ->
50
50
  session.cancelStream()
51
51
  }
52
52
 
53
+ Function("reset") { session: LLMSession ->
54
+ session.reset()
55
+ }
56
+
53
57
  // Tool calling stubs — not yet supported on Android
54
58
  Function("registerTool") { _: LLMSession, _: ToolConfig ->
55
59
  throw ToolNotSupportedException()
@@ -77,17 +77,24 @@ class LLMSession(
77
77
  return text
78
78
  }
79
79
 
80
- fun startStream(prompt: String) {
80
+ /**
81
+ * Stream a response, emitting `token` events along the way. Resolves with
82
+ * the final text when the stream completes, or with the partial text
83
+ * produced so far if the stream is cancelled. Emits `streamComplete` only
84
+ * on natural completion, `streamError` on failure.
85
+ */
86
+ suspend fun streamResponse(prompt: String): String {
81
87
  streamJob?.cancel()
82
88
  history.addUserMessage(prompt)
83
89
  val fullPrompt = history.buildPrompt()
84
90
 
85
- streamJob = scope.launch {
91
+ var accumulated = ""
92
+ var failure: Exception? = null
93
+ var completed = false
94
+ val job = scope.launch {
86
95
  try {
87
96
  val genModel = getOrCreateModel()
88
- var accumulated = ""
89
- val responseStream = genModel.generateContentStream(fullPrompt)
90
- responseStream.collect { chunk ->
97
+ genModel.generateContentStream(fullPrompt).collect { chunk ->
91
98
  val token = chunk.text ?: ""
92
99
  accumulated += token
93
100
  emit("token", mapOf(
@@ -95,14 +102,26 @@ class LLMSession(
95
102
  "accumulated" to accumulated
96
103
  ))
97
104
  }
98
- history.addAssistantMessage(accumulated)
99
- emit("streamComplete", mapOf("text" to accumulated))
105
+ completed = true
100
106
  } catch (e: CancellationException) {
101
107
  // User-initiated cancellation — not an error
108
+ throw e
102
109
  } catch (e: Exception) {
110
+ failure = e
103
111
  emit("streamError", mapOf("error" to (e.message ?: "Unknown error")))
104
112
  }
105
113
  }
114
+ streamJob = job
115
+ job.join()
116
+
117
+ failure?.let { throw StreamException(it.message ?: "Unknown error") }
118
+ if (accumulated.isNotEmpty()) {
119
+ history.addAssistantMessage(accumulated)
120
+ }
121
+ if (completed) {
122
+ emit("streamComplete", mapOf("text" to accumulated))
123
+ }
124
+ return accumulated
106
125
  }
107
126
 
108
127
  fun cancelStream() {
@@ -110,6 +129,16 @@ class LLMSession(
110
129
  streamJob = null
111
130
  }
112
131
 
132
+ /**
133
+ * Clear the conversation history (keeps instructions and generation
134
+ * options). Cancels any in-flight stream.
135
+ */
136
+ fun reset() {
137
+ streamJob?.cancel()
138
+ streamJob = null
139
+ history.clear()
140
+ }
141
+
113
142
  override fun deallocate() {
114
143
  super.deallocate()
115
144
  streamJob?.cancel()
@@ -2,12 +2,35 @@ import { SharedObject } from "expo-modules-core";
2
2
  import type { SessionConfig, LLMSessionEvents, ToolConfig } from "./ExpoLocalLlm.types";
3
3
  export declare class LLMSession extends SharedObject<LLMSessionEvents> {
4
4
  respond: (prompt: string) => Promise<string>;
5
- streamResponse: (prompt: string) => Promise<void>;
5
+ /**
6
+ * Streams a response, emitting `token`/`partial` events along the way.
7
+ * Resolves with the final text when the stream completes, or with the
8
+ * partial text produced so far if the stream is cancelled via
9
+ * `cancelStream()`. Rejects if the stream fails.
10
+ */
11
+ streamResponse: (prompt: string) => Promise<string>;
6
12
  cancelStream: () => Promise<void>;
13
+ /**
14
+ * Clears the conversation transcript while keeping instructions, tools,
15
+ * schema, and generation options. Cancels any in-flight stream. Cheap —
16
+ * the model weights are OS-shared; only the conversation state is rebuilt.
17
+ */
18
+ reset: () => void;
7
19
  registerTool: (config: ToolConfig) => void;
8
20
  unregisterTool: (name: string) => void;
9
21
  resolveToolCall: (callId: string, result: string) => void;
10
22
  rejectToolCall: (callId: string, error: string) => void;
11
23
  }
12
24
  export declare function createLLMSession(config?: SessionConfig): LLMSession;
25
+ /**
26
+ * One-shot, stateless generation: creates a session, responds once, and
27
+ * releases the session. Use this for classification/extraction calls where
28
+ * conversation history is unwanted — no session bookkeeping, no transcript
29
+ * accumulation across calls.
30
+ *
31
+ * Throws `UnavailabilityError` when the native module is missing; rejects
32
+ * with the native error when the model is not available. Callers that need
33
+ * an availability check first can use `ExpoLocalLlmModule.getAvailability()`.
34
+ */
35
+ export declare function generate(prompt: string, config?: SessionConfig): Promise<string>;
13
36
  //# sourceMappingURL=LLMSession.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LLMSession.d.ts","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACX,MAAM,sBAAsB,CAAC;AAI9B,qBAAa,UAAW,SAAQ,YAAY,CAAC,gBAAgB,CAAC;IAG5D,OAAO,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,YAAY,EAAG,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,eAAe,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAkBvE"}
1
+ {"version":3,"file":"LLMSession.d.ts","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACX,MAAM,sBAAsB,CAAC;AAI9B,qBAAa,UAAW,SAAQ,YAAY,CAAC,gBAAgB,CAAC;IAG5D,OAAO,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;;OAKG;IACH,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,YAAY,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC;;;;OAIG;IACH,KAAK,EAAG,MAAM,IAAI,CAAC;IACnB,YAAY,EAAG,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,eAAe,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAkBvE;AAED;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,MAAM,CAAC,CAOjB"}
@@ -5,8 +5,20 @@ export class LLMSession extends SharedObject {
5
5
  // Methods are implemented natively via the Class() DSL.
6
6
  // TypeScript declarations provide type safety only.
7
7
  respond;
8
+ /**
9
+ * Streams a response, emitting `token`/`partial` events along the way.
10
+ * Resolves with the final text when the stream completes, or with the
11
+ * partial text produced so far if the stream is cancelled via
12
+ * `cancelStream()`. Rejects if the stream fails.
13
+ */
8
14
  streamResponse;
9
15
  cancelStream;
16
+ /**
17
+ * Clears the conversation transcript while keeping instructions, tools,
18
+ * schema, and generation options. Cancels any in-flight stream. Cheap —
19
+ * the model weights are OS-shared; only the conversation state is rebuilt.
20
+ */
21
+ reset;
10
22
  registerTool;
11
23
  unregisterTool;
12
24
  resolveToolCall;
@@ -29,4 +41,23 @@ export function createLLMSession(config = {}) {
29
41
  };
30
42
  return new ExpoLocalLlmModule.LLMSession(nativeConfig);
31
43
  }
44
+ /**
45
+ * One-shot, stateless generation: creates a session, responds once, and
46
+ * releases the session. Use this for classification/extraction calls where
47
+ * conversation history is unwanted — no session bookkeeping, no transcript
48
+ * accumulation across calls.
49
+ *
50
+ * Throws `UnavailabilityError` when the native module is missing; rejects
51
+ * with the native error when the model is not available. Callers that need
52
+ * an availability check first can use `ExpoLocalLlmModule.getAvailability()`.
53
+ */
54
+ export async function generate(prompt, config = {}) {
55
+ const session = createLLMSession(config);
56
+ try {
57
+ return await session.respond(prompt);
58
+ }
59
+ finally {
60
+ session.release();
61
+ }
62
+ }
32
63
  //# sourceMappingURL=LLMSession.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LLMSession.js","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAOtE,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,MAAM,OAAO,UAAW,SAAQ,YAA8B;IAC5D,wDAAwD;IACxD,oDAAoD;IACpD,OAAO,CAAuC;IAC9C,cAAc,CAAqC;IACnD,YAAY,CAAuB;IACnC,YAAY,CAAgC;IAC5C,cAAc,CAA0B;IACxC,eAAe,CAA4C;IAC3D,cAAc,CAA2C;CAC1D;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE;IACzD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,MAAM,YAAY,GAAG;QACnB,GAAG,MAAM;QACT,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;KACzD,CAAC;IACF,OAAO,IAAI,kBAAkB,CAAC,UAAU,CAAC,YAAY,CAAe,CAAC;AACvE,CAAC","sourcesContent":["import { SharedObject, UnavailabilityError } from \"expo-modules-core\";\n\nimport type {\n SessionConfig,\n LLMSessionEvents,\n ToolConfig,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { validateSchema, SchemaInvalidError } from \"./validateSchema\";\n\nexport class LLMSession extends SharedObject<LLMSessionEvents> {\n // Methods are implemented natively via the Class() DSL.\n // TypeScript declarations provide type safety only.\n respond!: (prompt: string) => Promise<string>;\n streamResponse!: (prompt: string) => Promise<void>;\n cancelStream!: () => Promise<void>;\n registerTool!: (config: ToolConfig) => void;\n unregisterTool!: (name: string) => void;\n resolveToolCall!: (callId: string, result: string) => void;\n rejectToolCall!: (callId: string, error: string) => void;\n}\n\nexport function createLLMSession(config: SessionConfig = {}): LLMSession {\n if (!ExpoLocalLlmModule) {\n throw new UnavailabilityError(\"ExpoLocalLlm\", \"createLLMSession\");\n }\n\n if (config.schema) {\n const result = validateSchema(config.schema);\n if (!result.ok) {\n throw new SchemaInvalidError(result.errors);\n }\n }\n\n // Strip handler functions before passing to native — native doesn't need them\n const nativeConfig = {\n ...config,\n tools: config.tools?.map(({ handler, ...rest }) => rest),\n };\n return new ExpoLocalLlmModule.LLMSession(nativeConfig) as LLMSession;\n}\n"]}
1
+ {"version":3,"file":"LLMSession.js","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAOtE,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,MAAM,OAAO,UAAW,SAAQ,YAA8B;IAC5D,wDAAwD;IACxD,oDAAoD;IACpD,OAAO,CAAuC;IAC9C;;;;;OAKG;IACH,cAAc,CAAuC;IACrD,YAAY,CAAuB;IACnC;;;;OAIG;IACH,KAAK,CAAc;IACnB,YAAY,CAAgC;IAC5C,cAAc,CAA0B;IACxC,eAAe,CAA4C;IAC3D,cAAc,CAA2C;CAC1D;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE;IACzD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,MAAM,YAAY,GAAG;QACnB,GAAG,MAAM;QACT,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;KACzD,CAAC;IACF,OAAO,IAAI,kBAAkB,CAAC,UAAU,CAAC,YAAY,CAAe,CAAC;AACvE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAc,EACd,SAAwB,EAAE;IAE1B,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC;AACH,CAAC","sourcesContent":["import { SharedObject, UnavailabilityError } from \"expo-modules-core\";\n\nimport type {\n SessionConfig,\n LLMSessionEvents,\n ToolConfig,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { validateSchema, SchemaInvalidError } from \"./validateSchema\";\n\nexport class LLMSession extends SharedObject<LLMSessionEvents> {\n // Methods are implemented natively via the Class() DSL.\n // TypeScript declarations provide type safety only.\n respond!: (prompt: string) => Promise<string>;\n /**\n * Streams a response, emitting `token`/`partial` events along the way.\n * Resolves with the final text when the stream completes, or with the\n * partial text produced so far if the stream is cancelled via\n * `cancelStream()`. Rejects if the stream fails.\n */\n streamResponse!: (prompt: string) => Promise<string>;\n cancelStream!: () => Promise<void>;\n /**\n * Clears the conversation transcript while keeping instructions, tools,\n * schema, and generation options. Cancels any in-flight stream. Cheap —\n * the model weights are OS-shared; only the conversation state is rebuilt.\n */\n reset!: () => void;\n registerTool!: (config: ToolConfig) => void;\n unregisterTool!: (name: string) => void;\n resolveToolCall!: (callId: string, result: string) => void;\n rejectToolCall!: (callId: string, error: string) => void;\n}\n\nexport function createLLMSession(config: SessionConfig = {}): LLMSession {\n if (!ExpoLocalLlmModule) {\n throw new UnavailabilityError(\"ExpoLocalLlm\", \"createLLMSession\");\n }\n\n if (config.schema) {\n const result = validateSchema(config.schema);\n if (!result.ok) {\n throw new SchemaInvalidError(result.errors);\n }\n }\n\n // Strip handler functions before passing to native — native doesn't need them\n const nativeConfig = {\n ...config,\n tools: config.tools?.map(({ handler, ...rest }) => rest),\n };\n return new ExpoLocalLlmModule.LLMSession(nativeConfig) as LLMSession;\n}\n\n/**\n * One-shot, stateless generation: creates a session, responds once, and\n * releases the session. Use this for classification/extraction calls where\n * conversation history is unwanted — no session bookkeeping, no transcript\n * accumulation across calls.\n *\n * Throws `UnavailabilityError` when the native module is missing; rejects\n * with the native error when the model is not available. Callers that need\n * an availability check first can use `ExpoLocalLlmModule.getAvailability()`.\n */\nexport async function generate(\n prompt: string,\n config: SessionConfig = {}\n): Promise<string> {\n const session = createLLMSession(config);\n try {\n return await session.respond(prompt);\n } finally {\n session.release();\n }\n}\n"]}
package/build/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { useLocalLLM } from "./useLocalLLM";
2
- export { LLMSession, createLLMSession } from "./LLMSession";
2
+ export { LLMSession, createLLMSession, generate } from "./LLMSession";
3
3
  export { default as ExpoLocalLlmModule } from "./ExpoLocalLlmModule";
4
4
  export { validateSchema, SchemaInvalidError, type SchemaValidationError, type SchemaValidationResult, } from "./validateSchema";
5
5
  export * from "./ExpoLocalLlm.types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,cAAc,sBAAsB,CAAC"}
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { useLocalLLM } from "./useLocalLLM";
2
- export { LLMSession, createLLMSession } from "./LLMSession";
2
+ export { LLMSession, createLLMSession, generate } from "./LLMSession";
3
3
  export { default as ExpoLocalLlmModule } from "./ExpoLocalLlmModule";
4
4
  export { validateSchema, SchemaInvalidError, } from "./validateSchema";
5
5
  export * from "./ExpoLocalLlm.types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,kBAAkB,GAGnB,MAAM,kBAAkB,CAAC;AAC1B,cAAc,sBAAsB,CAAC","sourcesContent":["export { useLocalLLM } from \"./useLocalLLM\";\nexport { LLMSession, createLLMSession } from \"./LLMSession\";\nexport { default as ExpoLocalLlmModule } from \"./ExpoLocalLlmModule\";\nexport {\n validateSchema,\n SchemaInvalidError,\n type SchemaValidationError,\n type SchemaValidationResult,\n} from \"./validateSchema\";\nexport * from \"./ExpoLocalLlm.types\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,kBAAkB,GAGnB,MAAM,kBAAkB,CAAC;AAC1B,cAAc,sBAAsB,CAAC","sourcesContent":["export { useLocalLLM } from \"./useLocalLLM\";\nexport { LLMSession, createLLMSession, generate } from \"./LLMSession\";\nexport { default as ExpoLocalLlmModule } from \"./ExpoLocalLlmModule\";\nexport {\n validateSchema,\n SchemaInvalidError,\n type SchemaValidationError,\n type SchemaValidationResult,\n} from \"./validateSchema\";\nexport * from \"./ExpoLocalLlm.types\";\n"]}
@@ -12,8 +12,9 @@ type UseLocalLLMResult = {
12
12
  error: string | null;
13
13
  activeToolCalls: ActiveToolCall[];
14
14
  respond?: (prompt: string) => Promise<string>;
15
- streamResponse?: (prompt: string) => Promise<void>;
15
+ streamResponse?: (prompt: string) => Promise<string>;
16
16
  cancelStream?: () => Promise<void>;
17
+ reset?: () => void;
17
18
  downloadModel?: () => Promise<void>;
18
19
  };
19
20
  export declare function useLocalLLM(options?: UseLocalLLMOptions): UseLocalLLMResult;
@@ -1 +1 @@
1
- {"version":3,"file":"useLocalLLM.d.ts","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EAQb,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,KAAK,kBAAkB,GAAG,aAAa,CAAC;AAExC,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,iBAAiB,CAAC;IAChC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAEF,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAgSnB"}
1
+ {"version":3,"file":"useLocalLLM.d.ts","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EAQb,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,KAAK,kBAAkB,GAAG,aAAa,CAAC;AAExC,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,iBAAiB,CAAC;IAChC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAEF,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAoTnB"}
@@ -154,7 +154,11 @@ export function useLocalLLM(options = {}) {
154
154
  subs.forEach((s) => s.remove());
155
155
  session?.release();
156
156
  };
157
- }, [session]);
157
+ // isJSONMode: used by the streamComplete listener. Today it can only
158
+ // change together with a session recreation (responseFormat/schema are
159
+ // in stableConfig), but depending on it directly removes the reliance
160
+ // on that invariant.
161
+ }, [session, isJSONMode]);
158
162
  const respond = useCallback(async (prompt) => {
159
163
  if (!session)
160
164
  throw new Error("Session not available");
@@ -181,13 +185,17 @@ export function useLocalLLM(options = {}) {
181
185
  setStreamedObject(null);
182
186
  setIsGenerating(true);
183
187
  try {
184
- await session.streamResponse(prompt);
188
+ // Resolves with the final text at stream end (or the partial text
189
+ // if cancelled); rejects if the stream fails.
190
+ return await session.streamResponse(prompt);
185
191
  }
186
192
  catch (e) {
187
193
  setRuntimeError(e.message);
188
- setIsGenerating(false);
189
194
  throw e;
190
195
  }
196
+ finally {
197
+ setIsGenerating(false);
198
+ }
191
199
  }, [session]);
192
200
  const cancelStream = useCallback(async () => {
193
201
  if (!session)
@@ -195,6 +203,17 @@ export function useLocalLLM(options = {}) {
195
203
  await session.cancelStream();
196
204
  setIsGenerating(false);
197
205
  }, [session]);
206
+ const reset = useCallback(() => {
207
+ if (!session)
208
+ return;
209
+ session.reset();
210
+ setStreamedText("");
211
+ setStreamedJSON("");
212
+ setStreamedObject(null);
213
+ setRuntimeError(null);
214
+ setActiveToolCalls([]);
215
+ setIsGenerating(false);
216
+ }, [session]);
198
217
  const downloadModel = useCallback(async () => {
199
218
  if (!ExpoLocalLlmModule)
200
219
  return;
@@ -226,6 +245,8 @@ export function useLocalLLM(options = {}) {
226
245
  respond: session && availability === "available" ? respond : undefined,
227
246
  streamResponse: session && availability === "available" ? streamResponse : undefined,
228
247
  cancelStream: session && availability === "available" ? cancelStream : undefined,
248
+ // Resetting is safe in any availability state — gate on session only.
249
+ reset: session ? reset : undefined,
229
250
  downloadModel,
230
251
  };
231
252
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useLocalLLM.js","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState, useCallback, useMemo, useRef } from \"react\";\n\nimport type {\n ModelAvailability,\n SessionConfig,\n TokenEvent,\n PartialEvent,\n StreamCompleteEvent,\n StreamErrorEvent,\n DownloadProgress,\n ToolDefinition,\n ToolCallEvent,\n ActiveToolCall,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { createLLMSession, LLMSession } from \"./LLMSession\";\n\ntype UseLocalLLMOptions = SessionConfig;\n\ntype UseLocalLLMResult = {\n availability: ModelAvailability;\n session: LLMSession | null;\n isGenerating: boolean;\n streamedText: string;\n streamedJSON: string;\n streamedObject: unknown;\n downloadProgress: number | null;\n error: string | null;\n activeToolCalls: ActiveToolCall[];\n respond?: (prompt: string) => Promise<string>;\n streamResponse?: (prompt: string) => Promise<void>;\n cancelStream?: () => Promise<void>;\n downloadModel?: () => Promise<void>;\n};\n\nexport function useLocalLLM(\n options: UseLocalLLMOptions = {},\n): UseLocalLLMResult {\n const [availability, setAvailability] = useState<ModelAvailability>(() =>\n ExpoLocalLlmModule\n ? (ExpoLocalLlmModule.getAvailability() as ModelAvailability)\n : \"unknown\",\n );\n const [isGenerating, setIsGenerating] = useState(false);\n const [streamedText, setStreamedText] = useState(\"\");\n const [streamedJSON, setStreamedJSON] = useState(\"\");\n const [streamedObject, setStreamedObject] = useState<unknown>(null);\n const [downloadProgress, setDownloadProgress] = useState<number | null>(null);\n const [runtimeError, setRuntimeError] = useState<string | null>(null);\n const [activeToolCalls, setActiveToolCalls] = useState<ActiveToolCall[]>([]);\n\n // Keep a ref to the current tool handlers so the event listener\n // always sees the latest handlers without needing to recreate the session.\n const toolHandlersRef = useRef<Map<string, ToolDefinition[\"handler\"]>>(\n new Map(),\n );\n\n // Update handler ref whenever tools change\n useEffect(() => {\n const handlers = new Map<string, ToolDefinition[\"handler\"]>();\n if (options.tools) {\n for (const tool of options.tools) {\n handlers.set(tool.name, tool.handler);\n }\n }\n toolHandlersRef.current = handlers;\n }, [options.tools]);\n\n // Stabilize options to avoid unnecessary session recreation\n // Include tool names/descriptions so session recreates when tools change structurally\n const toolsFingerprint = useMemo(\n () =>\n options.tools\n ?.map(\n (t) => `${t.name}:${t.description}:${JSON.stringify(t.parameters)}`,\n )\n .join(\"|\") ?? \"\",\n [options.tools],\n );\n\n // Fingerprint the schema so session recreates when it changes\n const schemaFingerprint = useMemo(\n () => (options.schema ? JSON.stringify(options.schema) : \"\"),\n [options.schema],\n );\n\n const stableConfig = useMemo(\n () => options,\n [\n options.instructions,\n options.options?.temperature,\n options.options?.maxTokens,\n options.options?.topK,\n options.toolTimeout,\n options.responseFormat,\n options.includeSchemaInPrompt,\n schemaFingerprint,\n toolsFingerprint,\n ],\n );\n\n const isJSONMode = options.responseFormat === \"json\" && !!options.schema;\n\n const sessionResult = useMemo<{\n session: LLMSession | null;\n creationError: string | null;\n }>(() => {\n if (!ExpoLocalLlmModule) return { session: null, creationError: null };\n try {\n return { session: createLLMSession(stableConfig), creationError: null };\n } catch (e: any) {\n return {\n session: null,\n creationError: e.message ?? \"Failed to create LLM session\",\n };\n }\n }, [stableConfig]);\n const session = sessionResult.session;\n const error = sessionResult.creationError ?? runtimeError;\n\n useEffect(() => {\n if (!ExpoLocalLlmModule) return;\n\n const subs: { remove(): void }[] = [];\n\n subs.push(\n ExpoLocalLlmModule.addListener(\n \"downloadProgress\",\n (event: DownloadProgress) => {\n setDownloadProgress(event.progress);\n },\n ),\n );\n subs.push(\n ExpoLocalLlmModule.addListener(\"availabilityChange\", (event) => {\n setAvailability(event.availability);\n }),\n );\n\n if (session) {\n subs.push(\n session.addListener(\"token\", (event: TokenEvent) => {\n setStreamedText(event.accumulated);\n }),\n );\n subs.push(\n session.addListener(\"partial\", (event: PartialEvent) => {\n setStreamedJSON(event.json);\n try {\n setStreamedObject(JSON.parse(event.json));\n } catch {\n // Partial may not be parseable yet; keep the previous parsed value.\n }\n }),\n );\n subs.push(\n session.addListener(\"streamComplete\", (event: StreamCompleteEvent) => {\n if (isJSONMode) {\n setStreamedJSON(event.text);\n try {\n setStreamedObject(JSON.parse(event.text));\n } catch {\n // Final JSON should always parse; leave previous value if it doesn't.\n }\n } else {\n setStreamedText(event.text);\n }\n setIsGenerating(false);\n }),\n );\n subs.push(\n session.addListener(\"streamError\", (event: StreamErrorEvent) => {\n setRuntimeError(event.error);\n setIsGenerating(false);\n }),\n );\n\n // Tool call event listener\n subs.push(\n session.addListener(\"toolCall\", (event: ToolCallEvent) => {\n const activeCall = { callId: event.callId, toolName: event.toolName };\n setActiveToolCalls((prev) => [...prev, activeCall]);\n\n const removeActiveCall = () => {\n setActiveToolCalls((prev) =>\n prev.filter((c) => c.callId !== event.callId),\n );\n };\n\n const handler = toolHandlersRef.current.get(event.toolName);\n if (!handler) {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n `No handler registered for tool: ${event.toolName}`,\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n let result: Promise<string>;\n try {\n result = handler(event.arguments);\n } catch (e: any) {\n // Handler threw synchronously before returning a promise\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\",\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n result\n .then((value) => {\n removeActiveCall();\n session.resolveToolCall(event.callId, value);\n })\n .catch((e: any) => {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\",\n );\n } catch {\n // Ignore if session is already torn down\n }\n });\n }),\n );\n }\n\n return () => {\n subs.forEach((s) => s.remove());\n session?.release();\n };\n }, [session]);\n\n const respond = useCallback(\n async (prompt: string): Promise<string> => {\n if (!session) throw new Error(\"Session not available\");\n setRuntimeError(null);\n setIsGenerating(true);\n try {\n const result = await session.respond(prompt);\n return result;\n } catch (e: any) {\n setRuntimeError(e.message);\n throw e;\n } finally {\n setIsGenerating(false);\n }\n },\n [session],\n );\n\n const streamResponse = useCallback(\n async (prompt: string): Promise<void> => {\n if (!session) throw new Error(\"Session not available\");\n setRuntimeError(null);\n setStreamedText(\"\");\n setStreamedJSON(\"\");\n setStreamedObject(null);\n setIsGenerating(true);\n try {\n await session.streamResponse(prompt);\n } catch (e: any) {\n setRuntimeError(e.message);\n setIsGenerating(false);\n throw e;\n }\n },\n [session],\n );\n\n const cancelStream = useCallback(async (): Promise<void> => {\n if (!session) return;\n await session.cancelStream();\n setIsGenerating(false);\n }, [session]);\n\n const downloadModel = useCallback(async (): Promise<void> => {\n if (!ExpoLocalLlmModule) return;\n await ExpoLocalLlmModule.downloadModel();\n }, []);\n\n if (!ExpoLocalLlmModule) {\n return {\n availability: \"notEligible\",\n session: null,\n isGenerating: false,\n streamedText: \"\",\n streamedJSON: \"\",\n streamedObject: null,\n downloadProgress: null,\n error: null,\n activeToolCalls: [],\n };\n }\n\n return {\n availability,\n session,\n isGenerating,\n streamedText,\n streamedJSON,\n streamedObject,\n downloadProgress,\n error,\n activeToolCalls,\n respond: session && availability === \"available\" ? respond : undefined,\n streamResponse:\n session && availability === \"available\" ? streamResponse : undefined,\n cancelStream:\n session && availability === \"available\" ? cancelStream : undefined,\n downloadModel,\n };\n}\n"]}
1
+ {"version":3,"file":"useLocalLLM.js","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState, useCallback, useMemo, useRef } from \"react\";\n\nimport type {\n ModelAvailability,\n SessionConfig,\n TokenEvent,\n PartialEvent,\n StreamCompleteEvent,\n StreamErrorEvent,\n DownloadProgress,\n ToolDefinition,\n ToolCallEvent,\n ActiveToolCall,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { createLLMSession, LLMSession } from \"./LLMSession\";\n\ntype UseLocalLLMOptions = SessionConfig;\n\ntype UseLocalLLMResult = {\n availability: ModelAvailability;\n session: LLMSession | null;\n isGenerating: boolean;\n streamedText: string;\n streamedJSON: string;\n streamedObject: unknown;\n downloadProgress: number | null;\n error: string | null;\n activeToolCalls: ActiveToolCall[];\n respond?: (prompt: string) => Promise<string>;\n streamResponse?: (prompt: string) => Promise<string>;\n cancelStream?: () => Promise<void>;\n reset?: () => void;\n downloadModel?: () => Promise<void>;\n};\n\nexport function useLocalLLM(\n options: UseLocalLLMOptions = {}\n): UseLocalLLMResult {\n const [availability, setAvailability] = useState<ModelAvailability>(() =>\n ExpoLocalLlmModule\n ? (ExpoLocalLlmModule.getAvailability() as ModelAvailability)\n : \"unknown\"\n );\n const [isGenerating, setIsGenerating] = useState(false);\n const [streamedText, setStreamedText] = useState(\"\");\n const [streamedJSON, setStreamedJSON] = useState(\"\");\n const [streamedObject, setStreamedObject] = useState<unknown>(null);\n const [downloadProgress, setDownloadProgress] = useState<number | null>(null);\n const [runtimeError, setRuntimeError] = useState<string | null>(null);\n const [activeToolCalls, setActiveToolCalls] = useState<ActiveToolCall[]>([]);\n\n // Keep a ref to the current tool handlers so the event listener\n // always sees the latest handlers without needing to recreate the session.\n const toolHandlersRef = useRef<Map<string, ToolDefinition[\"handler\"]>>(\n new Map()\n );\n\n // Update handler ref whenever tools change\n useEffect(() => {\n const handlers = new Map<string, ToolDefinition[\"handler\"]>();\n if (options.tools) {\n for (const tool of options.tools) {\n handlers.set(tool.name, tool.handler);\n }\n }\n toolHandlersRef.current = handlers;\n }, [options.tools]);\n\n // Stabilize options to avoid unnecessary session recreation\n // Include tool names/descriptions so session recreates when tools change structurally\n const toolsFingerprint = useMemo(\n () =>\n options.tools\n ?.map(\n (t) => `${t.name}:${t.description}:${JSON.stringify(t.parameters)}`\n )\n .join(\"|\") ?? \"\",\n [options.tools]\n );\n\n // Fingerprint the schema so session recreates when it changes\n const schemaFingerprint = useMemo(\n () => (options.schema ? JSON.stringify(options.schema) : \"\"),\n [options.schema]\n );\n\n const stableConfig = useMemo(\n () => options,\n [\n options.instructions,\n options.options?.temperature,\n options.options?.maxTokens,\n options.options?.topK,\n options.toolTimeout,\n options.responseFormat,\n options.includeSchemaInPrompt,\n schemaFingerprint,\n toolsFingerprint,\n ]\n );\n\n const isJSONMode = options.responseFormat === \"json\" && !!options.schema;\n\n const sessionResult = useMemo<{\n session: LLMSession | null;\n creationError: string | null;\n }>(() => {\n if (!ExpoLocalLlmModule) return { session: null, creationError: null };\n try {\n return { session: createLLMSession(stableConfig), creationError: null };\n } catch (e: any) {\n return {\n session: null,\n creationError: e.message ?? \"Failed to create LLM session\",\n };\n }\n }, [stableConfig]);\n const session = sessionResult.session;\n const error = sessionResult.creationError ?? runtimeError;\n\n useEffect(() => {\n if (!ExpoLocalLlmModule) return;\n\n const subs: { remove(): void }[] = [];\n\n subs.push(\n ExpoLocalLlmModule.addListener(\n \"downloadProgress\",\n (event: DownloadProgress) => {\n setDownloadProgress(event.progress);\n }\n )\n );\n subs.push(\n ExpoLocalLlmModule.addListener(\"availabilityChange\", (event) => {\n setAvailability(event.availability);\n })\n );\n\n if (session) {\n subs.push(\n session.addListener(\"token\", (event: TokenEvent) => {\n setStreamedText(event.accumulated);\n })\n );\n subs.push(\n session.addListener(\"partial\", (event: PartialEvent) => {\n setStreamedJSON(event.json);\n try {\n setStreamedObject(JSON.parse(event.json));\n } catch {\n // Partial may not be parseable yet; keep the previous parsed value.\n }\n })\n );\n subs.push(\n session.addListener(\"streamComplete\", (event: StreamCompleteEvent) => {\n if (isJSONMode) {\n setStreamedJSON(event.text);\n try {\n setStreamedObject(JSON.parse(event.text));\n } catch {\n // Final JSON should always parse; leave previous value if it doesn't.\n }\n } else {\n setStreamedText(event.text);\n }\n setIsGenerating(false);\n })\n );\n subs.push(\n session.addListener(\"streamError\", (event: StreamErrorEvent) => {\n setRuntimeError(event.error);\n setIsGenerating(false);\n })\n );\n\n // Tool call event listener\n subs.push(\n session.addListener(\"toolCall\", (event: ToolCallEvent) => {\n const activeCall = { callId: event.callId, toolName: event.toolName };\n setActiveToolCalls((prev) => [...prev, activeCall]);\n\n const removeActiveCall = () => {\n setActiveToolCalls((prev) =>\n prev.filter((c) => c.callId !== event.callId)\n );\n };\n\n const handler = toolHandlersRef.current.get(event.toolName);\n if (!handler) {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n `No handler registered for tool: ${event.toolName}`\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n let result: Promise<string>;\n try {\n result = handler(event.arguments);\n } catch (e: any) {\n // Handler threw synchronously before returning a promise\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\"\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n result\n .then((value) => {\n removeActiveCall();\n session.resolveToolCall(event.callId, value);\n })\n .catch((e: any) => {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\"\n );\n } catch {\n // Ignore if session is already torn down\n }\n });\n })\n );\n }\n\n return () => {\n subs.forEach((s) => s.remove());\n session?.release();\n };\n // isJSONMode: used by the streamComplete listener. Today it can only\n // change together with a session recreation (responseFormat/schema are\n // in stableConfig), but depending on it directly removes the reliance\n // on that invariant.\n }, [session, isJSONMode]);\n\n const respond = useCallback(\n async (prompt: string): Promise<string> => {\n if (!session) throw new Error(\"Session not available\");\n setRuntimeError(null);\n setIsGenerating(true);\n try {\n const result = await session.respond(prompt);\n return result;\n } catch (e: any) {\n setRuntimeError(e.message);\n throw e;\n } finally {\n setIsGenerating(false);\n }\n },\n [session]\n );\n\n const streamResponse = useCallback(\n async (prompt: string): Promise<string> => {\n if (!session) throw new Error(\"Session not available\");\n setRuntimeError(null);\n setStreamedText(\"\");\n setStreamedJSON(\"\");\n setStreamedObject(null);\n setIsGenerating(true);\n try {\n // Resolves with the final text at stream end (or the partial text\n // if cancelled); rejects if the stream fails.\n return await session.streamResponse(prompt);\n } catch (e: any) {\n setRuntimeError(e.message);\n throw e;\n } finally {\n setIsGenerating(false);\n }\n },\n [session]\n );\n\n const cancelStream = useCallback(async (): Promise<void> => {\n if (!session) return;\n await session.cancelStream();\n setIsGenerating(false);\n }, [session]);\n\n const reset = useCallback((): void => {\n if (!session) return;\n session.reset();\n setStreamedText(\"\");\n setStreamedJSON(\"\");\n setStreamedObject(null);\n setRuntimeError(null);\n setActiveToolCalls([]);\n setIsGenerating(false);\n }, [session]);\n\n const downloadModel = useCallback(async (): Promise<void> => {\n if (!ExpoLocalLlmModule) return;\n await ExpoLocalLlmModule.downloadModel();\n }, []);\n\n if (!ExpoLocalLlmModule) {\n return {\n availability: \"notEligible\",\n session: null,\n isGenerating: false,\n streamedText: \"\",\n streamedJSON: \"\",\n streamedObject: null,\n downloadProgress: null,\n error: null,\n activeToolCalls: [],\n };\n }\n\n return {\n availability,\n session,\n isGenerating,\n streamedText,\n streamedJSON,\n streamedObject,\n downloadProgress,\n error,\n activeToolCalls,\n respond: session && availability === \"available\" ? respond : undefined,\n streamResponse:\n session && availability === \"available\" ? streamResponse : undefined,\n cancelStream:\n session && availability === \"available\" ? cancelStream : undefined,\n // Resetting is safe in any availability state — gate on session only.\n reset: session ? reset : undefined,\n downloadModel,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validateSchema.d.ts","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,EAAE,CAAA;CAAE,CAAC;AAEnD,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAM5C;AAqFD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,sBAAsB,CAmBtE;AAGD,YAAY,EAAE,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"validateSchema.d.ts","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,EAAE,CAAA;CAAE,CAAC;AAEnD,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAM5C;AAuFD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,sBAAsB,CAmBtE;AAGD,YAAY,EAAE,MAAM,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"validateSchema.js","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Schema } from \"./ExpoLocalLlm.types\";\n\nexport type SchemaValidationError = {\n path: string;\n message: string;\n};\n\nexport type SchemaValidationResult =\n | { ok: true }\n | { ok: false; errors: SchemaValidationError[] };\n\nexport class SchemaInvalidError extends Error {\n readonly errors: SchemaValidationError[];\n constructor(errors: SchemaValidationError[]) {\n const summary = errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n super(`Invalid schema:\\n${summary}`);\n this.name = \"SchemaInvalidError\";\n this.errors = errors;\n }\n}\n\nconst ALLOWED_TYPES = new Set([\n \"string\",\n \"number\",\n \"integer\",\n \"boolean\",\n \"array\",\n \"object\",\n]);\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction validateField(\n field: unknown,\n path: string,\n errors: SchemaValidationError[],\n): void {\n if (!isPlainObject(field)) {\n errors.push({ path, message: \"field must be an object\" });\n return;\n }\n\n const type = field.type;\n if (typeof type !== \"string\" || !ALLOWED_TYPES.has(type)) {\n errors.push({\n path,\n message: `type must be one of: string, number, integer, boolean, array, object (got: ${JSON.stringify(type)})`,\n });\n return;\n }\n\n if (\"enum\" in field) {\n if (type !== \"string\") {\n errors.push({\n path,\n message: `enum is only valid on type 'string' (got type: '${type}')`,\n });\n } else if (!Array.isArray(field.enum) || field.enum.length === 0) {\n errors.push({\n path,\n message: \"enum must be a non-empty array of strings\",\n });\n } else if (!field.enum.every((v) => typeof v === \"string\")) {\n errors.push({\n path,\n message: \"enum values must all be strings\",\n });\n }\n }\n\n if (type === \"array\") {\n if (!(\"items\" in field)) {\n errors.push({ path, message: \"type 'array' requires 'items'\" });\n } else {\n validateField(field.items, `${path}.items`, errors);\n }\n }\n\n if (type === \"object\") {\n if (!(\"properties\" in field)) {\n errors.push({ path, message: \"type 'object' requires 'properties'\" });\n } else if (!isPlainObject(field.properties)) {\n errors.push({\n path: `${path}.properties`,\n message: \"properties must be an object\",\n });\n } else {\n const entries = Object.entries(field.properties);\n if (entries.length === 0) {\n errors.push({\n path: `${path}.properties`,\n message: \"properties must be non-empty\",\n });\n } else {\n for (const [key, subField] of entries) {\n validateField(subField, `${path}.${key}`, errors);\n }\n }\n }\n }\n}\n\nexport function validateSchema(schema: unknown): SchemaValidationResult {\n const errors: SchemaValidationError[] = [];\n\n if (!isPlainObject(schema)) {\n errors.push({ path: \"$\", message: \"schema must be an object\" });\n return { ok: false, errors };\n }\n\n const entries = Object.entries(schema);\n if (entries.length === 0) {\n errors.push({ path: \"$\", message: \"schema must be a non-empty object\" });\n return { ok: false, errors };\n }\n\n for (const [key, field] of entries) {\n validateField(field, key, errors);\n }\n\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n}\n\n// Re-export Schema so callers building schemas at runtime have one import surface.\nexport type { Schema };\n"]}
1
+ {"version":3,"file":"validateSchema.js","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Schema } from \"./ExpoLocalLlm.types\";\n\nexport type SchemaValidationError = {\n path: string;\n message: string;\n};\n\nexport type SchemaValidationResult =\n | { ok: true }\n | { ok: false; errors: SchemaValidationError[] };\n\nexport class SchemaInvalidError extends Error {\n readonly errors: SchemaValidationError[];\n constructor(errors: SchemaValidationError[]) {\n const summary = errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n super(`Invalid schema:\\n${summary}`);\n this.name = \"SchemaInvalidError\";\n this.errors = errors;\n }\n}\n\nconst ALLOWED_TYPES = new Set([\n \"string\",\n \"number\",\n \"integer\",\n \"boolean\",\n \"array\",\n \"object\",\n]);\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction validateField(\n field: unknown,\n path: string,\n errors: SchemaValidationError[]\n): void {\n if (!isPlainObject(field)) {\n errors.push({ path, message: \"field must be an object\" });\n return;\n }\n\n const type = field.type;\n if (typeof type !== \"string\" || !ALLOWED_TYPES.has(type)) {\n errors.push({\n path,\n message: `type must be one of: string, number, integer, boolean, array, object (got: ${JSON.stringify(\n type\n )})`,\n });\n return;\n }\n\n if (\"enum\" in field) {\n if (type !== \"string\") {\n errors.push({\n path,\n message: `enum is only valid on type 'string' (got type: '${type}')`,\n });\n } else if (!Array.isArray(field.enum) || field.enum.length === 0) {\n errors.push({\n path,\n message: \"enum must be a non-empty array of strings\",\n });\n } else if (!field.enum.every((v) => typeof v === \"string\")) {\n errors.push({\n path,\n message: \"enum values must all be strings\",\n });\n }\n }\n\n if (type === \"array\") {\n if (!(\"items\" in field)) {\n errors.push({ path, message: \"type 'array' requires 'items'\" });\n } else {\n validateField(field.items, `${path}.items`, errors);\n }\n }\n\n if (type === \"object\") {\n if (!(\"properties\" in field)) {\n errors.push({ path, message: \"type 'object' requires 'properties'\" });\n } else if (!isPlainObject(field.properties)) {\n errors.push({\n path: `${path}.properties`,\n message: \"properties must be an object\",\n });\n } else {\n const entries = Object.entries(field.properties);\n if (entries.length === 0) {\n errors.push({\n path: `${path}.properties`,\n message: \"properties must be non-empty\",\n });\n } else {\n for (const [key, subField] of entries) {\n validateField(subField, `${path}.${key}`, errors);\n }\n }\n }\n }\n}\n\nexport function validateSchema(schema: unknown): SchemaValidationResult {\n const errors: SchemaValidationError[] = [];\n\n if (!isPlainObject(schema)) {\n errors.push({ path: \"$\", message: \"schema must be an object\" });\n return { ok: false, errors };\n }\n\n const entries = Object.entries(schema);\n if (entries.length === 0) {\n errors.push({ path: \"$\", message: \"schema must be a non-empty object\" });\n return { ok: false, errors };\n }\n\n for (const [key, field] of entries) {\n validateField(field, key, errors);\n }\n\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n}\n\n// Re-export Schema so callers building schemas at runtime have one import surface.\nexport type { Schema };\n"]}
@@ -78,13 +78,17 @@ final class DynamicTool: Tool {
78
78
  return try await withCheckedThrowingContinuation { continuation in
79
79
  continuationStore.store(callId: callId, continuation: continuation)
80
80
 
81
- // Schedule timeout
82
- Task {
81
+ // Schedule timeout — cancelled by the store when the call resolves,
82
+ // so it doesn't linger for the full duration after a fast handler.
83
+ let timeoutSeconds = self.timeoutSeconds
84
+ let timeoutTask = Task { [continuationStore] in
83
85
  try? await Task.sleep(for: .seconds(timeoutSeconds))
86
+ guard !Task.isCancelled else { return }
84
87
  if let timedOut = continuationStore.remove(callId: callId) {
85
88
  timedOut.resume(throwing: DynamicToolError.timeout)
86
89
  }
87
90
  }
91
+ continuationStore.attachTimeout(callId: callId, task: timeoutTask)
88
92
  }
89
93
  }
90
94
 
@@ -111,20 +115,39 @@ final class DynamicTool: Tool {
111
115
  /// Thread-safe store for pending tool call continuations.
112
116
  @available(iOS 26, *)
113
117
  final class ToolContinuationStore: @unchecked Sendable {
114
- private var continuations: [String: CheckedContinuation<String, Error>] = [:]
118
+ private struct Pending {
119
+ let continuation: CheckedContinuation<String, Error>
120
+ var timeoutTask: Task<Void, Never>?
121
+ }
122
+
123
+ private var pending: [String: Pending] = [:]
115
124
  private let lock = NSLock()
116
125
 
117
126
  func store(callId: String, continuation: CheckedContinuation<String, Error>) {
118
127
  lock.lock()
119
- continuations[callId] = continuation
128
+ pending[callId] = Pending(continuation: continuation, timeoutTask: nil)
120
129
  lock.unlock()
121
130
  }
122
131
 
132
+ /// Attach the timeout task for a call so it can be cancelled on resolve.
133
+ /// If the call already resolved, the task is cancelled immediately.
134
+ func attachTimeout(callId: String, task: Task<Void, Never>) {
135
+ lock.lock()
136
+ if pending[callId] != nil {
137
+ pending[callId]?.timeoutTask = task
138
+ lock.unlock()
139
+ } else {
140
+ lock.unlock()
141
+ task.cancel()
142
+ }
143
+ }
144
+
123
145
  func remove(callId: String) -> CheckedContinuation<String, Error>? {
124
146
  lock.lock()
125
- let cont = continuations.removeValue(forKey: callId)
147
+ let entry = pending.removeValue(forKey: callId)
126
148
  lock.unlock()
127
- return cont
149
+ entry?.timeoutTask?.cancel()
150
+ return entry?.continuation
128
151
  }
129
152
 
130
153
  /// Resolve a pending continuation with a result string.
@@ -144,11 +167,12 @@ final class ToolContinuationStore: @unchecked Sendable {
144
167
  /// Cancel all pending continuations (e.g., on session teardown).
145
168
  func cancelAll() {
146
169
  lock.lock()
147
- let pending = continuations
148
- continuations.removeAll()
170
+ let entries = pending
171
+ pending.removeAll()
149
172
  lock.unlock()
150
- for (_, continuation) in pending {
151
- continuation.resume(throwing: CancellationError())
173
+ for (_, entry) in entries {
174
+ entry.timeoutTask?.cancel()
175
+ entry.continuation.resume(throwing: CancellationError())
152
176
  }
153
177
  }
154
178
  }
@@ -28,14 +28,18 @@ public class ExpoLocalLlmModule: Module {
28
28
  return try await session.respond(to: prompt)
29
29
  }
30
30
 
31
- AsyncFunction("streamResponse") { (session: LLMSession, prompt: String) in
32
- try session.startStream(prompt: prompt)
31
+ AsyncFunction("streamResponse") { (session: LLMSession, prompt: String) -> String in
32
+ return try await session.streamResponse(prompt: prompt)
33
33
  }
34
34
 
35
35
  AsyncFunction("cancelStream") { (session: LLMSession) in
36
36
  session.cancelStream()
37
37
  }
38
38
 
39
+ Function("reset") { (session: LLMSession) in
40
+ try session.reset()
41
+ }
42
+
39
43
  // Tool calling support
40
44
  Function("registerTool") { (session: LLMSession, config: ToolConfig) in
41
45
  session.registerTool(config: config)