Skip to content
Open
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
99 changes: 99 additions & 0 deletions model_capability_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Ollama model capability verification for autonomous NeuroRift agent mode."""

from __future__ import annotations

import json
import subprocess
from typing import Any

CAPABILITY_PROMPT = """Evaluate whether you can reliably perform the following tasks:

* structured tool invocation
* Linux command generation
* file creation and modification
* interpreting tool outputs
* multi-step reasoning for autonomous agents

Return a JSON response:
{
"tool_usage": true/false,
"command_generation": true/false,
"filesystem_operations": true/false,
"multi_step_reasoning": true/false,
"agent_ready": true/false
}"""


def _extract_json(raw_text: str) -> dict[str, Any]:
raw_text = (raw_text or "").strip()
start = raw_text.find("{")
end = raw_text.rfind("}")
if start == -1 or end == -1 or end <= start:
raise ValueError("No JSON object found in model response")
return json.loads(raw_text[start : end + 1])
Comment on lines +28 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: JSON extraction is fragile because it slices from the first { to the last } in the whole response. If the model returns a valid JSON object plus any extra brace in trailing commentary, parsing fails and the model is incorrectly rejected. Parse the first valid JSON object instead of using a greedy substring. [logic error]

Severity Level: Major ⚠️
- ❌ run-agent startup fails on otherwise usable model output.
- ⚠️ Capability check becomes brittle to minor output noise.
Suggested change
start = raw_text.find("{")
end = raw_text.rfind("}")
if start == -1 or end == -1 or end <= start:
raise ValueError("No JSON object found in model response")
return json.loads(raw_text[start : end + 1])
decoder = json.JSONDecoder()
for idx, ch in enumerate(raw_text):
if ch != "{":
continue
try:
obj, _ = decoder.raw_decode(raw_text[idx:])
except json.JSONDecodeError:
continue
if isinstance(obj, dict):
return obj
raise ValueError("No JSON object found in model response")
Steps of Reproduction ✅
1. Launch autonomous mode through real CLI path: `neurorift_cli.py:51-52` forwards args to
`neurorift_main.main()`, and parser exposes `run-agent` at `neurorift_main.py:937`.

2. Run `neurorift run-agent --model test-model` so `neurorift_main.py:1007-1018` calls
`verify_model_capabilities()` from `model_capability_check.py:35`.

3. Have `ollama run` return text containing a valid JSON object followed by commentary
with an extra `}`; parser in `_extract_json` (`model_capability_check.py:28-32`) slices
from first `{` to last `}`, creating invalid JSON.

4. `json.loads` fails, `verify_model_capabilities` returns `invalid_capability_json` at
`model_capability_check.py:59-64`, and `run-agent` is blocked by readiness gate at
`neurorift_main.py:1019-1023`.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** model_capability_check.py
**Line:** 28:32
**Comment:**
	*Logic Error: JSON extraction is fragile because it slices from the first `{` to the last `}` in the whole response. If the model returns a valid JSON object plus any extra brace in trailing commentary, parsing fails and the model is incorrectly rejected. Parse the first valid JSON object instead of using a greedy substring.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎



def verify_model_capabilities(model_name: str) -> dict[str, Any]:
try:
proc = subprocess.run(
["ollama", "run", model_name, CAPABILITY_PROMPT],
capture_output=True,
text=True,
timeout=120,
check=False,
)
except FileNotFoundError:
return {"ok": False, "error": "ollama_missing", "agent_ready": False}
except Exception as exc: # defensive for unstable environments
return {
"ok": False,
"error": f"ollama_exec_error:{type(exc).__name__}",
"agent_ready": False,
}

if proc.returncode != 0:
return {
"ok": False,
"error": f"ollama_returned_{proc.returncode}",
"stderr": proc.stderr,
"agent_ready": False,
}

try:
parsed = _extract_json(proc.stdout)
except Exception as exc:
return {
"ok": False,
"error": f"invalid_capability_json:{type(exc).__name__}",
"raw": proc.stdout[:1000],
"agent_ready": False,
}

required = {
"tool_usage",
"command_generation",
"filesystem_operations",
"multi_step_reasoning",
"agent_ready",
}
if not required.issubset(parsed):
return {
"ok": False,
"error": "capability_fields_missing",
"parsed": parsed,
"agent_ready": False,
}

parsed["ok"] = bool(parsed.get("agent_ready"))
return parsed
Comment on lines +74 to +83
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Capability fields are only checked for presence, not type, so string values like "false" are treated as truthy by callers and can incorrectly allow non-ready models to pass. Enforce boolean types for all required fields and compute readiness from a real boolean. [type error]

Severity Level: Critical 🚨
- ❌ Non-ready models can pass autonomous readiness gate.
- ⚠️ Autonomous execution reliability drops with weak model capability.
Suggested change
if not required.issubset(parsed):
return {
"ok": False,
"error": "capability_fields_missing",
"parsed": parsed,
"agent_ready": False,
}
parsed["ok"] = bool(parsed.get("agent_ready"))
return parsed
if not required.issubset(parsed):
return {
"ok": False,
"error": "capability_fields_missing",
"parsed": parsed,
"agent_ready": False,
}
if not all(isinstance(parsed.get(field), bool) for field in required):
return {
"ok": False,
"error": "capability_fields_invalid_type",
"parsed": parsed,
"agent_ready": False,
}
parsed["ok"] = parsed["agent_ready"]
return parsed
Steps of Reproduction ✅
1. Trigger autonomous startup via `neurorift_cli.py:51-52` into `run-agent` path in
`neurorift_main.py:1007`.

2. Execute with a model output where capability fields are strings (e.g., `"agent_ready":
"false"`), which is realistic for LLM formatting drift; `verify_model_capabilities` parses
it at `model_capability_check.py:57-58`.

3. Current code only checks key presence (`model_capability_check.py:74`) and then
computes `ok` using `bool(parsed.get("agent_ready"))` (`model_capability_check.py:82`), so
`"false"` is treated truthy.

4. Caller checks raw `capability.get("agent_ready")` at `neurorift_main.py:1019`;
non-empty string bypasses rejection, allowing autonomous run with a non-ready model.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** model_capability_check.py
**Line:** 74:83
**Comment:**
	*Type Error: Capability fields are only checked for presence, not type, so string values like `"false"` are treated as truthy by callers and can incorrectly allow non-ready models to pass. Enforce boolean types for all required fields and compute readiness from a real boolean.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎



if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(
description="Check Ollama model capability for NeuroRift agent mode"
)
parser.add_argument("--model", required=True)
args = parser.parse_args()
print(json.dumps(verify_model_capabilities(args.model), indent=2))
1 change: 1 addition & 0 deletions neurorift/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions neurorift/channels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/channels/channel_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ChannelRouter:
def route(self, msg: dict):
return msg
12 changes: 12 additions & 0 deletions neurorift/channels/cli_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Channel:
def connect(self):
return True

def receive_message(self, payload=None):
return payload or {}

def send_message(self, message):
return message

def close(self):
return True
12 changes: 12 additions & 0 deletions neurorift/channels/discord_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Channel:
def connect(self):
return True

def receive_message(self, payload=None):
return payload or {}

def send_message(self, message):
return message

def close(self):
return True
12 changes: 12 additions & 0 deletions neurorift/channels/telegram_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Channel:
def connect(self):
return True

def receive_message(self, payload=None):
return payload or {}

def send_message(self, message):
return message

def close(self):
return True
12 changes: 12 additions & 0 deletions neurorift/channels/web_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Channel:
def connect(self):
return True

def receive_message(self, payload=None):
return payload or {}

def send_message(self, message):
return message

def close(self):
return True
1 change: 1 addition & 0 deletions neurorift/clawhub/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions neurorift/clawhub/clawhub_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class ClawHubAPI:
base_url = "https://clawhub.example/api"
18 changes: 18 additions & 0 deletions neurorift/clawhub/clawhub_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pathlib import Path
import shutil


class ClawHubClient:
def __init__(self, cache_dir: Path):
self.cache_dir = cache_dir
cache_dir.mkdir(parents=True, exist_ok=True)

def fetch_skill(self, skill_name: str, source_dir: Path) -> Path:
src = source_dir / skill_name
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: skill_name is directly joined into the source path without containment checks, so values like ../... can escape the intended skill directory and copy arbitrary local folders. Resolve the path and verify it still stays under source_dir before using it. [security]

Severity Level: Critical 🚨
- ⚠️ ClawHub install reads directories outside skill_store.
- ❌ Unauthorized local folder contents copied into cache.
Suggested change
src = source_dir / skill_name
src = (source_dir / skill_name).resolve()
if src.parent != source_dir.resolve():
raise ValueError(f"Invalid skill name: {skill_name}")
Steps of Reproduction ✅
1. Run CLI install flow with user-controlled argument: `python3 neurorift_main.py
--clawhub ../../..`; `--clawhub` is accepted at `neurorift_main.py:914` and executed at
`neurorift_main.py:983`.

2. Call chain is `main()` (`neurorift_main.py:1472`) → `_async_main()`
(`neurorift_main.py:973`) → `SkillManager.install_clawhub()`
(`neurorift/skills/skill_manager.py:13-14`) → `ClawHubClient.fetch_skill()`
(`neurorift/clawhub/clawhub_client.py:5-6`).

3. At `clawhub_client.py:6`, `src = source_dir / skill_name` allows `..` traversal, so
`src` can resolve outside `self.examples` (`skill_manager.py:12`) without any containment
check.

4. `shutil.copytree(src, dst)` at `clawhub_client.py:10` then copies an out-of-scope local
directory into cache before validation, proving source-directory escape.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** neurorift/clawhub/clawhub_client.py
**Line:** 6:6
**Comment:**
	*Security: `skill_name` is directly joined into the source path without containment checks, so values like `../...` can escape the intended skill directory and copy arbitrary local folders. Resolve the path and verify it still stays under `source_dir` before using it.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

if not src.exists():
raise FileNotFoundError(skill_name)
dst = self.cache_dir / skill_name
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The destination path is also built from untrusted skill_name, so traversal like ../../... can make rmtree and copytree operate outside the cache directory. Resolve and enforce that the destination remains inside cache_dir. [security]

Severity Level: Critical 🚨
- ❌ Recursive delete may target non-cache directories.
- ❌ Skill copy may overwrite paths outside cache.
Suggested change
dst = self.cache_dir / skill_name
dst = (self.cache_dir / skill_name).resolve()
if dst.parent != self.cache_dir.resolve():
raise ValueError(f"Invalid skill name: {skill_name}")
Steps of Reproduction ✅
1. Trigger ClawHub install path with traversal input, e.g. `python3 neurorift_main.py
--clawhub ../../..`; this argument is passed unchanged except `.strip()` at
`neurorift_main.py:983`.

2. The same call chain reaches `ClawHubClient.fetch_skill()`
(`neurorift/skills/skill_manager.py:14``neurorift/clawhub/clawhub_client.py:5`).

3. At `clawhub_client.py:8`, `dst` is built with untrusted `skill_name`; with `../../..`,
`dst` points outside `self.cache_dir` (created from `SkillInstaller.cache` at
`skill_manager.py:11`, `installer.py:8-9`).

4. If that external path exists, `shutil.rmtree(dst)` at `clawhub_client.py:9` attempts
recursive deletion outside cache, then `copytree` writes there at line 10.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** neurorift/clawhub/clawhub_client.py
**Line:** 8:8
**Comment:**
	*Security: The destination path is also built from untrusted `skill_name`, so traversal like `../../...` can make `rmtree` and `copytree` operate outside the cache directory. Resolve and enforce that the destination remains inside `cache_dir`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
return dst
3 changes: 3 additions & 0 deletions neurorift/clawhub/clawhub_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ClawHubResolver:
def resolve(self, skill_name: str) -> str:
return f"https://clawhub.example/skills/{skill_name}"
1 change: 1 addition & 0 deletions neurorift/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

7 changes: 7 additions & 0 deletions neurorift/cli/neurorift_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Package-level CLI helper that delegates to global entrypoint."""

