caudate-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
cognos_mcp/config.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Load MCP server definitions from data/mcp_servers.json.
|
|
2
|
+
|
|
3
|
+
Format:
|
|
4
|
+
{
|
|
5
|
+
"servers": [
|
|
6
|
+
{
|
|
7
|
+
"name": "filesystem",
|
|
8
|
+
"command": "npx",
|
|
9
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
10
|
+
"env": {"KEY": "value"}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MCPServerConfig(BaseModel):
|
|
28
|
+
name: str
|
|
29
|
+
command: str
|
|
30
|
+
args: list[str] = Field(default_factory=list)
|
|
31
|
+
env: dict[str, str] = Field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_servers(path: Path) -> list[MCPServerConfig]:
|
|
35
|
+
"""Load MCP server configs from a JSON file, returning an empty list on miss."""
|
|
36
|
+
if not path.exists():
|
|
37
|
+
return []
|
|
38
|
+
try:
|
|
39
|
+
raw = json.loads(path.read_text())
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.warning(f"Failed to parse MCP server config {path}: {e}")
|
|
42
|
+
return []
|
|
43
|
+
servers: list[MCPServerConfig] = []
|
|
44
|
+
for entry in raw.get("servers", []):
|
|
45
|
+
try:
|
|
46
|
+
servers.append(MCPServerConfig.model_validate(entry))
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning(f"Invalid MCP server entry {entry}: {e}")
|
|
49
|
+
return servers
|
cognos_mcp/server.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Cognos MCP Server — expose all Cognos tools to external MCP clients.
|
|
2
|
+
|
|
3
|
+
Runs over stdio by default, matching the Claude Desktop / Claude Code
|
|
4
|
+
MCP conventions. Start with: `python3 -m cognos_mcp.server`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mcp.server.fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
from execution.executor import Executor
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_server() -> FastMCP:
|
|
21
|
+
"""Build a FastMCP server wrapping all Cognos tools."""
|
|
22
|
+
server = FastMCP("cognos")
|
|
23
|
+
executor = Executor()
|
|
24
|
+
|
|
25
|
+
for name in executor.list_tools():
|
|
26
|
+
tool = executor.get_tool(name)
|
|
27
|
+
if tool is None:
|
|
28
|
+
continue
|
|
29
|
+
_register_tool(server, executor, tool)
|
|
30
|
+
|
|
31
|
+
return server
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _register_tool(server: FastMCP, executor: Executor, tool) -> None:
|
|
35
|
+
"""Register a single Cognos tool with FastMCP.
|
|
36
|
+
|
|
37
|
+
FastMCP infers the MCP tool schema from the Python function signature.
|
|
38
|
+
We wrap each tool in an async shim that accepts **kwargs and delegates
|
|
39
|
+
to the executor, then we manually attach the tool's input_schema via
|
|
40
|
+
FastMCP's lower-level add_tool API.
|
|
41
|
+
"""
|
|
42
|
+
name = tool.name
|
|
43
|
+
description = tool.description
|
|
44
|
+
|
|
45
|
+
async def _run(**kwargs: Any) -> str:
|
|
46
|
+
result = await executor.execute_tool(name, kwargs)
|
|
47
|
+
if result.status.value != "success":
|
|
48
|
+
raise RuntimeError(result.error or "tool error")
|
|
49
|
+
return str(result.output)
|
|
50
|
+
|
|
51
|
+
_run.__name__ = name
|
|
52
|
+
_run.__doc__ = description
|
|
53
|
+
|
|
54
|
+
# FastMCP tool() decorator handles both registration and schema
|
|
55
|
+
server.tool(name=name, description=description)(_run)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def main() -> None:
|
|
59
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
60
|
+
server = build_server()
|
|
61
|
+
# FastMCP's default run() uses stdio
|
|
62
|
+
await server.run_stdio_async()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
asyncio.run(main())
|
config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Cognos configuration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Paths
|
|
6
|
+
BASE_DIR = Path(__file__).parent
|
|
7
|
+
DATA_DIR = BASE_DIR / "data"
|
|
8
|
+
LOGS_DIR = BASE_DIR / "logs"
|
|
9
|
+
CHROMA_DIR = DATA_DIR / "chroma"
|
|
10
|
+
SQLITE_PATH = DATA_DIR / "cognos.db"
|
|
11
|
+
|
|
12
|
+
# Ensure dirs exist
|
|
13
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
14
|
+
LOGS_DIR.mkdir(exist_ok=True)
|
|
15
|
+
|
|
16
|
+
# LLM settings
|
|
17
|
+
LLM_PROVIDER = "ollama" # "ollama", "anthropic", "openai"
|
|
18
|
+
LLM_MODEL = "ollama/gemma3:27b" # primary fallback if no `model` in ~/.cognos/settings.json
|
|
19
|
+
LLM_TEMPERATURE = 0.7
|
|
20
|
+
LLM_MAX_TOKENS = 4096
|
|
21
|
+
|
|
22
|
+
# Memory settings
|
|
23
|
+
WORKING_MEMORY_CAPACITY = 20 # max items in working memory
|
|
24
|
+
EPISODIC_RECALL_LIMIT = 5 # max episodes to recall
|
|
25
|
+
SEMANTIC_RECALL_LIMIT = 5 # max knowledge items to recall
|
|
26
|
+
|
|
27
|
+
# Planning settings
|
|
28
|
+
MAX_PLAN_DEPTH = 5 # max depth of task decomposition
|
|
29
|
+
MAX_REPLAN_ATTEMPTS = 3 # max re-planning attempts before giving up
|
|
30
|
+
# Per-task failure budget within a single plan. Mirrors the dual-brain
|
|
31
|
+
# escalation rule: when the same task fails this many times, stop
|
|
32
|
+
# retrying it and force a replan that avoids that approach. Prevents
|
|
33
|
+
# burning replan attempts on a task the planner keeps re-issuing.
|
|
34
|
+
MAX_TASK_FAILURES = 2
|
|
35
|
+
|
|
36
|
+
# Reflection settings
|
|
37
|
+
SUCCESS_THRESHOLD = 0.7 # minimum score to consider a step successful
|
|
38
|
+
STRATEGY_UPDATE_INTERVAL = 5 # update strategies every N reflections
|
|
39
|
+
|
|
40
|
+
# Permissions
|
|
41
|
+
PERMISSION_MODE = "default" # "default" | "plan" | "accept_edits" | "bypass"
|
|
42
|
+
AUDIT_LOG_PATH = DATA_DIR / "audit.jsonl"
|
|
43
|
+
|
|
44
|
+
# Hooks
|
|
45
|
+
HOOKS_CONFIG_PATH = DATA_DIR / "hooks.json"
|
|
46
|
+
|
|
47
|
+
# Sessions & compaction
|
|
48
|
+
SESSIONS_DIR = DATA_DIR / "sessions"
|
|
49
|
+
CONTEXT_WINDOW_SIZE = 32000 # approximate token budget
|
|
50
|
+
COMPACT_THRESHOLD = 0.8 # fraction of context window before compaction kicks in
|
|
51
|
+
COMPACT_KEEP_RECENT = 6 # recent messages preserved verbatim
|
|
52
|
+
|
|
53
|
+
# MCP
|
|
54
|
+
MCP_CONFIG_PATH = DATA_DIR / "mcp_servers.json"
|
|
55
|
+
|
|
56
|
+
# Personality
|
|
57
|
+
IDENTITY_PATH = DATA_DIR / "identity.json"
|
|
58
|
+
MOOD_PATH = DATA_DIR / "mood.json"
|
|
59
|
+
PERSONALITY_ENABLED = True # set False for a sterile, voice-less agent
|
|
60
|
+
|
|
61
|
+
# Plugins (Phase 5B)
|
|
62
|
+
PLUGINS_DIR = BASE_DIR / "plugins"
|
|
63
|
+
|
|
64
|
+
# Dual-process brain (Phase 4)
|
|
65
|
+
# When both are set, Cognos routes calls between System 1 (fast) and System 2
|
|
66
|
+
# (slow). Set either to empty string or None to disable routing and use the
|
|
67
|
+
# primary LLM_MODEL for everything.
|
|
68
|
+
# Kept in sync with ~/.cognos/settings.json (system1/system2 keys).
|
|
69
|
+
# settings.json wins at runtime if it sets these — these are the fallback
|
|
70
|
+
# defaults so a fresh clone with no settings file behaves the same.
|
|
71
|
+
SYSTEM_1_MODEL = "ollama/glm-5.1:cloud"
|
|
72
|
+
SYSTEM_2_MODEL = "anthropic/claude-haiku-4-5"
|
|
73
|
+
ROUTER_COMPLEXITY_THRESHOLD = 0.40
|
|
74
|
+
|
|
75
|
+
# Prompt caching hints (Anthropic provider only)
|
|
76
|
+
# When True, Cognos wraps the system prompt + tool definitions in cache_control
|
|
77
|
+
# content blocks so Anthropic caches them across turns. Ignored by Ollama/others.
|
|
78
|
+
# Safe to leave on — adds a tiny amount of overhead on non-Anthropic providers.
|
|
79
|
+
PROMPT_CACHING = True
|
|
80
|
+
|
|
81
|
+
# Files API (data/files/)
|
|
82
|
+
FILES_DIR = DATA_DIR / "files"
|
core/__init__.py
ADDED
|
File without changes
|
core/agent.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""Agent — the top-level orchestrator for the Cognos cognitive architecture.
|
|
2
|
+
|
|
3
|
+
Supports two modes:
|
|
4
|
+
- "agentic" (default): real-time tool-calling loop (Claude SDK style)
|
|
5
|
+
- "plan": DAG-based planner + executor (the original Phase 1 loop)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from core.schemas import Goal
|
|
13
|
+
from core.loop import CognitiveLoop
|
|
14
|
+
from core.agentic_loop import AgenticLoop
|
|
15
|
+
from core.citations import CitationBlock, Document
|
|
16
|
+
from core.compaction import ContextCompactor
|
|
17
|
+
from core.files import FileStore
|
|
18
|
+
from core.hooks import HookManager
|
|
19
|
+
from core.permissions import (
|
|
20
|
+
PermissionManager, PermissionMode,
|
|
21
|
+
default_cli_prompt, rules_from_settings,
|
|
22
|
+
)
|
|
23
|
+
from core.settings import Settings
|
|
24
|
+
from core.session import Session, SessionManager
|
|
25
|
+
from core.subagent import SubagentManager
|
|
26
|
+
from cognos_mcp.bridge import MCPToolBridge
|
|
27
|
+
from cognos_mcp.client import MCPClient
|
|
28
|
+
from cognos_mcp.config import load_servers
|
|
29
|
+
from llm.router import DualLLMProvider, RoutingPolicy
|
|
30
|
+
from personality import PersonalityEngine
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_preset_sync(name_or_preset: str) -> str:
|
|
34
|
+
"""Resolve a preset like 'fast'/'balanced'/'powerful' to a concrete model id.
|
|
35
|
+
|
|
36
|
+
Returns the input unchanged if it's not a known preset. Uses subprocess
|
|
37
|
+
synchronously to avoid issues when called from inside an async context.
|
|
38
|
+
"""
|
|
39
|
+
preset = name_or_preset.lower()
|
|
40
|
+
if preset not in ("fast", "balanced", "powerful"):
|
|
41
|
+
return name_or_preset
|
|
42
|
+
|
|
43
|
+
import subprocess
|
|
44
|
+
from llm.models import ModelInfo, ModelRegistry, _classify, _parse_size
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
out = subprocess.run(
|
|
48
|
+
["ollama", "list"],
|
|
49
|
+
capture_output=True, text=True, timeout=10,
|
|
50
|
+
).stdout
|
|
51
|
+
except Exception:
|
|
52
|
+
return name_or_preset
|
|
53
|
+
|
|
54
|
+
reg = ModelRegistry()
|
|
55
|
+
for line in out.splitlines()[1:]:
|
|
56
|
+
parts = line.split()
|
|
57
|
+
if len(parts) < 3:
|
|
58
|
+
continue
|
|
59
|
+
name = parts[0]
|
|
60
|
+
size_str = parts[2] + " " + parts[3] if len(parts) >= 4 else "0 MB"
|
|
61
|
+
tool_calling, json_mode, context = _classify(name)
|
|
62
|
+
reg._models[f"ollama/{name}"] = ModelInfo(
|
|
63
|
+
id=f"ollama/{name}",
|
|
64
|
+
name=name,
|
|
65
|
+
provider="ollama",
|
|
66
|
+
supports_tool_calling=tool_calling,
|
|
67
|
+
supports_json_mode=json_mode,
|
|
68
|
+
context_window=context,
|
|
69
|
+
size_bytes=_parse_size(size_str),
|
|
70
|
+
)
|
|
71
|
+
return reg.resolve(name_or_preset)
|
|
72
|
+
from execution.executor import Executor
|
|
73
|
+
from llm.provider import LLMProvider
|
|
74
|
+
from memory.working import WorkingMemory
|
|
75
|
+
from memory.episodic import EpisodicMemory
|
|
76
|
+
from memory.semantic import SemanticMemory
|
|
77
|
+
|
|
78
|
+
logger = logging.getLogger(__name__)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CognosAgent:
|
|
82
|
+
"""Top-level agent that manages goals and runs the cognitive loop."""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
model: str | None = None,
|
|
87
|
+
mode: str = "agentic",
|
|
88
|
+
thinking: bool = False,
|
|
89
|
+
on_thinking=None,
|
|
90
|
+
permission_mode: str | None = None,
|
|
91
|
+
prompt_fn=None,
|
|
92
|
+
session_id: str | None = None,
|
|
93
|
+
personality: bool = True,
|
|
94
|
+
verbose_personality: bool = False,
|
|
95
|
+
on_inner_voice=None,
|
|
96
|
+
system1: str | None = None,
|
|
97
|
+
system2: str | None = None,
|
|
98
|
+
):
|
|
99
|
+
from config import (
|
|
100
|
+
LLM_MODEL, PERMISSION_MODE, AUDIT_LOG_PATH, HOOKS_CONFIG_PATH,
|
|
101
|
+
SESSIONS_DIR, CONTEXT_WINDOW_SIZE, COMPACT_THRESHOLD, COMPACT_KEEP_RECENT,
|
|
102
|
+
DATA_DIR, SYSTEM_1_MODEL, SYSTEM_2_MODEL, ROUTER_COMPLEXITY_THRESHOLD,
|
|
103
|
+
FILES_DIR,
|
|
104
|
+
)
|
|
105
|
+
# Layered config — settings file < explicit kwargs.
|
|
106
|
+
settings = Settings.load()
|
|
107
|
+
raw_model = model or settings.get("model") or LLM_MODEL
|
|
108
|
+
resolved = _resolve_preset_sync(raw_model)
|
|
109
|
+
primary_llm = LLMProvider(model=resolved)
|
|
110
|
+
|
|
111
|
+
# Optional fallback chain — wraps the primary so any retryable
|
|
112
|
+
# failure transparently falls through to the next model.
|
|
113
|
+
fallback_models = settings.get("fallback_models") or []
|
|
114
|
+
if fallback_models:
|
|
115
|
+
from llm.fallback import build_fallback
|
|
116
|
+
primary_llm = build_fallback(primary_llm, list(fallback_models))
|
|
117
|
+
|
|
118
|
+
# Dual-process brain — if System 1 and System 2 are both configured,
|
|
119
|
+
# wrap them in a DualLLMProvider that routes per call. Settings file
|
|
120
|
+
# values participate in the same priority chain as `model`.
|
|
121
|
+
s1 = (system1 if system1 is not None
|
|
122
|
+
else settings.get("system1") or SYSTEM_1_MODEL)
|
|
123
|
+
s2 = (system2 if system2 is not None
|
|
124
|
+
else settings.get("system2") or SYSTEM_2_MODEL)
|
|
125
|
+
if s1 and s2:
|
|
126
|
+
self.llm_fast = LLMProvider(model=_resolve_preset_sync(s1))
|
|
127
|
+
self.llm_slow = LLMProvider(model=_resolve_preset_sync(s2))
|
|
128
|
+
policy = RoutingPolicy(complexity_threshold=ROUTER_COMPLEXITY_THRESHOLD)
|
|
129
|
+
self.llm = DualLLMProvider(
|
|
130
|
+
fast=self.llm_fast, slow=self.llm_slow, policy=policy,
|
|
131
|
+
)
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Dual-brain routing enabled: fast={self.llm_fast.model}, "
|
|
134
|
+
f"slow={self.llm_slow.model}"
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
self.llm_fast = primary_llm
|
|
138
|
+
self.llm_slow = primary_llm
|
|
139
|
+
self.llm = primary_llm
|
|
140
|
+
|
|
141
|
+
self.mode = mode
|
|
142
|
+
self._goal_history: list[Goal] = []
|
|
143
|
+
|
|
144
|
+
# Plan mode uses the full Phase 1 loop. Its planner/reflector/
|
|
145
|
+
# meta-learner always want slow-tier — pass the router, which will
|
|
146
|
+
# route them via their caller tags. (If no router, llm_slow IS llm.)
|
|
147
|
+
self.loop = CognitiveLoop(self.llm)
|
|
148
|
+
|
|
149
|
+
# Build permission manager — allow/deny rules come from settings.
|
|
150
|
+
pm_value = permission_mode or settings.get("permission_mode") or PERMISSION_MODE
|
|
151
|
+
perms_cfg = settings.get("permissions") or {}
|
|
152
|
+
permissions = PermissionManager(
|
|
153
|
+
mode=PermissionMode(pm_value),
|
|
154
|
+
allow=rules_from_settings(perms_cfg.get("allow")),
|
|
155
|
+
deny=rules_from_settings(perms_cfg.get("deny")),
|
|
156
|
+
audit_log=AUDIT_LOG_PATH,
|
|
157
|
+
prompt=prompt_fn or default_cli_prompt,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Build hook manager and load any configured hooks
|
|
161
|
+
hooks = HookManager()
|
|
162
|
+
loaded = hooks.load_from_file(HOOKS_CONFIG_PATH)
|
|
163
|
+
if loaded:
|
|
164
|
+
logger.info(f"Loaded {loaded} hooks from {HOOKS_CONFIG_PATH}")
|
|
165
|
+
|
|
166
|
+
# Context compaction
|
|
167
|
+
compactor = ContextCompactor(
|
|
168
|
+
llm=self.llm,
|
|
169
|
+
context_window=CONTEXT_WINDOW_SIZE,
|
|
170
|
+
compact_threshold=COMPACT_THRESHOLD,
|
|
171
|
+
keep_recent=COMPACT_KEEP_RECENT,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Session management — load existing session or prepare a new one
|
|
175
|
+
self.sessions = SessionManager(SESSIONS_DIR)
|
|
176
|
+
self.session: Session
|
|
177
|
+
if session_id:
|
|
178
|
+
loaded_session = self.sessions.load(session_id)
|
|
179
|
+
if loaded_session is None:
|
|
180
|
+
logger.warning(f"Session {session_id} not found — starting new")
|
|
181
|
+
self.session = Session(id=session_id, model=self.llm.model)
|
|
182
|
+
else:
|
|
183
|
+
self.session = loaded_session
|
|
184
|
+
else:
|
|
185
|
+
self.session = Session(model=self.llm.model)
|
|
186
|
+
|
|
187
|
+
# Agentic mode shares the same executor + memory but drives them
|
|
188
|
+
# via a real-time tool-calling loop
|
|
189
|
+
# Personality — identity + mood + inner voice
|
|
190
|
+
self.personality: PersonalityEngine | None = None
|
|
191
|
+
if personality:
|
|
192
|
+
self.personality = PersonalityEngine.load(
|
|
193
|
+
data_dir=DATA_DIR,
|
|
194
|
+
verbose_thinking=verbose_personality,
|
|
195
|
+
on_thinking_display=on_inner_voice,
|
|
196
|
+
)
|
|
197
|
+
self.personality.register_with(hooks)
|
|
198
|
+
# Give the router access to mood so frustration escalates to System 2
|
|
199
|
+
if isinstance(self.llm, DualLLMProvider):
|
|
200
|
+
self.llm.set_mood(self.personality.mood)
|
|
201
|
+
|
|
202
|
+
system_prompt_provider = (
|
|
203
|
+
self.personality.wrap_system_prompt if self.personality else None
|
|
204
|
+
)
|
|
205
|
+
thinking_instruction_wrapper = (
|
|
206
|
+
self.personality.augment_thinking_instruction if self.personality else None
|
|
207
|
+
)
|
|
208
|
+
on_thinking_combined = on_thinking
|
|
209
|
+
if self.personality is not None:
|
|
210
|
+
inner_on_thinking = self.personality.inner_voice.on_thinking
|
|
211
|
+
if on_thinking is None:
|
|
212
|
+
on_thinking_combined = inner_on_thinking
|
|
213
|
+
else:
|
|
214
|
+
# Chain both callbacks
|
|
215
|
+
def _combined(blocks, _ot=on_thinking, _iv=inner_on_thinking):
|
|
216
|
+
_iv(blocks)
|
|
217
|
+
_ot(blocks)
|
|
218
|
+
on_thinking_combined = _combined
|
|
219
|
+
|
|
220
|
+
# File store (attachments / Files-API equivalent)
|
|
221
|
+
self.files = FileStore(root=FILES_DIR)
|
|
222
|
+
|
|
223
|
+
self.agentic = AgenticLoop(
|
|
224
|
+
llm=self.llm,
|
|
225
|
+
executor=self.loop.executor,
|
|
226
|
+
episodic=self.loop.episodic,
|
|
227
|
+
semantic=self.loop.semantic,
|
|
228
|
+
working=self.loop.working,
|
|
229
|
+
thinking=thinking,
|
|
230
|
+
on_thinking=on_thinking_combined,
|
|
231
|
+
permissions=permissions,
|
|
232
|
+
hooks=hooks,
|
|
233
|
+
compactor=compactor,
|
|
234
|
+
session_id=self.session.id,
|
|
235
|
+
system_prompt_provider=system_prompt_provider,
|
|
236
|
+
thinking_instruction_wrapper=thinking_instruction_wrapper,
|
|
237
|
+
file_store=self.files,
|
|
238
|
+
)
|
|
239
|
+
self.permissions = permissions
|
|
240
|
+
self.hooks = hooks
|
|
241
|
+
self.compactor = compactor
|
|
242
|
+
|
|
243
|
+
# Subagent support — wire the AgentTool's manager reference
|
|
244
|
+
self.subagents = SubagentManager(
|
|
245
|
+
llm=self.llm,
|
|
246
|
+
parent_executor=self.loop.executor,
|
|
247
|
+
episodic=self.loop.episodic,
|
|
248
|
+
semantic=self.loop.semantic,
|
|
249
|
+
permissions=permissions,
|
|
250
|
+
hooks=hooks,
|
|
251
|
+
)
|
|
252
|
+
agent_tool = self.loop.executor.get_tool("Agent")
|
|
253
|
+
if agent_tool is not None and hasattr(agent_tool, "set_manager"):
|
|
254
|
+
agent_tool.set_manager(self.subagents)
|
|
255
|
+
|
|
256
|
+
# Wire the Draw tool to the FileStore so generated images get
|
|
257
|
+
# persisted and referenceable. Settings can override the
|
|
258
|
+
# backend (default: diffusers/SDXL-Turbo).
|
|
259
|
+
draw_tool = self.loop.executor.get_tool("Draw")
|
|
260
|
+
if draw_tool is not None and hasattr(draw_tool, "set_file_store"):
|
|
261
|
+
draw_tool.set_file_store(self.files)
|
|
262
|
+
img_cfg = settings.get("image") or {}
|
|
263
|
+
if img_cfg.get("backend"):
|
|
264
|
+
draw_tool._backend_name = img_cfg["backend"]
|
|
265
|
+
if img_cfg.get("backend_kwargs"):
|
|
266
|
+
draw_tool._backend_kwargs = dict(img_cfg["backend_kwargs"])
|
|
267
|
+
|
|
268
|
+
# Wire the Artifact tool to the same FileStore so artifact panes
|
|
269
|
+
# (HTML, SVG, markdown, code) get persisted under /files/ for the
|
|
270
|
+
# chat UI to fetch.
|
|
271
|
+
artifact_tool = self.loop.executor.get_tool("Artifact")
|
|
272
|
+
if artifact_tool is not None and hasattr(artifact_tool, "set_file_store"):
|
|
273
|
+
artifact_tool.set_file_store(self.files)
|
|
274
|
+
|
|
275
|
+
# Wire the Speak tool too — synthesized audio is persisted as
|
|
276
|
+
# WAV under /files/ and surfaced to the chat UI as a markdown
|
|
277
|
+
# audio link the LLM echoes verbatim.
|
|
278
|
+
speak_tool = self.loop.executor.get_tool("Speak")
|
|
279
|
+
if speak_tool is not None and hasattr(speak_tool, "set_file_store"):
|
|
280
|
+
speak_tool.set_file_store(self.files)
|
|
281
|
+
|
|
282
|
+
# Wire EditImage to the FileStore so it can resolve `files/<id>`
|
|
283
|
+
# references from prior Draw / DescribeImage tool outputs and
|
|
284
|
+
# save edited images back to the same store.
|
|
285
|
+
edit_image_tool = self.loop.executor.get_tool("EditImage")
|
|
286
|
+
if edit_image_tool is not None and hasattr(edit_image_tool, "set_file_store"):
|
|
287
|
+
edit_image_tool.set_file_store(self.files)
|
|
288
|
+
|
|
289
|
+
# Wire LongCat Avatar — needs the FileStore to resolve input
|
|
290
|
+
# ref_image / audio FileStore ids AND to persist the output video.
|
|
291
|
+
# Also wire the Speak tool so the agent can pass `text` instead
|
|
292
|
+
# of an `audio` clip — LongCatAvatar will synthesize via Kokoro
|
|
293
|
+
# TTS internally and feed the result into the WAN-S2V graph.
|
|
294
|
+
longcat_avatar_tool = self.loop.executor.get_tool("LongCatAvatar")
|
|
295
|
+
if longcat_avatar_tool is not None and hasattr(longcat_avatar_tool, "set_file_store"):
|
|
296
|
+
longcat_avatar_tool.set_file_store(self.files)
|
|
297
|
+
speak_for_avatar = self.loop.executor.get_tool("Speak")
|
|
298
|
+
if speak_for_avatar is not None and hasattr(longcat_avatar_tool, "set_speak_tool"):
|
|
299
|
+
longcat_avatar_tool.set_speak_tool(speak_for_avatar)
|
|
300
|
+
|
|
301
|
+
# Wire Storyboard — it needs the FileStore to persist panels
|
|
302
|
+
# AND the live LLM (for beat breakdown). It also reuses the
|
|
303
|
+
# already-instantiated image backends from Draw / EditImage so
|
|
304
|
+
# we don't pay the FLUX-schnell + Kontext load cost twice.
|
|
305
|
+
storyboard_tool = self.loop.executor.get_tool("Storyboard")
|
|
306
|
+
if storyboard_tool is not None:
|
|
307
|
+
storyboard_tool.set_file_store(self.files)
|
|
308
|
+
try:
|
|
309
|
+
if draw_tool is not None and hasattr(draw_tool, "_get_backend"):
|
|
310
|
+
storyboard_tool.set_image_gen(draw_tool._get_backend())
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.warning(f"Storyboard: could not share Draw backend: {e}")
|
|
313
|
+
try:
|
|
314
|
+
if edit_image_tool is not None and hasattr(edit_image_tool, "_get_backend"):
|
|
315
|
+
storyboard_tool.set_image_edit(edit_image_tool._get_backend())
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.warning(f"Storyboard: could not share EditImage backend: {e}")
|
|
318
|
+
|
|
319
|
+
# MCP clients (optional) — lazily connected in `start()`
|
|
320
|
+
self._mcp_clients: list[MCPClient] = []
|
|
321
|
+
|
|
322
|
+
# Wire TaskCreate + MCPListServers to this live agent so they
|
|
323
|
+
# can spawn sub-prompts and inspect the MCP roster respectively.
|
|
324
|
+
for late_wire_name in ("TaskCreate", "MCPListServers"):
|
|
325
|
+
t = self.loop.executor.get_tool(late_wire_name)
|
|
326
|
+
if t is not None and hasattr(t, "set_agent"):
|
|
327
|
+
t.set_agent(self)
|
|
328
|
+
|
|
329
|
+
# Caudate — the action-selection neural net. Loaded if a checkpoint
|
|
330
|
+
# exists; the agent works fine if not. The observer also captures
|
|
331
|
+
# live (state, action, reward) into a replay buffer and triggers
|
|
332
|
+
# auto-training in the background.
|
|
333
|
+
self.caudate = None
|
|
334
|
+
try:
|
|
335
|
+
from nn.observer import CaudateObserver
|
|
336
|
+
self.caudate = CaudateObserver()
|
|
337
|
+
self.agentic.caudate = self.caudate
|
|
338
|
+
# Hand Caudate to the router so she can override tier
|
|
339
|
+
# selection once she earns the ADVISOR trust level.
|
|
340
|
+
if isinstance(self.llm, DualLLMProvider):
|
|
341
|
+
try:
|
|
342
|
+
self.llm.router.set_caudate(self.caudate)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
if self.caudate.advisor is not None:
|
|
346
|
+
level = self.caudate.policy.level.label
|
|
347
|
+
logger.info(f"Caudate loaded — trust level: {level}")
|
|
348
|
+
else:
|
|
349
|
+
logger.info(
|
|
350
|
+
"Caudate has no checkpoint yet — observing only. "
|
|
351
|
+
f"After {self.caudate.cfg.min_episodes_to_train} "
|
|
352
|
+
"samples she'll auto-train and start predicting."
|
|
353
|
+
)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.warning(f"Caudate init failed (continuing without): {e}")
|
|
356
|
+
|
|
357
|
+
# Restore any prior messages from the loaded session
|
|
358
|
+
if self.session.messages:
|
|
359
|
+
self.agentic.load_messages(self.session.messages)
|
|
360
|
+
|
|
361
|
+
async def pursue_goal(
|
|
362
|
+
self,
|
|
363
|
+
description: str,
|
|
364
|
+
success_criteria: list[str] | None = None,
|
|
365
|
+
verbose: bool = True,
|
|
366
|
+
) -> Goal:
|
|
367
|
+
"""Create and pursue a goal through the cognitive loop.
|
|
368
|
+
|
|
369
|
+
Always uses plan mode (DAG-based) regardless of agent mode — use
|
|
370
|
+
`chat()` for the agentic conversational loop.
|
|
371
|
+
"""
|
|
372
|
+
goal = Goal(
|
|
373
|
+
description=description,
|
|
374
|
+
success_criteria=success_criteria or [],
|
|
375
|
+
)
|
|
376
|
+
self._goal_history.append(goal)
|
|
377
|
+
|
|
378
|
+
result = await self.loop.run(goal, verbose=verbose)
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
async def start(self) -> None:
|
|
382
|
+
"""Connect to configured MCP servers and register their tools.
|
|
383
|
+
|
|
384
|
+
Safe to call multiple times — a no-op after the first successful
|
|
385
|
+
connection for each server.
|
|
386
|
+
"""
|
|
387
|
+
from config import MCP_CONFIG_PATH
|
|
388
|
+
if self._mcp_clients:
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
for cfg in load_servers(MCP_CONFIG_PATH):
|
|
392
|
+
client = MCPClient(
|
|
393
|
+
name=cfg.name,
|
|
394
|
+
command=cfg.command,
|
|
395
|
+
args=cfg.args,
|
|
396
|
+
env=cfg.env,
|
|
397
|
+
)
|
|
398
|
+
try:
|
|
399
|
+
await client.connect()
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.warning(f"MCP server {cfg.name!r} connect failed: {e}")
|
|
402
|
+
continue
|
|
403
|
+
self._mcp_clients.append(client)
|
|
404
|
+
for remote in client.tools:
|
|
405
|
+
bridged = MCPToolBridge(client, remote)
|
|
406
|
+
self.loop.executor.register_tool(bridged)
|
|
407
|
+
logger.info(f"Registered MCP tool: {bridged.name}")
|
|
408
|
+
|
|
409
|
+
async def stop(self) -> None:
|
|
410
|
+
"""Disconnect any open MCP clients."""
|
|
411
|
+
for c in self._mcp_clients:
|
|
412
|
+
try:
|
|
413
|
+
await c.close()
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.debug(f"MCP close failed for {c.name}: {e}")
|
|
416
|
+
self._mcp_clients = []
|
|
417
|
+
|
|
418
|
+
async def chat(
|
|
419
|
+
self,
|
|
420
|
+
message: str,
|
|
421
|
+
attachments: list[str] | None = None,
|
|
422
|
+
documents: list[Document] | None = None,
|
|
423
|
+
) -> str:
|
|
424
|
+
"""Multi-turn conversational entry point (agentic loop).
|
|
425
|
+
|
|
426
|
+
`attachments` is a list of file_ids previously registered with
|
|
427
|
+
`self.files.upload()`. `documents` is an optional list of Document
|
|
428
|
+
objects the model may cite; structured citation blocks are
|
|
429
|
+
available on `self.agentic.last_citations` after each call.
|
|
430
|
+
|
|
431
|
+
Persists the conversation to the current session after each turn.
|
|
432
|
+
"""
|
|
433
|
+
await self.start()
|
|
434
|
+
reply = await self.agentic.run(
|
|
435
|
+
message, attachments=attachments, documents=documents,
|
|
436
|
+
)
|
|
437
|
+
self.session.messages = list(self.agentic.messages)
|
|
438
|
+
self.session.model = self.llm.model
|
|
439
|
+
if not self.session.title and message:
|
|
440
|
+
self.session.title = message[:80]
|
|
441
|
+
self.sessions.save(self.session)
|
|
442
|
+
return reply
|
|
443
|
+
|
|
444
|
+
def reset_conversation(self) -> None:
|
|
445
|
+
"""Clear the agentic loop's conversation history (starts a new session)."""
|
|
446
|
+
self.agentic.reset()
|
|
447
|
+
self.session = Session(model=self.llm.model)
|
|
448
|
+
self.agentic.session_id = self.session.id
|
|
449
|
+
|
|
450
|
+
def register_tool(self, tool) -> None:
|
|
451
|
+
"""Register a custom tool with the executor."""
|
|
452
|
+
self.loop.executor.register_tool(tool)
|
|
453
|
+
|
|
454
|
+
def switch_model(self, model: str) -> None:
|
|
455
|
+
"""Switch the underlying LLM model."""
|
|
456
|
+
self.llm.switch_model(model)
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def goal_history(self) -> list[Goal]:
|
|
460
|
+
return self._goal_history
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def strategies(self):
|
|
464
|
+
return self.loop.procedural.get_strategies()
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def performance_history(self):
|
|
468
|
+
return self.loop.procedural.get_performance_history()
|