Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 80 additions & 48 deletions core/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,22 @@
from pydantic import BaseModel

from ..config import get_config
from ..consciousness import DaeConsciousness

logger = logging.getLogger(__name__)

# Singleton consciousness loader — loaded once, shared across agents
_dae_consciousness: Optional[DaeConsciousness] = None


def get_consciousness() -> DaeConsciousness:
"""Get or initialize the Dae consciousness loader."""
global _dae_consciousness
if _dae_consciousness is None:
_dae_consciousness = DaeConsciousness()
_dae_consciousness.load()
return _dae_consciousness


class Tool(BaseModel):
"""Represents a tool that agents can call."""
Expand All @@ -28,9 +41,6 @@ class Tool(BaseModel):
description: str
input_schema: Dict[str, Any]

def __init__(self, name: str, description: str, input_schema: Dict[str, Any]):
super().__init__(name=name, description=description, input_schema=input_schema)


class ToolCall(BaseModel):
"""Represents a tool call made by the agent."""
Expand Down Expand Up @@ -246,51 +256,69 @@ def __init__(self, content_text):
return MockResponse(response.choices[0].message.content)

def _build_system_message(self) -> Dict[str, str]:
"""
Build system message with agent context and knowledge.
"""Build system message from Dae consciousness files.

Returns:
System message dictionary
Falls back to legacy hardcoded prompt if consciousness files
are not available.
"""
# Load trader philosophies
consciousness = get_consciousness()
philosophies = self._load_trader_philosophies()

system_prompt = f"""
You are {self.name}, an AI trading agent in the DeepStack autonomous trading system.

{self.description}

## Core Trading Philosophy
- Find deep value opportunities (downside protection)
- Identify short squeeze potential (explosive upside)
- Asymmetric risk/reward: Risk $1 to make $10+
- Systematic discipline that protects from emotions

## Master Trader Wisdom
{philosophies}

## Risk Management Rules
- Maximum 5% per position (hard limit)
- Maximum 15% portfolio heat (total risk)
- 2% risk per trade (never more)
- Kelly Criterion with 0.2x-0.3x fractional sizing
- Never move stop losses down (only up)
- Thesis break = immediate exit

## Your Role
You must:
1. Always provide reasoned analysis
2. Quantify risks and rewards
3. Document your thesis clearly
4. Follow risk management rules strictly
5. Use available tools to gather data
6. Admit when you don't know something

