mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
166 lines
5.9 KiB
TypeScript
166 lines
5.9 KiB
TypeScript
/** Host BFF entry and Loader shell for the Remote contribution assembly. */
|
|
|
|
import { homedir } from 'node:os'
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type {
|
|
TypertRemoteEventDispatch,
|
|
TypertRemoteEventInvocation,
|
|
TypertRemoteEventOutcome,
|
|
TypertRemoteEventSource,
|
|
} from '@deepseek-ai/dsh-api-gateway'
|
|
import { Deque } from '@deepseek-ai/dsh-deque'
|
|
import { carrierKeyOf } from '@deepseek-ai/dsh-scope'
|
|
import { isJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
import { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts'
|
|
|
|
// The owner packages' client-safe `./types` exports carry the cordis `Events`
|
|
// declarations for every allowlisted event. Pulling them into this face is what
|
|
// makes the shape assertion below judge real signatures rather than an empty
|
|
// event vocabulary.
|
|
import type {} from '@deepseek-ai/dsh-commands/types'
|
|
import type {} from '@deepseek-ai/dsh-cordis-host-runner/types'
|
|
import type {} from '@deepseek-ai/dsh-credentials/types'
|
|
import type {} from '@deepseek-ai/dsh-llm/types'
|
|
import type {} from '@deepseek-ai/dsh-agent-presets/types'
|
|
import type {} from '@deepseek-ai/dsh-settings/types'
|
|
import type {} from '@deepseek-ai/dsh-user-approval'
|
|
import type {} from '@deepseek-ai/dsh-user-questions'
|
|
export type {} from '@deepseek-ai/dsh-api-session-controller/types'
|
|
|
|
export { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts'
|
|
export type { ApiRemoteForwardedEvent } from './types.ts'
|
|
|
|
/** Required Host service: the Gateway owns the physical Remote stream mux. */
|
|
export const inject = ['typertGateway']
|
|
|
|
/** Host plugin body registering this application's selected Cordis event source. */
|
|
export function apply(ctx: Context): void {
|
|
ctx.effect(
|
|
() => ctx.typertGateway.registerRemoteEvents(remoteEventSource(ctx), { home: homedir() }),
|
|
'api-remotes: forwarded Cordis event source',
|
|
)
|
|
}
|
|
|
|
/** Create the sole queue and listener set consumed by the registered Gateway. */
|
|
function remoteEventSource(ctx: Context): TypertRemoteEventSource {
|
|
return (signal) => {
|
|
const queue = new RemoteEventQueue()
|
|
const disposers = API_REMOTE_FORWARDED_EVENTS.map(({ event, mode }) => {
|
|
if (mode === 'emit') {
|
|
return ctx.on(event as never, ((...args: unknown[]) => {
|
|
queue.push({ event, args: assertJsonArgs(event, args) })
|
|
}) as never)
|
|
}
|
|
return ctx.on(event as never, (function (
|
|
this: unknown,
|
|
request: object,
|
|
next: () => unknown,
|
|
) {
|
|
const subject = carrierKeyOf(this)
|
|
if (subject === undefined) return next()
|
|
const value = Reflect.get(subject, 'ctx') as unknown
|
|
if (typeof value !== 'object' || value === null) {
|
|
throw new TypeError(`forwarded scoped event ${JSON.stringify(event)} has no live Context`)
|
|
}
|
|
return forwardWaterfall(
|
|
queue,
|
|
event,
|
|
request,
|
|
{ value: value as Context, subject },
|
|
next,
|
|
)
|
|
}) as never)
|
|
})
|
|
return queue.iterate(signal, () => {
|
|
for (const dispose of disposers) dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
/** One pull-driven queue bridging synchronous Cordis listeners to an AsyncIterable. */
|
|
class RemoteEventQueue {
|
|
private readonly buffer = new Deque<TypertRemoteEventDispatch>()
|
|
private waiter: (() => void) | undefined
|
|
private done = false
|
|
|
|
push(frame: TypertRemoteEventDispatch): boolean {
|
|
if (this.done) return false
|
|
this.buffer.pushBack(frame)
|
|
this.waiter?.()
|
|
return true
|
|
}
|
|
|
|
private end(reason: unknown): void {
|
|
if (this.done) return
|
|
this.done = true
|
|
while (this.buffer.size > 0) {
|
|
const dispatch = this.buffer.popFront() as TypertRemoteEventDispatch
|
|
if ('context' in dispatch) dispatch.reject(reason)
|
|
}
|
|
this.waiter?.()
|
|
}
|
|
|
|
async *iterate(signal: AbortSignal, cleanup: () => void): AsyncGenerator<TypertRemoteEventDispatch> {
|
|
const abort = (): void => { this.end(remoteEventSourceEndReason(signal)) }
|
|
signal.addEventListener('abort', abort, { once: true })
|
|
try {
|
|
while (true) {
|
|
if (this.done || signal.aborted) return
|
|
while (this.buffer.size > 0) yield this.buffer.popFront() as TypertRemoteEventDispatch
|
|
await new Promise<void>((resolve) => { this.waiter = resolve })
|
|
this.waiter = undefined
|
|
}
|
|
} finally {
|
|
signal.removeEventListener('abort', abort)
|
|
this.end(remoteEventSourceEndReason(signal))
|
|
cleanup()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize an event-source shutdown for pending Host waterfalls.
|
|
* @param signal - source lifetime whose reason wins after cancellation.
|
|
* @returns the cancellation reason or an unexpected-end failure.
|
|
*/
|
|
function remoteEventSourceEndReason(signal: AbortSignal): unknown {
|
|
if (signal.aborted) return signal.reason
|
|
return new Error('api-remotes: forwarded Remote event source ended')
|
|
}
|
|
|
|
/** Bridge one Cordis waterfall listener through the Gateway-owned pending event. */
|
|
function forwardWaterfall(
|
|
queue: RemoteEventQueue,
|
|
event: string,
|
|
request: object,
|
|
context: TypertRemoteEventInvocation['context'],
|
|
next: () => unknown,
|
|
): Promise<unknown> {
|
|
const settled = Promise.withResolvers<unknown>()
|
|
const dispatch: TypertRemoteEventInvocation = {
|
|
event,
|
|
request,
|
|
context,
|
|
resolve: (outcome: TypertRemoteEventOutcome) => {
|
|
if (outcome.kind === 'result') {
|
|
settled.resolve(outcome.value)
|
|
return
|
|
}
|
|
void Promise.resolve().then(next).then(settled.resolve, settled.reject)
|
|
},
|
|
reject: settled.reject,
|
|
}
|
|
if (!queue.push(dispatch)) void Promise.resolve().then(next).then(settled.resolve, settled.reject)
|
|
return settled.promise
|
|
}
|
|
|
|
/** Reject an allowlisted event whose runtime arguments are not lossless JSON data. */
|
|
function assertJsonArgs(event: string, args: readonly unknown[]): JsonValue[] {
|
|
for (const [index, arg] of args.entries()) {
|
|
if (!isJsonValue(arg)) {
|
|
throw new Error(`forwarded host event "${event}" argument ${String(index)} is not lossless JSON data`)
|
|
}
|
|
}
|
|
return args as JsonValue[]
|
|
}
|