# Agent Scope

**Source:** `src/agents/agent-scope.ts` (217 lines)  
**Category:** Agent Management  
**Purpose:** Agent identity resolution, configuration inheritance, and workspace mapping

## Overview

Agent Scope provides the resolution layer for mapping session keys to agent configurations. It handles agent ID normalization, default agent selection, workspace directory resolution, and per-agent model/skill configuration with proper inheritance chains.

```
┌─────────────────────────────────────────────────────────────┐
│                    User Input                               │
│  "agent:legal:contract-review" or "session-123"            │
└─────────────────────┬───────────────────────────────────────┘
                      │ parseAgentSessionKey()
                      v
┌─────────────────────────────────────────────────────────────┐
│                 Agent Scope Resolution                      │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  resolveSessionAgentIds()                            │  │
│  │  1. Extract agent ID from session key                │  │
│  │  2. Validate against configured agents               │  │
│  │  3. Fallback to default agent                        │  │
│  └──────────────────────────────────────────────────────┘  │
│                          │                                  │
│  ┌───────────────────────▼──────────────────────────────┐  │
│  │  resolveAgentConfig()                                │  │
│  │  - Load agent-specific config                        │  │
│  │  - Apply defaults for missing fields                 │  │
│  │  - Resolve workspace/agent directories               │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      v
┌─────────────────────────────────────────────────────────────┐
│                Resolved Agent Context                       │
│  - agentId: "legal"                                         │
│  - workspace: "/home/user/workspace-legal"                  │
│  - agentDir: "~/.sophiaclaw/agents/legal/agent"             │
│  - model: { primary: "anthropic/claude-sonnet", ... }       │
│  - skills: ["legal.*", "contract-review"]                   │
│  - sandbox: { enabled: true, allowedDirs: [...] }           │
└─────────────────────────────────────────────────────────────┘
```

## Agent ID Normalization

### Canonical Form

```typescript
// Agent IDs are normalized to lowercase for consistency
function normalizeAgentId(agentId: string): string {
  return (agentId ?? DEFAULT_AGENT_ID).trim().toLowerCase()
}

// Examples:
normalizeAgentId("Legal")      → "legal"
normalizeAgentId(" LEGAL ")    → "legal"
normalizeAgentId("")           → "default"
normalizeAgentId(undefined)    → "default"
```

**Design Rationale:**

- Case-insensitive matching prevents `agent:Legal` ≠ `agent:legal` duplicates
- Trimming handles whitespace from user input
- Default fallback ensures always valid

### Default Agent ID

```typescript
const DEFAULT_AGENT_ID = "default";

export function resolveDefaultAgentId(cfg: SophiaClawConfig): string {
  const agents = listAgentEntries(cfg);

  // No agents configured → use hardcoded default
  if (agents.length === 0) {
    return DEFAULT_AGENT_ID;
  }

  // Find agents marked as default
  const defaults = agents.filter((agent) => agent?.default);

  if (defaults.length > 1 && !defaultAgentWarned) {
    defaultAgentWarned = true;
    log.warn("Multiple agents marked default=true; using the first entry as default.");
  }

  // Use first default, or first agent if none marked default
  const chosen = (defaults[0] ?? agents[0])?.id?.trim();
  return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
}
```

**Configuration Example:**

```json
{
  "agents": {
    "list": [
      {
        "id": "general",
        "name": "General Assistant",
        "default": true
      },
      {
        "id": "legal",
        "name": "Legal Counsel"
      }
    ]
  }
}
```

**Resolution:**

- `general` agent marked as `default: true` → becomes default
- If multiple have `default: true` → first one wins (with warning)
- If none have `default: true` → first agent in list

## Session Key to Agent ID Resolution

### Session Key Format

```
Format: agent:{agentId}:{sessionId?}
        │      │         │
        │      │         └─ Optional session name
        │      │
        │      └─ Agent ID (required for agent: prefix)
        │
        └─ Prefix indicating agent-scoped session

Special keys:
- "global" → Shared across all agents
- "unknown" → Unidentified sender (messaging channels)
- "{sessionId}" → Uses current agent + session name
```

