426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
227 lines
8.4 KiB
Swift
227 lines
8.4 KiB
Swift
import Foundation
|
|
import JavaScriptCore
|
|
|
|
// MARK: - JSContext queue affinity
|
|
|
|
/// Thread/queue-affinity helpers for the JSContext-bound dispatch queue.
|
|
///
|
|
/// JSContext is single-threaded. The runtime owns a serial DispatchQueue
|
|
/// (`ai.eliza.bun.runtime`). All host-function bodies execute on that queue.
|
|
/// I/O bridges (HTTP fetch, llama generation) dispatch off the runtime queue
|
|
/// to do work, then post results back via a wrapper that re-enters the
|
|
/// JSContext queue.
|
|
public enum RuntimeQueue {
|
|
public static let label = "ai.eliza.bun.runtime"
|
|
|
|
/// Returns the JS thread's dispatch queue. Created lazily by the runtime
|
|
/// at startup and stored here for the bridge modules to use.
|
|
public static var current: DispatchQueue?
|
|
|
|
/// Dispatches a block onto the JSContext queue. If the queue is not yet
|
|
/// set up (e.g. early-bridge construction during install), runs inline.
|
|
public static func dispatchOnJS(_ block: @escaping () -> Void) {
|
|
if let q = current {
|
|
q.async(execute: block)
|
|
} else {
|
|
block()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - JSValue conveniences
|
|
|
|
public extension JSValue {
|
|
/// Returns `true` when the value is `null` or `undefined`.
|
|
var isNullish: Bool {
|
|
return isNull || isUndefined
|
|
}
|
|
|
|
/// Bridges a `JSValue` Uint8Array (or ArrayBuffer) to a Swift `Data` blob.
|
|
/// Returns `nil` if the value isn't a TypedArray / ArrayBuffer-backed view.
|
|
func toData() -> Data? {
|
|
guard let ctx = context else { return nil }
|
|
|
|
// Common path: JSC exposes typed-array length via .length and per-element
|
|
// access via subscript. That's slow for large blobs. Faster: call back
|
|
// into JS to expose a byteLength + slice() to a Uint8Array, then walk
|
|
// the byte values. We keep it portable across JSC builds without
|
|
// relying on `JSObjectGetTypedArrayBytesPtr` which is gated by
|
|
// availability flags.
|
|
|
|
let lenValue = forProperty("length")
|
|
guard let lenValue = lenValue, lenValue.isNumber else {
|
|
return nil
|
|
}
|
|
let count = Int(lenValue.toInt32())
|
|
if count == 0 { return Data() }
|
|
|
|
let global = ctx.globalObject
|
|
let helper = global?.forProperty("__eliza_uint8_to_array")
|
|
if helper == nil || helper?.isUndefined == true {
|
|
let install = "globalThis.__eliza_uint8_to_array = function(u){const o=new Array(u.length); for (let i=0;i<u.length;i++){o[i]=u[i]|0;} return o;};"
|
|
ctx.evaluateScript(install)
|
|
}
|
|
|
|
guard let arrayValue = ctx.evaluateScript("globalThis.__eliza_uint8_to_array")?
|
|
.call(withArguments: [self]),
|
|
let nsArray = arrayValue.toArray() else {
|
|
return nil
|
|
}
|
|
var out = Data(count: nsArray.count)
|
|
out.withUnsafeMutableBytes { (raw: UnsafeMutableRawBufferPointer) in
|
|
guard let base = raw.baseAddress else { return }
|
|
for (i, entry) in nsArray.enumerated() {
|
|
let n = (entry as? NSNumber)?.uint8Value ?? 0
|
|
base.storeBytes(of: n, toByteOffset: i, as: UInt8.self)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
/// Returns a string array from a JS array of strings. Returns nil if not
|
|
/// an array.
|
|
func toStringArray() -> [String]? {
|
|
guard isArray else { return nil }
|
|
guard let arr = toArray() else { return nil }
|
|
var out: [String] = []
|
|
out.reserveCapacity(arr.count)
|
|
for item in arr {
|
|
if let s = item as? String {
|
|
out.append(s)
|
|
} else if let n = item as? NSNumber {
|
|
out.append(n.stringValue)
|
|
} else {
|
|
out.append(String(describing: item))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
/// Returns a [String: String] map from a JS object whose values are strings.
|
|
func toStringMap() -> [String: String] {
|
|
var out: [String: String] = [:]
|
|
guard let obj = toObject() as? [String: Any] else { return out }
|
|
for (k, v) in obj {
|
|
if let s = v as? String {
|
|
out[k] = s
|
|
} else if let n = v as? NSNumber {
|
|
out[k] = n.stringValue
|
|
} else {
|
|
out[k] = String(describing: v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
}
|
|
|
|
public extension JSContext {
|
|
/// Returns a fresh Uint8Array JSValue for the given Swift `Data`.
|
|
func newUint8Array(_ data: Data) -> JSValue {
|
|
// Constructing a Uint8Array directly from raw bytes through the public
|
|
// JSC API requires either JSObjectMakeTypedArrayWithBytesNoCopy or
|
|
// round-tripping through JS. We avoid the C API for portability.
|
|
// Strategy: stash bytes in a JS Array of numbers, then convert via
|
|
// Uint8Array.from().
|
|
let helper = """
|
|
(function(arr){return Uint8Array.from(arr);})
|
|
"""
|
|
let factory = evaluateScript(helper)
|
|
var ints: [Int] = []
|
|
ints.reserveCapacity(data.count)
|
|
data.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in
|
|
guard let base = raw.baseAddress else { return }
|
|
for i in 0..<data.count {
|
|
let b = base.load(fromByteOffset: i, as: UInt8.self)
|
|
ints.append(Int(b))
|
|
}
|
|
}
|
|
let result = factory?.call(withArguments: [ints])
|
|
return result ?? JSValue(undefinedIn: self)
|
|
}
|
|
|
|
/// Installs a host function on `globalThis.__ELIZA_BRIDGE__[name]`.
|
|
/// `body` runs on the JSContext queue (the caller's thread).
|
|
func installBridgeFunction(name: String, _ body: @escaping ([JSValue]) -> Any?) {
|
|
let block: @convention(block) () -> Any? = {
|
|
let args = JSContext.currentArguments() as? [JSValue] ?? []
|
|
return body(args)
|
|
}
|
|
let global = globalObject!
|
|
var bridge = global.forProperty("__ELIZA_BRIDGE__")
|
|
if bridge == nil || bridge?.isUndefined == true || bridge?.isNull == true {
|
|
evaluateScript("globalThis.__ELIZA_BRIDGE__ = {};")
|
|
bridge = global.forProperty("__ELIZA_BRIDGE__")
|
|
}
|
|
bridge?.setObject(unsafeBitCast(block, to: AnyObject.self), forKeyedSubscript: name as NSString)
|
|
}
|
|
}
|
|
|
|
// MARK: - Exception bridging
|
|
|
|
public struct JSRuntimeError: Error, CustomStringConvertible {
|
|
public let message: String
|
|
public let stack: String?
|
|
|
|
public init(message: String, stack: String? = nil) {
|
|
self.message = message
|
|
self.stack = stack
|
|
}
|
|
|
|
public var description: String {
|
|
if let s = stack, !s.isEmpty {
|
|
return "\(message)\n\(s)"
|
|
}
|
|
return message
|
|
}
|
|
}
|
|
|
|
public extension JSContext {
|
|
/// Reads and clears the pending exception on the context. Returns a
|
|
/// Swift error if one was present.
|
|
func takeException() -> JSRuntimeError? {
|
|
guard let exc = self.exception else { return nil }
|
|
defer { self.exception = nil }
|
|
let message = exc.toString() ?? "Unknown JS error"
|
|
let stack = exc.objectForKeyedSubscript("stack")?.toString()
|
|
return JSRuntimeError(message: message, stack: stack)
|
|
}
|
|
}
|
|
|
|
// MARK: - JSManagedValue wrapper
|
|
|
|
/// Holds a JS callback function safely across Swift queue hops. JSManagedValue
|
|
/// is required because raw JSValue retention can deadlock the GC.
|
|
public final class ManagedCallback {
|
|
public let managed: JSManagedValue
|
|
public weak var context: JSContext?
|
|
|
|
public init?(value: JSValue) {
|
|
guard value.isObject else { return nil }
|
|
self.context = value.context
|
|
self.managed = JSManagedValue(value: value)
|
|
// Hand the managed value to the VM so it survives GC sweeps.
|
|
value.context?.virtualMachine.addManagedReference(self.managed, withOwner: self)
|
|
}
|
|
|
|
deinit {
|
|
context?.virtualMachine.removeManagedReference(managed, withOwner: self)
|
|
}
|
|
|
|
/// Invokes the callback on the JSContext queue.
|
|
public func call(args: [Any] = []) {
|
|
RuntimeQueue.dispatchOnJS { [weak self] in
|
|
guard let self = self else { return }
|
|
guard let value = self.managed.value else { return }
|
|
_ = value.call(withArguments: args)
|
|
}
|
|
}
|
|
|
|
/// Synchronous-on-queue call, intended for use when already on the
|
|
/// JSContext queue. Returns the call's JSValue result.
|
|
@discardableResult
|
|
public func callSync(args: [Any] = []) -> JSValue? {
|
|
guard let value = managed.value else { return nil }
|
|
return value.call(withArguments: args)
|
|
}
|
|
}
|