from __future__ import annotations

from neurorift_cli import build_parser, install_global_wrapper, main

__all__ = ["build_parser", "install_global_wrapper", "main"]
1 change: 1 addition & 0 deletions neurorift/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions neurorift/config/channel_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CHANNELS = ["cli", "websocket", "discord", "telegram", "api"]
1 change: 1 addition & 0 deletions neurorift/config/model_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MODEL_PROVIDERS = ["deepseek", "mistral", "llama", "openai"]
7 changes: 7 additions & 0 deletions neurorift/config/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from dataclasses import dataclass
from pathlib import Path


@dataclass
class Settings:
data_dir: Path = Path.home() / ".neurorift"
1 change: 1 addition & 0 deletions neurorift/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/core/agent_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class AgentLoop:
async def handle_message(self, session, message: str) -> dict:
return {"success": True, "response": f"Session {session.session_id}: {message}"}
17 changes: 17 additions & 0 deletions neurorift/core/agent_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from dataclasses import dataclass
from neurorift.core.agent_loop import AgentLoop
from neurorift.sessions.session_manager import SessionManager


@dataclass
class AgentHandle:
session_id: str
user_id: str
channel: str


class AgentManager:
def __init__(self, session_manager: SessionManager, agent_loop: AgentLoop):
self.session_manager = session_manager
self.agent_loop = agent_loop
self.handles: dict[str, AgentHandle] = {}
16 changes: 16 additions & 0 deletions neurorift/core/planner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass


