polymath-agent 0.4.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.
- polymath/__init__.py +2 -0
- polymath/adapters/__init__.py +7 -0
- polymath/adapters/base.py +175 -0
- polymath/adapters/claude.py +280 -0
- polymath/adapters/gemini.py +186 -0
- polymath/adapters/ollama.py +117 -0
- polymath/adapters/openai_adapter.py +168 -0
- polymath/bootstrap.py +159 -0
- polymath/command_registry.py +41 -0
- polymath/command_service.py +572 -0
- polymath/compressor.py +90 -0
- polymath/config.py +293 -0
- polymath/context_manager.py +76 -0
- polymath/context_store.py +336 -0
- polymath/detector.py +442 -0
- polymath/domain.py +78 -0
- polymath/execution_service.py +325 -0
- polymath/main.py +1293 -0
- polymath/memory/__init__.py +15 -0
- polymath/memory/chunker.py +6 -0
- polymath/memory/embedder.py +179 -0
- polymath/memory/migrate.py +2 -0
- polymath/memory/retriever.py +2 -0
- polymath/memory/store.py +9 -0
- polymath/memory/sync.py +2 -0
- polymath/memory/writer.py +9 -0
- polymath/model_policy.py +172 -0
- polymath/orchestrator/__init__.py +68 -0
- polymath/orchestrator/attempt_ledger.py +34 -0
- polymath/orchestrator/ensemble.py +229 -0
- polymath/orchestrator/fanout.py +322 -0
- polymath/orchestrator/output_policy.py +61 -0
- polymath/orchestrator/race.py +311 -0
- polymath/orchestrator/run_controller.py +91 -0
- polymath/orchestrator/speculative_review.py +120 -0
- polymath/orchestrator/state_responder.py +184 -0
- polymath/orchestrator/worker_pool.py +37 -0
- polymath/permissions.py +82 -0
- polymath/pipeline.py +700 -0
- polymath/project_config.py +229 -0
- polymath/project_runtime.py +109 -0
- polymath/router.py +127 -0
- polymath/setup_wizard.py +106 -0
- polymath/slash_commands.py +566 -0
- polymath/subagents.py +486 -0
- polymath/tools.py +333 -0
- polymath/ui_state.py +84 -0
- polymath/workspace.py +66 -0
- polymath_agent-0.4.0.dist-info/METADATA +693 -0
- polymath_agent-0.4.0.dist-info/RECORD +54 -0
- polymath_agent-0.4.0.dist-info/WHEEL +5 -0
- polymath_agent-0.4.0.dist-info/entry_points.txt +2 -0
- polymath_agent-0.4.0.dist-info/licenses/LICENSE +21 -0
- polymath_agent-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
from polymath.adapters.base import BaseAdapter
|
|
7
|
+
from polymath.model_policy import adapter_for, try_fallback_adapter
|
|
8
|
+
from polymath.orchestrator import AttemptLedger, OutputPolicy, WorkerPool
|
|
9
|
+
from polymath.pipeline import Pipeline, quick_answer
|
|
10
|
+
from polymath.router import select_primary
|
|
11
|
+
from polymath.config import TaskType
|
|
12
|
+
from polymath.domain import PermissionRequest
|
|
13
|
+
from polymath.router import select_reviewer
|
|
14
|
+
from polymath.subagents import run_subagent, select_auto_subagents
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ExecutionCallbacks:
|
|
19
|
+
on_step: Callable[[Any, str], None]
|
|
20
|
+
on_token: Callable[[str], None]
|
|
21
|
+
on_ask: Callable[[str], Awaitable[str] | str]
|
|
22
|
+
on_permission: Callable[[PermissionRequest], Awaitable[bool] | bool]
|
|
23
|
+
on_plan_approval: Callable[[str], Awaitable[str] | str]
|
|
24
|
+
on_model_switch: Callable[[Any], None] | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ExecutionResult:
|
|
29
|
+
last_output: str = ""
|
|
30
|
+
usage: dict[str, tuple[int, int, int]] | None = None
|
|
31
|
+
summary_line: str = ""
|
|
32
|
+
notify_message: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _adapter_usage(adapter: BaseAdapter | None, model_info: Any) -> tuple[int, int, int]:
|
|
36
|
+
if not adapter or not model_info:
|
|
37
|
+
return (0, 0, 0)
|
|
38
|
+
return (
|
|
39
|
+
getattr(adapter, "_last_input_tokens", 0),
|
|
40
|
+
getattr(adapter, "_last_output_tokens", 0),
|
|
41
|
+
getattr(model_info, "context_window", 0),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_failover(
|
|
46
|
+
registry: list,
|
|
47
|
+
primary_info,
|
|
48
|
+
primary_adapter: BaseAdapter | None,
|
|
49
|
+
cfg: dict,
|
|
50
|
+
profile: str,
|
|
51
|
+
excluded: set[str] | None = None,
|
|
52
|
+
) -> tuple[object | None, BaseAdapter | None]:
|
|
53
|
+
"""Pick the best non-primary available model as a race partner."""
|
|
54
|
+
if not primary_info:
|
|
55
|
+
return None, None
|
|
56
|
+
skip = set(excluded or set())
|
|
57
|
+
skip.add(primary_info.id)
|
|
58
|
+
candidates = [m for m in registry if m.available and m.id not in skip]
|
|
59
|
+
if not candidates:
|
|
60
|
+
return None, None
|
|
61
|
+
pick = select_primary(candidates, TaskType.GENERAL, profile)
|
|
62
|
+
if not pick or pick.id == primary_info.id:
|
|
63
|
+
return None, None
|
|
64
|
+
adapter = adapter_for(pick.provider, cfg)
|
|
65
|
+
if not adapter:
|
|
66
|
+
return None, None
|
|
67
|
+
return pick, adapter
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _refresh_reviewer(
|
|
71
|
+
registry: list[Any],
|
|
72
|
+
primary_info: Any,
|
|
73
|
+
primary_adapter: BaseAdapter | None,
|
|
74
|
+
reviewer_info: Any,
|
|
75
|
+
reviewer_adapter: BaseAdapter | None,
|
|
76
|
+
cfg: dict,
|
|
77
|
+
preferred_profile: str,
|
|
78
|
+
) -> tuple[Any, BaseAdapter | None]:
|
|
79
|
+
if not primary_info:
|
|
80
|
+
return reviewer_info, reviewer_adapter
|
|
81
|
+
if reviewer_info and reviewer_info.id != primary_info.id and reviewer_adapter:
|
|
82
|
+
return reviewer_info, reviewer_adapter
|
|
83
|
+
next_reviewer = select_reviewer(registry, primary_info, preferred_profile)
|
|
84
|
+
if not next_reviewer:
|
|
85
|
+
return primary_info, primary_adapter
|
|
86
|
+
from polymath.model_policy import adapter_for
|
|
87
|
+
next_adapter = adapter_for(next_reviewer.provider, cfg)
|
|
88
|
+
if not next_adapter:
|
|
89
|
+
return primary_info, primary_adapter
|
|
90
|
+
return next_reviewer, next_adapter
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def run_ask_flow(
|
|
94
|
+
*,
|
|
95
|
+
text: str,
|
|
96
|
+
session_id: str,
|
|
97
|
+
active_project: str,
|
|
98
|
+
task_type: Any,
|
|
99
|
+
primary_info: Any,
|
|
100
|
+
primary_adapter: Any,
|
|
101
|
+
summarizer_info: Any,
|
|
102
|
+
summarizer_adapter: Any,
|
|
103
|
+
profile: str,
|
|
104
|
+
cfg: dict,
|
|
105
|
+
registry: list[Any],
|
|
106
|
+
print_line: Callable[[str], None],
|
|
107
|
+
suppress_echo: Any,
|
|
108
|
+
token_printer: Callable[[str], None],
|
|
109
|
+
tried_model_ids: set[str] | None = None,
|
|
110
|
+
on_model_switch: Callable[[Any], None] | None = None,
|
|
111
|
+
attempt_ledger: AttemptLedger | None = None,
|
|
112
|
+
output_policy: OutputPolicy | None = None,
|
|
113
|
+
) -> ExecutionResult:
|
|
114
|
+
policy = output_policy or OutputPolicy()
|
|
115
|
+
ledger = attempt_ledger or AttemptLedger()
|
|
116
|
+
attempted = set(tried_model_ids or set())
|
|
117
|
+
phase = "chat"
|
|
118
|
+
while primary_info and primary_adapter:
|
|
119
|
+
attempted.add(primary_info.id)
|
|
120
|
+
ledger.note_attempt(phase, primary_info.id)
|
|
121
|
+
try:
|
|
122
|
+
failover_info, failover_adapter = _resolve_failover(
|
|
123
|
+
registry, primary_info, primary_adapter, cfg, profile, excluded=attempted,
|
|
124
|
+
)
|
|
125
|
+
with suppress_echo():
|
|
126
|
+
await quick_answer(
|
|
127
|
+
session_id=session_id,
|
|
128
|
+
question=text,
|
|
129
|
+
primary_adapter=primary_adapter,
|
|
130
|
+
primary_model=primary_info,
|
|
131
|
+
active_project=active_project,
|
|
132
|
+
task_type=task_type,
|
|
133
|
+
summarizer_adapter=summarizer_adapter,
|
|
134
|
+
summarizer_model=summarizer_info.id if summarizer_info else "",
|
|
135
|
+
on_token=token_printer,
|
|
136
|
+
cfg=cfg,
|
|
137
|
+
failover_adapter=failover_adapter,
|
|
138
|
+
failover_model=failover_info,
|
|
139
|
+
)
|
|
140
|
+
usage = {
|
|
141
|
+
primary_info.display_name: (
|
|
142
|
+
getattr(primary_adapter, "_last_input_tokens", 0),
|
|
143
|
+
getattr(primary_adapter, "_last_output_tokens", 0),
|
|
144
|
+
primary_info.context_window,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return ExecutionResult(usage=usage)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
decision = policy.classify_failure(e)
|
|
150
|
+
ledger.note_failure(phase, primary_info.id, decision.kind, retryable=decision.retryable)
|
|
151
|
+
if not decision.retryable:
|
|
152
|
+
raise
|
|
153
|
+
print_line(f"\n{decision.user_message}")
|
|
154
|
+
fallback = try_fallback_adapter(registry, primary_info, profile, cfg, attempted)
|
|
155
|
+
if fallback:
|
|
156
|
+
primary_info, primary_adapter = fallback
|
|
157
|
+
if on_model_switch:
|
|
158
|
+
on_model_switch(primary_info)
|
|
159
|
+
print_line(f"[dim]Falling back to {primary_info.display_name}[/dim]\n")
|
|
160
|
+
continue
|
|
161
|
+
for line in policy.final_failure_lines(decision, e):
|
|
162
|
+
if line:
|
|
163
|
+
print_line(line)
|
|
164
|
+
return ExecutionResult()
|
|
165
|
+
return ExecutionResult()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def run_pipeline_flow(
|
|
169
|
+
*,
|
|
170
|
+
text: str,
|
|
171
|
+
complexity: Any,
|
|
172
|
+
task_type: Any,
|
|
173
|
+
verify: bool,
|
|
174
|
+
simplify: bool,
|
|
175
|
+
session_id: str,
|
|
176
|
+
active_project: str,
|
|
177
|
+
primary_info: Any,
|
|
178
|
+
primary_adapter: Any,
|
|
179
|
+
reviewer_info: Any,
|
|
180
|
+
reviewer_adapter: Any,
|
|
181
|
+
summarizer_info: Any,
|
|
182
|
+
summarizer_adapter: Any,
|
|
183
|
+
profile: str,
|
|
184
|
+
cfg: dict,
|
|
185
|
+
registry: list[Any],
|
|
186
|
+
subs: dict[str, Any],
|
|
187
|
+
callbacks: ExecutionCallbacks,
|
|
188
|
+
print_line: Callable[[str], None],
|
|
189
|
+
render_panel: Callable[[str, str], None],
|
|
190
|
+
suppress_echo: Any,
|
|
191
|
+
session_cost: Callable[[str], tuple[int, float]],
|
|
192
|
+
tried_model_ids: set[str] | None = None,
|
|
193
|
+
attempt_ledger: AttemptLedger | None = None,
|
|
194
|
+
output_policy: OutputPolicy | None = None,
|
|
195
|
+
) -> ExecutionResult:
|
|
196
|
+
policy = output_policy or OutputPolicy()
|
|
197
|
+
ledger = attempt_ledger or AttemptLedger()
|
|
198
|
+
workers = WorkerPool()
|
|
199
|
+
attempted = set(tried_model_ids or set())
|
|
200
|
+
phase = "pipeline"
|
|
201
|
+
while primary_info and primary_adapter:
|
|
202
|
+
attempted.add(primary_info.id)
|
|
203
|
+
ledger.note_attempt(phase, primary_info.id)
|
|
204
|
+
try:
|
|
205
|
+
failover_info, failover_adapter = _resolve_failover(
|
|
206
|
+
registry, primary_info, primary_adapter, cfg, profile, excluded=attempted,
|
|
207
|
+
)
|
|
208
|
+
pipeline = Pipeline(
|
|
209
|
+
session_id=session_id,
|
|
210
|
+
primary_adapter=primary_adapter,
|
|
211
|
+
primary_model=primary_info,
|
|
212
|
+
reviewer_adapter=reviewer_adapter,
|
|
213
|
+
reviewer_model=reviewer_info,
|
|
214
|
+
summarizer_adapter=summarizer_adapter,
|
|
215
|
+
summarizer_model=summarizer_info.id if summarizer_info else "",
|
|
216
|
+
review_rounds=cfg.get("review_rounds", 2),
|
|
217
|
+
active_project=active_project,
|
|
218
|
+
cfg=cfg,
|
|
219
|
+
failover_adapter=failover_adapter,
|
|
220
|
+
failover_model=failover_info,
|
|
221
|
+
on_step=callbacks.on_step,
|
|
222
|
+
on_token=callbacks.on_token,
|
|
223
|
+
on_ask=callbacks.on_ask,
|
|
224
|
+
on_permission=callbacks.on_permission,
|
|
225
|
+
on_plan_approval=callbacks.on_plan_approval,
|
|
226
|
+
)
|
|
227
|
+
with suppress_echo():
|
|
228
|
+
result = await pipeline.run(text, complexity, task_type, active_project=active_project)
|
|
229
|
+
last_output = result.output
|
|
230
|
+
primary_usage = _adapter_usage(primary_adapter, primary_info)
|
|
231
|
+
reviewer_usage = _adapter_usage(reviewer_adapter, reviewer_info)
|
|
232
|
+
|
|
233
|
+
subagent_flags = {"verify": verify, "simplify": simplify}
|
|
234
|
+
subagent_configs = select_auto_subagents(task_type, subagent_flags, subs)
|
|
235
|
+
concurrent_subagents = [sa for sa in subagent_configs if sa.name != "verify" and sa.max_iterations <= 1]
|
|
236
|
+
sequential_subagents = [sa for sa in subagent_configs if sa not in concurrent_subagents]
|
|
237
|
+
for sa_config in subagent_configs:
|
|
238
|
+
print_line(f"\n[dim]▸ subagent: {sa_config.name}[/dim]")
|
|
239
|
+
|
|
240
|
+
async def _run_single_subagent(sa_config):
|
|
241
|
+
return sa_config, await run_subagent(
|
|
242
|
+
config=sa_config,
|
|
243
|
+
pipeline_output=result.output,
|
|
244
|
+
primary_adapter=primary_adapter,
|
|
245
|
+
primary_model=primary_info,
|
|
246
|
+
session_id=session_id,
|
|
247
|
+
on_token=lambda _: None,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
subagent_results = await workers.gather(
|
|
251
|
+
[lambda sa_config=sa_config: _run_single_subagent(sa_config) for sa_config in concurrent_subagents]
|
|
252
|
+
)
|
|
253
|
+
for item in subagent_results:
|
|
254
|
+
if isinstance(item, Exception):
|
|
255
|
+
print_line(f"[red]Subagent failed: {item}[/red]")
|
|
256
|
+
continue
|
|
257
|
+
sa_config, sa_result = item
|
|
258
|
+
if sa_result.output:
|
|
259
|
+
render_panel(sa_result.output, sa_config.name)
|
|
260
|
+
if sa_result.iterations > 1:
|
|
261
|
+
status = "passed" if sa_result.success else "failed"
|
|
262
|
+
print_line(f"[dim]{sa_config.name}: {sa_result.iterations} iterations — {status}[/dim]")
|
|
263
|
+
last_output = sa_result.output or last_output
|
|
264
|
+
|
|
265
|
+
for sa_config in sequential_subagents:
|
|
266
|
+
try:
|
|
267
|
+
_, sa_result = await _run_single_subagent(sa_config)
|
|
268
|
+
if sa_result.output:
|
|
269
|
+
render_panel(sa_result.output, sa_config.name)
|
|
270
|
+
if sa_result.iterations > 1:
|
|
271
|
+
status = "passed" if sa_result.success else "failed"
|
|
272
|
+
print_line(f"[dim]{sa_config.name}: {sa_result.iterations} iterations — {status}[/dim]")
|
|
273
|
+
last_output = sa_result.output or last_output
|
|
274
|
+
except Exception as e:
|
|
275
|
+
print_line(f"[red]Subagent {sa_config.name} failed: {e}[/red]")
|
|
276
|
+
|
|
277
|
+
usage: dict[str, tuple[int, int, int]] = {
|
|
278
|
+
primary_info.display_name: primary_usage
|
|
279
|
+
}
|
|
280
|
+
if reviewer_adapter and reviewer_info and reviewer_info.id != primary_info.id:
|
|
281
|
+
usage[reviewer_info.display_name] = reviewer_usage
|
|
282
|
+
tokens, _ = session_cost(session_id)
|
|
283
|
+
token_str = f" | ~{tokens} tokens" if tokens > 0 else ""
|
|
284
|
+
summary_line = (
|
|
285
|
+
f"[dim]steps: {' → '.join(result.steps_run)} "
|
|
286
|
+
f"| primary: {result.primary_model}"
|
|
287
|
+
+ (f" | reviewer: {result.reviewer_model}" if result.reviewer_model else "")
|
|
288
|
+
+ token_str
|
|
289
|
+
+ "[/dim]"
|
|
290
|
+
)
|
|
291
|
+
return ExecutionResult(
|
|
292
|
+
last_output=last_output,
|
|
293
|
+
usage=usage,
|
|
294
|
+
summary_line=summary_line,
|
|
295
|
+
notify_message=f"Done — {result.primary_model}",
|
|
296
|
+
)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
decision = policy.classify_failure(e)
|
|
299
|
+
ledger.note_failure(phase, primary_info.id, decision.kind, retryable=decision.retryable)
|
|
300
|
+
if not decision.retryable:
|
|
301
|
+
raise
|
|
302
|
+
print_line(f"\n{decision.user_message}")
|
|
303
|
+
fallback = try_fallback_adapter(registry, primary_info, profile, cfg, attempted)
|
|
304
|
+
if fallback:
|
|
305
|
+
primary_info, primary_adapter = fallback
|
|
306
|
+
reviewer_info, reviewer_adapter = _refresh_reviewer(
|
|
307
|
+
registry,
|
|
308
|
+
primary_info,
|
|
309
|
+
primary_adapter,
|
|
310
|
+
reviewer_info,
|
|
311
|
+
reviewer_adapter,
|
|
312
|
+
cfg,
|
|
313
|
+
profile,
|
|
314
|
+
)
|
|
315
|
+
if callbacks.on_model_switch:
|
|
316
|
+
callbacks.on_model_switch(primary_info)
|
|
317
|
+
print_line(f"[dim]Falling back to {primary_info.display_name}[/dim]\n")
|
|
318
|
+
continue
|
|
319
|
+
for line in policy.final_failure_lines(decision, e):
|
|
320
|
+
if line:
|
|
321
|
+
print_line(line)
|
|
322
|
+
return ExecutionResult()
|
|
323
|
+
finally:
|
|
324
|
+
await workers.cancel_all()
|
|
325
|
+
return ExecutionResult()
|