@independo/capacitor-inderun 1.0.1-dev.1 → 1.1.0-dev.2
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/Package.swift +6 -2
- package/README.md +97 -9
- package/android/build.gradle.kts +5 -5
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunCapacitorPlugin.kt +119 -0
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunSerializer.kt +106 -1
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunStreamRegistry.kt +118 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerStreamTest.kt +226 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunStreamRegistryTest.kt +163 -0
- package/dist/definitions.d.ts +54 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -13
- package/dist/streaming.d.ts +17 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +303 -0
- package/dist/web.d.ts +17 -2
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +70 -0
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorBridge.swift +121 -0
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorPlugin.swift +70 -1
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorStreamRegistry.swift +121 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamCodecTests.swift +178 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamPumpTests.swift +215 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamRegistryTests.swift +154 -0
- package/package.json +18 -6
package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerStreamTest.kt
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
package app.independo.inderun.capacitor
|
|
2
|
+
|
|
3
|
+
import app.independo.inderun.contracts.FinishReason
|
|
4
|
+
import app.independo.inderun.contracts.IndeRunErrorClass
|
|
5
|
+
import app.independo.inderun.contracts.Outcome
|
|
6
|
+
import app.independo.inderun.contracts.Payload
|
|
7
|
+
import app.independo.inderun.contracts.PayloadError
|
|
8
|
+
import app.independo.inderun.contracts.PayloadTelemetry
|
|
9
|
+
import app.independo.inderun.contracts.PayloadUsage
|
|
10
|
+
import app.independo.inderun.contracts.Phase
|
|
11
|
+
import app.independo.inderun.contracts.SchemaVersion
|
|
12
|
+
import app.independo.inderun.contracts.StreamEvent
|
|
13
|
+
import app.independo.inderun.contracts.StreamRunHandle
|
|
14
|
+
import org.junit.Assert.assertEquals
|
|
15
|
+
import org.junit.Assert.assertFalse
|
|
16
|
+
import org.junit.Assert.assertTrue
|
|
17
|
+
import org.junit.Test
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encoding is what the JS side actually consumes, so these assert exact key sets: a
|
|
21
|
+
* field that encodes as null instead of being absent changes which union branch the
|
|
22
|
+
* consumer reconstitutes.
|
|
23
|
+
*/
|
|
24
|
+
class IndeRunSerializerStreamTest {
|
|
25
|
+
|
|
26
|
+
private fun event(
|
|
27
|
+
type: String,
|
|
28
|
+
payload: Payload?,
|
|
29
|
+
sequence: Long = 0,
|
|
30
|
+
runId: String = "run_1"
|
|
31
|
+
): StreamEvent = StreamEvent(
|
|
32
|
+
payload = payload,
|
|
33
|
+
runId = runId,
|
|
34
|
+
schemaVersion = SchemaVersion.V1_0,
|
|
35
|
+
sequence = sequence,
|
|
36
|
+
timestamp = 1_700_000_000_000.0,
|
|
37
|
+
type = type
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
private fun keysOf(json: org.json.JSONObject): Set<String> =
|
|
41
|
+
json.keys().asSequence().toSet()
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun `encodes a content delta with only the text field`() {
|
|
45
|
+
val encoded = IndeRunSerializer.encodeStreamEvent(
|
|
46
|
+
event(type = "content_delta", payload = Payload(text = "hello"))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
assertEquals("1.0", encoded.getString("schemaVersion"))
|
|
50
|
+
assertEquals("run_1", encoded.getString("runId"))
|
|
51
|
+
assertEquals(0L, encoded.getLong("sequence"))
|
|
52
|
+
assertEquals("content_delta", encoded.getString("type"))
|
|
53
|
+
assertEquals(setOf("text"), keysOf(encoded.getJSONObject("payload")))
|
|
54
|
+
assertEquals("hello", encoded.getJSONObject("payload").getString("text"))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun `encodes a completed terminal outcome`() {
|
|
59
|
+
val payload = Payload(
|
|
60
|
+
finalText = "all done",
|
|
61
|
+
finishReason = FinishReason.STOP,
|
|
62
|
+
outcome = Outcome.Completed,
|
|
63
|
+
runId = "run_1",
|
|
64
|
+
schemaVersion = SchemaVersion.V1_0,
|
|
65
|
+
telemetry = PayloadTelemetry(providerUsed = "openai", totalMs = 42.0),
|
|
66
|
+
usage = PayloadUsage(inputTokens = 3, outputTokens = 5, totalTokens = 8)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
val encoded = IndeRunSerializer.encodeStreamEvent(
|
|
70
|
+
event(type = "terminal", payload = payload, sequence = 4)
|
|
71
|
+
).getJSONObject("payload")
|
|
72
|
+
|
|
73
|
+
assertEquals("completed", encoded.getString("outcome"))
|
|
74
|
+
assertEquals("all done", encoded.getString("finalText"))
|
|
75
|
+
assertEquals("stop", encoded.getString("finishReason"))
|
|
76
|
+
assertEquals(8L, encoded.getJSONObject("usage").getLong("totalTokens"))
|
|
77
|
+
assertEquals("openai", encoded.getJSONObject("telemetry").getString("providerUsed"))
|
|
78
|
+
// The other branches' fields must not leak into a completion.
|
|
79
|
+
assertFalse(encoded.has("error"))
|
|
80
|
+
assertFalse(encoded.has("partialText"))
|
|
81
|
+
assertFalse(encoded.has("reason"))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun `encodes an error terminal outcome with its nested error`() {
|
|
86
|
+
val payload = Payload(
|
|
87
|
+
outcome = Outcome.Error,
|
|
88
|
+
runId = "run_1",
|
|
89
|
+
schemaVersion = SchemaVersion.V1_0,
|
|
90
|
+
error = PayloadError(
|
|
91
|
+
details = mapOf("endpoint" to "https://example.test"),
|
|
92
|
+
errorClass = IndeRunErrorClass.RateLimited,
|
|
93
|
+
message = "Too many requests.",
|
|
94
|
+
providerId = "openai",
|
|
95
|
+
retryable = true,
|
|
96
|
+
retryAfterMs = 2_000
|
|
97
|
+
),
|
|
98
|
+
partialText = "half "
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
val encoded = IndeRunSerializer.encodeStreamEvent(
|
|
102
|
+
event(type = "terminal", payload = payload, sequence = 7)
|
|
103
|
+
).getJSONObject("payload")
|
|
104
|
+
|
|
105
|
+
assertEquals("error", encoded.getString("outcome"))
|
|
106
|
+
assertEquals("half ", encoded.getString("partialText"))
|
|
107
|
+
|
|
108
|
+
val error = encoded.getJSONObject("error")
|
|
109
|
+
assertEquals("RateLimited", error.getString("errorClass"))
|
|
110
|
+
assertEquals(2_000L, error.getLong("retryAfterMs"))
|
|
111
|
+
assertTrue(error.getBoolean("retryable"))
|
|
112
|
+
assertEquals(
|
|
113
|
+
"https://example.test",
|
|
114
|
+
error.getJSONObject("details").getString("endpoint")
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun `omits the cancellation reason entirely when there is none`() {
|
|
120
|
+
val withReason = IndeRunSerializer.encodeStreamEvent(
|
|
121
|
+
event(
|
|
122
|
+
type = "terminal",
|
|
123
|
+
payload = Payload(
|
|
124
|
+
outcome = Outcome.Cancelled,
|
|
125
|
+
partialText = "part",
|
|
126
|
+
reason = "user left"
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
).getJSONObject("payload")
|
|
130
|
+
assertEquals("user left", withReason.getString("reason"))
|
|
131
|
+
|
|
132
|
+
val withoutReason = IndeRunSerializer.encodeStreamEvent(
|
|
133
|
+
event(
|
|
134
|
+
type = "terminal",
|
|
135
|
+
payload = Payload(outcome = Outcome.Cancelled, partialText = "part")
|
|
136
|
+
)
|
|
137
|
+
).getJSONObject("payload")
|
|
138
|
+
|
|
139
|
+
assertEquals("cancelled", withoutReason.getString("outcome"))
|
|
140
|
+
assertEquals("part", withoutReason.getString("partialText"))
|
|
141
|
+
// Absent, not null: a null would decode as a present-but-empty reason.
|
|
142
|
+
assertFalse(withoutReason.has("reason"))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
fun `encodes a lifecycle phase using its snake case wire value`() {
|
|
147
|
+
val encoded = IndeRunSerializer.encodeStreamEvent(
|
|
148
|
+
event(type = "lifecycle", payload = Payload(phase = Phase.ProviderSelected))
|
|
149
|
+
).getJSONObject("payload")
|
|
150
|
+
|
|
151
|
+
assertEquals("provider_selected", encoded.getString("phase"))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The schema closes its union with an open catch-all branch, so an unrecognized
|
|
156
|
+
* type must cross the bridge untouched rather than be rejected — unlike the
|
|
157
|
+
* inbound request enums, which throw.
|
|
158
|
+
*/
|
|
159
|
+
@Test
|
|
160
|
+
fun `passes an unknown event type through verbatim without a payload`() {
|
|
161
|
+
val encoded = IndeRunSerializer.encodeStreamEvent(
|
|
162
|
+
event(type = "some_future_type", payload = null, sequence = 2)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
assertEquals("some_future_type", encoded.getString("type"))
|
|
166
|
+
assertFalse(encoded.has("payload"))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
fun `encodes a diagnostic event with an empty payload object`() {
|
|
171
|
+
val encoded = IndeRunSerializer.encodeStreamEvent(
|
|
172
|
+
event(type = "diagnostic", payload = Payload())
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
assertTrue(encoded.has("payload"))
|
|
176
|
+
assertEquals(emptySet<String>(), keysOf(encoded.getJSONObject("payload")))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Test
|
|
180
|
+
fun `encodes a stream run handle with and without a provider id`() {
|
|
181
|
+
val withProvider = IndeRunSerializer.encodeStreamRunHandle(
|
|
182
|
+
StreamRunHandle(
|
|
183
|
+
providerId = "openai",
|
|
184
|
+
runId = "run_1",
|
|
185
|
+
schemaVersion = SchemaVersion.V1_0,
|
|
186
|
+
startedAt = 1_700_000_000_000.0
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
assertEquals("openai", withProvider.getString("providerId"))
|
|
190
|
+
assertEquals("run_1", withProvider.getString("runId"))
|
|
191
|
+
assertEquals(1_700_000_000_000.0, withProvider.getDouble("startedAt"), 0.0)
|
|
192
|
+
|
|
193
|
+
val withoutProvider = IndeRunSerializer.encodeStreamRunHandle(
|
|
194
|
+
StreamRunHandle(
|
|
195
|
+
runId = "run_1",
|
|
196
|
+
schemaVersion = SchemaVersion.V1_0,
|
|
197
|
+
startedAt = 1_700_000_000_000.0
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
assertFalse(withoutProvider.has("providerId"))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Outcome and Phase are generated without a rawValue, so their wire strings are
|
|
205
|
+
* hand-written. If a regeneration adds a constant, the `when` stops compiling —
|
|
206
|
+
* and if one is ever mapped to the wrong casing, this catches it.
|
|
207
|
+
*/
|
|
208
|
+
@Test
|
|
209
|
+
fun `every outcome and phase constant maps to a snake case wire value`() {
|
|
210
|
+
val outcomes = Outcome.entries.map { outcome ->
|
|
211
|
+
IndeRunSerializer.encodeStreamEvent(
|
|
212
|
+
event(type = "terminal", payload = Payload(outcome = outcome))
|
|
213
|
+
).getJSONObject("payload").getString("outcome")
|
|
214
|
+
}
|
|
215
|
+
assertEquals(listOf("cancelled", "completed", "error"), outcomes)
|
|
216
|
+
|
|
217
|
+
val phases = Phase.entries.map { phase ->
|
|
218
|
+
IndeRunSerializer.encodeStreamEvent(
|
|
219
|
+
event(type = "lifecycle", payload = Payload(phase = phase))
|
|
220
|
+
).getJSONObject("payload").getString("phase")
|
|
221
|
+
}
|
|
222
|
+
assertEquals(listOf("provider_selected", "started"), phases)
|
|
223
|
+
|
|
224
|
+
assertTrue((outcomes + phases).all { it.isNotEmpty() && it == it.lowercase() })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package app.independo.inderun.capacitor
|
|
2
|
+
|
|
3
|
+
import app.independo.inderun.contracts.SchemaVersion
|
|
4
|
+
import app.independo.inderun.contracts.StreamRunHandle
|
|
5
|
+
import app.independo.inderun.core.StreamRun
|
|
6
|
+
import kotlinx.coroutines.Job
|
|
7
|
+
import kotlinx.coroutines.flow.emptyFlow
|
|
8
|
+
import org.junit.Assert.assertEquals
|
|
9
|
+
import org.junit.Assert.assertFalse
|
|
10
|
+
import org.junit.Assert.assertTrue
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
|
|
13
|
+
class IndeRunStreamRegistryTest {
|
|
14
|
+
|
|
15
|
+
private fun run(record: (String?) -> Unit): StreamRun = StreamRun(
|
|
16
|
+
handle = StreamRunHandle(
|
|
17
|
+
runId = "run_1",
|
|
18
|
+
schemaVersion = SchemaVersion.V1_0,
|
|
19
|
+
startedAt = 1_700_000_000_000.0
|
|
20
|
+
),
|
|
21
|
+
events = emptyFlow(),
|
|
22
|
+
onCancel = record
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun `attaches and closes by stream id`() {
|
|
27
|
+
val registry = IndeRunStreamRegistry()
|
|
28
|
+
|
|
29
|
+
registry.open("s1")
|
|
30
|
+
assertEquals(AttachOutcome.Attached, registry.attach("s1", run {}))
|
|
31
|
+
assertEquals(1, registry.activeCount)
|
|
32
|
+
|
|
33
|
+
registry.close("s1")
|
|
34
|
+
assertEquals(0, registry.activeCount)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A cancel can beat the engine's route selection. It must be held and applied when
|
|
39
|
+
* the run finally attaches, not dropped.
|
|
40
|
+
*/
|
|
41
|
+
@Test
|
|
42
|
+
fun `cancel before attach is applied on attach with its reason`() {
|
|
43
|
+
val registry = IndeRunStreamRegistry()
|
|
44
|
+
val reasons = mutableListOf<String?>()
|
|
45
|
+
|
|
46
|
+
registry.open("s1")
|
|
47
|
+
registry.requestCancel("s1", "user left")
|
|
48
|
+
// Nothing to cancel yet, so nothing was called.
|
|
49
|
+
assertTrue(reasons.isEmpty())
|
|
50
|
+
|
|
51
|
+
val outcome = registry.attach("s1", run { reasons.add(it) })
|
|
52
|
+
assertEquals(AttachOutcome.CancelRequested("user left"), outcome)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `cancel after close is a no-op`() {
|
|
57
|
+
val registry = IndeRunStreamRegistry()
|
|
58
|
+
val reasons = mutableListOf<String?>()
|
|
59
|
+
|
|
60
|
+
registry.open("s1")
|
|
61
|
+
registry.attach("s1", run { reasons.add(it) })
|
|
62
|
+
registry.close("s1")
|
|
63
|
+
|
|
64
|
+
registry.requestCancel("s1", "too late")
|
|
65
|
+
assertTrue(reasons.isEmpty())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun `cancel for an unknown stream id is a no-op`() {
|
|
70
|
+
val registry = IndeRunStreamRegistry()
|
|
71
|
+
|
|
72
|
+
registry.requestCancel("never_opened", null)
|
|
73
|
+
assertEquals(0, registry.activeCount)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun `repeated cancel cancels the run exactly once`() {
|
|
78
|
+
val registry = IndeRunStreamRegistry()
|
|
79
|
+
val reasons = mutableListOf<String?>()
|
|
80
|
+
|
|
81
|
+
registry.open("s1")
|
|
82
|
+
registry.attach("s1", run { reasons.add(it) })
|
|
83
|
+
|
|
84
|
+
registry.requestCancel("s1", "first")
|
|
85
|
+
registry.requestCancel("s1", "second")
|
|
86
|
+
|
|
87
|
+
assertEquals(listOf("first"), reasons)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Test
|
|
91
|
+
fun `concurrent streams are isolated`() {
|
|
92
|
+
val registry = IndeRunStreamRegistry()
|
|
93
|
+
val first = mutableListOf<String?>()
|
|
94
|
+
val second = mutableListOf<String?>()
|
|
95
|
+
|
|
96
|
+
registry.open("s1")
|
|
97
|
+
registry.open("s2")
|
|
98
|
+
registry.attach("s1", run { first.add(it) })
|
|
99
|
+
registry.attach("s2", run { second.add(it) })
|
|
100
|
+
|
|
101
|
+
registry.requestCancel("s1", "only the first")
|
|
102
|
+
|
|
103
|
+
assertEquals(listOf("only the first"), first)
|
|
104
|
+
assertTrue(second.isEmpty())
|
|
105
|
+
assertEquals(2, registry.activeCount)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test
|
|
109
|
+
fun `cancelAll cancels every run once and empties the registry`() {
|
|
110
|
+
val registry = IndeRunStreamRegistry()
|
|
111
|
+
val first = mutableListOf<String?>()
|
|
112
|
+
val second = mutableListOf<String?>()
|
|
113
|
+
|
|
114
|
+
registry.open("s1")
|
|
115
|
+
registry.open("s2")
|
|
116
|
+
registry.attach("s1", run { first.add(it) })
|
|
117
|
+
registry.attach("s2", run { second.add(it) })
|
|
118
|
+
|
|
119
|
+
registry.cancelAll("torn down")
|
|
120
|
+
|
|
121
|
+
assertEquals(listOf("torn down"), first)
|
|
122
|
+
assertEquals(listOf("torn down"), second)
|
|
123
|
+
assertEquals(0, registry.activeCount)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Teardown also cancels the collecting jobs — at that point nobody is left to
|
|
128
|
+
* receive a terminal event.
|
|
129
|
+
*/
|
|
130
|
+
@Test
|
|
131
|
+
fun `cancelAll cancels the collecting job`() {
|
|
132
|
+
val registry = IndeRunStreamRegistry()
|
|
133
|
+
val job = Job()
|
|
134
|
+
|
|
135
|
+
registry.open("s1")
|
|
136
|
+
registry.attach("s1", run {})
|
|
137
|
+
registry.attachJob("s1", job)
|
|
138
|
+
|
|
139
|
+
assertFalse(job.isCancelled)
|
|
140
|
+
registry.cancelAll(null)
|
|
141
|
+
assertTrue(job.isCancelled)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Test
|
|
145
|
+
fun `attach after teardown reports cancel requested`() {
|
|
146
|
+
val registry = IndeRunStreamRegistry()
|
|
147
|
+
|
|
148
|
+
registry.open("s1")
|
|
149
|
+
registry.cancelAll("torn down")
|
|
150
|
+
|
|
151
|
+
// The entry is gone, so the run that arrives late must be cancelled by its caller.
|
|
152
|
+
assertEquals(AttachOutcome.CancelRequested(null), registry.attach("s1", run {}))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun `a job attached to a closed stream is cancelled immediately`() {
|
|
157
|
+
val registry = IndeRunStreamRegistry()
|
|
158
|
+
val job = Job()
|
|
159
|
+
|
|
160
|
+
registry.attachJob("never_opened", job)
|
|
161
|
+
assertTrue(job.isCancelled)
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dist/definitions.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PluginListenerHandle } from "@capacitor/core";
|
|
2
|
+
import type { IndeRunError, StreamEvent, StreamRunHandle, TaskRequest, TaskResult } from "@independo/inderun-contracts";
|
|
2
3
|
export interface OpenAIProviderBootstrapOptions {
|
|
3
4
|
model: string;
|
|
4
5
|
endpointUrl?: string;
|
|
@@ -10,8 +11,60 @@ export interface ConfigureOptions {
|
|
|
10
11
|
openAI?: OpenAIProviderBootstrapOptions;
|
|
11
12
|
allowDirectOpenAIEndpoint?: boolean;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Bridge-local correlation id for one streaming run.
|
|
16
|
+
*
|
|
17
|
+
* Not a contract field, and never placed inside the `TaskRequest`: `runId` stays the
|
|
18
|
+
* engine's to mint. `startStream` resolves only *after* the engine has minted one, so
|
|
19
|
+
* keying the JS-side event sink on `runId` would leave a window in which native can
|
|
20
|
+
* emit events for a run JS has not yet heard of. The `streamId` is generated before
|
|
21
|
+
* the call instead, which lets the sink be bound first and makes `cancel()`
|
|
22
|
+
* well-defined even before a handle exists.
|
|
23
|
+
*/
|
|
24
|
+
export interface StartStreamOptions {
|
|
25
|
+
streamId: string;
|
|
26
|
+
request: TaskRequest;
|
|
27
|
+
}
|
|
28
|
+
export interface CancelStreamOptions {
|
|
29
|
+
streamId: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Payload of `"indeRunStreamEvent"`. `event` is the canonical, unmodified `StreamEvent`. */
|
|
33
|
+
export interface StreamEventNotification {
|
|
34
|
+
streamId: string;
|
|
35
|
+
event: StreamEvent;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Payload of `"indeRunStreamError"`: a failure of the *bridge*, not an outcome of the
|
|
39
|
+
* run. A run that fails ends in a terminal event with `payload.outcome === "error"`,
|
|
40
|
+
* delivered through `"indeRunStreamEvent"` like any other event.
|
|
41
|
+
*/
|
|
42
|
+
export interface StreamErrorNotification {
|
|
43
|
+
streamId: string;
|
|
44
|
+
error: IndeRunError;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Structurally identical to `StreamRun` in `@independo/inderun-web`, so bridge and
|
|
48
|
+
* direct-SDK consumers read the same. `events` terminates in exactly one terminal
|
|
49
|
+
* event and is single-use; `cancel()` is idempotent.
|
|
50
|
+
*/
|
|
51
|
+
export interface StreamRun {
|
|
52
|
+
handle: StreamRunHandle;
|
|
53
|
+
events: AsyncIterable<StreamEvent>;
|
|
54
|
+
cancel(reason?: string): void;
|
|
55
|
+
}
|
|
13
56
|
export interface IndeRunCapacitorPlugin {
|
|
14
57
|
configure(options?: ConfigureOptions): Promise<void>;
|
|
15
58
|
run(request: TaskRequest): Promise<TaskResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Unlike `run(request)`, which passes the request at the options root, this nests it
|
|
61
|
+
* under `request` so the envelope can also carry `streamId`. Both native decode sites
|
|
62
|
+
* carry a matching comment.
|
|
63
|
+
*/
|
|
64
|
+
startStream(options: StartStreamOptions): Promise<StreamRunHandle>;
|
|
65
|
+
cancelStream(options: CancelStreamOptions): Promise<void>;
|
|
66
|
+
addListener(eventName: "indeRunStreamEvent", listenerFunc: (notification: StreamEventNotification) => void): Promise<PluginListenerHandle>;
|
|
67
|
+
addListener(eventName: "indeRunStreamError", listenerFunc: (notification: StreamErrorNotification) => void): Promise<PluginListenerHandle>;
|
|
68
|
+
removeAllListeners(): Promise<void>;
|
|
16
69
|
}
|
|
17
70
|
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../src/definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../src/definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,eAAe,EACf,WAAW,EACX,UAAU,EACX,MAAM,8BAA8B,CAAC;AAEtC,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,8BAA8B,CAAC;IACxC,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,6FAA6F;AAC7F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C;;;;OAIG;IACH,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACnE,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,WAAW,CACT,SAAS,EAAE,oBAAoB,EAC/B,YAAY,EAAE,CAAC,YAAY,EAAE,uBAAuB,KAAK,IAAI,GAC5D,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC,WAAW,CACT,SAAS,EAAE,oBAAoB,EAC/B,YAAY,EAAE,CAAC,YAAY,EAAE,uBAAuB,KAAK,IAAI,GAC5D,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type IndeRunError } from "@independo/inderun-contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Unwraps whatever the Capacitor boundary hands back into the canonical
|
|
4
|
+
* `IndeRunError` the native side rejected with.
|
|
5
|
+
*
|
|
6
|
+
* Capacitor wraps a native rejection's structured data under `error.data`, so a
|
|
7
|
+
* thrown value is either already an `IndeRunError` (the web implementation throws
|
|
8
|
+
* one directly) or carries one there. Anything else is passed through untouched
|
|
9
|
+
* rather than guessed at.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizePluginError(error: unknown): unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Mints an `IndeRunError` for a failure of the bridge itself — event loss across
|
|
14
|
+
* the JS boundary, a malformed handle — as opposed to one reported by a provider
|
|
15
|
+
* or the engine. `Internal` is the correct class: nothing about the request or the
|
|
16
|
+
* provider is at fault, the transport is.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createBridgeError(message: string, runId?: string): IndeRunError;
|
|
19
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAe5D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY,CAO/E"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { validateIndeRunError } from "@independo/inderun-contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Unwraps whatever the Capacitor boundary hands back into the canonical
|
|
4
|
+
* `IndeRunError` the native side rejected with.
|
|
5
|
+
*
|
|
6
|
+
* Capacitor wraps a native rejection's structured data under `error.data`, so a
|
|
7
|
+
* thrown value is either already an `IndeRunError` (the web implementation throws
|
|
8
|
+
* one directly) or carries one there. Anything else is passed through untouched
|
|
9
|
+
* rather than guessed at.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizePluginError(error) {
|
|
12
|
+
if (validateIndeRunError(error)) {
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
if (typeof error === "object" &&
|
|
16
|
+
error !== null &&
|
|
17
|
+
"data" in error &&
|
|
18
|
+
validateIndeRunError(error.data)) {
|
|
19
|
+
return error.data;
|
|
20
|
+
}
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mints an `IndeRunError` for a failure of the bridge itself — event loss across
|
|
25
|
+
* the JS boundary, a malformed handle — as opposed to one reported by a provider
|
|
26
|
+
* or the engine. `Internal` is the correct class: nothing about the request or the
|
|
27
|
+
* provider is at fault, the transport is.
|
|
28
|
+
*/
|
|
29
|
+
export function createBridgeError(message, runId) {
|
|
30
|
+
return {
|
|
31
|
+
schemaVersion: "1.0",
|
|
32
|
+
errorClass: "Internal",
|
|
33
|
+
message,
|
|
34
|
+
...(runId !== undefined ? { runId } : {})
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ConfigureOptions, IndeRunCapacitorPlugin as IndeRunCapacitorPluginContract, OpenAIProviderBootstrapOptions } from "./definitions.js";
|
|
3
|
-
|
|
1
|
+
import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
|
|
2
|
+
import type { CancelStreamOptions, ConfigureOptions, IndeRunCapacitorPlugin as IndeRunCapacitorPluginContract, OpenAIProviderBootstrapOptions, StartStreamOptions, StreamErrorNotification, StreamEventNotification, StreamRun } from "./definitions.js";
|
|
3
|
+
/**
|
|
4
|
+
* The plugin contract plus the ergonomic Mode 2 entry point. `stream()` is not a
|
|
5
|
+
* plugin method — it is `startStream`, the event listeners, and `cancelStream`
|
|
6
|
+
* reassembled into the same shape the platform SDKs return from `stream()`.
|
|
7
|
+
*/
|
|
8
|
+
export interface IndeRunCapacitorApi extends IndeRunCapacitorPluginContract {
|
|
9
|
+
stream(request: TaskRequest): Promise<StreamRun>;
|
|
10
|
+
}
|
|
11
|
+
export declare const IndeRunCapacitor: IndeRunCapacitorApi;
|
|
4
12
|
export interface IndeRunCapacitorInstance {
|
|
5
13
|
run(request: TaskRequest): Promise<TaskResult>;
|
|
14
|
+
stream(request: TaskRequest): Promise<StreamRun>;
|
|
6
15
|
}
|
|
7
16
|
export declare function createIndeRunCapacitor(options?: ConfigureOptions): IndeRunCapacitorInstance;
|
|
8
|
-
export
|
|
17
|
+
export { STREAM_ERROR_NAME, STREAM_EVENT_NAME } from "./streaming.js";
|
|
18
|
+
export type { CancelStreamOptions, ConfigureOptions, IndeRunCapacitorPluginContract as IndeRunCapacitorPlugin, OpenAIProviderBootstrapOptions, StartStreamOptions, StreamErrorNotification, StreamEventNotification, StreamRun, TaskRequest, TaskResult };
|
|
9
19
|
export type * from "@independo/inderun-contracts";
|
|
10
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE7F,OAAO,KAAK,EACV,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,IAAI,8BAA8B,EACxD,8BAA8B,EAC9B,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,SAAS,EACV,MAAM,kBAAkB,CAAC;AAQ1B;;;;GAIG;AACH,MAAM,WAAW,mBAAoB,SAAQ,8BAA8B;IACzE,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,gBAAgB,EAAE,mBA4C9B,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CAClD;AAED,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,wBAAwB,CAsB3F;AAED,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEtE,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,8BAA8B,IAAI,sBAAsB,EACxD,8BAA8B,EAC9B,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,SAAS,EACT,WAAW,EACX,UAAU,EACX,CAAC;AAEF,mBAAmB,8BAA8B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import { validateIndeRunError } from "@independo/inderun-contracts";
|
|
2
1
|
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
import { normalizePluginError } from "./errors.js";
|
|
3
|
+
import { startCapacitorStream } from "./streaming.js";
|
|
3
4
|
const IndeRunCapacitorNative = registerPlugin("IndeRunCapacitor", {
|
|
4
5
|
web: () => import("./web.js").then((module) => new module.IndeRunWeb())
|
|
5
6
|
});
|
|
6
|
-
function normalizePluginError(error) {
|
|
7
|
-
if (validateIndeRunError(error)) {
|
|
8
|
-
return error;
|
|
9
|
-
}
|
|
10
|
-
if (typeof error === "object" &&
|
|
11
|
-
error !== null &&
|
|
12
|
-
"data" in error &&
|
|
13
|
-
validateIndeRunError(error.data)) {
|
|
14
|
-
return error.data;
|
|
15
|
-
}
|
|
16
|
-
return error;
|
|
17
|
-
}
|
|
18
7
|
export const IndeRunCapacitor = {
|
|
19
8
|
async configure(options) {
|
|
20
9
|
try {
|
|
@@ -31,6 +20,29 @@ export const IndeRunCapacitor = {
|
|
|
31
20
|
catch (error) {
|
|
32
21
|
throw normalizePluginError(error);
|
|
33
22
|
}
|
|
23
|
+
},
|
|
24
|
+
async startStream(options) {
|
|
25
|
+
try {
|
|
26
|
+
return await IndeRunCapacitorNative.startStream(options);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw normalizePluginError(error);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async cancelStream(options) {
|
|
33
|
+
try {
|
|
34
|
+
await IndeRunCapacitorNative.cancelStream(options);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw normalizePluginError(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
addListener: IndeRunCapacitorNative.addListener.bind(IndeRunCapacitorNative),
|
|
41
|
+
async removeAllListeners() {
|
|
42
|
+
await IndeRunCapacitorNative.removeAllListeners();
|
|
43
|
+
},
|
|
44
|
+
stream(request) {
|
|
45
|
+
return startCapacitorStream(IndeRunCapacitorNative, request);
|
|
34
46
|
}
|
|
35
47
|
};
|
|
36
48
|
export function createIndeRunCapacitor(options) {
|
|
@@ -46,6 +58,11 @@ export function createIndeRunCapacitor(options) {
|
|
|
46
58
|
async run(request) {
|
|
47
59
|
await ensureConfigured();
|
|
48
60
|
return IndeRunCapacitor.run(request);
|
|
61
|
+
},
|
|
62
|
+
async stream(request) {
|
|
63
|
+
await ensureConfigured();
|
|
64
|
+
return IndeRunCapacitor.stream(request);
|
|
49
65
|
}
|
|
50
66
|
};
|
|
51
67
|
}
|
|
68
|
+
export { STREAM_ERROR_NAME, STREAM_EVENT_NAME } from "./streaming.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type TaskRequest } from "@independo/inderun-contracts";
|
|
2
|
+
import type { IndeRunCapacitorPlugin, StreamRun } from "./definitions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Listener event names. These are **public contract**: an app may attach its own
|
|
5
|
+
* listener, and native code emits exactly these. Renaming one is a breaking change.
|
|
6
|
+
*/
|
|
7
|
+
export declare const STREAM_EVENT_NAME = "indeRunStreamEvent";
|
|
8
|
+
export declare const STREAM_ERROR_NAME = "indeRunStreamError";
|
|
9
|
+
/**
|
|
10
|
+
* Starts a Mode 2 run over the bridge and reassembles it into the same `StreamRun`
|
|
11
|
+
* shape `@independo/inderun-web` returns directly.
|
|
12
|
+
*
|
|
13
|
+
* The listeners are registered *before* `startStream` is called, so no event can be
|
|
14
|
+
* emitted for a run whose sink does not exist yet.
|
|
15
|
+
*/
|
|
16
|
+
export declare function startCapacitorStream(plugin: IndeRunCapacitorPlugin, request: TaskRequest): Promise<StreamRun>;
|
|
17
|
+
//# sourceMappingURL=streaming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,sBAAsB,EAGtB,SAAS,EACV,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AACtD,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AA6QtD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,CAAC,CAwDpB"}
|