MemberJunction / MemberJunction/MJ
AgentRunner.RunAgent resolves the agent class against the wrong ClassFactory registry (same defect as #4111), plus a reachable null-key path to an arbitrary agent
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
## Summary
`AgentRunner.RunAgent` resolves the agent class the same way `CreateBridgeRealtimeSession` did before #4111 was fixed, and has the same dead guard:
```ts
const driverClass = params.agent.DriverClass || agentType.DriverClass;
const agentInstance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseAgent, driverClass);
if (!agentInstance) { throw ... } // unreachable — CreateInstance never returns null for an unmatched key
```
`AIAgentType.DriverClass` names a **`BaseAgentType`** subclass; the key this call needs is a **`BaseAgent`** one. Separate registries, no overlapping keys, so the fallback resolves nothing and the run silently lands on plain `BaseAgent`.
This is the path every **non-realtime** agent run takes, including the target agent a realtime co-agent delegates to via `invoke-target-agent`.
## Additional hazard: the null-key path
`AIAgentType.DriverClass` is `string | null` in the generated ORM. If both it and `agent.DriverClass` are null, `driverClass` is null — and `ClassFactory.GetAllRegistrations` skips the key filter entirely for a null key (`normalizedKey === null ? true : …`), so `resolveRegistration` returns the **highest-priority registered `BaseAgent` subclass**: an arbitrary, unrelated agent.
This is latent today only because all three shipped agent types seed a `DriverClass`. It is reachable in principle, and it is the one path that really does run somebody else's agent.
## Why now
PR #3987 fixed the realtime factory: it uses the agent's own `DriverClass` via `TryCreateInstance` (so an unregistered key raises rather than falling back), and constructs `new BaseAgent()` directly when there is none — deliberately *not* `CreateInstance(BaseAgent, null)`, precisely to avoid the arbitrary-highest-priority path above.
After that landed, the two code paths resolve agents differently with nothing recording which behaviour is intended. Worth aligning, or documenting the divergence.
Note: dropping the agent-type fallback loses nothing. Agent-type behaviour is resolved separately inside `BaseAgent` via `BaseAgentType.GetAgentTypeInstance` (`base-agent.ts:1162`, `:3439`, `:3560`), so an agent on the plain `BaseAgent` still gets Loop/Realtime type semantics.
Contributor guide
Research direction
Start at AgentRunner.RunAgent and compare its factory resolution with the realtime path fixed by PR #3987. Read base-agent.ts at the referenced GetAgentTypeInstance locations to confirm how agent-type behavior is resolved separately. Done means non-realtime and invoke-target-agent runs use the intended BaseAgent registration, while a missing key cannot select an arbitrary agent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100