feat: add agent fallback and preemptive-compaction restoration

- Add agent visibility fallback for first-run scenarios
- Restore preemptive-compaction hook
- Update migration and schema for preemptive-compaction restoration
This commit is contained in:
YeonGyu-Kim
2026-02-02 22:40:59 +09:00
parent 1bba54c7f6
commit 39dc2ff303
10 changed files with 259 additions and 14 deletions
+15 -6
View File
@@ -49,9 +49,10 @@ describe("createBuiltinAgents with model overrides", () => {
expect(agents.sisyphus.thinking).toBeUndefined()
})
test("Sisyphus is not created when no availableModels provided (requiresAnyModel)", async () => {
test("Sisyphus is created on first run when no availableModels or cache exist", async () => {
// #given
const systemDefaultModel = "anthropic/claude-opus-4-5"
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
try {
@@ -59,8 +60,10 @@ describe("createBuiltinAgents with model overrides", () => {
const agents = await createBuiltinAgents([], {}, undefined, systemDefaultModel, undefined, undefined, [], {})
// #then
expect(agents.sisyphus).toBeUndefined()
expect(agents.sisyphus).toBeDefined()
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
} finally {
cacheSpy.mockRestore()
fetchSpy.mockRestore()
}
})
@@ -229,8 +232,9 @@ describe("createBuiltinAgents with requiresModel gating", () => {
}
})
test("hephaestus is not created when availableModels is empty", async () => {
test("hephaestus is created on first run when no availableModels or cache exist", async () => {
// #given
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
try {
@@ -238,8 +242,10 @@ describe("createBuiltinAgents with requiresModel gating", () => {
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
// #then
expect(agents.hephaestus).toBeUndefined()
expect(agents.hephaestus).toBeDefined()
expect(agents.hephaestus.model).toBe("openai/gpt-5.2-codex")
} finally {
cacheSpy.mockRestore()
fetchSpy.mockRestore()
}
})
@@ -283,8 +289,9 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
}
})
test("sisyphus is not created when availableModels is empty", async () => {
test("sisyphus is created on first run when no availableModels or cache exist", async () => {
// #given
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
try {
@@ -292,8 +299,10 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
// #then
expect(agents.sisyphus).toBeUndefined()
expect(agents.sisyphus).toBeDefined()
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
} finally {
cacheSpy.mockRestore()
fetchSpy.mockRestore()
}
})
+26 -2
View File
@@ -167,6 +167,18 @@ function applyModelResolution(input: {
})
}
function getFirstFallbackModel(requirement?: {
fallbackChain?: { providers: string[]; model: string; variant?: string }[]
}) {
const entry = requirement?.fallbackChain?.[0]
if (!entry || entry.providers.length === 0) return undefined
return {
model: `${entry.providers[0]}/${entry.model}`,
provenance: "provider-fallback" as const,
variant: entry.variant,
}
}
function applyEnvironmentContext(config: AgentConfig, directory?: string): AgentConfig {
if (!directory || !config.prompt) return config
const envContext = createEnvContext()
@@ -230,6 +242,8 @@ export async function createBuiltinAgents(
const availableModels = await fetchAvailableModels(undefined, {
connectedProviders: connectedProviders ?? undefined,
})
const isFirstRunNoCache =
availableModels.size === 0 && (!connectedProviders || connectedProviders.length === 0)
const result: Record<string, AgentConfig> = {}
const availableAgents: AvailableAgent[] = []
@@ -334,10 +348,11 @@ export async function createBuiltinAgents(
const meetsSisyphusAnyModelRequirement =
!sisyphusRequirement?.requiresAnyModel ||
hasSisyphusExplicitConfig ||
isFirstRunNoCache ||
isAnyFallbackModelAvailable(sisyphusRequirement.fallbackChain, availableModels)
if (!disabledAgents.includes("sisyphus") && meetsSisyphusAnyModelRequirement) {
const sisyphusResolution = applyModelResolution({
let sisyphusResolution = applyModelResolution({
uiSelectedModel,
userModel: sisyphusOverride?.model,
requirement: sisyphusRequirement,
@@ -345,6 +360,10 @@ export async function createBuiltinAgents(
systemDefaultModel,
})
if (isFirstRunNoCache && !sisyphusOverride?.model && !uiSelectedModel) {
sisyphusResolution = getFirstFallbackModel(sisyphusRequirement)
}
if (sisyphusResolution) {
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
@@ -375,16 +394,21 @@ export async function createBuiltinAgents(
const hasRequiredModel =
!hephaestusRequirement?.requiresModel ||
hasHephaestusExplicitConfig ||
isFirstRunNoCache ||
(availableModels.size > 0 && isModelAvailable(hephaestusRequirement.requiresModel, availableModels))
if (hasRequiredModel) {
const hephaestusResolution = applyModelResolution({
let hephaestusResolution = applyModelResolution({
userModel: hephaestusOverride?.model,
requirement: hephaestusRequirement,
availableModels,
systemDefaultModel,
})
if (isFirstRunNoCache && !hephaestusOverride?.model) {
hephaestusResolution = getFirstFallbackModel(hephaestusRequirement)
}
if (hephaestusResolution) {
const { model: hephaestusModel, variant: hephaestusResolvedVariant } = hephaestusResolution