### Resolution Algorithm

```typescript
function resolveSessionAgentIds(params: {
  sessionKey?: string;
  config?: SophiaClawConfig;
  agentId?: string; // Explicit override
}): {
  defaultAgentId: string;
  sessionAgentId: string;
} {
  // Step 1: Get default agent from config
  const defaultAgentId = resolveDefaultAgentId(params.config ?? {});

  // Step 2: Check explicit agent ID override (CLI --agent flag)
  const explicitAgentIdRaw =
    typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : "";
  const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null;

  // Step 3: Parse session key
  const sessionKey = params.sessionKey?.trim();
  const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined;
  const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;

  // Step 4: Priority chain
  const sessionAgentId =
    explicitAgentId ?? // 1. CLI --agent flag (highest priority)
    (parsed?.agentId ? normalizeAgentId(parsed.agentId) : null) ?? // 2. Session key
    defaultAgentId; // 3. Config default (fallback)

  return { defaultAgentId, sessionAgentId };
}
```

**Priority Chain:**

```
1. CLI --agent="legal"        (explicit override)
          ↓ if not provided
2. Session key "agent:legal:..." (from message)
          ↓ if not agent-prefixed
3. Default agent from config
```

**Examples:**

| sessionKey             | agentId (CLI) | Result                    |
| ---------------------- | ------------- | ------------------------- |
| `"agent:legal:case-1"` | (none)        | `"legal"`                 |
| `"main"`               | (none)        | `defaultAgentId`          |
| `"global"`             | (none)        | `"global"` (special case) |
| `"anything"`           | `"legal"`     | `"legal"` (explicit wins) |
| (none)                 | (none)        | `defaultAgentId`          |

## Agent Configuration Resolution

### Configuration Schema

```typescript
type AgentEntry = {
  id: string;
  name?: string;
  workspace?: string; // Working directory
  agentDir?: string; // Agent state directory
  model?:
    | string
    | {
        // Model configuration
        primary: string;
        fallbacks?: string[];
      };
  skills?: string[]; // Skill filter patterns
  memorySearch?: {
    // Memory search config
    maxResults?: number;
    threshold?: number;
  };
  humanDelay?: number; // Delay before human notification
  heartbeat?: {
    // Heartbeat config
    enabled: boolean;
    intervalMs: number;
  };
  identity?: {
    // Agent persona
    name: string;
    description: string;
  };
  groupChat?: {
    // Multi-agent config
    enabled: boolean;
    participants: string[];
  };
  subagents?: {
    // Sub-agent spawning
    enabled: boolean;
    maxConcurrent: number;
  };
  sandbox?: {
    // Execution sandbox
    enabled: boolean;
    allowedDirs: string[];
    deniedCommands: string[];
  };
  tools?: {
    // Tool restrictions
    allow: string[];
    deny: string[];
  };
};
```

### Resolution with Fallbacks

```typescript
function resolveAgentConfig(
  cfg: SophiaClawConfig,
  agentId: string,
): ResolvedAgentConfig | undefined {
  const id = normalizeAgentId(agentId);
  const entry = listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id);

  if (!entry) {
    return undefined; // Agent not found
  }

  // Map raw config to resolved config with type safety
  return {
    name: typeof entry.name === "string" ? entry.name : undefined,
    workspace: typeof entry.workspace === "string" ? entry.workspace : undefined,
    agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,

    // Model can be string or object
    model:
      typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
        ? entry.model
        : undefined,

    // Arrays require explicit check
    skills: Array.isArray(entry.skills) ? entry.skills : undefined,

    // Nested objects
    subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined,

    // Pass through other fields
    sandbox: entry.sandbox,
    tools: entry.tools,
    memorySearch: entry.memorySearch,
    humanDelay: entry.humanDelay,
    heartbeat: entry.heartbeat,
    identity: entry.identity,
    groupChat: entry.groupChat,
  };
}
```

## Model Resolution

### Primary Model Selection