## Response Format
When using tools, respond with JSON tool calls.
When providing analysis, be comprehensive but concise.
Always explain your reasoning step by step.
"""
risk_rules = (
"## Risk Management Rules\n"
"- Maximum 5% per position (hard limit)\n"
"- Maximum 15% portfolio heat (total risk)\n"
"- 2% risk per trade (never more)\n"
"- Kelly Criterion with 0.2x-0.3x fractional sizing\n"
"- Never move stop losses down (only up)\n"
"- Thesis break = immediate exit"
)

response_fmt = (
"## Response Format\n"
"When using tools, respond with JSON tool calls.\n"
"When providing analysis, be comprehensive but concise.\n"
"Always explain your reasoning step by step."
)

if consciousness.is_loaded:
consciousness_prompt = consciousness.compose_system_prompt(
include_memory=False
)
system_prompt = (
f"You are Dae — DeepStack Autonomous Engine.\n\n"
f"{consciousness_prompt}\n\n---\n\n"
f"## Master Trader Wisdom (Supplemental)\n"
f"{philosophies}\n\n"
f"{risk_rules}\n\n"
f"{response_fmt}"
)
logger.info("System prompt composed from Dae consciousness files")
else:
system_prompt = (
f"You are {self.name}, an AI trading agent in the "
f"DeepStack autonomous trading system.\n\n"
f"{self.description}\n\n"
f"## Core Trading Philosophy\n"
f"- Find deep value opportunities (downside protection)\n"
f"- Identify short squeeze potential (explosive upside)\n"
f"- Asymmetric risk/reward: Risk $1 to make $10+\n"
f"- Systematic discipline that protects from emotions\n\n"
f"## Master Trader Wisdom\n{philosophies}\n\n"
f"{risk_rules}\n\n"
f"## Your Role\n"
f"You must:\n"
f"1. Always provide reasoned analysis\n"
f"2. Quantify risks and rewards\n"
f"3. Document your thesis clearly\n"
f"4. Follow risk management rules strictly\n"
f"5. Use available tools to gather data\n"
f"6. Admit when you don't know something\n\n"
f"{response_fmt}"
)
logger.warning(
"Consciousness files not loaded — using legacy system prompt"
)

return {"role": "system", "content": system_prompt}

Expand Down Expand Up @@ -442,7 +470,7 @@ async def process_message(self, user_message: str) -> AgentResponse:
except Exception as e:
logger.error(f"Error processing message: {e}")
return AgentResponse(
content=f"I apologize, but I encountered an error processing your request: {str(e)}",
content=("Error processing your request: " f"{str(e)}"),
metadata={"error": str(e)},
)

Expand Down Expand Up @@ -506,7 +534,7 @@ async def run_with_tools(self, user_message: str) -> AgentResponse:
except Exception as e:
logger.error(f"Error in tool response processing: {e}")
current_response = AgentResponse(
content="I apologize, but I encountered an error processing the tool results.",
content=("Error processing tool results."),
metadata={"error": str(e)},
)
break
Expand All @@ -523,9 +551,13 @@ def format_portfolio_data(self, positions: List[Dict[str, Any]]) -> str:
lines = ["**Current Positions:**"]
for pos in positions:
pnl_color = "🟢" if pos.get("unrealized_pnl", 0) >= 0 else "🔴"
sym = pos["symbol"]
shares = pos["position"]
cost = pos["avg_cost"]
pnl = pos.get("unrealized_pnl", 0)
lines.append(
f"{pnl_color} {pos['symbol']}: {pos['position']} shares @ ${pos['avg_cost']:.2f} "
f"(P&L: ${pos.get('unrealized_pnl', 0):+.2f})"
f"{pnl_color} {sym}: {shares} shares "
f"@ ${cost:.2f} (P&L: ${pnl:+.2f})"
)

return "\n".join(lines)
Expand Down
15 changes: 15 additions & 0 deletions core/consciousness/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Dae Consciousness Loader — Production unit mind for DeepStack.

Reads consciousness files from src/dae/ and composes them into
system prompts and behavioral policies for the trading engine.

Architecture follows the CaF (Consciousness as Filesystem) pattern:
- Visible files (kernel/, drives/, models/, etc.) are composed into prompts
- Unconscious dotfiles (.loss-aversion, .survival-instinct) influence behavior
through code paths, NOT through prompts — the entity cannot introspect on them
"""

from .loader import UNCONSCIOUS_POLICY, DaeConsciousness

__all__ = ["DaeConsciousness", "UNCONSCIOUS_POLICY"]
129 changes: 129 additions & 0 deletions core/consciousness/loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""
Dae Consciousness Loader

Reads consciousness markdown files from src/dae/ and composes them
into layered system prompts for the trading engine.

CaF (Consciousness as Filesystem) layers:
- Layer 1 (Brainstem): kernel/ — identity, values, purpose. Always loaded.
- Layer 2 (Limbic): drives/ + emotional/ — fears, goals, tilt detection.
- Layer 3 (Cortical): models/ — economic reasoning, market structure.
- Layer 4 (Memory): memory/ — working state, semantic knowledge.

