@ulam/halohalo 0.3.2 → 0.3.3

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/angular.js CHANGED
@@ -1,102 +1,102 @@
1
- /**
2
- * @ulam/halohalo/angular: Angular adapter
3
- *
4
- * Injectable services wrapping the vanilla halohalo core with Angular signal
5
- * reactivity. Provide once at the application root via provideHalohalo().
6
- */
7
- import { Injectable, signal, computed } from '@angular/core'
8
- import { createCompletion } from './createCompletion.js'
9
- import { createProviderConfig } from './createProviderConfig.js'
10
-
11
- /**
12
- * Service for AI completions. Each inject() call gets its own completion
13
- * instance; loading/animating state is per-instance.
14
- *
15
- * Usage:
16
- * private completion = inject(CompletionService)
17
- *
18
- * await this.completion.complete({ prompt: 'Rewrite for mobile.' })
19
- * // this.completion.loading() is true while running
20
- */
21
- @Injectable()
22
- export class CompletionService {
23
- #instance = createCompletion()
24
- #loading = signal(false)
25
- #animating = signal(false)
26
-
27
- loading = computed(() => this.#loading())
28
- animating = computed(() => this.#animating())
29
-
30
- constructor() {
31
- this.#instance.subscribe(({ loading, animating }) => {
32
- this.#loading.set(loading)
33
- this.#animating.set(animating)
34
- })
35
- }
36
-
37
- complete(options) {
38
- return this.#instance.complete(options)
39
- }
40
-
41
- cancel() {
42
- this.#instance.cancel()
43
- }
44
-
45
- ngOnDestroy() {
46
- this.#instance.cancel()
47
- }
48
- }
49
-
50
- /**
51
- * Service for AI provider and model configuration. One config store per application.
52
- *
53
- * Usage:
54
- * private providerConfig = inject(ProviderConfigService)
55
- *
56
- * this.providerConfig.setProvider('openai')
57
- * const current = this.providerConfig.provider() // Signal<string>
58
- */
59
- @Injectable({ providedIn: 'root' })
60
- export class ProviderConfigService {
61
- #config = createProviderConfig()
62
- #provider = signal(this.#config.provider)
63
- #models = signal(this.#config.models)
64
- #mode = signal(this.#config.mode)
65
-
66
- provider = computed(() => this.#provider())
67
- models = computed(() => this.#models())
68
- mode = computed(() => this.#mode())
69
- providers = this.#config.providers
70
-
71
- constructor() {
72
- this.#config.subscribe(() => {
73
- this.#provider.set(this.#config.provider)
74
- this.#models.set(this.#config.models)
75
- this.#mode.set(this.#config.mode)
76
- })
77
- }
78
-
79
- setProvider(id) { this.#config.setProvider(id) }
80
- setModel(pid, mid) { this.#config.setModel(pid, mid) }
81
- setMode(v) { this.#config.setMode(v) }
82
- setKey(pid, v) { return this.#config.setKey(pid, v) }
83
- getKey(pid) { return this.#config.getKey(pid) }
84
- getModel(pid) { return this.#config.getModel(pid) }
85
- getLabel(pid) { return this.#config.getLabel(pid) }
86
- }
87
-
88
- /**
89
- * Standalone provider function. Use in bootstrapApplication() or as a
90
- * component-level provider when you need scoped completion instances.
91
- *
92
- * App-level (shared singleton):
93
- * bootstrapApplication(AppComponent, {
94
- * providers: [provideHalohalo()]
95
- * })
96
- *
97
- * Component-level (scoped instance):
98
- * @Component({ providers: [CompletionService] })
99
- */
100
- export function provideHalohalo() {
101
- return [ProviderConfigService, CompletionService]
102
- }
1
+ /**
2
+ * @ulam/halohalo/angular: Angular adapter
3
+ *
4
+ * Injectable services wrapping the vanilla halohalo core with Angular signal
5
+ * reactivity. Provide once at the application root via provideHalohalo().
6
+ */
7
+ import { Injectable, signal, computed } from '@angular/core'
8
+ import { createCompletion } from './createCompletion.js'
9
+ import { createProviderConfig } from './createProviderConfig.js'
10
+
11
+ /**
12
+ * Service for AI completions. Each inject() call gets its own completion
13
+ * instance; loading/animating state is per-instance.
14
+ *
15
+ * Usage:
16
+ * private completion = inject(CompletionService)
17
+ *
18
+ * await this.completion.complete({ prompt: 'Rewrite for mobile.' })
19
+ * // this.completion.loading() is true while running
20
+ */
21
+ @Injectable()
22
+ export class CompletionService {
23
+ #instance = createCompletion()
24
+ #loading = signal(false)
25
+ #animating = signal(false)
26
+
27
+ loading = computed(() => this.#loading())
28
+ animating = computed(() => this.#animating())
29
+
30
+ constructor() {
31
+ this.#instance.subscribe(({ loading, animating }) => {
32
+ this.#loading.set(loading)
33
+ this.#animating.set(animating)
34
+ })
35
+ }
36
+
37
+ complete(options) {
38
+ return this.#instance.complete(options)
39
+ }
40
+
41
+ cancel() {
42
+ this.#instance.cancel()
43
+ }
44
+
45
+ ngOnDestroy() {
46
+ this.#instance.cancel()
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Service for AI provider and model configuration. One config store per application.
52
+ *
53
+ * Usage:
54
+ * private providerConfig = inject(ProviderConfigService)
55
+ *
56
+ * this.providerConfig.setProvider('openai')
57
+ * const current = this.providerConfig.provider() // Signal<string>
58
+ */
59
+ @Injectable({ providedIn: 'root' })
60
+ export class ProviderConfigService {
61
+ #config = createProviderConfig()
62
+ #provider = signal(this.#config.provider)
63
+ #models = signal(this.#config.models)
64
+ #mode = signal(this.#config.mode)
65
+
66
+ provider = computed(() => this.#provider())
67
+ models = computed(() => this.#models())
68
+ mode = computed(() => this.#mode())
69
+ providers = this.#config.providers
70
+
71
+ constructor() {
72
+ this.#config.subscribe(() => {
73
+ this.#provider.set(this.#config.provider)
74
+ this.#models.set(this.#config.models)
75
+ this.#mode.set(this.#config.mode)
76
+ })
77
+ }
78
+
79
+ setProvider(id) { this.#config.setProvider(id) }
80
+ setModel(pid, mid) { this.#config.setModel(pid, mid) }
81
+ setMode(v) { this.#config.setMode(v) }
82
+ setKey(pid, v) { return this.#config.setKey(pid, v) }
83
+ getKey(pid) { return this.#config.getKey(pid) }
84
+ getModel(pid) { return this.#config.getModel(pid) }
85
+ getLabel(pid) { return this.#config.getLabel(pid) }
86
+ }
87
+
88
+ /**
89
+ * Standalone provider function. Use in bootstrapApplication() or as a
90
+ * component-level provider when you need scoped completion instances.
91
+ *
92
+ * App-level (shared singleton):
93
+ * bootstrapApplication(AppComponent, {
94
+ * providers: [provideHalohalo()]
95
+ * })
96
+ *
97
+ * Component-level (scoped instance):
98
+ * @Component({ providers: [CompletionService] })
99
+ */
100
+ export function provideHalohalo() {
101
+ return [ProviderConfigService, CompletionService]
102
+ }
package/connectivity.js CHANGED
@@ -1,17 +1,17 @@
1
- /**
2
- * Probes a list of endpoints in parallel to check network reachability.
3
- *
4
- * @param {Array<{ label: string, url: string }>} probes
5
- * @param {number} [timeoutMs=4000]
6
- * @returns {Promise<Array<{ label: string, ok: boolean }>>}
7
- */
8
- export async function checkConnectivity(probes, timeoutMs = 4000) {
9
- const results = await Promise.allSettled(
10
- probes.map(p =>
11
- fetch(p.url, { method: 'HEAD', mode: 'no-cors', signal: AbortSignal.timeout(timeoutMs) })
12
- .then(() => ({ label: p.label, ok: true }))
13
- .catch(() => ({ label: p.label, ok: false }))
14
- )
15
- )
16
- return results.map(r => r.value || r.reason)
17
- }
1
+ /**
2
+ * Probes a list of endpoints in parallel to check network reachability.
3
+ *
4
+ * @param {Array<{ label: string, url: string }>} probes
5
+ * @param {number} [timeoutMs=4000]
6
+ * @returns {Promise<Array<{ label: string, ok: boolean }>>}
7
+ */
8
+ export async function checkConnectivity(probes, timeoutMs = 4000) {
9
+ const results = await Promise.allSettled(
10
+ probes.map(p =>
11
+ fetch(p.url, { method: 'HEAD', mode: 'no-cors', signal: AbortSignal.timeout(timeoutMs) })
12
+ .then(() => ({ label: p.label, ok: true }))
13
+ .catch(() => ({ label: p.label, ok: false }))
14
+ )
15
+ )
16
+ return results.map(r => r.value || r.reason)
17
+ }
@@ -1,113 +1,113 @@
1
- import { callProvider, callAnthropicWithTools } from './fetch.js'
2
- import { AiApiError } from './providers.js'
3
-
4
- const DEFAULT_TYPEWRITER = { tickMs: 33, minCharsPerTick: 2, charDivisor: 40 }
5
-
6
- /**
7
- * Vanilla completion runner. No React required.
8
- * Returns { complete(options), cancel() } plus a subscribe() for state changes.
9
- *
10
- * State: { loading: boolean, animating: boolean }
11
- *
12
- * options for complete():
13
- * provider, model, key, prompt, maxTokens: for standard completions
14
- * agentOptions: { system, tools, messages, maxTurns, onToolCall }: for agentic
15
- * onResult(fields) : called with parsed result after completion
16
- * onError(error) : called with AiApiError on failure
17
- * parseResponse(text) : maps raw text to result fields object
18
- * typewriter : { tickMs, minCharsPerTick, charDivisor } or false
19
- * onAnimate(field, text) : called each tick with field + current text slice
20
- * onAnimateDone() : called when typewriter finishes
21
- */
22
- export function createCompletion() {
23
- let loading = false
24
- let animating = false
25
- let timer = null
26
- const listeners = new Set()
27
- const notify = () => listeners.forEach(fn => fn({ loading, animating }))
28
-
29
- function runTypewriter(fields, { typewriter, onAnimate, onAnimateDone }) {
30
- clearTimeout(timer)
31
- const { tickMs, minCharsPerTick, charDivisor } = { ...DEFAULT_TYPEWRITER, ...typewriter }
32
-
33
- const entries = Object.entries(fields).filter(([, v]) => typeof v === 'string' && v.length > 0)
34
- if (!entries.length) {
35
- animating = false
36
- notify()
37
- onAnimateDone?.()
38
- return
39
- }
40
-
41
- const total = entries.reduce((sum, [, v]) => sum + v.length, 0)
42
- const charsPerTick = Math.max(minCharsPerTick, Math.ceil(total / charDivisor))
43
- const indices = Object.fromEntries(entries.map(([k]) => [k, 0]))
44
- let entryIdx = 0
45
-
46
- function tick() {
47
- const [field, text] = entries[entryIdx]
48
- indices[field] = Math.min(indices[field] + charsPerTick, text.length)
49
- onAnimate?.(field, text.slice(0, indices[field]))
50
- if (indices[field] >= text.length) entryIdx++
51
- if (entryIdx >= entries.length) {
52
- animating = false
53
- notify()
54
- onAnimateDone?.()
55
- } else {
56
- timer = setTimeout(tick, tickMs)
57
- }
58
- }
59
-
60
- timer = setTimeout(tick, tickMs)
61
- }
62
-
63
- return {
64
- get loading() { return loading },
65
- get animating() { return animating },
66
-
67
- subscribe(fn) {
68
- listeners.add(fn)
69
- return () => listeners.delete(fn)
70
- },
71
-
72
- async complete({ provider, model, key, prompt, maxTokens, agentOptions, onResult, onError, parseResponse, typewriter = DEFAULT_TYPEWRITER, onAnimate, onAnimateDone }) {
73
- loading = true
74
- notify()
75
-
76
- try {
77
- let text
78
- if (agentOptions) {
79
- const { system, tools, messages, maxTurns, onToolCall } = agentOptions
80
- text = await callAnthropicWithTools({ key, model, system, tools, messages, maxTokens, maxTurns, onToolCall })
81
- } else {
82
- text = await callProvider({ provider, model, key, prompt, maxTokens })
83
- }
84
-
85
- const result = parseResponse ? parseResponse(text) : { text }
86
- loading = false
87
-
88
- if (typewriter && onAnimate) {
89
- animating = true
90
- notify()
91
- onResult?.(result)
92
- runTypewriter(result, { typewriter, onAnimate, onAnimateDone })
93
- } else {
94
- notify()
95
- onResult?.(result)
96
- onAnimateDone?.()
97
- }
98
- } catch (e) {
99
- loading = false
100
- animating = false
101
- notify()
102
- onError?.(e instanceof AiApiError ? e : new AiApiError('api_error'))
103
- }
104
- },
105
-
106
- cancel() {
107
- clearTimeout(timer)
108
- loading = false
109
- animating = false
110
- notify()
111
- },
112
- }
113
- }
1
+ import { callProvider, callAnthropicWithTools } from './fetch.js'
2
+ import { AiApiError } from './providers.js'
3
+
4
+ const DEFAULT_TYPEWRITER = { tickMs: 33, minCharsPerTick: 2, charDivisor: 40 }
5
+
6
+ /**
7
+ * Vanilla completion runner. No React required.
8
+ * Returns { complete(options), cancel() } plus a subscribe() for state changes.
9
+ *
10
+ * State: { loading: boolean, animating: boolean }
11
+ *
12
+ * options for complete():
13
+ * provider, model, key, prompt, maxTokens: for standard completions
14
+ * agentOptions: { system, tools, messages, maxTurns, onToolCall }: for agentic
15
+ * onResult(fields) : called with parsed result after completion
16
+ * onError(error) : called with AiApiError on failure
17
+ * parseResponse(text) : maps raw text to result fields object
18
+ * typewriter : { tickMs, minCharsPerTick, charDivisor } or false
19
+ * onAnimate(field, text) : called each tick with field + current text slice
20
+ * onAnimateDone() : called when typewriter finishes
21
+ */
22
+ export function createCompletion() {
23
+ let loading = false
24
+ let animating = false
25
+ let timer = null
26
+ const listeners = new Set()
27
+ const notify = () => listeners.forEach(fn => fn({ loading, animating }))
28
+
29
+ function runTypewriter(fields, { typewriter, onAnimate, onAnimateDone }) {
30
+ clearTimeout(timer)
31
+ const { tickMs, minCharsPerTick, charDivisor } = { ...DEFAULT_TYPEWRITER, ...typewriter }
32
+
33
+ const entries = Object.entries(fields).filter(([, v]) => typeof v === 'string' && v.length > 0)
34
+ if (!entries.length) {
35
+ animating = false
36
+ notify()
37
+ onAnimateDone?.()
38
+ return
39
+ }
40
+
41
+ const total = entries.reduce((sum, [, v]) => sum + v.length, 0)
42
+ const charsPerTick = Math.max(minCharsPerTick, Math.ceil(total / charDivisor))
43
+ const indices = Object.fromEntries(entries.map(([k]) => [k, 0]))
44
+ let entryIdx = 0
45
+
46
+ function tick() {
47
+ const [field, text] = entries[entryIdx]
48
+ indices[field] = Math.min(indices[field] + charsPerTick, text.length)
49
+ onAnimate?.(field, text.slice(0, indices[field]))
50
+ if (indices[field] >= text.length) entryIdx++
51
+ if (entryIdx >= entries.length) {
52
+ animating = false
53
+ notify()
54
+ onAnimateDone?.()
55
+ } else {
56
+ timer = setTimeout(tick, tickMs)
57
+ }
58
+ }
59
+
60
+ timer = setTimeout(tick, tickMs)
61
+ }
62
+
63
+ return {
64
+ get loading() { return loading },
65
+ get animating() { return animating },
66
+
67
+ subscribe(fn) {
68
+ listeners.add(fn)
69
+ return () => listeners.delete(fn)
70
+ },
71
+
72
+ async complete({ provider, model, key, prompt, maxTokens, agentOptions, onResult, onError, parseResponse, typewriter = DEFAULT_TYPEWRITER, onAnimate, onAnimateDone }) {
73
+ loading = true
74
+ notify()
75
+
76
+ try {
77
+ let text
78
+ if (agentOptions) {
79
+ const { system, tools, messages, maxTurns, onToolCall } = agentOptions
80
+ text = await callAnthropicWithTools({ key, model, system, tools, messages, maxTokens, maxTurns, onToolCall })
81
+ } else {
82
+ text = await callProvider({ provider, model, key, prompt, maxTokens })
83
+ }
84
+
85
+ const result = parseResponse ? parseResponse(text) : { text }
86
+ loading = false
87
+
88
+ if (typewriter && onAnimate) {
89
+ animating = true
90
+ notify()
91
+ onResult?.(result)
92
+ runTypewriter(result, { typewriter, onAnimate, onAnimateDone })
93
+ } else {
94
+ notify()
95
+ onResult?.(result)
96
+ onAnimateDone?.()
97
+ }
98
+ } catch (e) {
99
+ loading = false
100
+ animating = false
101
+ notify()
102
+ onError?.(e instanceof AiApiError ? e : new AiApiError('api_error'))
103
+ }
104
+ },
105
+
106
+ cancel() {
107
+ clearTimeout(timer)
108
+ loading = false
109
+ animating = false
110
+ notify()
111
+ },
112
+ }
113
+ }
@@ -1,85 +1,85 @@
1
- import { DEFAULT_MODELS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_LABELS } from './providers.js'
2
-
3
- async function getAdapter() {
4
- const mod = await import('@ulam/sawsawan')
5
- return mod.getAdapter()
6
- }
7
-
8
- /**
9
- * Vanilla provider config store. No React required.
10
- * Returns a plain object with getters, setters, and a subscribe() for change notifications.
11
- *
12
- * storageKeys: { provider, modelPrefix, keyPrefix, mode? }
13
- * providers: optional array of { id, label, defaultModel? }
14
- */
15
- export async function createProviderConfig(storageKeys, providers = DEFAULT_PROVIDERS) {
16
- const { provider: providerKey, modelPrefix, keyPrefix, mode: modeKey } = storageKeys
17
- const adapter = await getAdapter()
18
-
19
- const providerList = providers.map(p =>
20
- typeof p === 'string' ? { id: p, label: DEFAULT_PROVIDER_LABELS[p] || p } : p
21
- )
22
-
23
- let provider = adapter.readPref(providerKey) || providerList[0]?.id || 'anthropic'
24
-
25
- let models = Object.fromEntries(
26
- providerList.map(p => [
27
- p.id,
28
- adapter.readPref(`${modelPrefix}${p.id}`) || p.defaultModel || DEFAULT_MODELS[p.id] || '',
29
- ])
30
- )
31
-
32
- let mode = modeKey ? adapter.readPref(modeKey) === 'true' : false
33
-
34
- const listeners = new Set()
35
- const notify = () => listeners.forEach(fn => fn())
36
-
37
- return {
38
- get provider() { return provider },
39
- get models() { return { ...models } },
40
- get mode() { return mode },
41
- get providers() { return providerList },
42
-
43
- setProvider(id) {
44
- adapter.writePref(providerKey, id)
45
- provider = id
46
- notify()
47
- },
48
-
49
- setModel(providerId, modelId) {
50
- adapter.writePref(`${modelPrefix}${providerId}`, modelId)
51
- models = { ...models, [providerId]: modelId }
52
- notify()
53
- },
54
-
55
- setMode(value) {
56
- if (!modeKey) return
57
- adapter.writePref(modeKey, value ? 'true' : 'false')
58
- mode = value
59
- notify()
60
- },
61
-
62
- async setKey(providerId, value) {
63
- await adapter.setKey(`${keyPrefix}${providerId}`, value)
64
- },
65
-
66
- async getKey(providerId) {
67
- return (await adapter.getKey(`${keyPrefix}${providerId}`)) || ''
68
- },
69
-
70
- getModel(providerId) {
71
- return models[providerId] || DEFAULT_MODELS[providerId] || ''
72
- },
73
-
74
- getLabel(providerId) {
75
- return providerList.find(p => p.id === providerId)?.label
76
- || DEFAULT_PROVIDER_LABELS[providerId]
77
- || providerId
78
- },
79
-
80
- subscribe(fn) {
81
- listeners.add(fn)
82
- return () => listeners.delete(fn)
83
- },
84
- }
85
- }
1
+ import { DEFAULT_MODELS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_LABELS } from './providers.js'
2
+
3
+ async function getAdapter() {
4
+ const mod = await import('@ulam/sawsawan')
5
+ return mod.getAdapter()
6
+ }
7
+
8
+ /**
9
+ * Vanilla provider config store. No React required.
10
+ * Returns a plain object with getters, setters, and a subscribe() for change notifications.
11
+ *
12
+ * storageKeys: { provider, modelPrefix, keyPrefix, mode? }
13
+ * providers: optional array of { id, label, defaultModel? }
14
+ */
15
+ export async function createProviderConfig(storageKeys, providers = DEFAULT_PROVIDERS) {
16
+ const { provider: providerKey, modelPrefix, keyPrefix, mode: modeKey } = storageKeys
17
+ const adapter = await getAdapter()
18
+
19
+ const providerList = providers.map(p =>
20
+ typeof p === 'string' ? { id: p, label: DEFAULT_PROVIDER_LABELS[p] || p } : p
21
+ )
22
+
23
+ let provider = adapter.readPref(providerKey) || providerList[0]?.id || 'anthropic'
24
+
25
+ let models = Object.fromEntries(
26
+ providerList.map(p => [
27
+ p.id,
28
+ adapter.readPref(`${modelPrefix}${p.id}`) || p.defaultModel || DEFAULT_MODELS[p.id] || '',
29
+ ])
30
+ )
31
+
32
+ let mode = modeKey ? adapter.readPref(modeKey) === 'true' : false
33
+
34
+ const listeners = new Set()
35
+ const notify = () => listeners.forEach(fn => fn())
36
+
37
+ return {
38
+ get provider() { return provider },
39
+ get models() { return { ...models } },
40
+ get mode() { return mode },
41
+ get providers() { return providerList },
42
+
43
+ setProvider(id) {
44
+ adapter.writePref(providerKey, id)
45
+ provider = id
46
+ notify()
47
+ },
48
+
49
+ setModel(providerId, modelId) {
50
+ adapter.writePref(`${modelPrefix}${providerId}`, modelId)
51
+ models = { ...models, [providerId]: modelId }
52
+ notify()
53
+ },
54
+
55
+ setMode(value) {
56
+ if (!modeKey) return
57
+ adapter.writePref(modeKey, value ? 'true' : 'false')
58
+ mode = value
59
+ notify()
60
+ },
61
+
62
+ async setKey(providerId, value) {
63
+ await adapter.setKey(`${keyPrefix}${providerId}`, value)
64
+ },
65
+
66
+ async getKey(providerId) {
67
+ return (await adapter.getKey(`${keyPrefix}${providerId}`)) || ''
68
+ },
69
+
70
+ getModel(providerId) {
71
+ return models[providerId] || DEFAULT_MODELS[providerId] || ''
72
+ },
73
+
74
+ getLabel(providerId) {
75
+ return providerList.find(p => p.id === providerId)?.label
76
+ || DEFAULT_PROVIDER_LABELS[providerId]
77
+ || providerId
78
+ },
79
+
80
+ subscribe(fn) {
81
+ listeners.add(fn)
82
+ return () => listeners.delete(fn)
83
+ },
84
+ }
85
+ }