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

Open
#4,112 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.