@dataclass
class PlannedStep:
order: int
action: str


class Planner:
def create_plan(self, objective: str) -> list[PlannedStep]:
return (
[PlannedStep(order=1, action=f"Investigate: {objective}")]
if objective
else []
)
3 changes: 3 additions & 0 deletions neurorift/core/task_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class TaskRouter:
def route(self, task: str) -> dict:
return {"task": task, "route": "agent_loop"}
1 change: 1 addition & 0 deletions neurorift/execution/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

12 changes: 12 additions & 0 deletions neurorift/execution/command_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import subprocess


class CommandRunner:
def run(self, cmd: list[str]):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Calling subprocess.run with an empty command list raises IndexError, so this method can crash instead of returning the structured result object. Add an explicit empty-input check and return a failure payload when no executable is provided. [logic error]

Severity Level: Major ⚠️
- ⚠️ CommandRunner contract breaks: exception replaces structured result dict.
- ⚠️ SandboxRunner inherits same crash path for empty commands.
Suggested change
def run(self, cmd: list[str]):
def run(self, cmd: list[str]):
if not cmd:
return {"success": False, "stdout": "", "stderr": "empty command", "exit_code": -1}
Steps of Reproduction ✅
1. Confirm actual execution surface: `CommandRunner` is defined at
`neurorift/execution/command_runner.py:2-5`, and `SandboxRunner` inherits it at
`neurorift/execution/sandbox_runner.py:1-3`.

