@roj-ai/isolate-runtime 0.0.0 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +64 -0
  2. package/dist/alarm-scheduler.d.ts +78 -0
  3. package/dist/alarm-scheduler.d.ts.map +1 -0
  4. package/dist/alarm-scheduler.js +168 -0
  5. package/dist/alarm-scheduler.js.map +1 -0
  6. package/dist/build.d.ts +19 -0
  7. package/dist/build.d.ts.map +1 -0
  8. package/dist/build.js +86 -0
  9. package/dist/build.js.map +1 -0
  10. package/dist/host-contract.d.ts +23 -0
  11. package/dist/host-contract.d.ts.map +1 -0
  12. package/dist/host-contract.js +6 -0
  13. package/dist/host-contract.js.map +1 -0
  14. package/dist/host.d.ts +9 -0
  15. package/dist/host.d.ts.map +1 -0
  16. package/dist/host.js +7 -0
  17. package/dist/host.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +4 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/info.d.ts +4 -0
  23. package/dist/info.d.ts.map +1 -0
  24. package/dist/info.js +5 -0
  25. package/dist/info.js.map +1 -0
  26. package/dist/modules.d.ts +26 -0
  27. package/dist/modules.d.ts.map +1 -0
  28. package/dist/modules.js +52 -0
  29. package/dist/modules.js.map +1 -0
  30. package/dist/runtime.d.ts +48 -0
  31. package/dist/runtime.d.ts.map +1 -0
  32. package/dist/runtime.js +80 -0
  33. package/dist/runtime.js.map +1 -0
  34. package/dist/testing.d.ts +17 -0
  35. package/dist/testing.d.ts.map +1 -0
  36. package/dist/testing.js +25 -0
  37. package/dist/testing.js.map +1 -0
  38. package/package.json +53 -4
  39. package/src/alarm-scheduler.ts +234 -0
  40. package/src/build.ts +95 -0
  41. package/src/host-contract.ts +25 -0
  42. package/src/host.ts +9 -0
  43. package/src/index.ts +12 -0
  44. package/src/info.ts +6 -0
  45. package/src/modules.ts +58 -0
  46. package/src/runtime.ts +115 -0
  47. package/src/testing.ts +33 -0
  48. package/src/worker/facet.ts +29 -0
  49. package/src/worker/main.ts +6 -0
  50. package/src/worker/roj-app-bundle.d.ts +4 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The contract between the runtime and the Durable Object hosting it. Kept free of imports,
