mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
fix(mobile): keep the saved model list right across late reads and account switches (#39470)
* fix(mobile): keep held model lists through late preloads and running chats * fix(mobile): let progress answers fill empty lists and decide recording per read * fix(mobile): record a pre-send listing on rejoin and leave unheld retirements alone * test(desktop): use a valid listed catalog in the preload race test * test(mobile): type the catalog cache test's renderer
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { createElement, useLayoutEffect } from 'react'
|
||||
import { act, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AgentSessionModelCatalogResult } from '../../../src/shared/agent-session/agent-session-wire'
|
||||
import { AGENT_SESSION_MODEL_CATALOG_SAVED_ONLY_RUNTIME_CAPABILITY } from '../../../src/shared/agent-session/structured-agent-session-surface-capabilities'
|
||||
import { createWithQueryClient } from '../test-support/query-client-test-renderer'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { recordRuntimeStatusProbe } from '../transport/runtime-status-probe'
|
||||
import { useHostModelCatalogCache } from './mobile-host-model-catalog-query'
|
||||
import type { HostModelCatalogCache } from './use-mobile-host-model-catalog-upgrade'
|
||||
|
||||
function listed(id: string): AgentSessionModelCatalogResult {
|
||||
return {
|
||||
origin: 'probe',
|
||||
models: [{ id, label: id, isDefault: true, efforts: [] }],
|
||||
fetchedAt: 1,
|
||||
listingNamesConfiguredModel: false
|
||||
}
|
||||
}
|
||||
|
||||
function heldPreloadClient() {
|
||||
let answer: (catalog: AgentSessionModelCatalogResult) => void = () => {}
|
||||
const client: RpcClient = {
|
||||
sendRequest: () =>
|
||||
new Promise((resolve) => {
|
||||
answer = (catalog) =>
|
||||
resolve({ id: 'r', ok: true, result: catalog, _meta: { runtimeId: 'runtime-1' } })
|
||||
}),
|
||||
subscribe: () => () => {},
|
||||
updateTerminalSubscriptionViewport: () => {},
|
||||
getState: () => 'connected',
|
||||
getReconnectAttempt: () => 0,
|
||||
getLastConnectedAt: () => 1,
|
||||
onStateChange: () => () => {},
|
||||
notifyForeground: () => {},
|
||||
close: () => {}
|
||||
}
|
||||
recordRuntimeStatusProbe(client, {
|
||||
capabilities: [AGENT_SESSION_MODEL_CATALOG_SAVED_ONLY_RUNTIME_CAPABILITY]
|
||||
})
|
||||
return { client, answer: (catalog: AgentSessionModelCatalogResult) => answer(catalog) }
|
||||
}
|
||||
|
||||
async function mountCache(client: RpcClient) {
|
||||
const mounted: { cache: HostModelCatalogCache | null } = { cache: null }
|
||||
function Probe(): null {
|
||||
const current = useHostModelCatalogCache({
|
||||
hostId: 'host-1',
|
||||
client,
|
||||
agent: 'claude',
|
||||
enabled: true
|
||||
})
|
||||
useLayoutEffect(() => {
|
||||
mounted.cache = current
|
||||
})
|
||||
return null
|
||||
}
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
await act(async () => {
|
||||
renderer = createWithQueryClient(createElement(Probe))
|
||||
})
|
||||
return { mounted, unmount: () => act(async () => renderer?.unmount()) }
|
||||
}
|
||||
|
||||
describe('useHostModelCatalogCache', () => {
|
||||
it("keeps a new chat's answer that lands while an older preload is out", async () => {
|
||||
const host = heldPreloadClient()
|
||||
const { mounted, unmount } = await mountCache(host.client)
|
||||
|
||||
await act(async () => mounted.cache?.record('wt-1', listed('opus')))
|
||||
await act(async () => host.answer(listed('sonnet')))
|
||||
|
||||
expect(mounted.cache?.seed({ newLaunch: true, worktree: 'wt-1', seedsModel: false })).toEqual(
|
||||
listed('opus')
|
||||
)
|
||||
await unmount()
|
||||
})
|
||||
})
|
||||
@@ -72,11 +72,22 @@ export function useHostModelCatalogCache(args: {
|
||||
launch
|
||||
),
|
||||
// A new chat's answer is the account's a new chat pins, so the next chat starts from it.
|
||||
record: (worktree, catalog) =>
|
||||
queryClient.setQueryData<HostModelCatalogSnapshot | null>(
|
||||
queryKey,
|
||||
(snapshot) => withHostModelCatalogAnswer(snapshot ?? undefined, worktree, catalog) ?? null
|
||||
)
|
||||
record: (worktree, catalog) => {
|
||||
const held =
|
||||
queryClient.getQueryData<HostModelCatalogSnapshot | null>(queryKey) ?? undefined
|
||||
const next = withHostModelCatalogAnswer(held, worktree, catalog)
|
||||
if (next === held) {
|
||||
return
|
||||
}
|
||||
queryClient.setQueryData<HostModelCatalogSnapshot | null>(queryKey, next ?? null)
|
||||
// A preload sent before this list is older; cancelling it keeps the list just set.
|
||||
if (
|
||||
next?.byWorktree.has(worktree) &&
|
||||
queryClient.isFetching({ queryKey, exact: true }) > 0
|
||||
) {
|
||||
void queryClient.cancelQueries({ queryKey, exact: true }, { silent: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [agent, exec, hostId, queryClient])
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ export function useMobileHostModelCatalogUpgrade(args: {
|
||||
newLaunch: boolean
|
||||
/** Where a new chat runs: the host names no default its config could replace. */
|
||||
worktree?: string
|
||||
/** The chat has a turn or a send, so its answers are its own account's, which a new chat may no longer pin. */
|
||||
hasTurn?: boolean
|
||||
fence: number | null
|
||||
optionCatalog: AgentSessionOptionCatalog | null
|
||||
receiveCatalog: (
|
||||
@@ -62,9 +64,12 @@ export function useMobileHostModelCatalogUpgrade(args: {
|
||||
worktree
|
||||
} = args
|
||||
const receiver = useRef(args.receiveCatalog)
|
||||
const recordsNewChat = useRef(newLaunch && args.hasTurn !== true)
|
||||
const recordingWait = useRef<string | null>(null)
|
||||
useEffect(() => {
|
||||
receiver.current = args.receiveCatalog
|
||||
}, [args.receiveCatalog])
|
||||
recordsNewChat.current = newLaunch && args.hasTurn !== true
|
||||
}, [args.receiveCatalog, newLaunch, args.hasTurn])
|
||||
const [rereads, setRereads] = useState(0)
|
||||
const selectedModel = usesModelSpecificAcpEfforts(agent ?? '') ? args.selectedModel : undefined
|
||||
const recheck = useCallback(() => setRereads((count) => count + 1), [])
|
||||
@@ -73,6 +78,12 @@ export function useMobileHostModelCatalogUpgrade(args: {
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
const key = `${agent}\u0000${sessionId}`
|
||||
// A read sent before the chat's first turn or send answers for the account a new chat pins,
|
||||
// and so does the listing it waits on, whichever later run picks that up.
|
||||
const records =
|
||||
recordsNewChat.current ||
|
||||
(recordingWait.current === key && isHostModelListingWaitInFlight(key, client))
|
||||
const params = {
|
||||
agent,
|
||||
sessionId,
|
||||
@@ -98,7 +109,7 @@ export function useMobileHostModelCatalogUpgrade(args: {
|
||||
return
|
||||
}
|
||||
// A new chat's answer is the account's a new chat pins, so the next chat starts from it.
|
||||
if (newLaunch) {
|
||||
if (records) {
|
||||
modelCatalogCache?.record(worktree ?? '', catalog)
|
||||
}
|
||||
receiver.current(catalog, settled ? 'settled' : event)
|
||||
@@ -109,9 +120,9 @@ export function useMobileHostModelCatalogUpgrade(args: {
|
||||
receiver.current(null, 'demand')
|
||||
}
|
||||
}
|
||||
const key = `${agent}\u0000${sessionId}`
|
||||
let leave: (() => void) | undefined
|
||||
const wait = () => {
|
||||
recordingWait.current = records ? key : null
|
||||
leave = joinHostModelListingWait(
|
||||
key,
|
||||
() => read(true),
|
||||
|
||||
@@ -187,6 +187,7 @@ export function useMobileStructuredAgentOptions(args: {
|
||||
fence,
|
||||
newLaunch: createdHere !== undefined,
|
||||
...(createdHere ? { worktree: createdHere.worktree } : {}),
|
||||
hasTurn: args.hasTurn,
|
||||
optionCatalog,
|
||||
activeOptionRecordRef,
|
||||
updateOptionState,
|
||||
|
||||
@@ -11,10 +11,7 @@ import { BoundedMap } from '../../../src/shared/bounded-map'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { AgentSessionOptionCatalog } from '../../../src/shared/agent-session/options/agent-session-option-catalog-types'
|
||||
import { usesInstalledAgentModelCatalog } from '../../../src/shared/agent-session/options/agent-model-catalog-ownership'
|
||||
import {
|
||||
applyStructuredAgentSessionModelCatalog,
|
||||
createStructuredAgentSessionOptionState
|
||||
} from '../../../src/shared/agent-session/options/structured-agent-session-options'
|
||||
import { structuredAgentSessionFirstFrameOptionState } from '../../../src/shared/agent-session/options/structured-agent-session-first-frame'
|
||||
import type { HostModelCatalogCache } from './use-mobile-host-model-catalog-upgrade'
|
||||
import { structuredAgentSessionReportsAtFence } from '../../../src/shared/agent-session/options/structured-agent-session-runtime-reports'
|
||||
|
||||
@@ -70,18 +67,13 @@ export function useMobileStructuredOptionState(args: {
|
||||
const providerStarting = summary?.hostExecutionPhase === 'starting'
|
||||
const coherentCatalog = usesInstalledAgentModelCatalog(agent ?? '', hostCoherentCatalog)
|
||||
// The host's last list for this agent when this world holds one, else the quiet placeholder until the host answers.
|
||||
const firstFrame = () => {
|
||||
const seeded = createStructuredAgentSessionOptionState(agent ?? 'codex', optionCatalog)
|
||||
const listed =
|
||||
optionCatalog &&
|
||||
modelCatalogCache?.seed({ newLaunch, seedsModel: false, ...(worktree ? { worktree } : {}) })
|
||||
return listed && optionCatalog
|
||||
? applyStructuredAgentSessionModelCatalog(seeded, optionCatalog, listed, {
|
||||
newLaunch,
|
||||
coherentCatalog
|
||||
})
|
||||
: seeded
|
||||
}
|
||||
const firstFrame = () =>
|
||||
structuredAgentSessionFirstFrameOptionState(
|
||||
agent ?? 'codex',
|
||||
optionCatalog,
|
||||
modelCatalogCache?.seed({ newLaunch, seedsModel: false, ...(worktree ? { worktree } : {}) }),
|
||||
{ newLaunch, coherentCatalog }
|
||||
)
|
||||
// One view serves every chat tab, so each chat keeps its own reader and its last answer on return.
|
||||
const [readers] = useState(
|
||||
() =>
|
||||
|
||||
+13
-2
@@ -38,6 +38,8 @@ export function useHostModelCatalogUpgrade(args: {
|
||||
newLaunch: boolean
|
||||
/** Where the launch runs: the host names no default its config could replace. */
|
||||
worktree?: string
|
||||
/** The chat has a turn or a send, so its answers are its own account's, which a new chat may no longer pin. */
|
||||
hasTurn?: boolean
|
||||
fence: number | null
|
||||
/** The chat's running turn: one running proves its start, which the host re-checks against. */
|
||||
turnId?: string | null
|
||||
@@ -70,9 +72,12 @@ export function useHostModelCatalogUpgrade(args: {
|
||||
worktree
|
||||
} = args
|
||||
const receiver = useRef(args.receiveCatalog)
|
||||
const recordsNewChat = useRef(newLaunch && args.hasTurn !== true)
|
||||
const recordingWait = useRef<string | null>(null)
|
||||
useEffect(() => {
|
||||
receiver.current = args.receiveCatalog
|
||||
}, [args.receiveCatalog])
|
||||
recordsNewChat.current = newLaunch && args.hasTurn !== true
|
||||
}, [args.receiveCatalog, newLaunch, args.hasTurn])
|
||||
const waitKey = `${structuredAgentSessionHostKey(target)}\u0000${agent}\u0000${sessionId}`
|
||||
const [verdict, setVerdict] = useState<{
|
||||
key: string
|
||||
@@ -104,6 +109,11 @@ export function useHostModelCatalogUpgrade(args: {
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
// A read sent before the chat's first turn or send answers for the account a new chat pins,
|
||||
// and so does the listing it waits on, whichever later run picks that up.
|
||||
const records =
|
||||
recordsNewChat.current ||
|
||||
(recordingWait.current === waitKey && isHostModelListingWaitInFlight(waitKey))
|
||||
const params = {
|
||||
agent,
|
||||
sessionId,
|
||||
@@ -134,13 +144,14 @@ export function useHostModelCatalogUpgrade(args: {
|
||||
: { key: waitKey, unavailable: next }
|
||||
})
|
||||
// A new chat's answer is the account's a new chat pins, so the next chat starts from it.
|
||||
if (catalog && newLaunch) {
|
||||
if (catalog && records) {
|
||||
recordHostModelCatalogSnapshot(target, agent, worktree ?? '', catalog)
|
||||
}
|
||||
receiver.current(catalog, settled ? 'settled' : event)
|
||||
}
|
||||
let leave: (() => void) | null = null
|
||||
const waitForListing = (): void => {
|
||||
recordingWait.current = records ? waitKey : null
|
||||
leave = joinHostModelListingWait(
|
||||
waitKey,
|
||||
() => read(true),
|
||||
|
||||
+9
-11
@@ -1,22 +1,20 @@
|
||||
import type { AgentType } from '../../../../../../../shared/agent-status/contracts/agent-status-types'
|
||||
import type { RuntimeClientTarget } from '../../../../../../../shared/runtime-client-target'
|
||||
import { structuredAgentSessionSeedCatalog } from '../../../../../../../shared/agent-session/options/structured-agent-session-seed-catalog'
|
||||
import {
|
||||
applyStructuredAgentSessionModelCatalog,
|
||||
createStructuredAgentSessionOptionState,
|
||||
type StructuredAgentSessionOptionState
|
||||
} from '../../../../../../../shared/agent-session/options/structured-agent-session-options'
|
||||
import type { StructuredAgentSessionOptionState } from '../../../../../../../shared/agent-session/options/structured-agent-session-options'
|
||||
import { structuredAgentSessionFirstFrameOptionState } from '../../../../../../../shared/agent-session/options/structured-agent-session-first-frame'
|
||||
import { readHostModelCatalogSnapshot } from '@/runtime/host-model-catalog-snapshots'
|
||||
|
||||
/** The first frame: the host's saved list when the renderer already holds it, else the seed,
|
||||
* which shows the quiet placeholder until the host answers. */
|
||||
/** The first frame from the host's saved list when the renderer already holds it. */
|
||||
export function firstFrameOptionState(
|
||||
agent: AgentType,
|
||||
target: RuntimeClientTarget,
|
||||
launch: { newLaunch: boolean; worktree?: string; seedsModel: boolean }
|
||||
): StructuredAgentSessionOptionState {
|
||||
const seed = structuredAgentSessionSeedCatalog(agent)
|
||||
const seeded = createStructuredAgentSessionOptionState(agent, seed)
|
||||
const snapshot = readHostModelCatalogSnapshot(target, agent, launch)
|
||||
return snapshot ? applyStructuredAgentSessionModelCatalog(seeded, seed, snapshot, launch) : seeded
|
||||
return structuredAgentSessionFirstFrameOptionState(
|
||||
agent,
|
||||
structuredAgentSessionSeedCatalog(agent),
|
||||
readHostModelCatalogSnapshot(target, agent, launch),
|
||||
launch
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -95,6 +95,7 @@ export function useStructuredAgentSessionOptions(args: StructuredAgentSessionOpt
|
||||
// A resumed conversation may keep its own model, so only a new one runs the listed default.
|
||||
newLaunch,
|
||||
...(launch?.worktree ? { worktree: launch.worktree } : {}),
|
||||
hasTurn: args.hasTurn,
|
||||
fence,
|
||||
turnId,
|
||||
reportedUnpickedModel:
|
||||
|
||||
@@ -31,6 +31,7 @@ import { getDefaultSettings } from '../../../shared/constants'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
preloadHostModelCatalogSnapshots,
|
||||
readHostModelCatalogSnapshot,
|
||||
recordHostModelCatalogSnapshot,
|
||||
resetHostModelCatalogSnapshotsForTests
|
||||
@@ -83,6 +84,31 @@ describe('host model catalog snapshots', () => {
|
||||
recordHostModelCatalogSnapshot(LOCAL, 'grok', 'wt-1', { origin: 'unknown' })
|
||||
expect(readHostModelCatalogSnapshot(LOCAL, 'grok', NEW_CHAT)).toBe(worldwide)
|
||||
})
|
||||
|
||||
it('keeps a held list through an answer whose listing is still running', () => {
|
||||
const held = list(true)
|
||||
recordHostModelCatalogSnapshot(LOCAL, 'claude', 'wt-1', held)
|
||||
recordHostModelCatalogSnapshot(LOCAL, 'claude', 'wt-1', {
|
||||
origin: 'unknown',
|
||||
listingInProgress: true
|
||||
})
|
||||
expect(readHostModelCatalogSnapshot(LOCAL, 'claude', NEW_CHAT)).toBe(held)
|
||||
const other = { ...NEW_CHAT, worktree: 'wt-2' }
|
||||
const filling = { ...list(true), listingInProgress: true as const }
|
||||
recordHostModelCatalogSnapshot(LOCAL, 'claude', 'wt-2', filling)
|
||||
expect(readHostModelCatalogSnapshot(LOCAL, 'claude', other)).toBe(filling)
|
||||
})
|
||||
|
||||
it("keeps a chat's answer that lands while an older preload is out", async () => {
|
||||
let answer: (catalog: AgentSessionModelCatalogResult) => void = () => {}
|
||||
mocks.call.mockImplementation(() => new Promise((resolve) => (answer = resolve)))
|
||||
const preload = preloadHostModelCatalogSnapshots(LOCAL, ['claude'], { savedOnly: true })
|
||||
const chat = list(true)
|
||||
recordHostModelCatalogSnapshot(LOCAL, 'claude', 'wt-1', chat)
|
||||
answer(list(false))
|
||||
await preload
|
||||
expect(readHostModelCatalogSnapshot(LOCAL, 'claude', NEW_CHAT)).toBe(chat)
|
||||
})
|
||||
})
|
||||
|
||||
const SAVED_ONLY = 'agent-session.model-catalog.saved-only.v1'
|
||||
|
||||
@@ -13,6 +13,8 @@ import { structuredAgentSessionHostKey } from './structured-agent-session-host-c
|
||||
const snapshots = new Map<string, HostModelCatalogSnapshot>()
|
||||
// Bumped when a host's answers are dropped, so a preload sent before then records nothing.
|
||||
const generations = new Map<string, number>()
|
||||
// Bumped per host and agent by each chat's answer, so an older preload never replaces it.
|
||||
const chatAnswers = new Map<string, number>()
|
||||
|
||||
function snapshotKey(hostKey: string, agent: string): string {
|
||||
return `${hostKey}\u0000${agent}`
|
||||
@@ -39,6 +41,9 @@ export function recordHostModelCatalogSnapshot(
|
||||
): void {
|
||||
const key = snapshotKey(structuredAgentSessionHostKey(target), agent)
|
||||
const next = withHostModelCatalogAnswer(snapshots.get(key), worktree, catalog)
|
||||
if (worktree !== null && next !== snapshots.get(key) && next?.byWorktree.has(worktree)) {
|
||||
chatAnswers.set(key, (chatAnswers.get(key) ?? 0) + 1)
|
||||
}
|
||||
if (next) {
|
||||
snapshots.set(key, next)
|
||||
} else {
|
||||
@@ -78,23 +83,29 @@ export async function preloadHostModelCatalogSnapshots(
|
||||
const hostKey = structuredAgentSessionHostKey(target)
|
||||
const generation = generations.get(hostKey) ?? 0
|
||||
await Promise.all(
|
||||
agents.map((agent) =>
|
||||
callStructuredAgentSession<AgentSessionModelCatalogResult>(
|
||||
agents.map((agent) => {
|
||||
const key = snapshotKey(hostKey, agent)
|
||||
const answersAtSend = chatAnswers.get(key) ?? 0
|
||||
return callStructuredAgentSession<AgentSessionModelCatalogResult>(
|
||||
target,
|
||||
'agentSession.modelCatalog',
|
||||
{ agent, ...options }
|
||||
)
|
||||
.then((catalog) => {
|
||||
if ((generations.get(hostKey) ?? 0) === generation) {
|
||||
if (
|
||||
(generations.get(hostKey) ?? 0) === generation &&
|
||||
(chatAnswers.get(key) ?? 0) === answersAtSend
|
||||
) {
|
||||
recordHostModelCatalogSnapshot(target, agent, null, catalog)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function resetHostModelCatalogSnapshotsForTests(): void {
|
||||
snapshots.clear()
|
||||
generations.clear()
|
||||
chatAnswers.clear()
|
||||
}
|
||||
|
||||
@@ -53,16 +53,30 @@ export function selectHostModelCatalogSnapshot(
|
||||
}
|
||||
|
||||
/** The snapshot after a host answer for `worktree` (null for a session-less read); undefined means keep none.
|
||||
* An unknown or empty answer proves absence and retires what it answered for. */
|
||||
* An unknown or empty answer proves absence and retires what it answered for. One whose listing is
|
||||
* still running proves nothing yet: it never replaces or retires a held list, but may fill an empty scope. */
|
||||
export function withHostModelCatalogAnswer(
|
||||
snapshot: HostModelCatalogSnapshot | undefined,
|
||||
worktree: string | null,
|
||||
catalog: AgentSessionModelCatalogResult
|
||||
): HostModelCatalogSnapshot | undefined {
|
||||
if (catalog.origin === 'unknown' || catalog.models.length === 0) {
|
||||
const listed = catalog.origin !== 'unknown' && catalog.models.length > 0
|
||||
if (catalog.listingInProgress) {
|
||||
const held =
|
||||
worktree === null
|
||||
? !!snapshot && (snapshot.account !== null || snapshot.byWorktree.size > 0)
|
||||
: snapshot?.byWorktree.has(worktree) === true
|
||||
if (held || !listed) {
|
||||
return snapshot
|
||||
}
|
||||
}
|
||||
if (!listed) {
|
||||
if (worktree === null || !snapshot) {
|
||||
return undefined
|
||||
}
|
||||
if (!snapshot.byWorktree.has(worktree)) {
|
||||
return snapshot
|
||||
}
|
||||
const retired = snapshot.byWorktree.get(worktree)
|
||||
const byWorktree = new Map(snapshot.byWorktree)
|
||||
byWorktree.delete(worktree)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AgentSessionModelCatalogResult } from '../agent-session-wire'
|
||||
import type { AgentSessionOptionCatalog } from './agent-session-option-catalog-types'
|
||||
import {
|
||||
applyStructuredAgentSessionModelCatalog,
|
||||
createStructuredAgentSessionOptionState,
|
||||
type StructuredAgentSessionOptionState
|
||||
} from './structured-agent-session-options'
|
||||
|
||||
/** A chat's first frame: the host's held list when the client has one, else the seed, which shows
|
||||
* the quiet placeholder until the host answers. */
|
||||
export function structuredAgentSessionFirstFrameOptionState(
|
||||
agent: string,
|
||||
seed: AgentSessionOptionCatalog | null,
|
||||
held: AgentSessionModelCatalogResult | undefined,
|
||||
options: { newLaunch: boolean; coherentCatalog?: boolean }
|
||||
): StructuredAgentSessionOptionState {
|
||||
const seeded = createStructuredAgentSessionOptionState(agent, seed)
|
||||
return held && seed
|
||||
? applyStructuredAgentSessionModelCatalog(seeded, seed, held, options)
|
||||
: seeded
|
||||
}
|
||||
Reference in New Issue
Block a user