2. Verify caller context: repo-wide search shows no current instantiation sites
(`CommandRunner(` has no matches), so direct module invocation is the only current
reproducible path.

3. Run `PYTHONPATH=/workspace/NeuroRift python3 -c "from
neurorift.execution.command_runner import CommandRunner; CommandRunner().run([])"`.

4. Observe crash at `neurorift/execution/command_runner.py:4` with `IndexError: list index
out of range` from `subprocess.run`, so method contract (dict result) is violated.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** neurorift/execution/command_runner.py
**Line:** 3:3
**Comment:**
	*Logic Error: Calling `subprocess.run` with an empty command list raises `IndexError`, so this method can crash instead of returning the structured result object. Add an explicit empty-input check and return a failure payload when no executable is provided.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

p = subprocess.run(cmd, capture_output=True, text=True, check=False)
return {
"success": p.returncode == 0,
"stdout": p.stdout,
"stderr": p.stderr,
"exit_code": p.returncode,
}
8 changes: 8 additions & 0 deletions neurorift/execution/resource_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass


@dataclass
class ResourceLimits:
timeout_seconds: int = 30
memory_limit_mb: int = 512
cpu_seconds: int = 20
9 changes: 9 additions & 0 deletions neurorift/execution/retry_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import time


class RetryManager:
def __init__(self, retries=2):
self.retries = retries

