|
|
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
|
|
import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api'
|
|
|
|
|
import { AbstractApiClient, RpcId, SESSION_SEARCH_RESULT_LIMIT } from './api.ts'
|
|
|
|
|
import { randomUuid } from './random-uuid.ts'
|
|
|
|
|
import type { ClientConnectionRpc } from '../rpc.ts'
|
|
|
|
|
|
|
|
|
|
/** The fake carrier mints like a real one (business code never mints). */
|
|
|
|
|
function rpcRequest<P>(payload: P): RpcRequest<P> {
|
|
|
|
|
@@ -1329,6 +1330,16 @@ class FxInbox<F> implements StreamConn<F> {
|
|
|
|
|
* @returns an ApiProxy backed entirely by in-memory state — no host process, no network.
|
|
|
|
|
*/
|
|
|
|
|
export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
return createFixtureWorld(options).api
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FixtureWorld {
|
|
|
|
|
readonly api: ApiProxy
|
|
|
|
|
readonly rpc: ClientConnectionRpc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Build the fixture's legacy API and Remote RPC faces over one state graph. */
|
|
|
|
|
function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
|
|
|
|
// The resident fixture sessions all carry history, so none of them is blank.
|
|
|
|
|
const sessions: SessionSummary[] = options.empty ? [] : [
|
|
|
|
|
{ sessionId: sid('fx-alpha'), updatedAt: Date.now(), running: true, blank: false, cwd: '/tmp/fixture' },
|
|
|
|
|
@@ -1507,31 +1518,136 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
return backscanGoal(log) as FxGoalProjection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Shared CAS mutation path of the goal verbs (undefined next = invalid transition). */
|
|
|
|
|
const fxMutateGoal = (
|
|
|
|
|
request: RpcRequest<{ sessionId: SessionId; ref: { id: string; revision: number } }>,
|
|
|
|
|
ref: { id: string; revision: number },
|
|
|
|
|
type FxGoalRef = { id: string; revision: number }
|
|
|
|
|
type FxGoalView = FxGoalProjection['goal'] & {
|
|
|
|
|
roundsStarted: number
|
|
|
|
|
createdAt: number
|
|
|
|
|
updatedAt: number
|
|
|
|
|
activation: 'armed' | 'disarmed'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const goalFailure = <T>(message: string): RpcResult<T> => ({
|
|
|
|
|
ok: false,
|
|
|
|
|
error: { code: 'internal', message, details: {} },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const requireGoalSession = (id: SessionId): RpcResult<never> | undefined => (
|
|
|
|
|
summaryOf(id) === undefined
|
|
|
|
|
? { ok: false, error: { code: 'session-not-found', message: `no session ${id}`, details: { sessionId: id } } }
|
|
|
|
|
: undefined
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const goalView = (projection: FxGoalProjection): FxGoalView => ({
|
|
|
|
|
...projection.goal,
|
|
|
|
|
roundsStarted: projection.roundsStarted,
|
|
|
|
|
createdAt: projection.createdAt,
|
|
|
|
|
updatedAt: projection.updatedAt,
|
|
|
|
|
activation: projection.goal.phase === 'active' ? 'armed' : 'disarmed',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/** Canonical fixture implementation of the generated Goal Remote contract. */
|
|
|
|
|
const goalRemotes = {
|
|
|
|
|
create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> {
|
|
|
|
|
const missing = requireGoalSession(id)
|
|
|
|
|
if (missing !== undefined) return missing
|
|
|
|
|
const current = backscanGoal(logOf(id))
|
|
|
|
|
if (current !== null && current.goal.phase !== 'complete') {
|
|
|
|
|
return goalFailure(`goal "${current.goal.id}" already exists`)
|
|
|
|
|
}
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
const projection = appendGoalChange(id, {
|
|
|
|
|
kind: 'goal/change', version: 1, operation: 'create',
|
|
|
|
|
goal: {
|
|
|
|
|
id: `fx-goal-${logOf(id).length}`,
|
|
|
|
|
revision: 1,
|
|
|
|
|
objective: request.objective,
|
|
|
|
|
phase: 'active',
|
|
|
|
|
maxGoalRounds: request.maxGoalRounds ?? 256,
|
|
|
|
|
},
|
|
|
|
|
roundsStarted: 0, createdAt: now, updatedAt: now,
|
|
|
|
|
})
|
|
|
|
|
return { ok: true, value: { ref: { id: projection.goal.id, revision: projection.goal.revision } } }
|
|
|
|
|
},
|
|
|
|
|
edit(id: SessionId, ref: FxGoalRef, request: { objective?: string; maxGoalRounds?: number }): RpcResult<FxGoalView> {
|
|
|
|
|
return mutateGoal(id, ref, current => ({
|
|
|
|
|
...current.goal,
|
|
|
|
|
revision: current.goal.revision + 1,
|
|
|
|
|
...request.objective === undefined ? {} : { objective: request.objective },
|
|
|
|
|
...request.maxGoalRounds === undefined ? {} : { maxGoalRounds: request.maxGoalRounds },
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
pause(id: SessionId, ref: FxGoalRef): RpcResult<FxGoalView> {
|
|
|
|
|
return mutateGoal(id, ref, current => (
|
|
|
|
|
current.goal.phase === 'active'
|
|
|
|
|
? { ...current.goal, revision: current.goal.revision + 1, phase: 'paused' }
|
|
|
|
|
: undefined
|
|
|
|
|
))
|
|
|
|
|
},
|
|
|
|
|
resume(id: SessionId, ref: FxGoalRef): RpcResult<FxGoalView> {
|
|
|
|
|
return mutateGoal(id, ref, current => (
|
|
|
|
|
current.goal.phase === 'paused' || current.goal.phase === 'blocked' || current.goal.phase === 'active'
|
|
|
|
|
? { ...current.goal, revision: current.goal.revision + 1, phase: 'active' }
|
|
|
|
|
: undefined
|
|
|
|
|
))
|
|
|
|
|
},
|
|
|
|
|
complete(id: SessionId, ref: FxGoalRef): RpcResult<FxGoalView> {
|
|
|
|
|
return mutateGoal(id, ref, current => (
|
|
|
|
|
current.goal.phase === 'complete'
|
|
|
|
|
? undefined
|
|
|
|
|
: { ...current.goal, revision: current.goal.revision + 1, phase: 'complete' }
|
|
|
|
|
))
|
|
|
|
|
},
|
|
|
|
|
clear(id: SessionId, ref: FxGoalRef): RpcResult<FxGoalRef> {
|
|
|
|
|
const missing = requireGoalSession(id)
|
|
|
|
|
if (missing !== undefined) return missing
|
|
|
|
|
const current = backscanGoal(logOf(id))
|
|
|
|
|
if (current === null || current.goal.id !== ref.id || current.goal.revision !== ref.revision) {
|
|
|
|
|
return goalFailure('stale or missing goal revision')
|
|
|
|
|
}
|
|
|
|
|
const tombstone = { id: current.goal.id, revision: current.goal.revision + 1 }
|
|
|
|
|
appendGoalChange(id, {
|
|
|
|
|
kind: 'goal/change', version: 1, operation: 'clear', cleared: tombstone, clearedAt: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
return { ok: true, value: tombstone }
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Shared CAS mutation path behind the canonical Remote verbs. */
|
|
|
|
|
function mutateGoal(
|
|
|
|
|
id: SessionId,
|
|
|
|
|
ref: FxGoalRef,
|
|
|
|
|
next: (current: FxGoalProjection) => FxGoalProjection['goal'] | undefined,
|
|
|
|
|
): Promise<RpcResponse<{ ref: { id: never; revision: number } }>> => {
|
|
|
|
|
const missing = requireSession(request)
|
|
|
|
|
): RpcResult<FxGoalView> {
|
|
|
|
|
const missing = requireGoalSession(id)
|
|
|
|
|
if (missing !== undefined) return missing
|
|
|
|
|
const id = request.payload.sessionId
|
|
|
|
|
const current = backscanGoal(logOf(id))
|
|
|
|
|
if (current === null || current.goal.id !== ref.id || current.goal.revision !== ref.revision) {
|
|
|
|
|
return err(request, { code: 'internal', message: 'stale or missing goal revision', details: { goalCode: 'GOAL_STALE_REVISION' } })
|
|
|
|
|
return goalFailure('stale or missing goal revision')
|
|
|
|
|
}
|
|
|
|
|
const goal = next(current)
|
|
|
|
|
if (goal === undefined) {
|
|
|
|
|
return err(request, { code: 'internal', message: `invalid goal transition from "${current.goal.phase}"`, details: { goalCode: 'GOAL_INVALID_TRANSITION' } })
|
|
|
|
|
return goalFailure(`invalid goal transition from "${current.goal.phase}"`)
|
|
|
|
|
}
|
|
|
|
|
const projection = appendGoalChange(id, {
|
|
|
|
|
kind: 'goal/change', version: 1,
|
|
|
|
|
operation: goal.phase === current.goal.phase ? 'edit' : goal.phase === 'paused' ? 'pause' : goal.phase === 'active' ? 'resume' : 'complete',
|
|
|
|
|
goal, roundsStarted: current.roundsStarted, createdAt: current.createdAt, updatedAt: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
return ok(request, { ref: { id: projection.goal.id as never, revision: projection.goal.revision } })
|
|
|
|
|
return { ok: true, value: goalView(projection) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mapGoalResult = <T, U>(result: RpcResult<T>, map: (value: T) => U): RpcResult<U> => (
|
|
|
|
|
result.ok ? { ok: true, value: map(result.value) } : result
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const goalRefResult = (result: RpcResult<FxGoalView>): RpcResult<{ ref: { id: never; revision: number } }> => (
|
|
|
|
|
mapGoalResult(result, view => ({ ref: { id: view.id as never, revision: view.revision } }))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const legacyGoalResponse = <P, T>(request: RpcRequest<P>, result: RpcResult<T>): Promise<RpcResponse<T>> => (
|
|
|
|
|
Promise.resolve({ rpcId: request.rpcId, result })
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/** At most one in-flight replay per session; cancel clears it. */
|
|
|
|
|
const replays = new Map<SessionId, { timer: ReturnType<typeof setTimeout>; finish(aborted: boolean): void }>()
|
|
|
|
|
|
|
|
|
|
@@ -1777,7 +1893,7 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
replays.set(id, { timer: setTimeout(tick, 80), finish })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
const api: ApiProxy = {
|
|
|
|
|
sessions: {
|
|
|
|
|
list: request => ok(request, { items: [...sessions].sort((a, b) => b.updatedAt - a.updatedAt) }),
|
|
|
|
|
search: (request, signal) => {
|
|
|
|
|
@@ -2334,60 +2450,44 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
goals: {
|
|
|
|
|
// Mutation-only mirror of the host handlers: each verb CAS-checks the
|
|
|
|
|
// projected current goal, appends the whole-value change (the mux
|
|
|
|
|
// stream and projection frame ride the shared append path), and
|
|
|
|
|
// acknowledges with the new ref only.
|
|
|
|
|
create: (request) => {
|
|
|
|
|
const missing = requireSession(request)
|
|
|
|
|
if (missing !== undefined) return missing
|
|
|
|
|
const id = request.payload.sessionId
|
|
|
|
|
const current = backscanGoal(logOf(id))
|
|
|
|
|
if (current !== null && current.goal.phase !== 'complete') {
|
|
|
|
|
return err(request, { code: 'internal', message: `goal "${current.goal.id}" already exists`, details: { goalCode: 'GOAL_ALREADY_EXISTS' } })
|
|
|
|
|
}
|
|
|
|
|
const projection = appendGoalChange(id, {
|
|
|
|
|
kind: 'goal/change', version: 1, operation: 'create',
|
|
|
|
|
goal: { id: `fx-goal-${logOf(id).length}`, revision: 1, objective: request.payload.objective, phase: 'active', maxGoalRounds: request.payload.maxGoalRounds ?? 256 },
|
|
|
|
|
roundsStarted: 0, createdAt: Date.now(), updatedAt: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
return ok(request, { ref: { id: projection.goal.id as never, revision: projection.goal.revision } })
|
|
|
|
|
},
|
|
|
|
|
edit: request => fxMutateGoal(request, request.payload.ref, current => ({
|
|
|
|
|
...current.goal,
|
|
|
|
|
revision: current.goal.revision + 1,
|
|
|
|
|
...request.payload.objective === undefined ? {} : { objective: request.payload.objective },
|
|
|
|
|
...request.payload.maxGoalRounds === undefined ? {} : { maxGoalRounds: request.payload.maxGoalRounds },
|
|
|
|
|
})),
|
|
|
|
|
pause: request => fxMutateGoal(request, request.payload.ref, current => (
|
|
|
|
|
current.goal.phase === 'active'
|
|
|
|
|
? { ...current.goal, revision: current.goal.revision + 1, phase: 'paused' }
|
|
|
|
|
: undefined
|
|
|
|
|
)),
|
|
|
|
|
resume: request => fxMutateGoal(request, request.payload.ref, current => (
|
|
|
|
|
current.goal.phase === 'paused' || current.goal.phase === 'blocked' || current.goal.phase === 'active'
|
|
|
|
|
? { ...current.goal, revision: current.goal.revision + 1, phase: 'active' }
|
|
|
|
|
: undefined
|
|
|
|
|
)),
|
|
|
|
|
complete: request => fxMutateGoal(request, request.payload.ref, current => (
|
|
|
|
|
current.goal.phase === 'complete'
|
|
|
|
|
? undefined
|
|
|
|
|
: { ...current.goal, revision: current.goal.revision + 1, phase: 'complete' }
|
|
|
|
|
)),
|
|
|
|
|
clear: (request) => {
|
|
|
|
|
const missing = requireSession(request)
|
|
|
|
|
if (missing !== undefined) return missing
|
|
|
|
|
const id = request.payload.sessionId
|
|
|
|
|
const current = backscanGoal(logOf(id))
|
|
|
|
|
if (current === null || current.goal.id !== request.payload.ref.id || current.goal.revision !== request.payload.ref.revision) {
|
|
|
|
|
return err(request, { code: 'internal', message: 'stale or missing goal revision', details: { goalCode: 'GOAL_STALE_REVISION' } })
|
|
|
|
|
}
|
|
|
|
|
appendGoalChange(id, {
|
|
|
|
|
kind: 'goal/change', version: 1, operation: 'clear',
|
|
|
|
|
cleared: { id: current.goal.id, revision: current.goal.revision + 1 }, clearedAt: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
return ok(request, { cleared: true as const })
|
|
|
|
|
},
|
|
|
|
|
// Compatibility face only: old API Proxy payloads and acknowledgements
|
|
|
|
|
// adapt to the canonical fixture Remote implementation above.
|
|
|
|
|
create: request => legacyGoalResponse(
|
|
|
|
|
request,
|
|
|
|
|
mapGoalResult(
|
|
|
|
|
goalRemotes.create(request.payload.sessionId, {
|
|
|
|
|
objective: request.payload.objective,
|
|
|
|
|
...request.payload.maxGoalRounds === undefined ? {} : { maxGoalRounds: request.payload.maxGoalRounds },
|
|
|
|
|
}),
|
|
|
|
|
value => ({ ref: { id: value.ref.id as never, revision: value.ref.revision } }),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
edit: request => legacyGoalResponse(
|
|
|
|
|
request,
|
|
|
|
|
goalRefResult(goalRemotes.edit(request.payload.sessionId, request.payload.ref, {
|
|
|
|
|
...request.payload.objective === undefined ? {} : { objective: request.payload.objective },
|
|
|
|
|
...request.payload.maxGoalRounds === undefined ? {} : { maxGoalRounds: request.payload.maxGoalRounds },
|
|
|
|
|
})),
|
|
|
|
|
),
|
|
|
|
|
pause: request => legacyGoalResponse(
|
|
|
|
|
request,
|
|
|
|
|
goalRefResult(goalRemotes.pause(request.payload.sessionId, request.payload.ref)),
|
|
|
|
|
),
|
|
|
|
|
resume: request => legacyGoalResponse(
|
|
|
|
|
request,
|
|
|
|
|
goalRefResult(goalRemotes.resume(request.payload.sessionId, request.payload.ref)),
|
|
|
|
|
),
|
|
|
|
|
complete: request => legacyGoalResponse(
|
|
|
|
|
request,
|
|
|
|
|
goalRefResult(goalRemotes.complete(request.payload.sessionId, request.payload.ref)),
|
|
|
|
|
),
|
|
|
|
|
clear: request => legacyGoalResponse(
|
|
|
|
|
request,
|
|
|
|
|
mapGoalResult(
|
|
|
|
|
goalRemotes.clear(request.payload.sessionId, request.payload.ref),
|
|
|
|
|
() => ({ cleared: true as const }),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
events: {
|
|
|
|
|
async *mux(_request, signal) {
|
|
|
|
|
@@ -2548,6 +2648,36 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
return Promise.resolve({ accepted: true })
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rpc: ClientConnectionRpc = {
|
|
|
|
|
call(channel, endpoint, payload) {
|
|
|
|
|
if (channel !== '/api') {
|
|
|
|
|
return Promise.reject(new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`))
|
|
|
|
|
}
|
|
|
|
|
const args = (payload as {
|
|
|
|
|
args: {
|
|
|
|
|
agentId: SessionId
|
|
|
|
|
ref?: { id: string; revision: number }
|
|
|
|
|
request?: { objective?: string; maxGoalRounds?: number }
|
|
|
|
|
}
|
|
|
|
|
}).args
|
|
|
|
|
const sessionId = args.agentId
|
|
|
|
|
switch (endpoint) {
|
|
|
|
|
case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, {
|
|
|
|
|
objective: args.request?.objective as string,
|
|
|
|
|
...args.request?.maxGoalRounds === undefined ? {} : { maxGoalRounds: args.request.maxGoalRounds },
|
|
|
|
|
}))
|
|
|
|
|
case 'goals/edit': return Promise.resolve(goalRemotes.edit(sessionId, args.ref as FxGoalRef, args.request ?? {}))
|
|
|
|
|
case 'goals/pause': return Promise.resolve(goalRemotes.pause(sessionId, args.ref as FxGoalRef))
|
|
|
|
|
case 'goals/resume': return Promise.resolve(goalRemotes.resume(sessionId, args.ref as FxGoalRef))
|
|
|
|
|
case 'goals/complete': return Promise.resolve(goalRemotes.complete(sessionId, args.ref as FxGoalRef))
|
|
|
|
|
case 'goals/clear': return Promise.resolve(goalRemotes.clear(sessionId, args.ref as FxGoalRef))
|
|
|
|
|
default:
|
|
|
|
|
return Promise.reject(new Error(`fixture connection RPC endpoint ${JSON.stringify(endpoint)} is unavailable`))
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
return { api, rpc }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -2559,10 +2689,14 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
*/
|
|
|
|
|
export class FixtureApiClient extends AbstractApiClient {
|
|
|
|
|
private readonly api: ApiProxy
|
|
|
|
|
/** Generic Remote caller backed by the same in-memory state as the legacy fixture API. */
|
|
|
|
|
readonly rpc: ClientConnectionRpc
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super()
|
|
|
|
|
this.api = createFixtureApi(fixtureOptionsFromLocation())
|
|
|
|
|
const world = createFixtureWorld(fixtureOptionsFromLocation())
|
|
|
|
|
this.api = world.api
|
|
|
|
|
this.rpc = world.rpc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected doFetch(): Promise<Response> {
|
|
|
|
|
|