# Role Precedence Rules in Flue: Harness, Session, and Call Levels Explained

> Understand Flue's role precedence rules at the harness, session, and call levels. Discover how the narrowest scope, call-level, always wins for precise control.

- Repository: [Astro/flue](https://github.com/withastro/flue)
- Tags: deep-dive
- Published: 2026-05-11

---

**Flue applies strict role precedence as call-level > session-level > harness-level, where the narrowest scope always wins and acts as a temporary system-prompt overlay for that specific interaction.**

Flue, Astro's LLM orchestration framework, enables developers to assign specialized AI personas through role definitions at three distinct scopes. Understanding how role precedence rules govern the interaction between harness, session, and call-level assignments ensures your agents adopt the correct context for every specific task without leaking instructions into persistent conversation history.

## The Three Scopes of Role Assignment

Flue allows role assignment at three distinct levels of granularity. Each scope serves a different purpose in the agent lifecycle, from global defaults to single-request overrides.

### Harness-Level Roles

The **harness** represents your global agent configuration initialized via `init()`. When you provide a role during harness creation, it serves as the default fallback for all subsequent operations.

In [`packages/sdk/src/harness.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/harness.ts), the `init()` function validates the supplied role using `assertRoleExists` (lines 70–77), ensuring the role is defined in your agent's [`AGENTS.md`](https://github.com/withastro/flue/blob/main/AGENTS.md) or `roles/` directory before the harness is created.

```typescript
const harness = await init({
  model: 'anthropic/claude-sonnet-4',
  role: 'coder',  // Fallback role for all calls
});

```

### Session-Level Roles

A **session** maintains stateful conversation context. When you create a session via `harness.session()`, you can specify a role that overrides the harness default for all calls within that session's lifetime.

The validation occurs in [`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts) (lines 379–387), where `assertRoleExists` checks the session-level role before opening the connection.

```typescript
const session = await harness.session('review-thread', {
  role: 'reviewer',  // Overrides harness role for this session
});

```

### Call-Level Roles

Individual **calls**—whether `session.prompt()`, `session.task()`, or `session.skill()`—accept a role option that takes precedence over both session and harness configurations. This role applies only to that specific request.

According to the source code, when the call executes, Flue merges this role into the request's system prompt, superseding any broader scope assignments.

```typescript
await session.prompt(
  'Review the latest changes.',
  { role: 'auditor' }  // Highest precedence: applies only to this prompt
);

```

## How Role Precedence Works

The precedence rule follows a simple hierarchical cascade: **call role > session role > harness role**. When processing a request, Flue evaluates roles from most specific to least specific, selecting the first available assignment.

Crucially, the chosen role is applied as a **call-scoped system-prompt overlay**. This means:

- The role instructions are visible to the model only for the duration of that specific call
- The role does **not** become part of the persistent message history or user-message chain
- Subsequent calls revert to the next applicable level in the hierarchy (session or harness)

This architecture prevents role contamination between different phases of a workflow, allowing seamless persona switching without historical baggage.

## Implementation in the Source Code

The precedence hierarchy is enforced through explicit validation steps and strategic merging logic across three key files:

**[`packages/sdk/src/harness.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/harness.ts)**: Validates harness-level roles during initialization and serves as the fallback provider when narrower scopes don't specify roles.

**[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)**: Handles session-level role validation and propagates the active role context to individual calls, serving as the intermediary between harness defaults and call-specific overrides.

**[`packages/sdk/src/roles.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/roles.ts)**: Defines the role structure and implements `assertRoleExists`, the validation utility used across all three scopes to ensure only defined roles can be assigned.

The README.md documents this behavior explicitly: *"Roles can be set at the harness, session, or call level. Precedence is `call role > session role > harness role`. Role instructions are applied as call-scoped system prompt overlays, not injected into the persisted user message history."*

## Practical Examples

This example demonstrates all three precedence levels in a single workflow:

```typescript
// Initialize with harness-level fallback role
const harness = await init({
  model: 'anthropic/claude-sonnet-4',
  role: 'coder',  // Default for all operations
});

// Create session with reviewer persona
const session = await harness.session('code-review', {
  role: 'reviewer',  // Overrides 'coder' for this session
});

// First prompt uses call-level auditor role
await session.prompt(
  'Audit the authentication logic for security vulnerabilities.',
  { role: 'auditor' }  // Overrides 'reviewer' for this call only
);

// Second prompt falls back to session-level reviewer role
await session.prompt(
  'Provide general feedback on code style.'
  // No call-level role specified, uses 'reviewer'
);

// Task with its own specialized role
const research = await session.task(
  'Research related CVEs.',
  { role: 'security-researcher' }  // Overrides session role for this task
);

```

**Execution flow:**

- The first prompt executes with the **auditor** role
- The second prompt executes with the **reviewer** role (session-level)
- The task executes with the **security-researcher** role
- Any subsequent calls on this session without explicit roles would use **reviewer**

## Summary

- **Role precedence in Flue follows a strict hierarchy**: call-level roles override session-level roles, which override harness-level roles.
- **Validation occurs at creation**: `assertRoleExists` in [`packages/sdk/src/roles.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/roles.ts) validates roles when initializing harnesses or sessions.
- **Call-level roles are ephemeral**: They exist as temporary system-prompt overlays for single requests and do not persist in conversation history.
- **Scope-specific flexibility**: You can maintain a default persona at the harness level, override it for specific conversation threads at the session level, and further specialize for individual prompts or tasks.

## Frequently Asked Questions

### What happens if I don't specify a role at the call level?

If you omit the role option in `session.prompt()` or `session.task()`, Flue automatically falls back to the session-level role configured when calling `harness.session()`. If no session-level role exists, it uses the harness-level role from `init()`. The system always selects the most specific available role in the hierarchy.

### Does a call-level role persist in the session conversation history?

No. According to the Flue source code, call-level roles are applied as **temporary system-prompt overlays** visible only during that specific request. They are not injected into the persisted user message chain, ensuring that specialized instructions don't leak into subsequent interactions or the agent's memory.

### How does Flue validate that a role exists before applying it?

Flue uses the `assertRoleExists` utility defined in [`packages/sdk/src/roles.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/roles.ts) to validate roles during harness creation ([`packages/sdk/src/harness.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/harness.ts), lines 70–77) and session creation ([`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts), lines 379–387). This ensures that only roles defined in your agent's [`AGENTS.md`](https://github.com/withastro/flue/blob/main/AGENTS.md) or `roles/` directory can be assigned, preventing runtime errors from undefined personas.

### Can I override a session role for just one message without affecting the rest of the session?

Yes. By passing the `role` option to an individual `session.prompt()` or `session.task()` call, you override the session-level role for that specific interaction only. After that call completes, subsequent calls on the same session revert to using the session-level role (or harness-level fallback if none was set at the session level).