techtide-swarm 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.
@@ -0,0 +1,30 @@
1
+ """TechTide Swarm 357 — public API."""
2
+
3
+ from techtide_swarm.agent import Agent, AgentConfig, AgentResult
4
+ from techtide_swarm.bash_gate import BashSecurityGate
5
+ from techtide_swarm.memory import MemoryManager
6
+ from techtide_swarm.memvid_bridge import MemvidBridge, MemvidBridgeError, resolve_bridge_binary
7
+ from techtide_swarm.persistence import SwarmStore
8
+ from techtide_swarm.swarm import CostController, Swarm, SwarmExecutionResult
9
+ from techtide_swarm.tools.registry import TOOLSET_MAP, ToolRegistry, registry
10
+ from techtide_swarm.ultra_plan import UltraPlan, UltraPlanConfig
11
+
12
+ __all__ = [
13
+ "Agent",
14
+ "AgentConfig",
15
+ "AgentResult",
16
+ "BashSecurityGate",
17
+ "CostController",
18
+ "MemvidBridge",
19
+ "MemvidBridgeError",
20
+ "MemoryManager",
21
+ "Swarm",
22
+ "SwarmExecutionResult",
23
+ "SwarmStore",
24
+ "TOOLSET_MAP",
25
+ "ToolRegistry",
26
+ "UltraPlan",
27
+ "UltraPlanConfig",
28
+ "registry",
29
+ "resolve_bridge_binary",
30
+ ]
@@ -0,0 +1,224 @@
1
+ """Single-agent runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from techtide_swarm.core.types import LayerType
14
+
15
+
16
+ class AgentConfig(BaseModel):
17
+ """Configuration for one agent."""
18
+
19
+ name: str
20
+ layer: LayerType
21
+ role: str
22
+ soul: str = ""
23
+ tools: list[str] = Field(default_factory=list)
24
+ model: str = "sonnet"
25
+ budget_limit_usd: float = 1.0
26
+ max_turns: int = 10
27
+
28
+
29
+ @dataclass
30
+ class AgentResult:
31
+ """Outcome of agent.run."""
32
+
33
+ output: str
34
+ cost_usd: float
35
+ latency_ms: int
36
+ status: str
37
+ agent_name: str = ""
38
+ error: str | None = None
39
+
40
+
41
+ class Agent:
42
+ """One Claude-powered agent."""
43
+
44
+ def __init__(self, config: AgentConfig) -> None:
45
+ self.config = config
46
+
47
+ def _load_system_prompt(self) -> str | None:
48
+ """Read the SOUL.md template and return the system prompt body (after YAML front-matter)."""
49
+ soul_path = self.config.soul
50
+ if not soul_path:
51
+ return None
52
+ path = Path(soul_path)
53
+ if not path.is_file():
54
+ # Resolve relative to repo root (two levels up from this package's src/)
55
+ pkg_src = Path(__file__).resolve().parent
56
+ candidates = [
57
+ pkg_src.parent.parent.parent.parent / soul_path, # repo root
58
+ pkg_src.parent.parent.parent / soul_path,
59
+ ]
60
+ for candidate in candidates:
61
+ if candidate.is_file():
62
+ path = candidate
63
+ break
64
+ else:
65
+ return None
66
+ text = path.read_text(encoding="utf-8")
67
+ # Strip YAML front-matter delimited by ---
68
+ if text.startswith("---"):
69
+ parts = text.split("---", 2)
70
+ if len(parts) >= 3:
71
+ return parts[2].strip()
72
+ return text.strip()
73
+
74
+ async def run(self, task: str) -> AgentResult:
75
+ """Execute task via Anthropic Messages API when API key is set; else deterministic stub."""
76
+ from techtide_swarm.llm import create_async_client, model_id as resolve_model_id, resolve_api_key
77
+
78
+ api_key = resolve_api_key()
79
+ if not api_key:
80
+ return self._stub_result(task)
81
+
82
+ started = time.perf_counter()
83
+ try:
84
+ from techtide_swarm.tools import get_anthropic_tools, execute_tool
85
+
86
+ client = create_async_client()
87
+ resolved_model = resolve_model_id(self.config.model)
88
+ system_prompt = self._load_system_prompt()
89
+
90
+ messages: list[dict[str, Any]] = [{"role": "user", "content": task}]
91
+ tools = get_anthropic_tools(self.config.tools)
92
+
93
+ total_cost = 0.0
94
+ final_text = ""
95
+ budget = self.config.budget_limit_usd
96
+
97
+ for turn in range(self.config.max_turns):
98
+ if total_cost >= budget:
99
+ final_text = f"Budget limit reached (${budget:.2f}). Stopping."
100
+ break
101
+
102
+ create_kwargs: dict[str, Any] = {
103
+ "model": resolved_model,
104
+ "max_tokens": 4096,
105
+ "messages": messages,
106
+ }
107
+ if system_prompt:
108
+ create_kwargs["system"] = system_prompt
109
+ if tools:
110
+ create_kwargs["tools"] = tools
111
+
112
+ message = await client.messages.create(**create_kwargs)
113
+ total_cost += self._estimate_cost(message, resolved_model)
114
+
115
+ messages.append({"role": "assistant", "content": message.content})
116
+
117
+ if total_cost >= budget:
118
+ final_text = self._extract_text(message) or (
119
+ f"Budget limit reached (${budget:.2f}). Stopping."
120
+ )
121
+ break
122
+
123
+ tool_uses = [block for block in message.content if getattr(block, "type", None) == "tool_use"]
124
+
125
+ if not tool_uses:
126
+ final_text = self._extract_text(message)
127
+ break
128
+
129
+ tool_results = []
130
+ for tool_use in tool_uses:
131
+ result_text = execute_tool(tool_use.name, tool_use.input)
132
+ tool_results.append({
133
+ "type": "tool_result",
134
+ "tool_use_id": tool_use.id,
135
+ "content": result_text
136
+ })
137
+
138
+ messages.append({"role": "user", "content": tool_results})
139
+ else:
140
+ final_text = "Max turns reached without completion."
141
+
142
+ elapsed_ms = int((time.perf_counter() - started) * 1000)
143
+
144
+ from techtide_swarm.telemetry import log_telemetry
145
+ log_telemetry("agent_run", {
146
+ "agent_name": self.config.name,
147
+ "layer": self.config.layer.value,
148
+ "cost_usd": total_cost,
149
+ "latency_ms": elapsed_ms,
150
+ "status": "success",
151
+ })
152
+
153
+ return AgentResult(
154
+ agent_name=self.config.name,
155
+ output=final_text,
156
+ cost_usd=total_cost,
157
+ latency_ms=elapsed_ms,
158
+ status="success",
159
+ )
160
+ except Exception as exc: # noqa: BLE001 — surface to CLI
161
+ elapsed_ms = int((time.perf_counter() - started) * 1000)
162
+
163
+ from techtide_swarm.telemetry import log_telemetry
164
+ log_telemetry("agent_run", {
165
+ "agent_name": self.config.name,
166
+ "layer": self.config.layer.value,
167
+ "cost_usd": 0.0,
168
+ "latency_ms": elapsed_ms,
169
+ "status": "error",
170
+ "error": str(exc),
171
+ })
172
+
173
+ return AgentResult(
174
+ agent_name=self.config.name,
175
+ output="",
176
+ cost_usd=0.0,
177
+ latency_ms=elapsed_ms,
178
+ status="error",
179
+ error=str(exc),
180
+ )
181
+
182
+ def _stub_result(self, task: str) -> AgentResult:
183
+ preview = task[:200].replace("\n", " ")
184
+ return AgentResult(
185
+ agent_name=self.config.name,
186
+ output=(
187
+ f"[stub] Agent {self.config.name} ({self.config.layer.value}) would process:\n"
188
+ f"{preview}"
189
+ ),
190
+ cost_usd=0.0,
191
+ latency_ms=0,
192
+ status="success",
193
+ )
194
+
195
+ @staticmethod
196
+ def _model_id(short: str) -> str:
197
+ from techtide_swarm.llm import model_id as resolve_model_id
198
+
199
+ return resolve_model_id(short)
200
+
201
+ @staticmethod
202
+ def _extract_text(message: Any) -> str:
203
+ parts: list[str] = []
204
+ for block in getattr(message, "content", []) or []:
205
+ btype = getattr(block, "type", None)
206
+ if btype == "text":
207
+ parts.append(getattr(block, "text", ""))
208
+ return "\n".join(parts) if parts else str(message)
209
+
210
+ @staticmethod
211
+ def _estimate_cost(message: Any, model_id: str) -> float:
212
+ usage = getattr(message, "usage", None)
213
+ if usage is None:
214
+ return 0.01
215
+ inp = float(getattr(usage, "input_tokens", 0) or 0)
216
+ out = float(getattr(usage, "output_tokens", 0) or 0)
217
+ # Approximate list pricing snapshot (Claude 4.x, 2026) — override with real billing later
218
+ if "opus" in model_id.lower():
219
+ rate_in, rate_out = 15.0 / 1e6, 75.0 / 1e6
220
+ elif "haiku" in model_id.lower():
221
+ rate_in, rate_out = 0.80 / 1e6, 4.0 / 1e6
222
+ else: # sonnet
223
+ rate_in, rate_out = 3.0 / 1e6, 15.0 / 1e6
224
+ return inp * rate_in + out * rate_out
@@ -0,0 +1,44 @@
1
+ """23-point style bash command validation (subset implemented; extend as needed)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shlex
7
+
8
+
9
+ class BashSecurityGate:
10
+ """Validate shell commands before execution."""
11
+
12
+ _BLOCK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
13
+ (re.compile(r"(?i)\brm\s+(-[rf]*\s*)*[/~]"), "recursive delete toward root or home"),
14
+ (re.compile(r"(?i)\bcurl\b.*\|\s*bash"), "curl pipe to bash"),
15
+ (re.compile(r"(?i)\bwget\b.*\|\s*(?:sh|bash)"), "wget pipe to shell"),
16
+ (re.compile(r"(?i)\b(?:sudo\s+)?chmod\s+777\b"), "dangerous chmod"),
17
+ (re.compile(r"(?i)\$[\w_]*(?:KEY|SECRET|TOKEN|PASSWORD)\b"), "references secret env var"),
18
+ (re.compile(r"(?i)\b(?:ANTHROPIC|OPENAI|AWS|GCP)_(?:API_)?KEY\b"), "API key literal in argv"),
19
+ (re.compile(r"(?i)>\s*/etc/"), "redirect to system config"),
20
+ (re.compile(r"(?i)\bmkfs\b"), "disk format"),
21
+ (re.compile(r"(?i)\bdd\s+if="), "raw disk write"),
22
+ (re.compile(r"(?i)\bpython[23]?\s+-c\s+.*(?:exec|eval|import\s+os)"), "python code exec with os access"),
23
+ (re.compile(r"(?i)\bsudo\s+(?:rm|mv|cp|chown)\b"), "elevated file operation"),
24
+ (re.compile(r"(?i)\bnc\s+-[el]"), "netcat listener"),
25
+ (re.compile(r"(?i)>\s*/dev/(?:sd|hd|nvme)"), "redirect to block device"),
26
+ )
27
+
28
+ @classmethod
29
+ def validate(cls, command: str) -> tuple[bool, str]:
30
+ """Return (safe, reason). Safe commands get reason ''."""
31
+ stripped = command.strip()
32
+ if not stripped:
33
+ return False, "empty command"
34
+ try:
35
+ parts = shlex.split(stripped)
36
+ except ValueError as exc:
37
+ return False, f"unparseable shell: {exc}"
38
+ if not parts:
39
+ return False, "empty command"
40
+ joined = " ".join(parts)
41
+ for pattern, reason in cls._BLOCK_PATTERNS:
42
+ if pattern.search(stripped) or pattern.search(joined):
43
+ return False, reason
44
+ return True, ""