def wait(self, attempt):
time.sleep(2**attempt)
5 changes: 5 additions & 0 deletions neurorift/execution/sandbox_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from neurorift.execution.command_runner import CommandRunner


class SandboxRunner(CommandRunner):
pass
1 change: 1 addition & 0 deletions neurorift/gateway/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/gateway/api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class APIServer:
def start(self):
return True
3 changes: 3 additions & 0 deletions neurorift/gateway/auth_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class AuthManager:
def validate(self, token: str):
return bool(token)
3 changes: 3 additions & 0 deletions neurorift/gateway/websocket_gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class WebSocketGateway:
def start(self):
return True
1 change: 1 addition & 0 deletions neurorift/memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

6 changes: 6 additions & 0 deletions neurorift/memory/long_term_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class LongTermMemory:
def __init__(self):
self.data = {}

def store(self, user_id: str, key: str, value):
self.data.setdefault(user_id, {})[key] = value
3 changes: 3 additions & 0 deletions neurorift/memory/memory_compaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MemoryCompaction:
def compact(self, messages: list[str]):
return {"summary": " ".join(messages[:10]), "kept": messages[-10:]}
9 changes: 9 additions & 0 deletions neurorift/memory/short_term_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from collections import defaultdict, deque


class ShortTermMemory:
def __init__(self, limit: int = 30):
self.buffers = defaultdict(lambda: deque(maxlen=limit))

def add(self, sid: str, msg: str):
self.buffers[sid].append(msg)
6 changes: 6 additions & 0 deletions neurorift/memory/vector_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class VectorMemory:
def semantic_search(self, user_id: str, query: str, top_k: int = 5):
return []

def time_based_retrieval(self, user_id: str, limit: int = 10):
return []
1 change: 1 addition & 0 deletions neurorift/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/models/context_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ContextBuilder:
def build(self, session, memories):
return {"session": session.session_id, "memories": memories}
3 changes: 3 additions & 0 deletions neurorift/models/model_failover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ModelFailover:
def pick(self, providers):
return providers[0] if providers else "fallback"
8 changes: 8 additions & 0 deletions neurorift/models/model_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ModelRouter:
async def generate(self, prompt: str):
return {
"response": prompt,
"model": "openai",
"tokens": len(prompt.split()),
"cost": 0.0,
}
3 changes: 3 additions & 0 deletions neurorift/models/prompt_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class PromptBuilder:
def build(self, message: str, context: dict):
return f"{context}\n{message}"
1 change: 1 addition & 0 deletions neurorift/sessions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

29 changes: 29 additions & 0 deletions neurorift/sessions/session_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime, timezone


class SessionState(str, Enum):
CREATED = "CREATED"
ACTIVE = "ACTIVE"
IDLE = "IDLE"
PAUSED = "PAUSED"
CLOSED = "CLOSED"


@dataclass
class SessionContext:
session_id: str
user_id: str
channel: str
state: SessionState = SessionState.CREATED
message_history: list[dict] = field(default_factory=list)
tool_usage: list[dict] = field(default_factory=list)
context_window: list[str] = field(default_factory=list)
memory_references: list[str] = field(default_factory=list)
created_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
updated_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
Loading