sourcebot 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.
- sourcebot/__init__.py +9 -0
- sourcebot/__main__.py +17 -0
- sourcebot/bus/__init__.py +4 -0
- sourcebot/bus/channel_adapter.py +21 -0
- sourcebot/bus/event_bus.py +15 -0
- sourcebot/bus/message_models.py +33 -0
- sourcebot/bus/outbound_dispatcher.py +15 -0
- sourcebot/bus/session_manager.py +20 -0
- sourcebot/cli/commands/core/__init__.py +3 -0
- sourcebot/cli/commands/core/command_line.py +26 -0
- sourcebot/cli/commands/init_commands/__init__.py +3 -0
- sourcebot/cli/commands/init_commands/init_global_config.py +30 -0
- sourcebot/cli/commands/init_commands/init_workspace_config.py +18 -0
- sourcebot/cli/commands/run_commands/__init__.py +3 -0
- sourcebot/cli/commands/run_commands/command_line_tool.py +345 -0
- sourcebot/cli/commands/run_commands/safe_runner.py +47 -0
- sourcebot/cli/main.py +28 -0
- sourcebot/config/__init__.py +15 -0
- sourcebot/config/base.py +13 -0
- sourcebot/config/config_manager.py +367 -0
- sourcebot/config/exceptions.py +4 -0
- sourcebot/config/global_config.py +55 -0
- sourcebot/config/provider_config.py +62 -0
- sourcebot/config/workspace_config.py +106 -0
- sourcebot/context/__init__.py +5 -0
- sourcebot/context/context_builder.py +78 -0
- sourcebot/context/identity.py +19 -0
- sourcebot/context/message_builder.py +154 -0
- sourcebot/context/skill/__init__.py +7 -0
- sourcebot/context/skill/skill.py +11 -0
- sourcebot/context/skill/skill_context.py +10 -0
- sourcebot/context/skill/skill_loader.py +57 -0
- sourcebot/context/skill/skill_metadata.py +27 -0
- sourcebot/context/skill/skill_requirements.py +25 -0
- sourcebot/context/skill/skill_summary.py +31 -0
- sourcebot/conversation/__init__.py +2 -0
- sourcebot/conversation/service.py +191 -0
- sourcebot/docker_sandbox/__init__.py +3 -0
- sourcebot/docker_sandbox/docker_sandbox.py +113 -0
- sourcebot/llm/__init__.py +3 -0
- sourcebot/llm/anthropic/__init__.py +2 -0
- sourcebot/llm/anthropic/adapter.py +30 -0
- sourcebot/llm/anthropic/anthropic_llm_client.py +38 -0
- sourcebot/llm/anthropic/converter.py +59 -0
- sourcebot/llm/core/adapter.py +16 -0
- sourcebot/llm/core/client.py +16 -0
- sourcebot/llm/core/delta.py +12 -0
- sourcebot/llm/core/message.py +53 -0
- sourcebot/llm/core/message_converter.py +33 -0
- sourcebot/llm/core/response.py +30 -0
- sourcebot/llm/core/tool.py +7 -0
- sourcebot/llm/core/tool_converter.py +30 -0
- sourcebot/llm/core/tool_delta_aggregator.py +38 -0
- sourcebot/llm/llm_client_factory.py +13 -0
- sourcebot/llm/openai/__init__.py +2 -0
- sourcebot/llm/openai/adapter.py +27 -0
- sourcebot/llm/openai/converter.py +53 -0
- sourcebot/llm/openai/openai_llm_client.py +47 -0
- sourcebot/logging/__init__.py +3 -0
- sourcebot/logging/setup.py +33 -0
- sourcebot/memory/__init__.py +5 -0
- sourcebot/memory/file_store.py +23 -0
- sourcebot/memory/llm_consolidator.py +79 -0
- sourcebot/memory/service.py +116 -0
- sourcebot/memory/window_policy.py +36 -0
- sourcebot/prompt/__init__.py +4 -0
- sourcebot/prompt/deeomposer_prompt.py +420 -0
- sourcebot/prompt/identity_prompt.py +98 -0
- sourcebot/prompt/subagent_prompt.py +25 -0
- sourcebot/runtime/__init__.py +3 -0
- sourcebot/runtime/agent/__init__.py +3 -0
- sourcebot/runtime/agent/agent.py +130 -0
- sourcebot/runtime/agent/agent_factory.py +83 -0
- sourcebot/runtime/dag/planner/__init__.py +3 -0
- sourcebot/runtime/dag/planner/dag_planner.py +26 -0
- sourcebot/runtime/dag/planner/execution_scheduler.py +35 -0
- sourcebot/runtime/dag/planner/parallelism_optimizer.py +44 -0
- sourcebot/runtime/dag/planner/task_decomposer.py +37 -0
- sourcebot/runtime/dag/scheduler/__init__.py +3 -0
- sourcebot/runtime/dag/scheduler/dag_scheduler.py +319 -0
- sourcebot/runtime/dag/scheduler/retry_policy.py +27 -0
- sourcebot/runtime/dag/scheduler/run_store.py +58 -0
- sourcebot/runtime/dag/scheduler/state_store.py +40 -0
- sourcebot/runtime/dag/scheduler/task_graph.py +29 -0
- sourcebot/runtime/init_system.py +182 -0
- sourcebot/runtime/tool_executor.py +30 -0
- sourcebot/security/policy.py +23 -0
- sourcebot/session/__init__.py +4 -0
- sourcebot/session/jsonl_repository.py +142 -0
- sourcebot/session/repository.py +19 -0
- sourcebot/session/service.py +44 -0
- sourcebot/session/session.py +53 -0
- sourcebot/storage/__init__.py +3 -0
- sourcebot/storage/rules_loader.py +72 -0
- sourcebot/storage/skill_storage.py +51 -0
- sourcebot/tools/__init__.py +7 -0
- sourcebot/tools/base.py +182 -0
- sourcebot/tools/registry.py +81 -0
- sourcebot/tools/rule_detail.py +70 -0
- sourcebot/tools/rule_list.py +57 -0
- sourcebot/tools/shell.py +93 -0
- sourcebot/tools/skill_detail.py +61 -0
- sourcebot/tools/skill_list.py +68 -0
- sourcebot/utils/__init__.py +2 -0
- sourcebot/utils/output.py +79 -0
- sourcebot-0.1.0.dist-info/METADATA +318 -0
- sourcebot-0.1.0.dist-info/RECORD +110 -0
- sourcebot-0.1.0.dist-info/WHEEL +5 -0
- sourcebot-0.1.0.dist-info/entry_points.txt +2 -0
- sourcebot-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# sourcebot/runtime/dag/scheduler/dag_scheduler.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from collections import deque
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from sourcebot.runtime.dag.scheduler.task_graph import TaskGraph
|
|
9
|
+
from sourcebot.runtime.dag.scheduler.state_store import StateStore
|
|
10
|
+
from sourcebot.runtime.dag.scheduler.retry_policy import RetryPolicy
|
|
11
|
+
from sourcebot.runtime.dag.scheduler.run_store import RunStore
|
|
12
|
+
from sourcebot.llm.core.message import Message
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DAGScheduler:
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
agent_factory,
|
|
22
|
+
workspace,
|
|
23
|
+
runs_dir="runs",
|
|
24
|
+
max_concurrent=4,
|
|
25
|
+
retry_policy=None,
|
|
26
|
+
|
|
27
|
+
):
|
|
28
|
+
|
|
29
|
+
self.agent_factory = agent_factory
|
|
30
|
+
|
|
31
|
+
self.runs_dir = Path(workspace)/runs_dir
|
|
32
|
+
|
|
33
|
+
self.max_concurrent = max_concurrent
|
|
34
|
+
|
|
35
|
+
self.retry_policy = retry_policy or RetryPolicy()
|
|
36
|
+
|
|
37
|
+
self.semaphore = asyncio.Semaphore(max_concurrent)
|
|
38
|
+
|
|
39
|
+
self.run_store = RunStore(self.runs_dir)
|
|
40
|
+
|
|
41
|
+
# Run DAG
|
|
42
|
+
async def run(self, tasks):
|
|
43
|
+
|
|
44
|
+
self.run_id, self.run_dir = self.run_store.create_run(tasks)
|
|
45
|
+
|
|
46
|
+
logger.info(f"DAG RUN {self.run_id}")
|
|
47
|
+
|
|
48
|
+
self.state = StateStore(self.run_dir)
|
|
49
|
+
|
|
50
|
+
graph = TaskGraph(tasks)
|
|
51
|
+
|
|
52
|
+
result = await self._execute(graph)
|
|
53
|
+
|
|
54
|
+
if result["failed"]:
|
|
55
|
+
self.run_store.update_status(self.run_dir, "failed")
|
|
56
|
+
else:
|
|
57
|
+
self.run_store.update_status(self.run_dir, "completed")
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
# Resume run
|
|
62
|
+
async def resume(self, run_id):
|
|
63
|
+
|
|
64
|
+
self.run_id = run_id
|
|
65
|
+
self.run_dir = self.runs_dir / run_id
|
|
66
|
+
|
|
67
|
+
tasks = self.run_store.load_dag(self.run_dir)
|
|
68
|
+
|
|
69
|
+
self.state = StateStore(self.run_dir)
|
|
70
|
+
|
|
71
|
+
graph = TaskGraph(tasks)
|
|
72
|
+
|
|
73
|
+
logger.info(f"Resuming {run_id}")
|
|
74
|
+
|
|
75
|
+
return await self._execute(graph)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Replay failed tasks
|
|
79
|
+
async def replay_failed(self, run_id):
|
|
80
|
+
|
|
81
|
+
self.run_id = run_id
|
|
82
|
+
self.run_dir = self.runs_dir / run_id
|
|
83
|
+
|
|
84
|
+
tasks = self.run_store.load_dag(self.run_dir)
|
|
85
|
+
|
|
86
|
+
self.state = StateStore(self.run_dir)
|
|
87
|
+
|
|
88
|
+
for t in list(self.state.state.keys()):
|
|
89
|
+
|
|
90
|
+
if self.state.state[t]["status"] == "failed":
|
|
91
|
+
self.state.state[t]["status"] = "pending"
|
|
92
|
+
|
|
93
|
+
self.state.save()
|
|
94
|
+
|
|
95
|
+
graph = TaskGraph(tasks)
|
|
96
|
+
|
|
97
|
+
logger.info(f"Replay failed tasks {run_id}")
|
|
98
|
+
|
|
99
|
+
return await self._execute(graph)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Core execution
|
|
103
|
+
async def _execute(self, graph):
|
|
104
|
+
|
|
105
|
+
ready = deque()
|
|
106
|
+
|
|
107
|
+
running = {}
|
|
108
|
+
|
|
109
|
+
future_map = {}
|
|
110
|
+
|
|
111
|
+
completed = set()
|
|
112
|
+
failed = set()
|
|
113
|
+
|
|
114
|
+
for tid, deg in graph.in_degree.items():
|
|
115
|
+
|
|
116
|
+
state = self.state.status(tid)
|
|
117
|
+
|
|
118
|
+
if state == "completed":
|
|
119
|
+
completed.add(tid)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if deg == 0:
|
|
123
|
+
ready.append(tid)
|
|
124
|
+
|
|
125
|
+
while ready or running:
|
|
126
|
+
|
|
127
|
+
while ready and len(running) < self.max_concurrent:
|
|
128
|
+
|
|
129
|
+
tid = ready.popleft()
|
|
130
|
+
|
|
131
|
+
task = graph.tasks[tid]
|
|
132
|
+
|
|
133
|
+
fut = asyncio.create_task(
|
|
134
|
+
self._execute_task(task)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
running[tid] = fut
|
|
138
|
+
future_map[fut] = tid
|
|
139
|
+
|
|
140
|
+
if not running:
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
done, _ = await asyncio.wait(
|
|
144
|
+
running.values(),
|
|
145
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
for fut in done:
|
|
149
|
+
|
|
150
|
+
tid = future_map[fut]
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
|
|
154
|
+
result = fut.result()
|
|
155
|
+
|
|
156
|
+
if result["success"]:
|
|
157
|
+
|
|
158
|
+
completed.add(tid)
|
|
159
|
+
|
|
160
|
+
for child in graph.children[tid]:
|
|
161
|
+
|
|
162
|
+
graph.in_degree[child] -= 1
|
|
163
|
+
|
|
164
|
+
if graph.in_degree[child] == 0:
|
|
165
|
+
ready.append(child)
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
failed.add(tid)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
|
|
172
|
+
failed.add(tid)
|
|
173
|
+
logger.error(f"{tid} crashed: {e}")
|
|
174
|
+
|
|
175
|
+
del running[tid]
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"completed": list(completed),
|
|
179
|
+
"failed": list(failed)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Task execution
|
|
184
|
+
async def _execute_task(self, task):
|
|
185
|
+
|
|
186
|
+
task_id = task["id"]
|
|
187
|
+
|
|
188
|
+
log_file = self.run_dir / "tasks" / f"{task_id}.log"
|
|
189
|
+
|
|
190
|
+
attempt = self.state.attempts(task_id) + 1
|
|
191
|
+
|
|
192
|
+
while True:
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
|
|
196
|
+
self.state.update(
|
|
197
|
+
task_id,
|
|
198
|
+
status = "running",
|
|
199
|
+
attempts = attempt,
|
|
200
|
+
started = str(datetime.utcnow())
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async with self.semaphore:
|
|
204
|
+
|
|
205
|
+
agent = self.agent_factory.build_sub_agent(
|
|
206
|
+
task_id = task_id,
|
|
207
|
+
task_description = task["description"]
|
|
208
|
+
)
|
|
209
|
+
messages = self._build_messages(task)
|
|
210
|
+
result, _, _ = await agent.run(messages)
|
|
211
|
+
|
|
212
|
+
self._write_log(log_file, result)
|
|
213
|
+
|
|
214
|
+
if self._is_success(result):
|
|
215
|
+
|
|
216
|
+
self.state.update(
|
|
217
|
+
task_id,
|
|
218
|
+
status = "completed",
|
|
219
|
+
result = result
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return {"success": True}
|
|
223
|
+
|
|
224
|
+
raise RuntimeError("task failed")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
|
|
228
|
+
self._write_log(log_file, str(e))
|
|
229
|
+
|
|
230
|
+
if not self.retry_policy.should_retry(attempt):
|
|
231
|
+
|
|
232
|
+
self.state.update(
|
|
233
|
+
task_id,
|
|
234
|
+
status = "failed",
|
|
235
|
+
error = str(e)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return {"success": False}
|
|
239
|
+
|
|
240
|
+
delay = self.retry_policy.get_delay(attempt)
|
|
241
|
+
|
|
242
|
+
logger.warning(
|
|
243
|
+
f"{task_id} retry in {delay}s"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
await asyncio.sleep(delay)
|
|
247
|
+
|
|
248
|
+
attempt += 1
|
|
249
|
+
|
|
250
|
+
# =================================
|
|
251
|
+
# Helpers
|
|
252
|
+
# =================================
|
|
253
|
+
|
|
254
|
+
def _write_log(self, file: Path, data):
|
|
255
|
+
|
|
256
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
|
|
258
|
+
with open(file, "a") as f:
|
|
259
|
+
|
|
260
|
+
f.write(
|
|
261
|
+
f"\n[{datetime.utcnow()}]\n"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if isinstance(data, str):
|
|
265
|
+
f.write(data + "\n")
|
|
266
|
+
else:
|
|
267
|
+
f.write(json.dumps(data, indent=2) + "\n")
|
|
268
|
+
|
|
269
|
+
def _is_success(self, result):
|
|
270
|
+
|
|
271
|
+
if isinstance(result, dict):
|
|
272
|
+
return result.get("success", True)
|
|
273
|
+
|
|
274
|
+
return bool(result)
|
|
275
|
+
|
|
276
|
+
def _build_messages(self, task):
|
|
277
|
+
return [
|
|
278
|
+
Message(role = "system", content = f"You are executing task {task['id']}"),
|
|
279
|
+
Message(role = "user", content = self.build_task_description(task)),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
def build_task_description(self, task):
|
|
283
|
+
"""Combine the task and context into a complete description."""
|
|
284
|
+
description = task.get("description", "")
|
|
285
|
+
context = task.get("context", {})
|
|
286
|
+
|
|
287
|
+
context_parts = []
|
|
288
|
+
|
|
289
|
+
if context.get("rules"):
|
|
290
|
+
rules = "\n • ".join(context["rules"])
|
|
291
|
+
context_parts.append(f"Rules:\n • {rules}")
|
|
292
|
+
|
|
293
|
+
if context.get("skills"):
|
|
294
|
+
skills = ", ".join(context["skills"])
|
|
295
|
+
context_parts.append(f"Required skills: {skills}")
|
|
296
|
+
|
|
297
|
+
if context.get("environment"):
|
|
298
|
+
env = context["environment"]
|
|
299
|
+
if env.get("required_tools"):
|
|
300
|
+
tools = ", ".join(env["required_tools"])
|
|
301
|
+
context_parts.append(f"Required tools: {tools}")
|
|
302
|
+
if env.get("working_dir"):
|
|
303
|
+
context_parts.append(f"Working directory: {env['working_dir']}")
|
|
304
|
+
|
|
305
|
+
if context.get("inherited_context"):
|
|
306
|
+
inherited = context["inherited_context"]
|
|
307
|
+
inherited_items = [f"{k}: {v}" for k, v in inherited.items()]
|
|
308
|
+
context_parts.append(f"Context: {', '.join(inherited_items)}")
|
|
309
|
+
|
|
310
|
+
if context_parts:
|
|
311
|
+
full_description = f"{description}\n\nContext Information:\n" + "\n".join(context_parts)
|
|
312
|
+
else:
|
|
313
|
+
full_description = description
|
|
314
|
+
|
|
315
|
+
return full_description
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def save_dag(self, plan_tasks):
|
|
319
|
+
return self.run_store.create_run(plan_tasks)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# sourcebot/runtime/dag/scheduler/retry_policy.py
|
|
2
|
+
import random
|
|
3
|
+
class RetryPolicy:
|
|
4
|
+
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
max_attempts: int = 3,
|
|
8
|
+
base_delay: int = 2,
|
|
9
|
+
max_delay: int = 30,
|
|
10
|
+
jitter: bool = True,
|
|
11
|
+
):
|
|
12
|
+
self.max_attempts = max_attempts
|
|
13
|
+
self.base_delay = base_delay
|
|
14
|
+
self.max_delay = max_delay
|
|
15
|
+
self.jitter = jitter
|
|
16
|
+
|
|
17
|
+
def should_retry(self, attempt: int) -> bool:
|
|
18
|
+
return attempt < self.max_attempts
|
|
19
|
+
|
|
20
|
+
def get_delay(self, attempt: int) -> float:
|
|
21
|
+
|
|
22
|
+
delay = min(self.base_delay * (2 ** (attempt - 1)), self.max_delay)
|
|
23
|
+
|
|
24
|
+
if self.jitter:
|
|
25
|
+
delay = delay * (0.5 + random.random())
|
|
26
|
+
|
|
27
|
+
return delay
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# sourcebot/runtime/dag/scheduler/run_store.py
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RunStore:
|
|
9
|
+
|
|
10
|
+
def __init__(self, runs_dir: Path):
|
|
11
|
+
|
|
12
|
+
self.runs_dir = runs_dir
|
|
13
|
+
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
def create_run(self, tasks):
|
|
16
|
+
|
|
17
|
+
run_id = f"run_{uuid.uuid4().hex[:8]}"
|
|
18
|
+
|
|
19
|
+
run_dir = self.runs_dir / run_id
|
|
20
|
+
run_dir.mkdir(parents=True)
|
|
21
|
+
|
|
22
|
+
(run_dir / "tasks").mkdir()
|
|
23
|
+
|
|
24
|
+
(run_dir / "dag.json").write_text(
|
|
25
|
+
json.dumps(tasks, indent=2)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
meta = {
|
|
29
|
+
"run_id": run_id,
|
|
30
|
+
"created_at": str(datetime.utcnow()),
|
|
31
|
+
"status": "running"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
(run_dir / "run_meta.json").write_text(
|
|
35
|
+
json.dumps(meta, indent=2)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return run_id, run_dir
|
|
39
|
+
|
|
40
|
+
def load_dag(self, run_dir):
|
|
41
|
+
|
|
42
|
+
file = run_dir / "dag.json"
|
|
43
|
+
|
|
44
|
+
if not file.exists():
|
|
45
|
+
raise RuntimeError("dag.json missing")
|
|
46
|
+
|
|
47
|
+
return json.loads(file.read_text())
|
|
48
|
+
|
|
49
|
+
def update_status(self, run_dir, status):
|
|
50
|
+
|
|
51
|
+
meta_file = run_dir / "run_meta.json"
|
|
52
|
+
|
|
53
|
+
meta = json.loads(meta_file.read_text())
|
|
54
|
+
|
|
55
|
+
meta["status"] = status
|
|
56
|
+
meta["updated_at"] = str(datetime.utcnow())
|
|
57
|
+
|
|
58
|
+
meta_file.write_text(json.dumps(meta, indent=2))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# sourcebot/runtime/dag/scheduler/state_store.py
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StateStore:
|
|
8
|
+
|
|
9
|
+
def __init__(self, run_dir: Path):
|
|
10
|
+
|
|
11
|
+
self.file = run_dir / "state.json"
|
|
12
|
+
|
|
13
|
+
if self.file.exists():
|
|
14
|
+
self.state = json.loads(self.file.read_text())
|
|
15
|
+
else:
|
|
16
|
+
self.state = {}
|
|
17
|
+
|
|
18
|
+
def save(self):
|
|
19
|
+
|
|
20
|
+
self.file.write_text(
|
|
21
|
+
json.dumps(self.state, indent=2)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def status(self, task_id):
|
|
25
|
+
|
|
26
|
+
return self.state.get(task_id, {}).get("status")
|
|
27
|
+
|
|
28
|
+
def attempts(self, task_id):
|
|
29
|
+
|
|
30
|
+
return self.state.get(task_id, {}).get("attempts", 0)
|
|
31
|
+
|
|
32
|
+
def update(self, task_id, **fields):
|
|
33
|
+
|
|
34
|
+
entry = self.state.setdefault(task_id, {})
|
|
35
|
+
|
|
36
|
+
entry.update(fields)
|
|
37
|
+
|
|
38
|
+
entry["updated_at"] = str(datetime.utcnow())
|
|
39
|
+
|
|
40
|
+
self.save()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# sourcebot/runtime/dag/scheduler/task_graph.py
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
class TaskGraph:
|
|
5
|
+
|
|
6
|
+
def __init__(self, tasks):
|
|
7
|
+
|
|
8
|
+
self.tasks = {t["id"]: t for t in tasks}
|
|
9
|
+
|
|
10
|
+
self.children = defaultdict(list)
|
|
11
|
+
self.in_degree = {}
|
|
12
|
+
|
|
13
|
+
for t in tasks:
|
|
14
|
+
|
|
15
|
+
tid = t["id"]
|
|
16
|
+
deps = t.get("depends_on", [])
|
|
17
|
+
|
|
18
|
+
self.in_degree[tid] = len(deps)
|
|
19
|
+
|
|
20
|
+
for d in deps:
|
|
21
|
+
self.children[d].append(tid)
|
|
22
|
+
|
|
23
|
+
def roots(self):
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
tid
|
|
27
|
+
for tid, deg in self.in_degree.items()
|
|
28
|
+
if deg == 0
|
|
29
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# sourcebot/runtime/init_system.py
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from sourcebot.context import ContextBuilder
|
|
6
|
+
from sourcebot.config import ConfigManager
|
|
7
|
+
from sourcebot.security.policy import SecurityPolicy
|
|
8
|
+
from sourcebot.bus import EventBus
|
|
9
|
+
from sourcebot.context import MessageBuilder
|
|
10
|
+
from sourcebot.conversation.service import ConversationService
|
|
11
|
+
from sourcebot.session import JsonlSessionRepository, SessionService
|
|
12
|
+
from sourcebot.runtime.dag.scheduler import DAGScheduler
|
|
13
|
+
from sourcebot.runtime.dag.planner import DAGPlanner
|
|
14
|
+
from sourcebot.memory import FileMemoryStore, LLMConsolidator, MemoryService, WindowMemoryPolicy
|
|
15
|
+
from sourcebot.runtime.agent import AgentFactory
|
|
16
|
+
from sourcebot.tools import ToolRegistry, ShellTool, SkillListTool, SkillDetailTool, RuleListTool, RuleDetailTool
|
|
17
|
+
from sourcebot.storage import SkillStorage, RulesLoader
|
|
18
|
+
from sourcebot.docker_sandbox import DockerSandbox
|
|
19
|
+
from sourcebot.llm import LLMClientFactory
|
|
20
|
+
import logging
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
class InitSystem:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.console = Console(
|
|
26
|
+
force_terminal=sys.stdout.isatty(),
|
|
27
|
+
soft_wrap=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
config_manager = ConfigManager()
|
|
31
|
+
|
|
32
|
+
# Config
|
|
33
|
+
app_root_path = config_manager.app_root_path
|
|
34
|
+
skill_dir_path = app_root_path/"skills"
|
|
35
|
+
|
|
36
|
+
# Workspace config
|
|
37
|
+
workspace_config = config_manager.load_workspace_config()
|
|
38
|
+
|
|
39
|
+
# Workspace config error
|
|
40
|
+
if workspace_config is None:
|
|
41
|
+
self.console.print("❌ Error: Workspace configuration not found")
|
|
42
|
+
self.console.print("Please run the initialization command in the root directory of the workspace first:")
|
|
43
|
+
self.console.print("[cyan]sourcebot init_workspace[/cyan]")
|
|
44
|
+
self.console.print("Or ensure the current directory is a valid Sourcebot workspace"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
# Agent config
|
|
49
|
+
main_provider_name = workspace_config.models["main_agent"].provider
|
|
50
|
+
main_model_name = workspace_config.models["main_agent"].model
|
|
51
|
+
sub_provider_name = workspace_config.models["sub_agent"].provider
|
|
52
|
+
sub_model_name = workspace_config.models["sub_agent"].model
|
|
53
|
+
|
|
54
|
+
# Host working path
|
|
55
|
+
host_workspace = Path(workspace_config.project_root)
|
|
56
|
+
|
|
57
|
+
# Docker working path
|
|
58
|
+
# The Docker working directory remains unchanged
|
|
59
|
+
docker_workspace = Path("/workspace")
|
|
60
|
+
# Skill storage
|
|
61
|
+
skill_storage = SkillStorage(
|
|
62
|
+
root_skills = skill_dir_path,
|
|
63
|
+
builtin_skills = host_workspace/"skills" # The work directory skill path should not appear in the prompt.
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Rules loader
|
|
67
|
+
rules_loader = RulesLoader(app_root_path)
|
|
68
|
+
|
|
69
|
+
# Context builder
|
|
70
|
+
# 🔴 Note that the host machine's workspace address should not appear in the prompt; it should always be docker_workspace.
|
|
71
|
+
context_builder = ContextBuilder(
|
|
72
|
+
workspace = docker_workspace,
|
|
73
|
+
skill_storage = skill_storage,
|
|
74
|
+
rules_loader = rules_loader,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Message builder
|
|
78
|
+
message_builder = MessageBuilder(context_builder)
|
|
79
|
+
|
|
80
|
+
# Security policy
|
|
81
|
+
# TODO: Security policy not yet implemented
|
|
82
|
+
security_policy = SecurityPolicy()
|
|
83
|
+
|
|
84
|
+
# Docker sandbox
|
|
85
|
+
self.docker_sandbox = DockerSandbox(
|
|
86
|
+
image = workspace_config.docker_image,
|
|
87
|
+
host_workspace = str(host_workspace)
|
|
88
|
+
)
|
|
89
|
+
# ====================
|
|
90
|
+
# Tool registry
|
|
91
|
+
# ====================
|
|
92
|
+
tools = ToolRegistry()
|
|
93
|
+
# Skill
|
|
94
|
+
tools.register(
|
|
95
|
+
SkillListTool(
|
|
96
|
+
skill_storage = skill_storage,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
tools.register(
|
|
100
|
+
SkillDetailTool(
|
|
101
|
+
skill_storage = skill_storage,
|
|
102
|
+
host_workspace = host_workspace,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
# Rule
|
|
106
|
+
tools.register(
|
|
107
|
+
RuleListTool(
|
|
108
|
+
rules_loader = rules_loader,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
tools.register(
|
|
112
|
+
RuleDetailTool(
|
|
113
|
+
rules_loader = rules_loader,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
# Shell
|
|
117
|
+
tools.register(
|
|
118
|
+
ShellTool(
|
|
119
|
+
sandbox = self.docker_sandbox,
|
|
120
|
+
timeout = 100,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Message bus
|
|
125
|
+
# TODO: Not yet enabled
|
|
126
|
+
self.bus = EventBus()
|
|
127
|
+
|
|
128
|
+
# Agent factory
|
|
129
|
+
agent_factory = AgentFactory(
|
|
130
|
+
bus = self.bus,
|
|
131
|
+
tools = tools,
|
|
132
|
+
message_builder = message_builder,
|
|
133
|
+
main_provider_name = main_provider_name,
|
|
134
|
+
main_model_name = main_model_name,
|
|
135
|
+
config_manager = config_manager,
|
|
136
|
+
policy = security_policy,
|
|
137
|
+
max_iterations = 40,
|
|
138
|
+
sub_provider_name = sub_provider_name,
|
|
139
|
+
sub_model_name = sub_model_name,
|
|
140
|
+
)
|
|
141
|
+
# DAG planner
|
|
142
|
+
main_llm = LLMClientFactory.create_client(
|
|
143
|
+
config_manager = config_manager,
|
|
144
|
+
provider_name = main_provider_name,
|
|
145
|
+
model_name = main_model_name
|
|
146
|
+
)
|
|
147
|
+
self.dag_planner = DAGPlanner(main_llm, context_builder)
|
|
148
|
+
# DAG scheduler
|
|
149
|
+
self.dag_scheduler = DAGScheduler(
|
|
150
|
+
agent_factory = agent_factory,
|
|
151
|
+
workspace = host_workspace
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Main agent runtime
|
|
156
|
+
main_agent = agent_factory.build_main_agent()
|
|
157
|
+
|
|
158
|
+
# Memory service
|
|
159
|
+
file_memory_store = FileMemoryStore(host_workspace)
|
|
160
|
+
window_memory_policy = WindowMemoryPolicy()
|
|
161
|
+
llm_consolidator = LLMConsolidator(main_llm)
|
|
162
|
+
memory_service = MemoryService(
|
|
163
|
+
store = file_memory_store,
|
|
164
|
+
policy = window_memory_policy,
|
|
165
|
+
consolidator = llm_consolidator
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Session service
|
|
169
|
+
session_repository = JsonlSessionRepository(host_workspace)
|
|
170
|
+
session_service = SessionService(session_repository)
|
|
171
|
+
|
|
172
|
+
# Conversation service
|
|
173
|
+
self.conversation_service = ConversationService(
|
|
174
|
+
session_service = session_service,
|
|
175
|
+
memory_service = memory_service,
|
|
176
|
+
agent = main_agent,
|
|
177
|
+
message_builder = message_builder,
|
|
178
|
+
bus = self.bus
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# sourcebot/runtime/tool_executor.py
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
class ToolExecutor:
|
|
5
|
+
|
|
6
|
+
def __init__(self, tools, timeout = 60, retries = 2):
|
|
7
|
+
self.tools = tools
|
|
8
|
+
self.timeout = timeout
|
|
9
|
+
self.retries = retries
|
|
10
|
+
|
|
11
|
+
async def execute(self, name: str, args):
|
|
12
|
+
|
|
13
|
+
last_error = None
|
|
14
|
+
|
|
15
|
+
for attempt in range(self.retries + 1):
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
|
|
19
|
+
return await asyncio.wait_for(
|
|
20
|
+
self.tools.execute(name, args),
|
|
21
|
+
timeout = self.timeout
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
except asyncio.TimeoutError:
|
|
25
|
+
last_error = f"Tool {name} timed out"
|
|
26
|
+
|
|
27
|
+
except Exception as e:
|
|
28
|
+
last_error = str(e)
|
|
29
|
+
|
|
30
|
+
return f"Tool failed after retries: {last_error}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# domain/security/policy.py
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
RULES_DB = [
|
|
4
|
+
{"pattern": "eval(", "safe_replacement": "# eval removed for safety"}
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
class SecurityPolicy:
|
|
8
|
+
# TODO: To be implemented
|
|
9
|
+
def apply_policy(self, code: str) -> str:
|
|
10
|
+
safe_code = code
|
|
11
|
+
for rule in RULES_DB:
|
|
12
|
+
if rule["pattern"] in code:
|
|
13
|
+
safe_code = safe_code.replace(rule["pattern"], rule["safe_replacement"])
|
|
14
|
+
return safe_code
|
|
15
|
+
async def before_llm(self, messages: List[Dict]) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
async def before_tool(self, tool_name: str, args: Dict[str, Any]) -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
async def after_tool(self, tool_name: str, result: str) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|