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.
Files changed (54) hide show
  1. polymath/__init__.py +2 -0
  2. polymath/adapters/__init__.py +7 -0
  3. polymath/adapters/base.py +175 -0
  4. polymath/adapters/claude.py +280 -0
  5. polymath/adapters/gemini.py +186 -0
  6. polymath/adapters/ollama.py +117 -0
  7. polymath/adapters/openai_adapter.py +168 -0
  8. polymath/bootstrap.py +159 -0
  9. polymath/command_registry.py +41 -0
  10. polymath/command_service.py +572 -0
  11. polymath/compressor.py +90 -0
  12. polymath/config.py +293 -0
  13. polymath/context_manager.py +76 -0
  14. polymath/context_store.py +336 -0
  15. polymath/detector.py +442 -0
  16. polymath/domain.py +78 -0
  17. polymath/execution_service.py +325 -0
  18. polymath/main.py +1293 -0
  19. polymath/memory/__init__.py +15 -0
  20. polymath/memory/chunker.py +6 -0
  21. polymath/memory/embedder.py +179 -0
  22. polymath/memory/migrate.py +2 -0
  23. polymath/memory/retriever.py +2 -0
  24. polymath/memory/store.py +9 -0
  25. polymath/memory/sync.py +2 -0
  26. polymath/memory/writer.py +9 -0
  27. polymath/model_policy.py +172 -0
  28. polymath/orchestrator/__init__.py +68 -0
  29. polymath/orchestrator/attempt_ledger.py +34 -0
  30. polymath/orchestrator/ensemble.py +229 -0
  31. polymath/orchestrator/fanout.py +322 -0
  32. polymath/orchestrator/output_policy.py +61 -0
  33. polymath/orchestrator/race.py +311 -0
  34. polymath/orchestrator/run_controller.py +91 -0
  35. polymath/orchestrator/speculative_review.py +120 -0
  36. polymath/orchestrator/state_responder.py +184 -0
  37. polymath/orchestrator/worker_pool.py +37 -0
  38. polymath/permissions.py +82 -0
  39. polymath/pipeline.py +700 -0
  40. polymath/project_config.py +229 -0
  41. polymath/project_runtime.py +109 -0
  42. polymath/router.py +127 -0
  43. polymath/setup_wizard.py +106 -0
  44. polymath/slash_commands.py +566 -0
  45. polymath/subagents.py +486 -0
  46. polymath/tools.py +333 -0
  47. polymath/ui_state.py +84 -0
  48. polymath/workspace.py +66 -0
  49. polymath_agent-0.4.0.dist-info/METADATA +693 -0
  50. polymath_agent-0.4.0.dist-info/RECORD +54 -0
  51. polymath_agent-0.4.0.dist-info/WHEEL +5 -0
  52. polymath_agent-0.4.0.dist-info/entry_points.txt +2 -0
  53. polymath_agent-0.4.0.dist-info/licenses/LICENSE +21 -0
  54. 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()