Browse Source

Merge pull request #643 from rgutzen/fix/subagent-model-fallback

Alvin 1 month ago
parent
commit
c1c675654d

+ 2 - 2
src/agents/index.test.ts

@@ -236,7 +236,7 @@ describe('orchestrator agent', () => {
       { id: 'github-copilot/claude-3.5-haiku' },
       { id: 'openai/gpt-4' },
     ]);
-    expect(orchestrator?.config.model).toBeUndefined();
+    expect(orchestrator?.config.model).toBe('google/gemini-3-pro');
   });
 });
 
@@ -258,7 +258,7 @@ describe('per-model variant in array config', () => {
       { id: 'google/gemini-3-flash', variant: 'low' },
       { id: 'openai/gpt-4o-mini' },
     ]);
-    expect(explorer?.config.model).toBeUndefined();
+    expect(explorer?.config.model).toBe('google/gemini-3-flash');
   });
 
   test('top-level variant preserved alongside per-model variants', () => {

+ 4 - 1
src/agents/index.ts

@@ -160,7 +160,10 @@ function applyOverrides(
       agent._modelArray = override.model.map((m) =>
         typeof m === 'string' ? { id: m } : m,
       );
-      agent.config.model = undefined; // cleared; runtime hook resolves from _modelArray
+      // Set config.model to the primary entry so the subagent has a valid
+      // model at launch time. ForegroundFallbackManager handles runtime
+      // failover to the remaining entries in _modelArray.
+      agent.config.model = agent._modelArray[0].id;
     } else {
       agent.config.model = override.model;
     }

+ 5 - 4
src/hooks/foreground-fallback/index.test.ts

@@ -579,7 +579,7 @@ describe('ForegroundFallbackManager deduplication', () => {
 // ---------------------------------------------------------------------------
 
 describe('ForegroundFallbackManager subagent.session.created', () => {
-  test('records agent name from subagent.session.created when agentName provided', async () => {
+  test('records agent name from subagent.session.created and falls back correctly', async () => {
     const { client, mocks } = createMockClient();
     const mgr = new ForegroundFallbackManager(client, makeChains(), true);
 
@@ -602,9 +602,10 @@ describe('ForegroundFallbackManager subagent.session.created', () => {
       },
     ];
     // explorer chain: ['openai/gpt-4o-mini', 'anthropic/claude-haiku']
-    // no current model tracked → first untried = openai/gpt-4o-mini
-    expect(call[0].body.model.providerID).toBe('openai');
-    expect(call[0].body.model.modelID).toBe('gpt-4o-mini');
+    // agentName known → currentModel inferred as chain[0] (primary)
+    // primary is tried → fallback picks claude-haiku
+    expect(call[0].body.model.providerID).toBe('anthropic');
+    expect(call[0].body.model.modelID).toBe('claude-haiku');
   });
 });
 

+ 10 - 1
src/hooks/foreground-fallback/index.ts

@@ -226,7 +226,7 @@ export class ForegroundFallbackManager {
 
     this.inProgress.add(sessionID);
     try {
-      const currentModel = this.sessionModel.get(sessionID);
+      let currentModel = this.sessionModel.get(sessionID);
       const agentName = this.sessionAgent.get(sessionID);
       const chain = this.resolveChain(agentName, currentModel);
       if (!chain.length) {
@@ -237,6 +237,15 @@ export class ForegroundFallbackManager {
         return;
       }
 
+      // When the agent is known but no model was captured (common for
+      // subagent error events that fire before message.updated), infer
+      // the current model as the chain's first entry. Without this, the
+      // fallback would incorrectly re-select the primary model as the
+      // "next" fallback target.
+      if (!currentModel && agentName && chain.length > 0) {
+        currentModel = chain[0];
+      }
+
       if (!this.sessionTried.has(sessionID)) {
         this.sessionTried.set(sessionID, new Set());
       }