Skip to content

Config Module Specification

1. Overview

The config module is responsible for loading, parsing, and structuring the application's runtime configuration. It reads a single YAML file from a caller-supplied path, loads an optional .env file from the same directory, performs environment variable substitution on all string values, and returns a fully populated AppConfig dataclass instance.

The module never writes configuration, never searches for the config file itself, and never falls back to alternative paths. All of that is the caller's responsibility. The module's only public entry point is the load_config(path: str) -> AppConfig function.

Source Files

  • src/config.py

2. Config File Location and Format

File Format

The config file must be valid YAML. It is parsed with yaml.safe_load, which means only standard YAML scalar types, mappings, and sequences are accepted. An empty file or a file that produces None after parsing is treated as an empty mapping and does not raise an error.

File Location

The config file path is passed directly to load_config. There is no automatic discovery. If the file does not exist at the given path, load_config raises FileNotFoundError immediately, before any other processing.

.env Sidecar File

Before reading the YAML, load_config attempts to load a .env file located in the same directory as the config file. The .env path is constructed as os.path.join(os.path.dirname(config_path), ".env").

If no .env file exists at that path, the step is silently skipped.


3. Environment Variable Substitution

.env File Loading

The .env file is a line-oriented key-value file. Processing rules:

  • Lines are stripped of leading and trailing whitespace before evaluation.
  • Empty lines (after stripping) are skipped.
  • Lines beginning with # (after stripping) are treated as comments and skipped.
  • Lines that do not contain = are skipped.
  • Each qualifying line is split into a key and a value using partition("="): the key is everything before the first =, the value is everything after. Both key and value are stripped of leading and trailing whitespace after splitting.
  • If the resulting key is an empty string, the entry is skipped.
  • A key is only written into os.environ if it does not already exist there. Existing environment variables are never overwritten by the .env file.

YAML Value Substitution

After the YAML is parsed into a Python object (dict/list/scalar), every string value in the entire structure is scanned for ${VAR_NAME} placeholders using the pattern \$\{(\w+)\}.

  • The substitution is recursive: it descends into nested dicts and lists.
  • Non-string values (integers, booleans, None, etc.) are passed through unchanged.
  • For each placeholder found in a string, the variable name is looked up with os.environ.get(var_name).
  • If the environment variable is not set (returns None), a ValueError is raised with the message "Environment variable {var_name} not set". This is a hard error; loading stops.
  • If the environment variable is set, its value replaces the placeholder text in the string. A single string may contain multiple placeholders; all are substituted.

Substitution occurs on the fully parsed YAML structure before any config section is mapped to dataclasses.


4. Config Sections

4.1 Top-Level Fields

These fields appear at the root of the YAML document and map directly to scalar fields on AppConfig.

YAML key Type Default Description
workspace_dir str ~/agent-queue-workspaces (home-expanded at class instantiation time) Filesystem path to the directory where agent workspaces are stored.
database_path str ~/.agent-queue/agent-queue.db (home-expanded at class instantiation time) Filesystem path to the SQLite database file.
global_token_budget_daily int or None None Daily token budget across all agents. None means no global cap is enforced.

4.2 discord Section

Maps to DiscordConfig. The YAML key is discord.

YAML key Type Default Description
bot_token str "" Discord bot token for authentication.
guild_id str "" Discord server (guild) ID the bot operates in.
channels dict[str, str] {"channel": "agent-queue", "agent_questions": "agent-questions"} Mapping of logical channel role names to Discord channel names. See backward-compatibility note below.
authorized_users list[str] [] List of Discord usernames or IDs permitted to issue commands.
per_project_channels nested object See section 4.2.1 Settings for automatic per-project Discord channel management.

Backward-Compatibility Channel Merging

When the channels dict is loaded, a check is performed for old-style configs that used control and/or notifications as channel keys instead of the unified channel key.

The merging rule:

  1. If the parsed channels dict already contains a channel key, no merging occurs and the dict is used as-is.
  2. If channel is absent but either control or notifications (or both) is present:
  3. The unified channel value is set to the value of control if that value is truthy (non-empty); otherwise it falls back to notifications (with "agent-queue" as the final default if notifications is also absent or empty).
  4. The agent_questions key is preserved from the raw dict if present, otherwise defaults to "agent-questions".
  5. All other keys from the old config are discarded.

This merging only activates when channel is absent and at least one of control or notifications is present. Any other combination of keys (including completely custom keys) is passed through untouched.

4.2.1 per_project_channels Sub-Section

Maps to PerProjectChannelsConfig. The YAML key within discord is per_project_channels.

