@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.
- package/README.md +64 -0
- package/dist/alarm-scheduler.d.ts +78 -0
- package/dist/alarm-scheduler.d.ts.map +1 -0
- package/dist/alarm-scheduler.js +168 -0
- package/dist/alarm-scheduler.js.map +1 -0
- package/dist/build.d.ts +19 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +86 -0
- package/dist/build.js.map +1 -0
- package/dist/host-contract.d.ts +23 -0
- package/dist/host-contract.d.ts.map +1 -0
- package/dist/host-contract.js +6 -0
- package/dist/host-contract.js.map +1 -0
- package/dist/host.d.ts +9 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +7 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/info.d.ts +4 -0
- package/dist/info.d.ts.map +1 -0
- package/dist/info.js +5 -0
- package/dist/info.js.map +1 -0
- package/dist/modules.d.ts +26 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +52 -0
- package/dist/modules.js.map +1 -0
- package/dist/runtime.d.ts +48 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +80 -0
- package/dist/runtime.js.map +1 -0
- package/dist/testing.d.ts +17 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +25 -0
- package/dist/testing.js.map +1 -0
- package/package.json +53 -4
- package/src/alarm-scheduler.ts +234 -0
- package/src/build.ts +95 -0
- package/src/host-contract.ts +25 -0
- package/src/host.ts +9 -0
- package/src/index.ts +12 -0
- package/src/info.ts +6 -0
- package/src/modules.ts +58 -0
- package/src/runtime.ts +115 -0
- package/src/testing.ts +33 -0
- package/src/worker/facet.ts +29 -0
- package/src/worker/main.ts +6 -0
- 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
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
|
+
}
|