3
+ * so a host type-checked against Workers types can use it without the SDK.
4
+ */
5
+
6
+ /** A user-output notification in the shape the sandbox runtime sends over its agent WebSocket. */
7
+ export interface RuntimeBroadcast {
8
+ type: string
9
+ payload: unknown
10
+ ts: number
11
+ }
12
+
13
+ /** What the host gives the runtime. Bound to one instance by the host; the runtime cannot name another. */
14
+ export interface RuntimeHost {
15
+ /** The single alarm slot of the host's Durable Object, lent to the runtime. */
16
+ setAlarm(scheduledTime: number): Promise<void>
17
+ deleteAlarm(): Promise<void>
18
+ broadcast(message: RuntimeBroadcast): Promise<void>
19
+ }
20
+
21
+ /** What the host calls on the facet's stub. */
22
+ export interface RuntimeFacetRpc {
23
+ fetch(request: Request): Promise<Response>
24
+ deliverWakes(): Promise<string[]>
25
+ }
package/src/host.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What a host needs at run time: the contract with the facet, the module map helpers and the
3
+ * runtime version. Imports nothing from the runtime itself, so it adds nothing to a host's bundle.
4
+ */
5
+
6
+ export type { RuntimeBroadcast, RuntimeFacetRpc, RuntimeHost } from './host-contract.js'
7
+ export { ISOLATE_RUNTIME_NAME, ISOLATE_RUNTIME_VERSION } from './info.js'
8
+ export type { LoaderModule, RuntimeModules } from './modules.js'
9
+ export { APP_BUNDLE_MODULE, unresolvedImports, withAppBundle } from './modules.js'
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type {
2
+ AlarmScheduler,
3
+ AlarmSchedulerHost,
4
+ AlarmSchedulerStats,
5
+ AlarmStorageLike,
6
+ SyncKvLike,
7
+ } from './alarm-scheduler.js'
8
+ export { createAlarmScheduler } from './alarm-scheduler.js'
9
+ export { ISOLATE_RUNTIME_NAME, ISOLATE_RUNTIME_VERSION } from './info.js'
10
+ export type { RuntimeBroadcast, RuntimeFacetRpc, RuntimeHost } from './host-contract.js'
11
+ export type { IsolateRuntime, IsolateRuntimeOptions, IsolateRuntimeState } from './runtime.js'
12
+ export { createIsolateRuntime, EGRESS_PROVIDED_KEY } from './runtime.js'
package/src/info.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Read from this package's own package.json, so the value tracks releases. */
2
+
3
+ import pkg from '../package.json' with { type: 'json' }
4
+
5
+ export const ISOLATE_RUNTIME_VERSION = pkg.version
6
+ export const ISOLATE_RUNTIME_NAME = pkg.name
package/src/modules.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The Worker Loader module map a host builds once per runtime version (`./build`) and completes with
3
+ * one app bundle per instance. Free of Bun and Node APIs, so a host can use it inside a Worker.
4
+ */
5
+
6
+ /** A Worker Loader module: a `.js` name maps to ES module source, any other name needs its type. */
7
+ export type LoaderModule = string | { js: string }
8
+
9
+ export interface RuntimeModules {
10
+ mainModule: string
11
+ modules: Record<string, LoaderModule>
12
+ }
13
+
14
+ /** The module name the main module imports the app's config from. */
15
+ export const APP_BUNDLE_MODULE = 'roj-app-bundle'
16
+
17
+ /** The runtime's modules plus one app bundle: what a host passes to `LOADER.get()`. */
18
+ export function withAppBundle(runtime: RuntimeModules, appBundle: string): RuntimeModules {
19
+ return { mainModule: runtime.mainModule, modules: { ...runtime.modules, [APP_BUNDLE_MODULE]: { js: appBundle } } }
20
+ }
21
+
22
+ /** The loader resolves a specifier against the importing module's directory, a bare one included. */
23
+ function resolveModuleName(importer: string, specifier: string): string {
24
+ if (specifier.startsWith('/')) return specifier.slice(1)
25
+ const parts = importer.split('/').slice(0, -1)
26
+ for (const part of specifier.split('/')) {
27
+ if (part === '..') parts.pop()
28
+ else if (part !== '.') parts.push(part)
29
+ }
30
+ return parts.join('/')
31
+ }
32
+
33
+ const STATIC_IMPORT = /^(?:import|export)\b[^;'"]*?\bfrom\s*["']([^"']+)["']|^import\s*["']([^"']+)["']/gm
34
+ const DYNAMIC_IMPORT = /\bimport\(\s*["']([^"']+)["']\s*\)/g
35
+ /** Bun's `target: 'bun'` output loads CommonJS dependencies through it; workerd has no such API. */
36
+ const BUN_REQUIRE = 'import.meta.require'
37
+
38
+ /**
39
+ * Imports that would not resolve under the loader's rules, plus any use of `import.meta.require`,
40
+ * reported with that as the specifier. Bun's unminified ESM output keeps every static import at the
41
+ * start of a line, which is what the scan relies on. Platform builtins (`node:`, `cloudflare:`) are not checked.
42
+ */
43
+ export function unresolvedImports(modules: Record<string, LoaderModule>): { module: string; specifier: string }[] {
44
+ const missing: { module: string; specifier: string }[] = []
45
+ for (const [name, module] of Object.entries(modules)) {
46
+ const source = typeof module === 'string' ? module : module.js
47
+ const specifiers = [
48
+ ...[...source.matchAll(STATIC_IMPORT)].map(match => match[1] ?? match[2]),
49
+ ...[...source.matchAll(DYNAMIC_IMPORT)].map(match => match[1]),
50
+ ]
51
+ for (const specifier of specifiers) {
52
+ if (specifier === undefined || /^(?:node|cloudflare):/.test(specifier)) continue
53
+ if (!(resolveModuleName(name, specifier) in modules)) missing.push({ module: name, specifier })
54
+ }
55
+ if (source.includes(BUN_REQUIRE)) missing.push({ module: name, specifier: BUN_REQUIRE })
56
+ }
57
+ return missing
58
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The Roj runtime for a Durable Object that runs untrusted app code.
3
+ *
4
+ * Everything persistent lives in the object's own SQLite: the kompjutr filesystem,
5
+ * the event log, the LLM call log and the wake rows. Everything else comes from
6
+ * {@link RuntimeHost}, the only capability the object holds. Outbound `fetch`
7
+ * goes wherever the host routed the isolate's global outbound, which is where
8
+ * provider keys and tool credentials are added — the runtime never holds them.
9
+ *
10
+ * It answers the same HTTP surface as the sandbox runtime (`/rpc`, `/status`,
11
+ * `/sessions/*`), so a host can reuse its sandbox client. Requests reach it only
12
+ * through the host, which authenticates the caller and stamps `_caller`.
13
+ */
14
+
15
+ import { type DurableObjectStorageLike, Workspace } from '@kompjutr/do'
16
+ import { createKompjutrPlatform, KompjutrEventStore } from '@roj-ai/kompjutr-platform'
17
+ import { bootstrap, createSystemFromServices } from '@roj-ai/sdk'
18
+ import type { Config, RojConfig } from '@roj-ai/sdk'
19
+ import { createApp } from '@roj-ai/sdk/transport/http/app'
20
+ import { createAlarmScheduler, type AlarmSchedulerStats, type SyncKvLike } from './alarm-scheduler.js'
21
+ import type { RuntimeHost } from './host-contract.js'
22
+ import { ISOLATE_RUNTIME_NAME, ISOLATE_RUNTIME_VERSION } from './info.js'
23
+
24
+ /** The parts of `DurableObjectState` the runtime uses. */
25
+ export interface IsolateRuntimeState {
26
+ storage: DurableObjectStorageLike & { kv: SyncKvLike }
27
+ waitUntil(promise: Promise<unknown>): void
28
+ }
29
+
30
+ export interface IsolateRuntimeOptions {
31
+ userConfig: RojConfig
32
+ host: RuntimeHost
33
+ state: IsolateRuntimeState
34
+ /** Overrides of the built-in config, e.g. `llmMock` in tests. */
35
+ config?: Partial<Config>
36
+ /** See `AlarmSchedulerHost.inFlightRetryMs`. */
37
+ inFlightRetryMs?: number
38
+ }
39
+
40
+ export interface IsolateRuntime {
41
+ /** Loads open sessions, which recovers agents an eviction left mid-turn. Await before serving. */
42
+ start(): Promise<void>
43
+ fetch(request: Request): Promise<Response>
44
+ /** Call from the host's `alarm()`. Returns the wake keys delivered. */
45
+ deliverWakes(): Promise<string[]>
46
+ schedulerStats(): AlarmSchedulerStats
47
+ }
48
+
49
+ const DATA_ROOT = '/data'
50
+
51
+ /**
52
+ * Stands in for the provider key, so the SDK's key check passes. The host's
53
+ * egress replaces the authorization header on provider requests.
54
+ */
55
+ export const EGRESS_PROVIDED_KEY = 'provided-by-host-egress'
56
+
57
+ export function createIsolateRuntime(options: IsolateRuntimeOptions): IsolateRuntime {
58
+ const { state, host } = options
59
+ // `loadConfig()` reads process.env and the cwd, which an isolate has neither of.
60
+ const config: Config = {
61
+ port: 0,
62
+ host: 'localhost',
63
+ dataPath: DATA_ROOT,
64
+ persistence: 'memory',
65
+ logLevel: 'warn',
66
+ logFormat: 'json',
67
+ llmLoggingEnabled: true,
68
+ openRouterApiKey: EGRESS_PROVIDED_KEY,
69
+ agentRuntime: { name: ISOLATE_RUNTIME_NAME, version: ISOLATE_RUNTIME_VERSION },
70
+ ...options.config,
71
+ }
72
+
73
+ const workspace = new Workspace({ storage: state.storage })
74
+ workspace.filesystem.mkdir(DATA_ROOT, { recursive: true })
75
+ const scheduler = createAlarmScheduler({
76
+ storage: {
77
+ kv: state.storage.kv,
78
+ setAlarm: scheduledTime => host.setAlarm(scheduledTime),
79
+ deleteAlarm: () => host.deleteAlarm(),
80
+ },
81
+ waitUntil: promise => state.waitUntil(promise),
82
+ inFlightRetryMs: options.inFlightRetryMs,
83
+ })
84
+ const services = bootstrap(config, options.userConfig, createKompjutrPlatform(workspace, { scheduler }), {
85
+ pluginProfile: 'isolate',
86
+ eventStore: new KompjutrEventStore(workspace.db),
87
+ })
88
+ const system = createSystemFromServices(services, {
89
+ onUserOutput: notification => {
90
+ state.waitUntil(host.broadcast({ type: notification.type, payload: notification.payload, ts: Date.now() }))
91
+ },
92
+ })
93
+ const app = createApp({ ...services, sessionRuntime: system.sessionManager })
94
+
95
+ return {
96
+ async start() {
97
+ await system.sessionManager.loadAllSessions()
98
+ await scheduler.flush()
99
+ },
100
+
101
+ async fetch(request) {
102
+ const response = await app.fetch(request)
103
+ await scheduler.flush()
104
+ return response
105
+ },
106
+
107
+ async deliverWakes() {
108
+ const keys = await scheduler.deliverDue(key => system.sessionManager.dispatchWake(key))
109
+ await scheduler.flush()
110
+ return keys
111
+ },
112
+
113
+ schedulerStats: () => scheduler.stats(),
114
+ }
115
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,33 @@
1
+ /** In-memory stand-ins for the Durable Object storage the scheduler uses. Tests only. */
2
+
3
+ import type { AlarmStorageLike, SyncKvLike } from './alarm-scheduler.js'
4
+
5
+ export class MemoryKv implements SyncKvLike {
6
+ readonly #rows = new Map<string, unknown>()
7
+
8
+ list(options?: { prefix?: string }): Iterable<[string, unknown]> {
9
+ const prefix = options?.prefix ?? ''
10
+ return [...this.#rows].filter(([key]) => key.startsWith(prefix)).sort(([a], [b]) => a.localeCompare(b))
11
+ }
12
+
13
+ put(key: string, value: unknown): void {
14
+ this.#rows.set(key, value)
15
+ }
16
+
17
+ delete(key: string): boolean {
18
+ return this.#rows.delete(key)
19
+ }
20
+ }
21
+
22
+ export class MemoryAlarmStorage implements AlarmStorageLike {
23
+ readonly kv = new MemoryKv()
24
+ alarm: number | null = null
25
+
26
+ async setAlarm(scheduledTime: number): Promise<void> {
27
+ this.alarm = scheduledTime
28
+ }
29
+
30
+ async deleteAlarm(): Promise<void> {
31
+ this.alarm = null
32
+ }
33
+ }
@@ -0,0 +1,29 @@
1
+ import type { RojConfig } from '@roj-ai/sdk'
2
+ import { DurableObject } from 'cloudflare:workers'
3
+ import type { RuntimeFacetRpc, RuntimeHost } from '../host-contract.js'
4
+ import { createIsolateRuntime, type IsolateRuntime } from '../runtime.js'
5
+
6
+ export interface RuntimeFacetEnv {
7
+ HOST: RuntimeHost
8
+ }
9
+
10
+ /** The Durable Object class a host gets through `getDurableObjectClass('Runtime')` and runs as a facet. */
11
+ export function createRuntimeFacet(userConfig: RojConfig) {
12
+ return class Runtime extends DurableObject<RuntimeFacetEnv> implements RuntimeFacetRpc {
13
+ readonly #runtime: IsolateRuntime
14
+
15
+ constructor(ctx: DurableObjectState, env: RuntimeFacetEnv) {
16
+ super(ctx, env)
17
+ this.#runtime = createIsolateRuntime({ userConfig, host: env.HOST, state: ctx })
18
+ void ctx.blockConcurrencyWhile(() => this.#runtime.start())
19
+ }
20
+
21
+ fetch(request: Request): Promise<Response> {
22
+ return this.#runtime.fetch(request)
23
+ }
24
+
25
+ deliverWakes(): Promise<string[]> {
26
+ return this.#runtime.deliverWakes()
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,6 @@
1
+ /** Main module of the loaded worker. `roj-app-bundle` is the app's `roj build` output, added by the host. */
2
+
3
+ import appConfig from 'roj-app-bundle'
4
+ import { createRuntimeFacet } from './facet.js'
5
+
6
+ export const Runtime = createRuntimeFacet(appConfig)
@@ -0,0 +1,4 @@
1
+ declare module 'roj-app-bundle' {
2
+ const config: import('@roj-ai/sdk').RojConfig
3
+ export default config
4
+ }