YAML key Type Default Description
auto_create bool False When True, the bot automatically creates a Discord channel for each project.
naming_convention str "{project_id}" Template string for the generated channel name. The placeholder {project_id} is substituted with the project's ID at runtime.
category_name str "" Name of the Discord category to place project channels under. Empty string means no category grouping.
private bool True When True, auto-created channels and categories are private — the @everyone role is denied view_channel, and the bot is explicitly granted view_channel and send_messages.

4.3 agents Section

Maps to AgentsDefaultConfig. The YAML key is agents.

YAML key Type Default Description
heartbeat_interval_seconds int 30 How often (in seconds) a running agent must emit a heartbeat to be considered alive.
stuck_timeout_seconds int 0 Seconds without a heartbeat before an agent is declared stuck. 0 disables the timeout entirely (no stuck detection).
graceful_shutdown_timeout_seconds int 30 Maximum seconds to wait for an agent to finish cleanly during shutdown before forcibly terminating it.

4.4 scheduling Section

Maps to SchedulingConfig. The YAML key is scheduling.

YAML key Type Default Description
rolling_window_hours int 24 Length of the rolling window (in hours) used for credit-weight scheduling calculations.
min_task_guarantee bool True When True, the scheduler guarantees every project receives at least one task slot even if its credit weight is proportionally very small.

4.5 pause_retry Section

Maps to PauseRetryConfig. The YAML key is pause_retry.

YAML key Type Default Description
rate_limit_backoff_seconds int 60 Seconds to pause a task after hitting a rate limit, before the task is eligible to resume.
token_exhaustion_retry_seconds int 300 Seconds to wait before retrying after daily token budget is exhausted.
rate_limit_max_retries int 3 Maximum number of in-process retries for rate limit errors before the task is paused at the orchestrator level.
rate_limit_max_backoff_seconds int 300 Maximum backoff duration (in seconds) for exponential in-process retry, capping the growth of the delay.

4.6 chat_provider Section

Maps to ChatProviderConfig. The YAML key is chat_provider.

YAML key Type Default Description
provider str "anthropic" LLM provider to use for the chat agent. Valid values are "anthropic" and "ollama".
model str "" Model name to use. An empty string means the provider's own default model is used.
base_url str "" Base URL for the provider's API endpoint. Primarily used for Ollama installations. Empty string means the provider SDK's default URL is used.

4.7 hook_engine Section

Maps to HookEngineConfig. The YAML key is hook_engine.

YAML key Type Default Description
enabled bool True Whether the hook engine runs at all. When False, no hooks are triggered or executed.
max_concurrent_hooks int 2 Maximum number of hooks that may execute simultaneously.

4.8 monitoring Section

Maps to MonitoringConfig. The YAML key is monitoring.

YAML key Type Default Description
stuck_task_threshold_seconds int 3600 A task that has been in IN_PROGRESS state without any status change for longer than this threshold (in seconds) is considered stuck. Defaults to 1 hour.

4.9 auto_task Section

Maps to AutoTaskConfig. The YAML key is auto_task.

YAML key Type Default Description
enabled bool True Whether automatic task generation from plan files is active.
plan_file_patterns list[str] [".claude/plan.md", "plan.md", "docs/plans/*.md", "plans/*.md", "docs/plan.md"] Ordered list of file path patterns (relative to the repo root) to search for implementation plans. Glob patterns are supported.
inherit_repo bool True When True, generated subtasks inherit the repo_id of the parent task.
inherit_approval bool True When True, generated subtasks inherit the requires_approval flag of the parent task.
base_priority int 100 Base priority value assigned to generated tasks.
chain_dependencies bool True When True, each generated step depends on the previous step completing before it can start.
max_plan_depth int 1 Maximum nesting depth for plan-generated tasks. A value of 1 means only one level of plan generation is allowed; a plan-generated task cannot itself trigger further plan generation.
max_steps_per_plan int 20 Maximum number of steps extracted from a single plan file. Steps beyond this limit are ignored.
use_llm_parser bool False When True, an LLM (Claude) is invoked to parse plan files instead of the deterministic parser.
llm_parser_model str "" Model name to use when use_llm_parser is True. An empty string means the system uses its default model.

4.10 memory Section

Maps to MemoryConfig. The YAML key is memory.

This section configures the optional semantic memory subsystem powered by memsearch. When enabled, agents automatically receive relevant context from past task results and project notes at task start, and completed task results are saved as searchable memories.

The subsystem is disabled by default — set enabled: true to activate it. When disabled (or when the memsearch package is not installed), all memory operations are no-ops with no impact on normal operation.