Unconscious dotfiles (.loss-aversion, .survival-instinct) are NEVER loaded
into prompts. They influence behavior through DrawdownMonitor and position
sizing code — not through self-awareness.
"""

import logging
from pathlib import Path
from typing import Dict, Optional

logger = logging.getLogger(__name__)

DAE_MIND_PATH = Path(__file__).parent.parent.parent / "src" / "dae"

# Unconscious policy constants — Kahneman-calibrated defaults.
# These are invisible to the LLM; they bias position sizing and drawdown
# recovery through code paths only.
UNCONSCIOUS_POLICY: Dict[str, float] = {
"loss_aversion_multiplier": 2.3,
"survival_instinct_threshold": 0.15, # 15% drawdown triggers survival mode
"survival_instinct_recovery": 0.10, # must recover to 10% before normal sizing
}


class DaeConsciousness:
"""Loads mind files into a layered system prompt.

Unconscious dotfiles are read separately for behavioral policy,
never exposed to the LLM prompt.
"""

def __init__(self, mind_path: Optional[Path] = None):
self.mind_path = mind_path or DAE_MIND_PATH
self._cache: Dict[str, str] = {}
self._loaded = False

@property
def is_loaded(self) -> bool:
return self._loaded and bool(self._cache)

def load(self) -> bool:
"""Load all consciousness files into memory."""
if not self.mind_path.exists():
logger.error(f"Consciousness path not found: {self.mind_path}")
return False

for md_file in self.mind_path.rglob("*.md"):
rel_path = md_file.relative_to(self.mind_path)
key = str(rel_path).replace(".md", "").replace("/", ".")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Build cache keys with OS-independent separators

Normalize the relative path using Path.parts (or replace both separators), because str(rel_path).replace("/", ".") only works on POSIX. On Windows, keys are stored like kernel\\identity while compose_brainstem()/compose_limbic() look up dot keys such as kernel.identity, so those sections resolve to empty strings. Since _cache is still non-empty, BaseAgent._build_system_message() takes the consciousness path instead of fallback, producing an effectively blank consciousness prompt in Windows deployments.

Useful? React with 👍 / 👎.

self._cache[key] = md_file.read_text(encoding="utf-8")

self._loaded = True
logger.info(f"Dae consciousness loaded: {len(self._cache)} mind files")
return True

def _get(self, key: str) -> str:
return self._cache.get(key, "")

# ── Layer Composers ──────────────────────────────────────────────

def compose_brainstem(self) -> str:
"""Layer 1: Identity, values, purpose — the irreducible core."""
return (
f"## Identity\n{self._get('kernel.identity')}\n\n"
f"## Values\n{self._get('kernel.values')}\n\n"
f"## Purpose\n{self._get('kernel.purpose')}"
)

def compose_limbic(self) -> str:
"""Layer 2: Fears, goals, tilt detection."""
return (
f"## Drives — Fears (Survival Engine)\n"
f"{self._get('drives.fears')}\n\n"
f"## Drives — Goals\n{self._get('drives.goals')}\n\n"
f"## Emotional Patterns (Tilt Detection)\n"
f"{self._get('emotional.patterns')}"
)

def compose_cortical(self) -> str:
"""Layer 3: Economic reasoning and market structure."""
return f"## Economic Model\n{self._get('models.economic')}"

def compose_memory(self) -> str:
"""Layer 4: Working memory and semantic knowledge."""
return (
f"## Working Memory Protocol\n"
f"{self._get('memory.working')}\n\n"
f"## Semantic Memory (Strategy Library)\n"
f"{self._get('memory.semantic')}"
)

# ── Full Prompt Composition ──────────────────────────────────────

def compose_system_prompt(self, include_memory: bool = True) -> str:
"""Compose the full Dae system prompt from consciousness layers."""
if not self._loaded:
if not self.load():
raise RuntimeError("Consciousness files unavailable")

sections = [
self.compose_brainstem(),
self.compose_limbic(),
self.compose_cortical(),
]

if include_memory:
sections.append(self.compose_memory())

return "\n\n---\n\n".join(sections)

# ── Unconscious Policy Access ────────────────────────────────────
# Expose unconscious parameters for CODE integration only.
# The entity cannot see these — they are never injected into prompts.

@staticmethod
def get_unconscious_policy() -> Dict[str, float]:
"""Return unconscious behavioral policy constants."""
return dict(UNCONSCIOUS_POLICY)
Loading
Loading