mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
Extends the cross-version harness, which covered the terminal stream only, with the structured surface. The old side's advertised capabilities and registered method names are read from the extracted release checkout rather than written by hand: a build cannot advertise a string its own source never names, so "the old side does not have it" stays a fact about a real release.
150 lines
5.5 KiB
TypeScript
150 lines
5.5 KiB
TypeScript
import { readFileSync, readdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { materializeReleaseCheckout, REPO_ROOT, type ReleaseCheckout } from './release-checkout'
|
|
|
|
/**
|
|
* The two things that decide whether a structured agent session exists for a given
|
|
* pairing: the capability strings a build can name, and the RPC methods it
|
|
* registers. Both are read per build, so "the old side does not have it" is a fact
|
|
* about a real release rather than a hand-written list.
|
|
*/
|
|
|
|
export const WORKING_TREE = 'working-tree' as const
|
|
|
|
export type RpcReply = {
|
|
id: string
|
|
ok: boolean
|
|
streaming?: true
|
|
result?: unknown
|
|
error?: { code: string; message: string }
|
|
}
|
|
|
|
export type RpcClientIdentity = {
|
|
clientKind?: 'mobile' | 'runtime'
|
|
clientCapabilities?: readonly string[]
|
|
connectionId?: string
|
|
clientId?: string
|
|
}
|
|
|
|
export type AgentSessionDispatcher = {
|
|
dispatchStreaming: (
|
|
request: { id: string; authToken: string; method: string; params?: unknown },
|
|
reply: (message: string) => void,
|
|
options?: RpcClientIdentity
|
|
) => Promise<void>
|
|
}
|
|
|
|
export type AgentSessionWireBuild = {
|
|
/** Human label used in test names and failure messages. */
|
|
label: string
|
|
/** `working-tree` for current code, otherwise the resolved release commit. */
|
|
revision: string
|
|
/** Capability strings this build defines. A peer cannot advertise — nor a client
|
|
* ask for — a string its own source never names. */
|
|
capabilities: readonly string[]
|
|
protocolVersion: number
|
|
/** RPC method names the build registers, read from source. */
|
|
methodNames: readonly string[]
|
|
/** A dispatcher carrying a method set this build really ships, so an
|
|
* unknown-method answer is about the method and not an empty registry. */
|
|
createDispatcher: (runtime: unknown) => AgentSessionDispatcher
|
|
}
|
|
|
|
type DispatcherModule = {
|
|
RpcDispatcher: new (options: { runtime: unknown; methods: unknown[] }) => AgentSessionDispatcher
|
|
}
|
|
|
|
// A dotted literal in a `name:` position. Deliberately loose: over-matching only
|
|
// makes "this build registers no agentSession method" a stronger claim.
|
|
const METHOD_NAME = /\bname:\s*'([A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z0-9]+)+)'/g
|
|
|
|
/**
|
|
* Method names declared under `runtime/rpc/methods`, scanned rather than imported:
|
|
* a released build's method manifest reaches Electron, which cannot load here.
|
|
*/
|
|
function scanMethodNames(root: string): string[] {
|
|
const names = new Set<string>()
|
|
const walk = (directory: string): void => {
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
const full = join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
walk(full)
|
|
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
for (const match of readFileSync(full, 'utf8').matchAll(METHOD_NAME)) {
|
|
names.add(match[1]!)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
walk(join(root, 'src', 'main', 'runtime', 'rpc', 'methods'))
|
|
return [...names].sort()
|
|
}
|
|
|
|
function capabilityStrings(module: Record<string, unknown>): readonly string[] {
|
|
const declared = module.RUNTIME_CAPABILITIES
|
|
if (!Array.isArray(declared) || declared.length === 0) {
|
|
throw new Error('Cross-version harness found no RUNTIME_CAPABILITIES to compare')
|
|
}
|
|
return declared as readonly string[]
|
|
}
|
|
|
|
async function loadWorkingTreeBuild(): Promise<AgentSessionWireBuild> {
|
|
const [protocol, dispatcher, structured] = await Promise.all([
|
|
import('../../../src/shared/protocol-version'),
|
|
import('../../../src/main/runtime/rpc/dispatcher'),
|
|
import('../../../src/main/runtime/rpc/methods/structured-agent-session')
|
|
])
|
|
const module = dispatcher as unknown as DispatcherModule
|
|
return {
|
|
label: WORKING_TREE,
|
|
revision: WORKING_TREE,
|
|
capabilities: capabilityStrings(protocol as unknown as Record<string, unknown>),
|
|
protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION,
|
|
methodNames: scanMethodNames(REPO_ROOT),
|
|
createDispatcher: (runtime) =>
|
|
new module.RpcDispatcher({
|
|
runtime,
|
|
methods: structured.STRUCTURED_AGENT_SESSION_METHODS as unknown[]
|
|
})
|
|
}
|
|
}
|
|
|
|
// Why @vite-ignore: the checkout is created at run time, so Vite cannot glob it at
|
|
// transform time. Vite-node still resolves and transforms the target on demand.
|
|
function importFromCheckout(specifier: string): Promise<Record<string, unknown>> {
|
|
return import(/* @vite-ignore */ specifier) as Promise<Record<string, unknown>>
|
|
}
|
|
|
|
async function loadReleaseBuild(checkout: ReleaseCheckout): Promise<AgentSessionWireBuild> {
|
|
const base = `${checkout.root}/src`
|
|
const [protocol, dispatcher, terminalMethods] = await Promise.all([
|
|
importFromCheckout(`${base}/shared/protocol-version.ts`),
|
|
importFromCheckout(`${base}/main/runtime/rpc/dispatcher.ts`),
|
|
importFromCheckout(`${base}/main/runtime/rpc/methods/terminal.ts`)
|
|
])
|
|
const module = dispatcher as unknown as DispatcherModule
|
|
return {
|
|
label: checkout.ref,
|
|
revision: checkout.commit,
|
|
capabilities: capabilityStrings(protocol),
|
|
protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION as number,
|
|
methodNames: scanMethodNames(checkout.root),
|
|
createDispatcher: (runtime) =>
|
|
new module.RpcDispatcher({
|
|
runtime,
|
|
methods: terminalMethods.TERMINAL_METHODS as unknown[]
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the structured-session wire surface for one build. `WORKING_TREE` imports
|
|
* current source; any other value is a git ref extracted into a cached checkout.
|
|
*/
|
|
export async function loadAgentSessionWireBuild(ref: string): Promise<AgentSessionWireBuild> {
|
|
if (ref === WORKING_TREE) {
|
|
return loadWorkingTreeBuild()
|
|
}
|
|
return loadReleaseBuild(materializeReleaseCheckout(ref))
|
|
}
|