superpos-agent-core 0.1.0__tar.gz
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.
- superpos_agent_core-0.1.0/PKG-INFO +59 -0
- superpos_agent_core-0.1.0/README.md +44 -0
- superpos_agent_core-0.1.0/pyproject.toml +44 -0
- superpos_agent_core-0.1.0/setup.cfg +4 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/__init__.py +67 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/config.py +157 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/executor.py +174 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/main.py +315 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/module_loader.py +148 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/module_setup.py +202 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-issues/SKILL.md +174 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-issues/module.yaml +7 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-issues/scripts/superpos-issues +269 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-knowledge/SKILL.md +167 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-knowledge/module.yaml +10 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-knowledge/scripts/superpos-knowledge +315 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-workflows/SKILL.md +401 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-workflows/module.yaml +7 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/modules/superpos-workflows/scripts/superpos-workflows +333 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/progress_reporter.py +165 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/py.typed +0 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/recent_tasks.py +64 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/redactor.py +44 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/runtime_config.py +84 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/session_store.py +186 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/sub_agent_sync.py +455 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/superpos_client.py +1450 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/superpos_poller.py +483 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/superpos_task.py +276 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/telegram_bot.py +422 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/telegram_gateway.py +316 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/telegram_streamer.py +432 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core/worktree_manager.py +159 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core.egg-info/PKG-INFO +59 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core.egg-info/SOURCES.txt +49 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core.egg-info/dependency_links.txt +1 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core.egg-info/requires.txt +7 -0
- superpos_agent_core-0.1.0/src/superpos_agent_core.egg-info/top_level.txt +1 -0
- superpos_agent_core-0.1.0/tests/test_executor_cancel.py +217 -0
- superpos_agent_core-0.1.0/tests/test_knowledge_module_script.py +261 -0
- superpos_agent_core-0.1.0/tests/test_module_loader_bundled.py +297 -0
- superpos_agent_core-0.1.0/tests/test_progress_reporter.py +369 -0
- superpos_agent_core-0.1.0/tests/test_session_store.py +217 -0
- superpos_agent_core-0.1.0/tests/test_sub_agent_sync.py +443 -0
- superpos_agent_core-0.1.0/tests/test_superpos_client_issues.py +388 -0
- superpos_agent_core-0.1.0/tests/test_superpos_client_knowledge.py +336 -0
- superpos_agent_core-0.1.0/tests/test_superpos_client_surface_fill.py +737 -0
- superpos_agent_core-0.1.0/tests/test_superpos_client_workflows.py +428 -0
- superpos_agent_core-0.1.0/tests/test_superpos_poller_knowledge.py +151 -0
- superpos_agent_core-0.1.0/tests/test_superpos_poller_resync.py +158 -0
- superpos_agent_core-0.1.0/tests/test_telegram_streamer.py +114 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: superpos-agent-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared runtime for Superpos slim agents (Telegram bot, Superpos poller, worktree manager, telegram streamer).
|
|
5
|
+
Author: Superpos
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: python-telegram-bot>=21.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# superpos-agent-core
|
|
17
|
+
|
|
18
|
+
Shared runtime for the Superpos "slim agent" family — the part that is identical across Claude, Codex, Gemini, Qwen, and any future LLM-CLI-backed agent.
|
|
19
|
+
|
|
20
|
+
## What lives here
|
|
21
|
+
|
|
22
|
+
- `Executor` protocol + `ExecutionRequest` — the contract every agent implementation must satisfy.
|
|
23
|
+
- `SuperposClient` — REST client for the Superpos backend (tasks, persona, agent lifecycle).
|
|
24
|
+
- `run_superpos_poller` — daemon that polls Superpos for tasks and enqueues them on an `Executor`.
|
|
25
|
+
- `TelegramGateway` + `TelegramStreamer` + `build_telegram_app` / `run_telegram_bot` — Telegram I/O with backpressure, flood-ban handling, message streaming.
|
|
26
|
+
- `WorktreeManager` helpers — per-branch isolation via `git worktree`.
|
|
27
|
+
- `SessionStore`, `RecentTasksLog`, `RuntimeConfig` — persistence and runtime knobs.
|
|
28
|
+
- `redact()` — strip known secret patterns from outbound text.
|
|
29
|
+
- `run_agent()` — orchestrator that wires everything together given a concrete `Executor` factory.
|
|
30
|
+
|
|
31
|
+
## What does NOT live here
|
|
32
|
+
|
|
33
|
+
- Per-agent executor implementations (`ClaudeExecutor`, `CodexExecutor`, `GeminiExecutor`, …) — they live in each agent's own repo and depend on this package.
|
|
34
|
+
- LLM SDK / CLI installation — each agent's Dockerfile is responsible.
|
|
35
|
+
- Per-agent config (model lists, auth env vars) — subclassed from `BaseConfig`.
|
|
36
|
+
|
|
37
|
+
## Using it
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from superpos_agent_core import run_agent, BaseConfig, Executor
|
|
41
|
+
|
|
42
|
+
class MyAgentConfig(BaseConfig):
|
|
43
|
+
my_api_key: str = ""
|
|
44
|
+
|
|
45
|
+
class MyExecutor:
|
|
46
|
+
# implements superpos_agent_core.Executor
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
import asyncio
|
|
51
|
+
config = MyAgentConfig.from_env(extra={"my_api_key": "MY_API_KEY"})
|
|
52
|
+
asyncio.run(run_agent(
|
|
53
|
+
config=config,
|
|
54
|
+
executor_factory=lambda cfg, runtime, superpos, gateway, persona:
|
|
55
|
+
MyExecutor(cfg, runtime, superpos, gateway, persona=persona),
|
|
56
|
+
))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See `Slim-Agent-Gemini/` for a complete working example.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# superpos-agent-core
|
|
2
|
+
|
|
3
|
+
Shared runtime for the Superpos "slim agent" family — the part that is identical across Claude, Codex, Gemini, Qwen, and any future LLM-CLI-backed agent.
|
|
4
|
+
|
|
5
|
+
## What lives here
|
|
6
|
+
|
|
7
|
+
- `Executor` protocol + `ExecutionRequest` — the contract every agent implementation must satisfy.
|
|
8
|
+
- `SuperposClient` — REST client for the Superpos backend (tasks, persona, agent lifecycle).
|
|
9
|
+
- `run_superpos_poller` — daemon that polls Superpos for tasks and enqueues them on an `Executor`.
|
|
10
|
+
- `TelegramGateway` + `TelegramStreamer` + `build_telegram_app` / `run_telegram_bot` — Telegram I/O with backpressure, flood-ban handling, message streaming.
|
|
11
|
+
- `WorktreeManager` helpers — per-branch isolation via `git worktree`.
|
|
12
|
+
- `SessionStore`, `RecentTasksLog`, `RuntimeConfig` — persistence and runtime knobs.
|
|
13
|
+
- `redact()` — strip known secret patterns from outbound text.
|
|
14
|
+
- `run_agent()` — orchestrator that wires everything together given a concrete `Executor` factory.
|
|
15
|
+
|
|
16
|
+
## What does NOT live here
|
|
17
|
+
|
|
18
|
+
- Per-agent executor implementations (`ClaudeExecutor`, `CodexExecutor`, `GeminiExecutor`, …) — they live in each agent's own repo and depend on this package.
|
|
19
|
+
- LLM SDK / CLI installation — each agent's Dockerfile is responsible.
|
|
20
|
+
- Per-agent config (model lists, auth env vars) — subclassed from `BaseConfig`.
|
|
21
|
+
|
|
22
|
+
## Using it
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from superpos_agent_core import run_agent, BaseConfig, Executor
|
|
26
|
+
|
|
27
|
+
class MyAgentConfig(BaseConfig):
|
|
28
|
+
my_api_key: str = ""
|
|
29
|
+
|
|
30
|
+
class MyExecutor:
|
|
31
|
+
# implements superpos_agent_core.Executor
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
import asyncio
|
|
36
|
+
config = MyAgentConfig.from_env(extra={"my_api_key": "MY_API_KEY"})
|
|
37
|
+
asyncio.run(run_agent(
|
|
38
|
+
config=config,
|
|
39
|
+
executor_factory=lambda cfg, runtime, superpos, gateway, persona:
|
|
40
|
+
MyExecutor(cfg, runtime, superpos, gateway, persona=persona),
|
|
41
|
+
))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See `Slim-Agent-Gemini/` for a complete working example.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "superpos-agent-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared runtime for Superpos slim agents (Telegram bot, Superpos poller, worktree manager, telegram streamer)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Superpos" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"python-telegram-bot>=21.0",
|
|
15
|
+
"httpx>=0.27.0",
|
|
16
|
+
"pyyaml>=6.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
"pytest-asyncio>=0.23",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
superpos_agent_core = [
|
|
30
|
+
"py.typed",
|
|
31
|
+
"modules/**/*.yaml",
|
|
32
|
+
"modules/**/*.md",
|
|
33
|
+
"modules/**/scripts/*",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 120
|
|
38
|
+
target-version = "py310"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint.per-file-ignores]
|
|
44
|
+
"tests/*" = ["F401"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Slim-agent core runtime: shared code for every Superpos LLM-backed agent."""
|
|
2
|
+
|
|
3
|
+
from .config import BaseConfig
|
|
4
|
+
from .executor import Executor, ExecutionRequest
|
|
5
|
+
from .main import ExecutorFactory, run_agent, setup_logging
|
|
6
|
+
from .module_loader import (
|
|
7
|
+
bundled_modules_dir,
|
|
8
|
+
collect_mcp_servers,
|
|
9
|
+
discover_modules,
|
|
10
|
+
generate_modules_doc,
|
|
11
|
+
)
|
|
12
|
+
from .module_setup import run_setup as run_module_setup
|
|
13
|
+
from .module_setup import symlink_module_scripts
|
|
14
|
+
from .sub_agent_sync import sync_sub_agents
|
|
15
|
+
from .progress_reporter import report_progress
|
|
16
|
+
from .recent_tasks import RecentTasksLog, TaskSummary
|
|
17
|
+
from .redactor import redact
|
|
18
|
+
from .runtime_config import RuntimeConfig
|
|
19
|
+
from .session_store import SessionStore
|
|
20
|
+
from .superpos_client import SuperposClient
|
|
21
|
+
from .superpos_poller import run_superpos_poller
|
|
22
|
+
from .telegram_bot import build_telegram_app, run_telegram_bot
|
|
23
|
+
from .telegram_gateway import Priority, TelegramGateway
|
|
24
|
+
from .telegram_streamer import TelegramStreamer
|
|
25
|
+
from .worktree_manager import (
|
|
26
|
+
ensure_worktree,
|
|
27
|
+
infer_branch,
|
|
28
|
+
is_git_repo,
|
|
29
|
+
prune_worktrees,
|
|
30
|
+
slot_key,
|
|
31
|
+
worktree_path,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BaseConfig",
|
|
36
|
+
"Executor",
|
|
37
|
+
"ExecutionRequest",
|
|
38
|
+
"ExecutorFactory",
|
|
39
|
+
"Priority",
|
|
40
|
+
"RecentTasksLog",
|
|
41
|
+
"RuntimeConfig",
|
|
42
|
+
"SessionStore",
|
|
43
|
+
"SuperposClient",
|
|
44
|
+
"TaskSummary",
|
|
45
|
+
"TelegramGateway",
|
|
46
|
+
"TelegramStreamer",
|
|
47
|
+
"build_telegram_app",
|
|
48
|
+
"bundled_modules_dir",
|
|
49
|
+
"collect_mcp_servers",
|
|
50
|
+
"discover_modules",
|
|
51
|
+
"ensure_worktree",
|
|
52
|
+
"generate_modules_doc",
|
|
53
|
+
"infer_branch",
|
|
54
|
+
"is_git_repo",
|
|
55
|
+
"prune_worktrees",
|
|
56
|
+
"redact",
|
|
57
|
+
"report_progress",
|
|
58
|
+
"run_agent",
|
|
59
|
+
"run_module_setup",
|
|
60
|
+
"run_superpos_poller",
|
|
61
|
+
"run_telegram_bot",
|
|
62
|
+
"setup_logging",
|
|
63
|
+
"slot_key",
|
|
64
|
+
"sync_sub_agents",
|
|
65
|
+
"symlink_module_scripts",
|
|
66
|
+
"worktree_path",
|
|
67
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Base configuration shared across every slim agent.
|
|
2
|
+
|
|
3
|
+
Each per-agent package defines its own subclass adding LLM-specific fields
|
|
4
|
+
(model id, API key env var name, reasoning effort, …). Superpos and Telegram
|
|
5
|
+
fields are universal and live here.
|
|
6
|
+
|
|
7
|
+
Fields are seeded from env at startup and may be mutated at runtime when the
|
|
8
|
+
Superpos ``/agents/me`` endpoint returns server-authoritative values for
|
|
9
|
+
``hive_id``, ``capabilities``, ``permissions``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class BaseConfig:
|
|
20
|
+
"""Universal slim-agent config. Subclass to add per-agent fields."""
|
|
21
|
+
|
|
22
|
+
# ── Superpos ──────────────────────────────────────────────────────
|
|
23
|
+
superpos_base_url: str = ""
|
|
24
|
+
superpos_hive_id: str = ""
|
|
25
|
+
superpos_agent_id: str = ""
|
|
26
|
+
superpos_api_token: str = ""
|
|
27
|
+
superpos_refresh_token: str = ""
|
|
28
|
+
superpos_capabilities: list[str] = field(default_factory=list)
|
|
29
|
+
superpos_permissions: list[str] = field(default_factory=list)
|
|
30
|
+
superpos_poll_interval: int = 5
|
|
31
|
+
|
|
32
|
+
# Knowledge retrieval injection: before dispatching a Superpos task, search
|
|
33
|
+
# the hive knowledge store for entries relevant to the task and prepend them
|
|
34
|
+
# to the prompt so the agent always starts with the hive's memory in context
|
|
35
|
+
# (instead of only seeing it if it happens to call the knowledge CLI).
|
|
36
|
+
superpos_knowledge_inject: bool = True
|
|
37
|
+
superpos_knowledge_inject_limit: int = 5
|
|
38
|
+
|
|
39
|
+
# ── Telegram ──────────────────────────────────────────────────────
|
|
40
|
+
telegram_bot_token: str = ""
|
|
41
|
+
telegram_allowed_users: list[int] = field(default_factory=list)
|
|
42
|
+
telegram_chat_id: str = ""
|
|
43
|
+
|
|
44
|
+
# ── Executor (LLM-agnostic) ───────────────────────────────────────
|
|
45
|
+
executor_kind: str = "generic" # subclasses set this: "claude", "codex", "gemini", …
|
|
46
|
+
executor_working_dir: str = "/workspace"
|
|
47
|
+
executor_worktree_isolation: bool = False
|
|
48
|
+
executor_max_parallel: int = 3
|
|
49
|
+
executor_max_turns: int = 30
|
|
50
|
+
|
|
51
|
+
# Filesystem layout — where this agent stores per-LLM session/config state.
|
|
52
|
+
# Defaults to /home/agent/.<executor_kind> at runtime in __post_init__.
|
|
53
|
+
home_dir: str = ""
|
|
54
|
+
|
|
55
|
+
# Voice transcription (optional — only used if Telegram receives voice notes).
|
|
56
|
+
# Whisper API is the default; OpenAI API key may already be set for other
|
|
57
|
+
# reasons (Codex), but Claude/Gemini agents can set this independently.
|
|
58
|
+
voice_transcribe_api_key: str = ""
|
|
59
|
+
|
|
60
|
+
# Module discovery root. Agents that don't ship modules can leave blank.
|
|
61
|
+
modules_dir: str = ""
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if not self.home_dir:
|
|
65
|
+
home = os.environ.get("HOME", "/home/agent")
|
|
66
|
+
self.home_dir = os.path.join(home, f".{self.executor_kind}")
|
|
67
|
+
if not self.modules_dir:
|
|
68
|
+
self.modules_dir = os.path.join(
|
|
69
|
+
self.executor_working_dir, f".{self.executor_kind}", "modules"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# ── Base env loader. Subclasses call super().from_env() and extend. ──
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _base_env_kwargs(cls) -> dict:
|
|
76
|
+
"""Pull universal fields from env vars. Subclasses extend this."""
|
|
77
|
+
allowed = os.environ.get("TELEGRAM_ALLOWED_USERS", "")
|
|
78
|
+
caps = os.environ.get("SUPERPOS_CAPABILITIES", "")
|
|
79
|
+
working_dir = os.environ.get("EXECUTOR_WORKING_DIR") or os.environ.get(
|
|
80
|
+
"WORKING_DIR", "/workspace"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
isolation_env = os.environ.get("EXECUTOR_WORKTREE_ISOLATION") or os.environ.get(
|
|
84
|
+
"WORKTREE_ISOLATION"
|
|
85
|
+
)
|
|
86
|
+
if isolation_env is not None:
|
|
87
|
+
worktree_isolation = isolation_env.lower() not in ("0", "false", "no")
|
|
88
|
+
else:
|
|
89
|
+
# Auto-enable when the working directory is a git repo
|
|
90
|
+
worktree_isolation = os.path.isdir(os.path.join(working_dir, ".git"))
|
|
91
|
+
|
|
92
|
+
return dict(
|
|
93
|
+
superpos_base_url=os.environ.get("SUPERPOS_BASE_URL", ""),
|
|
94
|
+
superpos_hive_id=os.environ.get("SUPERPOS_HIVE_ID", ""),
|
|
95
|
+
superpos_agent_id=os.environ.get("SUPERPOS_AGENT_ID", ""),
|
|
96
|
+
superpos_api_token=os.environ.get("SUPERPOS_API_TOKEN", ""),
|
|
97
|
+
superpos_refresh_token=os.environ.get("SUPERPOS_REFRESH_TOKEN", ""),
|
|
98
|
+
superpos_capabilities=[c.strip() for c in caps.split(",") if c.strip()],
|
|
99
|
+
superpos_poll_interval=int(os.environ.get("SUPERPOS_POLL_INTERVAL", "5")),
|
|
100
|
+
superpos_knowledge_inject=os.environ.get(
|
|
101
|
+
"SUPERPOS_KNOWLEDGE_INJECT", "true"
|
|
102
|
+
).lower()
|
|
103
|
+
not in ("0", "false", "no"),
|
|
104
|
+
superpos_knowledge_inject_limit=int(
|
|
105
|
+
os.environ.get("SUPERPOS_KNOWLEDGE_INJECT_LIMIT", "5")
|
|
106
|
+
),
|
|
107
|
+
telegram_bot_token=os.environ.get("TELEGRAM_BOT_TOKEN", ""),
|
|
108
|
+
telegram_allowed_users=[
|
|
109
|
+
int(u.strip()) for u in allowed.split(",") if u.strip()
|
|
110
|
+
],
|
|
111
|
+
telegram_chat_id=os.environ.get("TELEGRAM_CHAT_ID", ""),
|
|
112
|
+
executor_working_dir=working_dir,
|
|
113
|
+
executor_worktree_isolation=worktree_isolation,
|
|
114
|
+
executor_max_parallel=int(os.environ.get("EXECUTOR_MAX_PARALLEL", "3")),
|
|
115
|
+
executor_max_turns=int(os.environ.get("EXECUTOR_MAX_TURNS", "30")),
|
|
116
|
+
voice_transcribe_api_key=os.environ.get("VOICE_TRANSCRIBE_API_KEY", "")
|
|
117
|
+
or os.environ.get("OPENAI_API_KEY", ""),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_env(cls) -> "BaseConfig":
|
|
122
|
+
return cls(**cls._base_env_kwargs())
|
|
123
|
+
|
|
124
|
+
# ── Properties ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def superpos_enabled(self) -> bool:
|
|
128
|
+
return bool(
|
|
129
|
+
self.superpos_base_url
|
|
130
|
+
and self.superpos_hive_id
|
|
131
|
+
and self.superpos_agent_id
|
|
132
|
+
and self.superpos_api_token
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def telegram_enabled(self) -> bool:
|
|
137
|
+
return bool(self.telegram_bot_token)
|
|
138
|
+
|
|
139
|
+
def has_permission(self, permission: str) -> bool:
|
|
140
|
+
"""Check whether the agent has a given permission.
|
|
141
|
+
|
|
142
|
+
Matches exact, ``category:*`` wildcards, and the ``admin:*``
|
|
143
|
+
superwildcard. If permissions are empty (unknown — /me failed
|
|
144
|
+
and env doesn't carry them), returns True so the agent tries the
|
|
145
|
+
call; the server will reject if it truly lacks the right.
|
|
146
|
+
"""
|
|
147
|
+
if not self.superpos_permissions:
|
|
148
|
+
return True
|
|
149
|
+
if permission in self.superpos_permissions:
|
|
150
|
+
return True
|
|
151
|
+
if "admin:*" in self.superpos_permissions:
|
|
152
|
+
return True
|
|
153
|
+
if ":" in permission:
|
|
154
|
+
category = permission.split(":", 1)[0]
|
|
155
|
+
if f"{category}:*" in self.superpos_permissions:
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Executor contract — the seam between core and per-agent implementations.
|
|
2
|
+
|
|
3
|
+
Every concrete agent (Claude, Codex, Gemini, Qwen, …) ships a subclass of
|
|
4
|
+
:class:`Executor` that knows how to drive its specific LLM CLI/SDK. Core
|
|
5
|
+
modules (``superpos_poller``, ``telegram_bot``, ``telegram_streamer``) only
|
|
6
|
+
interact with the abstract surface defined here, so adding a new agent
|
|
7
|
+
means writing one executor — not re-porting the rest of the runtime.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import abc
|
|
13
|
+
import asyncio
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ExecutionRequest:
|
|
19
|
+
"""A single unit of work routed through the executor queue."""
|
|
20
|
+
|
|
21
|
+
prompt: str
|
|
22
|
+
chat_id: int | str
|
|
23
|
+
source: str # "telegram" | "superpos"
|
|
24
|
+
superpos_task_id: str | None = None
|
|
25
|
+
branch: str | None = None
|
|
26
|
+
image_paths: list[str] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Executor(abc.ABC):
|
|
30
|
+
"""Abstract base for per-agent LLM executors.
|
|
31
|
+
|
|
32
|
+
Subclasses MUST call ``super().__init__(max_parallel=…)`` so the queue,
|
|
33
|
+
in-flight set, and active counter are initialized. Subclasses then
|
|
34
|
+
own the consumer loop (``run``) and the actual LLM invocation.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, max_parallel: int = 1) -> None:
|
|
38
|
+
self.queue: asyncio.Queue[ExecutionRequest] = asyncio.Queue()
|
|
39
|
+
self._in_flight_superpos_tasks: set[str] = set()
|
|
40
|
+
self._max_parallel = max_parallel
|
|
41
|
+
self._active_count = 0
|
|
42
|
+
# Per-chat asyncio.Task tracking for /stop. Subclasses call
|
|
43
|
+
# ``_track_chat_task`` once they have the running task in hand;
|
|
44
|
+
# ``cancel_chat`` walks this map. Keyed by ``str(chat_id)`` so
|
|
45
|
+
# int/str keys interop with Telegram's int chat_ids and Superpos's
|
|
46
|
+
# string ones.
|
|
47
|
+
self._chat_tasks: dict[str, set[asyncio.Task]] = {}
|
|
48
|
+
|
|
49
|
+
# ── Abstract: must be implemented per agent ────────────────────────
|
|
50
|
+
|
|
51
|
+
@abc.abstractmethod
|
|
52
|
+
async def run(self) -> None:
|
|
53
|
+
"""Consume the queue forever, dispatching requests to the LLM."""
|
|
54
|
+
|
|
55
|
+
@abc.abstractmethod
|
|
56
|
+
def update_persona(self, prompt: str | None, version: int | None = None) -> None:
|
|
57
|
+
"""Replace the agent's persona/system prompt.
|
|
58
|
+
|
|
59
|
+
``version`` is informational — agents that track persona versions
|
|
60
|
+
(Claude) can persist it; agents that don't (Codex, Gemini) may ignore.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def clear_session(self, chat_id: int | str) -> None:
|
|
65
|
+
"""Drop any cached conversation state for this chat."""
|
|
66
|
+
|
|
67
|
+
# ── Optional: default implementations agents can override ──────────
|
|
68
|
+
|
|
69
|
+
async def preflight(self) -> None:
|
|
70
|
+
"""Verify auth/CLI installation before starting the main loop.
|
|
71
|
+
|
|
72
|
+
Default no-op. Agents should override to fail fast on bad creds.
|
|
73
|
+
"""
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
async def run_background(
|
|
77
|
+
self,
|
|
78
|
+
task_id: str,
|
|
79
|
+
prompt: str,
|
|
80
|
+
task_type: str = "dream",
|
|
81
|
+
timeout_seconds: int = 300,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Fire-and-forget execution for housekeeping tasks (dream, knowledge_fillin).
|
|
84
|
+
|
|
85
|
+
Default raises NotImplementedError — agents that want background work
|
|
86
|
+
must override.
|
|
87
|
+
"""
|
|
88
|
+
raise NotImplementedError(
|
|
89
|
+
f"{type(self).__name__} does not implement background tasks"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def cleanup_stale_sessions(self, max_age_hours: int = 24) -> dict[str, int]:
|
|
93
|
+
"""Delete LLM-specific stale session artifacts; return stats.
|
|
94
|
+
|
|
95
|
+
Returned dict should contain (at minimum) keys: ``projects``,
|
|
96
|
+
``session_env``, ``bytes_freed``. Default returns zeros so the
|
|
97
|
+
``/cleanup`` Telegram command works on agents without a
|
|
98
|
+
per-session disk footprint.
|
|
99
|
+
"""
|
|
100
|
+
return {"projects": 0, "session_env": 0, "bytes_freed": 0}
|
|
101
|
+
|
|
102
|
+
# ── Concrete: shared bookkeeping ───────────────────────────────────
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def pending(self) -> int:
|
|
106
|
+
return self.queue.qsize()
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_busy(self) -> bool:
|
|
110
|
+
return self._active_count > 0
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def has_free_slots(self) -> bool:
|
|
114
|
+
"""True if more concurrent work can be accepted.
|
|
115
|
+
|
|
116
|
+
Uses the in-flight task set rather than ``qsize()`` / ``_active_count``
|
|
117
|
+
because a task can be claimed but waiting for the semaphore —
|
|
118
|
+
``qsize()`` is 0 then, but the slot is taken.
|
|
119
|
+
"""
|
|
120
|
+
return len(self._in_flight_superpos_tasks) < self._max_parallel
|
|
121
|
+
|
|
122
|
+
def add_superpos_task(self, task_id: str) -> None:
|
|
123
|
+
self._in_flight_superpos_tasks.add(task_id)
|
|
124
|
+
|
|
125
|
+
def remove_superpos_task(self, task_id: str) -> None:
|
|
126
|
+
self._in_flight_superpos_tasks.discard(task_id)
|
|
127
|
+
|
|
128
|
+
def has_superpos_task(self, task_id: str) -> bool:
|
|
129
|
+
return task_id in self._in_flight_superpos_tasks
|
|
130
|
+
|
|
131
|
+
# ── /stop support ─────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def _track_chat_task(
|
|
134
|
+
self, chat_id: int | str, task: asyncio.Task,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Register an in-flight asyncio.Task so ``cancel_chat`` can find it.
|
|
137
|
+
|
|
138
|
+
Subclasses should call this from ``_execute`` (or wherever the
|
|
139
|
+
per-request worker is spawned) right after they create the task,
|
|
140
|
+
then trust the auto-removal on done. Calling more than once for
|
|
141
|
+
the same chat is fine — a chat with parallel work (e.g. branch-
|
|
142
|
+
scoped tasks) gets a set of tasks tracked.
|
|
143
|
+
"""
|
|
144
|
+
key = str(chat_id)
|
|
145
|
+
bucket = self._chat_tasks.setdefault(key, set())
|
|
146
|
+
bucket.add(task)
|
|
147
|
+
task.add_done_callback(lambda _t: self._untrack_chat_task(key, _t))
|
|
148
|
+
|
|
149
|
+
def _untrack_chat_task(self, chat_key: str, task: asyncio.Task) -> None:
|
|
150
|
+
bucket = self._chat_tasks.get(chat_key)
|
|
151
|
+
if not bucket:
|
|
152
|
+
return
|
|
153
|
+
bucket.discard(task)
|
|
154
|
+
if not bucket:
|
|
155
|
+
self._chat_tasks.pop(chat_key, None)
|
|
156
|
+
|
|
157
|
+
def cancel_chat(self, chat_id: int | str) -> int:
|
|
158
|
+
"""Cancel every in-flight task tracked for ``chat_id``.
|
|
159
|
+
|
|
160
|
+
Returns the number of tasks signalled (which may be 0 if nothing
|
|
161
|
+
was running). Subclasses that need richer behaviour (kill a
|
|
162
|
+
subprocess, send a Telegram "stopped" message, mark the Superpos
|
|
163
|
+
task as failed) should override and call ``super().cancel_chat``
|
|
164
|
+
to keep the asyncio cancellation path uniform.
|
|
165
|
+
"""
|
|
166
|
+
bucket = self._chat_tasks.get(str(chat_id))
|
|
167
|
+
if not bucket:
|
|
168
|
+
return 0
|
|
169
|
+
cancelled = 0
|
|
170
|
+
for task in list(bucket):
|
|
171
|
+
if not task.done():
|
|
172
|
+
task.cancel()
|
|
173
|
+
cancelled += 1
|
|
174
|
+
return cancelled
|