```typescript
function resolveAgentModelPrimary(cfg: SophiaClawConfig, agentId: string): string | undefined {
  const raw = resolveAgentConfig(cfg, agentId)?.model;

  if (!raw) {
    return undefined;
  }

  // String shorthand: "anthropic/claude-sonnet"
  if (typeof raw === "string") {
    return raw.trim() || undefined;
  }

  // Object form: { primary: "...", fallbacks: [...] }
  const primary = raw.primary?.trim();
  return primary || undefined;
}
```

**Configuration Examples:**

```json
// String shorthand
{
  "agents": {
    "list": [
      {
        "id": "general",
        "model": "anthropic/claude-sonnet"
      }
    ]
  }
}

// Object form with fallbacks
{
  "agents": {
    "list": [
      {
        "id": "legal",
        "model": {
          "primary": "anthropic/claude-opus",
          "fallbacks": ["anthropic/claude-sonnet"]
        }
      }
    ]
  }
}
```

### Fallback Override Detection

**Problem:** Session-level model overrides should disable agent fallbacks to prevent unexpected model switches.

```typescript
function resolveAgentModelFallbacksOverride(
  cfg: SophiaClawConfig,
  agentId: string,
): string[] | undefined {
  const raw = resolveAgentConfig(cfg, agentId)?.model;

  // String form has no fallbacks
  if (!raw || typeof raw === "string") {
    return undefined;
  }

  // CRITICAL: Treat explicitly empty array as override
  // This allows disabling global fallbacks per-agent
  if (!Object.hasOwn(raw, "fallbacks")) {
    return undefined; // fallbacks key not present → use global
  }

  // fallbacks key present (even if empty) → use agent-specific
  return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
}
```

**Behavior Matrix:**

| Config                  | hasSessionOverride | Result                         |
| ----------------------- | ------------------ | ------------------------------ |
| `fallbacks: ["a", "b"]` | false              | `["a", "b"]`                   |
| `fallbacks: ["a", "b"]` | true               | `undefined` (session controls) |
| `fallbacks: []`         | false              | `[]` (disable fallbacks)       |
| (no fallbacks key)      | false              | Use global defaults            |

## Workspace Directory Resolution

### Directory Priority Chain

```typescript
function resolveAgentWorkspaceDir(cfg: SophiaClawConfig, agentId: string): string {
  const id = normalizeAgentId(agentId);
  const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();

  // Priority 1: Explicit workspace in agent config
  if (configured) {
    return resolveUserPath(configured); // Expand ~/ and env vars
  }

  // Priority 2: Default agent → global default workspace
  const defaultAgentId = resolveDefaultAgentId(cfg);
  if (id === defaultAgentId) {
    const fallback = cfg.agents?.defaults?.workspace?.trim();
    if (fallback) {
      return resolveUserPath(fallback);
    }
    // Hardcoded default: ~/SophiaClaw
    return resolveDefaultAgentWorkspaceDir(process.env);
  }

  // Priority 3: Non-default agents → state-based workspace
  const stateDir = resolveStateDir(process.env);
  return path.join(stateDir, `workspace-${id}`);
}
```

**Resolution Examples:**

```
Agent "legal" with workspace: "/data/legal-workspace"
  → "/data/legal-workspace"

Agent "general" (default) with no workspace config:
  → ~/SophiaClaw (hardcoded default)

Agent "research" (non-default) with no workspace config:
  → ~/.sophiaclaw/workspace-research (state-based)
```

### Agent State Directory

```typescript
function resolveAgentDir(cfg: SophiaClawConfig, agentId: string): string {
  const id = normalizeAgentId(agentId);
  const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();

  // Priority 1: Explicit agentDir config
  if (configured) {
    return resolveUserPath(configured);
  }

  // Priority 2: State directory + agent ID
  const root = resolveStateDir(process.env);
  return path.join(root, "agents", id, "agent");
}
```

**Directory Structure:**

```
~/.sophiaclaw/
├── agents/
│   ├── default/
│   │   └── agent/        # Agent state, memories, skills
│   ├── legal/
│   │   └── agent/
│   └── research/
│       └── agent/
├── workspace-default/
├── workspace-legal/
└── sophiaclaw.json
```

