subagent-depth.ts 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import { DEFAULT_MAX_SUBAGENT_DEPTH } from '../config';
  2. import { log } from './logger';
  3. /**
  4. * Tracks subagent spawn depth to prevent excessive nesting.
  5. *
  6. * Depth 0 = root session (user's main conversation)
  7. * Depth 1 = agent spawned by root (e.g., explorer, council)
  8. * Depth 2 = agent spawned by depth-1 agent (e.g., councillor spawned by council)
  9. * Depth 3 = agent spawned by depth-2 agent (max depth by default)
  10. *
  11. * When max depth is exceeded, the spawn is blocked.
  12. */
  13. export class SubagentDepthTracker {
  14. private depthBySession = new Map<string, number>();
  15. private readonly _maxDepth: number;
  16. constructor(maxDepth: number = DEFAULT_MAX_SUBAGENT_DEPTH) {
  17. this._maxDepth = maxDepth;
  18. }
  19. /** Maximum allowed depth. */
  20. get maxDepth(): number {
  21. return this._maxDepth;
  22. }
  23. /**
  24. * Get the current depth of a session.
  25. * Root sessions (not tracked) have depth 0.
  26. */
  27. getDepth(sessionId: string): number {
  28. return this.depthBySession.get(sessionId) ?? 0;
  29. }
  30. /**
  31. * Register a child session and check if the spawn is allowed.
  32. * @returns true if allowed, false if max depth exceeded
  33. */
  34. registerChild(parentSessionId: string, childSessionId: string): boolean {
  35. const parentDepth = this.getDepth(parentSessionId);
  36. const childDepth = parentDepth + 1;
  37. if (childDepth > this.maxDepth) {
  38. log('[subagent-depth] spawn blocked: max depth exceeded', {
  39. parentSessionId,
  40. parentDepth,
  41. childDepth,
  42. maxDepth: this.maxDepth,
  43. });
  44. return false;
  45. }
  46. this.depthBySession.set(childSessionId, childDepth);
  47. log('[subagent-depth] child registered', {
  48. parentSessionId,
  49. childSessionId,
  50. childDepth,
  51. });
  52. return true;
  53. }
  54. /**
  55. * Clean up session tracking when a session is deleted.
  56. */
  57. cleanup(sessionId: string): void {
  58. this.depthBySession.delete(sessionId);
  59. }
  60. /**
  61. * Clean up all tracking data.
  62. */
  63. cleanupAll(): void {
  64. this.depthBySession.clear();
  65. }
  66. }