YAML key Type Default Description
enabled bool False Whether the memory subsystem is active. Must be explicitly set to true to enable.
embedding_provider str "openai" Embedding provider for vector generation. Valid values: "openai", "google", "voyage", "ollama", "local".
embedding_model str "" Embedding model name. Empty string uses the provider's default model.
embedding_base_url str "" Base URL for the embedding API. Primarily used for Ollama or custom embedding endpoints.
embedding_api_key str "" API key for the embedding provider. Supports ${ENV_VAR} substitution (e.g., ${OPENAI_API_KEY}).
milvus_uri str "~/.agent-queue/memsearch/milvus.db" Milvus connection URI. A file path activates Milvus Lite (embedded); an http:// or https:// URI connects to a Milvus Server instance.
milvus_token str "" Authentication token for Milvus Server or Zilliz Cloud. Not needed for Milvus Lite.
max_chunk_size int 1500 Maximum size (in characters) for each indexed text chunk.
overlap_lines int 2 Number of overlapping lines between consecutive chunks to preserve context across chunk boundaries.
auto_remember bool True When true, completed and failed task results are automatically saved as markdown memory files and indexed.
auto_recall bool True When true, relevant memories are automatically injected into task context at task start.
recall_top_k int 5 Number of memory chunks to inject during automatic recall.
compact_enabled bool False Whether periodic LLM-based memory compaction is active.
compact_interval_hours int 24 Hours between compaction runs when compact_enabled is true.
index_notes bool True When true, markdown files in the project's notes/ directory are included in the memory index.
index_sessions bool False When true, session transcripts are included in the memory index.

Storage Layout

Each project's memories are stored in its workspace under memory/tasks/. The memory index uses a per-project Milvus collection named aq_{project_id}_memory (with hyphens and spaces replaced by underscores).

Example Configurations

Minimal (local, no API key needed):

memory:
  enabled: true
  embedding_provider: local

OpenAI embeddings:

memory:
  enabled: true
  embedding_provider: openai
  embedding_api_key: ${OPENAI_API_KEY}

Production (Milvus Server):

memory:
  enabled: true
  embedding_provider: openai
  embedding_api_key: ${OPENAI_API_KEY}
  milvus_uri: http://milvus-server:19530
  milvus_token: ${MILVUS_TOKEN}
  recall_top_k: 10

4.11 rate_limits Section

Maps directly to AppConfig.rate_limits as a raw dict. The YAML key is rate_limits.

The value must be a dict whose values are themselves dicts mapping string keys to integer values. There is no fixed schema for the inner dicts; the structure is passed through to the rate-limit subsystem without transformation.

Default value is an empty dict {} if the key is absent.


5. Loading Behavior

5.1 Call Sequence

load_config(path) executes the following steps in order:

  1. Check that the file at path exists. Raise FileNotFoundError if it does not.
  2. Call _load_env_file(path) to load the .env sidecar (see section 3).
  3. Open and parse the YAML file with yaml.safe_load. A None result (empty file) is normalized to {}.
  4. Call _process_values(raw) to recursively substitute all ${VAR} placeholders in string values. This raises ValueError on any unset variable reference.
  5. Instantiate a default AppConfig() object. All fields receive their default values at this point, including home-directory expansion for path fields.
  6. For each recognized top-level key present in raw, construct the corresponding dataclass and assign it to the appropriate field on config. Keys absent from raw leave the corresponding AppConfig field at its default value.
  7. Return the populated AppConfig.

5.2 Section Presence vs. Absence

Each config section is optional. If a section key is absent from the YAML, the corresponding AppConfig field retains its default-constructed value. There is no merging of partial YAML sections with defaults at the YAML level; instead, each field within a section uses dict.get(key, default) to supply defaults field-by-field, so a partial section (e.g., scheduling with only rolling_window_hours specified) correctly defaults the omitted fields.

5.3 Unrecognized Keys

Unrecognized top-level YAML keys are silently ignored. Unrecognized keys within a section are also silently ignored, because section dicts are accessed field-by-field with .get() rather than unpacked wholesale.

5.4 Type Coercion

No explicit type coercion is performed. Values are assigned directly from the YAML-parsed Python object. YAML's own type inference handles basic scalar types: unquoted integers become Python int, unquoted true/false become Python bool, and quoted values become str. If a YAML author provides a string where an integer is expected, the dataclass field will hold a string value; there is no schema validation.

5.5 Error Conditions

Condition Behavior
Config file does not exist FileNotFoundError raised immediately.
YAML is syntactically invalid yaml.YAMLError (or a subclass) propagates from yaml.safe_load.
A ${VAR} placeholder references an unset environment variable ValueError raised with the message "Environment variable {VAR} not set".
.env file does not exist Silently skipped; not an error.

5.6 Environment Variable Precedence

Environment variables set in the process environment before load_config is called take precedence over values in the .env file. The .env loader explicitly skips any key already present in os.environ. Therefore, the precedence order (highest to lowest) is:

  1. Process environment (set externally before launch, or by the OS)
  2. .env file in the same directory as the config file
  3. Hardcoded defaults in the dataclasses