@mohou/runtime-provider 1.0.20

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 ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ status: locked
3
+ updated: 2026-10-01
4
+ ---
5
+
6
+ # @mohou/runtime-provider
7
+
8
+ Role: `provider`.
9
+
10
+ The brain interface and `echo` live here. Pi is `@mohou/runtime-pi`. Host consumes this package. It does not embed a vendor. Product: [provider injection](../../../docs/product/runtime/provider.md).
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@mohou/runtime-provider",
3
+ "version": "1.0.20",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^22.19.0 || >=24.0.0"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "lib/types",
15
+ "README.md",
16
+ "!**/*.tsbuildinfo"
17
+ ],
18
+ "main": "./src/index.ts",
19
+ "types": "./src/index.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./src/index.ts",
23
+ "default": "./src/index.ts"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "dependencies": {
29
+ "@mohou/contract": "^1.0.20"
30
+ }
31
+ }
package/src/codes.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** Codes this provider package emits. Callers match `code`. */
2
+
3
+ export const providerCodes = [
4
+ 'provider-missing',
5
+ 'provider-duplicate',
6
+ 'provider-unhealthy',
7
+ 'empty-completion',
8
+ 'cancelled',
9
+ 'unknown-model-provider',
10
+ 'unknown-model',
11
+ 'retry-exhausted',
12
+ ] as const
13
+
14
+ /** A failure emitted by a runtime provider or its registry. */
15
+ export type ProviderCode = (typeof providerCodes)[number]
16
+
17
+ /** Failure a caller branches on. The message is for a person. */
18
+ export class ProviderError extends Error {
19
+ readonly code: ProviderCode
20
+
21
+ /**
22
+ * @param code - one of {@link providerCodes}
23
+ * @param message - human text; not the match key
24
+ * @param options - optional `cause`
25
+ */
26
+ constructor(code: ProviderCode, message: string, options?: { cause?: unknown }) {
27
+ super(message, options)
28
+ this.name = 'ProviderError'
29
+ this.code = code
30
+ }
31
+ }
package/src/echo.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { ProviderError } from './codes.ts'
2
+ import type { RuntimeAgentOptions, RuntimeLlmOptions, RuntimeProvider } from './provider.ts'
3
+
4
+ /**
5
+ * The always-registered brain. `llm` returns the prompt. `agent` returns the goal.
6
+ * The tool set stays empty. A model name does not change it.
7
+ */
8
+ export function createEchoProvider(): RuntimeProvider {
9
+ let running = false
10
+ return {
11
+ id: 'echo',
12
+ label: 'Echo',
13
+ configure() {},
14
+ start() {
15
+ running = true
16
+ return Promise.resolve()
17
+ },
18
+ stop() {
19
+ running = false
20
+ return Promise.resolve()
21
+ },
22
+ healthy() {
23
+ return running
24
+ },
25
+ describe() {
26
+ return [{ name: 'model', kind: 'string' }]
27
+ },
28
+ llm(prompt, options) {
29
+ const failed = rejection(running, options?.signal)
30
+ if (failed) return failed
31
+ if (prompt.length === 0) {
32
+ return Promise.reject(new ProviderError('empty-completion', 'llm returned an empty completion'))
33
+ }
34
+ if (options?.stream === true) {
35
+ observeLlm(options, { type: 'status', status: 'running' })
36
+ observeLlm(options, { type: 'text-delta', text: prompt })
37
+ observeLlm(options, { type: 'done', text: prompt })
38
+ }
39
+ return Promise.resolve(prompt)
40
+ },
41
+ agent(goal, options) {
42
+ const failed = rejection(running, options?.signal)
43
+ if (failed) return failed
44
+ if (goal.length === 0) {
45
+ return Promise.reject(new ProviderError('empty-completion', 'agent returned an empty completion'))
46
+ }
47
+ observe(options, { type: 'status', status: 'running' })
48
+ observe(options, { type: 'done', text: goal })
49
+ return Promise.resolve(goal)
50
+ },
51
+ }
52
+ }
53
+
54
+ function rejection(running: boolean, signal: AbortSignal | undefined): Promise<never> | undefined {
55
+ if (signal?.aborted) return Promise.reject(new ProviderError('cancelled', 'cancelled'))
56
+ if (!running) return Promise.reject(new ProviderError('provider-unhealthy', 'runtime provider is not started'))
57
+ return undefined
58
+ }
59
+
60
+ function observeLlm(options: RuntimeLlmOptions, event: Parameters<NonNullable<RuntimeLlmOptions['onEvent']>>[0]): void {
61
+ try {
62
+ options.onEvent?.(event)
63
+ } catch {
64
+ // A throwing observer does not abort the completion.
65
+ }
66
+ }
67
+
68
+ function observe(options: RuntimeAgentOptions | undefined, event: Parameters<NonNullable<RuntimeAgentOptions['onEvent']>>[0]): void {
69
+ try {
70
+ options?.onEvent?.(event)
71
+ } catch {
72
+ // A throwing observer does not abort the run.
73
+ }
74
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Brain interface and the echo provider. @module @mohou/runtime-provider */
2
+
3
+ export const packageId = '@mohou/runtime-provider' as const
4
+
5
+ export { ProviderError, providerCodes, type ProviderCode } from './codes.ts'
6
+ export type { ModelListing, RuntimeAgentOptions, RuntimeLlmOptions, RuntimeProvider, RuntimeProviderConfig, SettingsField } from './provider.ts'
7
+ export { assertKnownModel, type ModelChoice } from './model-choice.ts'
8
+ export { createProviderRegistry, type ProviderRegistry } from './registry.ts'
9
+ export { createEchoProvider } from './echo.ts'
@@ -0,0 +1,39 @@
1
+ import { ProviderError } from './codes.ts'
2
+ import type { RuntimeProvider } from './provider.ts'
3
+
4
+ /** The vendor and model on one call. Neither is the brain id. */
5
+ export interface ModelChoice {
6
+ readonly provider?: string
7
+ readonly model?: string
8
+ }
9
+
10
+ /**
11
+ * Admit a call's vendor and model against the brain's catalog.
12
+ * A brain that does not implement `models` accepts any pair.
13
+ * This does not change the brain id or its tools.
14
+ * @param provider - the registered brain
15
+ * @param choice - call options, after host policy filled omissions
16
+ */
17
+ export async function assertKnownModel(provider: RuntimeProvider, choice: ModelChoice): Promise<void> {
18
+ if (provider.models === undefined) return
19
+ const listings = await provider.models()
20
+ const vendor = choice.provider
21
+ const model = choice.model
22
+ if (vendor === undefined && model === undefined) return
23
+
24
+ if (vendor !== undefined) {
25
+ const listing = listings.find(item => item.provider === vendor)
26
+ if (listing === undefined) {
27
+ throw new ProviderError('unknown-model-provider', `unknown model provider: ${vendor}`)
28
+ }
29
+ if (model !== undefined && !listing.models.includes(model)) {
30
+ throw new ProviderError('unknown-model', `unknown model: ${vendor}/${model}`)
31
+ }
32
+ return
33
+ }
34
+
35
+ const owners = listings.filter(item => item.models.includes(model ?? ''))
36
+ if (owners.length !== 1) {
37
+ throw new ProviderError('unknown-model', `unknown model: ${model ?? ''}`)
38
+ }
39
+ }
@@ -0,0 +1,49 @@
1
+ import type { AppAgentEvent, AppAgentOptions, AppLlmEvent, AppLlmOptions } from '@mohou/contract'
2
+
3
+ /** Provider-only listener. Authors read a stream. They do not pass this. */
4
+ export type RuntimeLlmOptions = AppLlmOptions & {
5
+ onEvent?: (event: AppLlmEvent) => void
6
+ }
7
+
8
+ /** Provider-only listener. Authors read a stream. They do not pass this. */
9
+ export type RuntimeAgentOptions = AppAgentOptions & {
10
+ onEvent?: (event: AppAgentEvent) => void
11
+ }
12
+
13
+ /** One settings field a panel can render. */
14
+ export interface SettingsField {
15
+ readonly name: string
16
+ readonly kind: 'string' | 'secret' | 'select'
17
+ readonly options?: readonly string[]
18
+ }
19
+
20
+ /** Models one vendor inside a brain exposes. The brain id is not this field. */
21
+ export interface ModelListing {
22
+ readonly provider: string
23
+ readonly models: readonly string[]
24
+ }
25
+
26
+ /** Config Shell injects. Numeric limits are not this package's policy. */
27
+ export interface RuntimeProviderConfig {
28
+ readonly provider?: string
29
+ readonly model?: string
30
+ readonly options?: Record<string, string>
31
+ }
32
+
33
+ /**
34
+ * One brain. Host calls `configure`, then `start`, then `llm` and `agent`.
35
+ * Switching model does not change the tools that provider already runs. That set is not a method on this interface.
36
+ */
37
+ export interface RuntimeProvider {
38
+ readonly id: string
39
+ readonly label?: string
40
+ configure?(config: RuntimeProviderConfig): void
41
+ start(): Promise<void>
42
+ stop(): Promise<void>
43
+ healthy(): boolean
44
+ /** Vendor groups. Absent means this brain does not reject a model name. Each call may fetch. */
45
+ models?(): Promise<readonly ModelListing[]>
46
+ llm(prompt: string, options?: RuntimeLlmOptions): Promise<string>
47
+ agent(goal: string, options?: RuntimeAgentOptions): Promise<string>
48
+ describe?(): readonly SettingsField[]
49
+ }
@@ -0,0 +1,35 @@
1
+ import { ProviderError } from './codes.ts'
2
+ import type { RuntimeProvider } from './provider.ts'
3
+
4
+ /** Registrations of brains Shell knows how to construct. */
5
+ export interface ProviderRegistry {
6
+ register(provider: RuntimeProvider): () => void
7
+ get(id: string): RuntimeProvider
8
+ ids(): readonly string[]
9
+ }
10
+
11
+ /** A registry whose `register` returns the disposer. A second id throws. */
12
+ export function createProviderRegistry(): ProviderRegistry {
13
+ const providers = new Map<string, RuntimeProvider>()
14
+ return {
15
+ register(provider) {
16
+ if (providers.has(provider.id)) {
17
+ throw new ProviderError('provider-duplicate', `already registered: ${provider.id}`)
18
+ }
19
+ providers.set(provider.id, provider)
20
+ return () => {
21
+ providers.delete(provider.id)
22
+ }
23
+ },
24
+ get(id) {
25
+ const found = providers.get(id)
26
+ if (found === undefined) {
27
+ throw new ProviderError('provider-missing', `no runtime provider: ${id}`)
28
+ }
29
+ return found
30
+ },
31
+ ids() {
32
+ return [...providers.keys()]
33
+ },
34
+ }
35
+ }