## Skills Filter Resolution

### Skill Pattern Normalization

```typescript
function resolveAgentSkillsFilter(cfg: SophiaClawConfig, agentId: string): string[] | undefined {
  const skills = resolveAgentConfig(cfg, agentId)?.skills;
  return normalizeSkillFilter(skills);
}

function normalizeSkillFilter(skills?: string[]): string[] | undefined {
  if (!skills || skills.length === 0) {
    return undefined; // No filter → all skills allowed
  }

  // Normalize patterns: trim, lowercase, remove duplicates
  const normalized = skills.map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0);

  return [...new Set(normalized)];
}
```

**Skill Pattern Matching:**

```json
{
  "agents": {
    "list": [
      {
        "id": "legal",
        "skills": [
          "legal.*", // All legal skills
          "contract-review", // Exact match
          "research.*" // All research skills
        ]
      }
    ]
  }
}
```

**Runtime Filtering:**

```typescript
function isSkillAllowed(skill: string, filter?: string[]): boolean {
  if (!filter) {
    return true; // No filter → allow all
  }

  for (const pattern of filter) {
    if (pattern.endsWith("*")) {
      // Prefix match: "legal.*" matches "legal.contract"
      const prefix = pattern.slice(0, -1);
      if (skill.startsWith(prefix)) {
        return true;
      }
    } else if (skill === pattern) {
      // Exact match
      return true;
    }
  }

  return false;
}
```

## Logging & Observability

### Subsystem Logger

```typescript
import { createSubsystemLogger } from "../logging/subsystem";

const log = createSubsystemLogger("agent-scope");

// Usage in resolution functions
export function resolveDefaultAgentId(cfg: SophiaClawConfig): string {
  const defaults = agents.filter((agent) => agent?.default);

  if (defaults.length > 1 && !defaultAgentWarned) {
    defaultAgentWarned = true;
    log.warn("Multiple agents marked default=true; using the first entry as default.");
  }

  // ...
}
```

**Log Output:**

```
[agent-scope WARN] Multiple agents marked default=true; using the first entry as default.
```

## Testing Strategy

### Unit Tests

```typescript
describe("resolveSessionAgentIds", () => {
  it("uses explicit agent ID from CLI flag", () => {
    const result = resolveSessionAgentIds({
      sessionKey: "agent:general:session1",
      agentId: "legal", // CLI override
      config: {},
    });

    expect(result.sessionAgentId).toBe("legal");
  });

  it("parses agent ID from session key", () => {
    const result = resolveSessionAgentIds({
      sessionKey: "agent:legal:case-1",
      config: {},
    });

    expect(result.sessionAgentId).toBe("legal");
  });

  it("falls back to default agent", () => {
    const config = {
      agents: {
        list: [{ id: "general", default: true }],
      },
    };

    const result = resolveSessionAgentIds({
      sessionKey: "main", // No agent prefix
      config,
    });

    expect(result.sessionAgentId).toBe("general");
  });

  it("normalizes agent IDs to lowercase", () => {
    const result = resolveSessionAgentIds({
      sessionKey: "agent:LEGAL:Case",
      config: {},
    });

    expect(result.sessionAgentId).toBe("legal");
  });
});

describe("resolveAgentWorkspaceDir", () => {
  it("uses explicit workspace config", () => {
    const config = {
      agents: {
        list: [{ id: "legal", workspace: "/data/legal" }],
      },
    };

    const workspace = resolveAgentWorkspaceDir(config, "legal");
    expect(workspace).toBe("/data/legal");
  });

  it("uses state-based directory for non-default agents", () => {
    const config = {
      agents: {
        list: [{ id: "research" }], // Not default
      },
    };

    const workspace = resolveAgentWorkspaceDir(config, "research");
    expect(workspace).toMatch(/~\/\.sophiaclaw\/workspace-research/);
  });
});
```

## Related Documentation

- [Session Key Format](/docs/routing/session-key.md)
- [Agent Configuration](/docs/configuration/agents.md)
- [Skills System](/docs/skills/filter.md)
