algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,267 @@
1
+ """B35. Declarative LLM Flow DAG + Evaluation Harness (PromptFlow Pattern).
2
+
3
+ Parses YAML-like flow definitions into a DAG of nodes, executes them in
4
+ topological order, and runs evaluation checks against the outputs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Callable
11
+ from collections import defaultdict
12
+
13
+
14
+ class FlowError(Exception):
15
+ pass
16
+
17
+
18
+ @dataclass
19
+ class FlowNode:
20
+ id: str
21
+ kind: str # "tool", "llm", "python"
22
+ tool: str | None = None
23
+ llm: str | None = None
24
+ prompt: str | None = None
25
+ inputs: dict[str, Any] = field(default_factory=dict)
26
+ source_code: str | None = None
27
+
28
+
29
+ @dataclass
30
+ class FlowEval:
31
+ name: str
32
+ kind: str # "assert_contains", "assert_not_empty", "custom"
33
+ expected: Any = None
34
+ check_fn: Callable[[str], bool] | None = None
35
+
36
+
37
+ @dataclass
38
+ class FlowDefinition:
39
+ name: str
40
+ inputs: dict[str, Any] = field(default_factory=dict)
41
+ nodes: list[FlowNode] = field(default_factory=list)
42
+ outputs: dict[str, str] = field(default_factory=dict)
43
+ evals: list[FlowEval] = field(default_factory=list)
44
+
45
+
46
+ @dataclass
47
+ class FlowTrace:
48
+ node_id: str
49
+ inputs: dict[str, Any]
50
+ output: str
51
+ duration_ms: float = 0.0
52
+ error: str | None = None
53
+
54
+
55
+ @dataclass
56
+ class FlowResult:
57
+ outputs: dict[str, Any]
58
+ traces: list[FlowTrace] = field(default_factory=list)
59
+ eval_results: dict[str, bool] = field(default_factory=dict)
60
+ success: bool = True
61
+
62
+
63
+ # ── parser ────────────────────────────────────────────────────────────
64
+
65
+
66
+ def parse_flow(data: dict) -> FlowDefinition:
67
+ """Parse a dict (from YAML/JSON) into a FlowDefinition."""
68
+ name = data.get("name", "unnamed")
69
+ inputs = data.get("inputs", {})
70
+ nodes = []
71
+ for nd in data.get("nodes", []):
72
+ nodes.append(FlowNode(
73
+ id=nd["id"],
74
+ kind=nd.get("kind", "tool"),
75
+ tool=nd.get("tool"),
76
+ llm=nd.get("llm"),
77
+ prompt=nd.get("prompt"),
78
+ inputs=nd.get("inputs", {}),
79
+ source_code=nd.get("source"),
80
+ ))
81
+ outputs = data.get("outputs", {})
82
+ evals = []
83
+ for ev in data.get("evals", []):
84
+ evals.append(FlowEval(
85
+ name=ev["name"],
86
+ kind=ev.get("kind", "assert_contains"),
87
+ expected=ev.get("expected"),
88
+ check_fn=ev.get("check_fn"),
89
+ ))
90
+ return FlowDefinition(name=name, inputs=inputs, nodes=nodes, outputs=outputs, evals=evals)
91
+
92
+
93
+ # ── DAG validation ────────────────────────────────────────────────────
94
+
95
+
96
+ def _build_adjacency(flow: FlowDefinition) -> tuple[dict[str, list[str]], dict[str, int]]:
97
+ """Build adjacency list and in-degree map from ${node.output} references."""
98
+ adj: dict[str, list[str]] = defaultdict(list)
99
+ in_deg: dict[str, int] = defaultdict(int)
100
+ node_ids = {n.id for n in flow.nodes}
101
+ for n in flow.nodes:
102
+ in_deg.setdefault(n.id, 0)
103
+ for val in n.inputs.values():
104
+ if isinstance(val, str) and "${" in val:
105
+ ref = _extract_ref(val)
106
+ if ref and ref in node_ids:
107
+ adj[ref].append(n.id)
108
+ in_deg[n.id] += 1
109
+ return adj, in_deg
110
+
111
+
112
+ def _extract_ref(expr: str) -> str | None:
113
+ """Extract node id from ${node_id.field} or ${node_id}."""
114
+ if "${" not in expr:
115
+ return None
116
+ start = expr.index("${") + 2
117
+ end = expr.index("}", start)
118
+ ref = expr[start:end]
119
+ # strip field accessor
120
+ return ref.split(".")[0]
121
+
122
+
123
+ def detect_cycles(flow: FlowDefinition) -> list[str] | None:
124
+ """Return cycle path if found, else None."""
125
+ adj, in_deg = _build_adjacency(flow)
126
+ # Kahn's algorithm
127
+ queue = [nid for nid, d in in_deg.items() if d == 0]
128
+ visited = 0
129
+ while queue:
130
+ nid = queue.pop(0)
131
+ visited += 1
132
+ for nxt in adj[nid]:
133
+ in_deg[nxt] -= 1
134
+ if in_deg[nxt] == 0:
135
+ queue.append(nxt)
136
+ if visited != len(in_deg):
137
+ # find a cycle path via DFS
138
+ return _find_cycle_dfs(adj, set(in_deg.keys()))
139
+ return None
140
+
141
+
142
+ def _find_cycle_dfs(adj: dict[str, list[str]], nodes: set[str]) -> list[str]:
143
+ visited: set[str] = set()
144
+ stack: list[str] = []
145
+ on_stack: set[str] = set()
146
+
147
+ def dfs(u: str) -> list[str] | None:
148
+ visited.add(u)
149
+ stack.append(u)
150
+ on_stack.add(u)
151
+ for v in adj.get(u, []):
152
+ if v not in visited:
153
+ result = dfs(v)
154
+ if result:
155
+ return result
156
+ elif v in on_stack:
157
+ idx = stack.index(v)
158
+ return stack[idx:] + [v]
159
+ stack.pop()
160
+ on_stack.discard(u)
161
+ return None
162
+
163
+ for n in nodes:
164
+ if n not in visited:
165
+ result = dfs(n)
166
+ if result:
167
+ return result
168
+ return []
169
+
170
+
171
+ def topological_sort(flow: FlowDefinition) -> list[str]:
172
+ """Return node ids in execution order."""
173
+ cycle = detect_cycles(flow)
174
+ if cycle:
175
+ raise FlowError(f"Cycle detected: {' -> '.join(cycle)}")
176
+ adj, in_deg = _build_adjacency(flow)
177
+ queue = sorted([nid for nid, d in in_deg.items() if d == 0])
178
+ order: list[str] = []
179
+ while queue:
180
+ nid = queue.pop(0)
181
+ order.append(nid)
182
+ nexts = sorted(adj[nid])
183
+ for nxt in nexts:
184
+ in_deg[nxt] -= 1
185
+ if in_deg[nxt] == 0:
186
+ queue.append(nxt)
187
+ return order
188
+
189
+
190
+ # ── executor ──────────────────────────────────────────────────────────
191
+
192
+
193
+ def _resolve_value(expr: Any, context: dict[str, Any]) -> Any:
194
+ """Resolve ${node.field} references from context."""
195
+ if not isinstance(expr, str) or "${" not in expr:
196
+ return expr
197
+ start = expr.index("${") + 2
198
+ end = expr.index("}", start)
199
+ ref = expr[start:end]
200
+ parts = ref.split(".")
201
+ val = context
202
+ for p in parts:
203
+ if isinstance(val, dict):
204
+ val = val.get(p)
205
+ else:
206
+ val = getattr(val, p, None)
207
+ # replace in string
208
+ return expr.replace(f"${{{ref}}}", str(val)) if isinstance(val, (str, int, float)) else val
209
+
210
+
211
+ class FlowExecutor:
212
+ """Executes a FlowDefinition with pluggable tool/llm handlers."""
213
+
214
+ def __init__(
215
+ self,
216
+ tool_handler: Callable[[str, dict], str] | None = None,
217
+ llm_handler: Callable[[str, str, dict], str] | None = None,
218
+ ):
219
+ self.tool_handler = tool_handler or (lambda tool, inputs: f"[tool:{tool}]")
220
+ self.llm_handler = llm_handler or (lambda model, prompt, inputs: f"[llm:{model}]")
221
+
222
+ def run(self, flow: FlowDefinition, inputs: dict[str, Any] | None = None) -> FlowResult:
223
+ context: dict[str, Any] = dict(flow.inputs)
224
+ if inputs:
225
+ context.update(inputs)
226
+ traces: list[FlowTrace] = []
227
+ order = topological_sort(flow)
228
+ node_map = {n.id: n for n in flow.nodes}
229
+ for nid in order:
230
+ node = node_map[nid]
231
+ resolved = {k: _resolve_value(v, context) for k, v in node.inputs.items()}
232
+ try:
233
+ if node.kind == "tool":
234
+ output = self.tool_handler(node.tool or "", resolved)
235
+ elif node.kind == "llm":
236
+ output = self.llm_handler(node.llm or "", node.prompt or "", resolved)
237
+ elif node.kind == "python":
238
+ output = self._run_python(node.source_code or "", resolved)
239
+ else:
240
+ output = ""
241
+ context[nid] = {**resolved, "text": output}
242
+ traces.append(FlowTrace(node_id=nid, inputs=resolved, output=output))
243
+ except Exception as e:
244
+ context[nid] = {"text": "", "error": str(e)}
245
+ traces.append(FlowTrace(node_id=nid, inputs=resolved, output="", error=str(e)))
246
+ # resolve outputs
247
+ outputs = {k: _resolve_value(v, context) for k, v in flow.outputs.items()}
248
+ # run evals
249
+ eval_results: dict[str, bool] = {}
250
+ for ev in flow.evals:
251
+ text = str(outputs.get(ev.name, ""))
252
+ if ev.kind == "assert_contains":
253
+ eval_results[ev.name] = str(ev.expected) in text
254
+ elif ev.kind == "assert_not_empty":
255
+ eval_results[ev.name] = bool(text.strip())
256
+ elif ev.kind == "custom" and ev.check_fn:
257
+ eval_results[ev.name] = ev.check_fn(text)
258
+ else:
259
+ eval_results[ev.name] = False
260
+ success = all(eval_results.values()) if eval_results else True
261
+ return FlowResult(outputs=outputs, traces=traces, eval_results=eval_results, success=success)
262
+
263
+ @staticmethod
264
+ def _run_python(source: str, inputs: dict) -> str:
265
+ local_ns = dict(inputs)
266
+ exec(source, {}, local_ns)
267
+ return str(local_ns.get("result", ""))
@@ -0,0 +1,223 @@
1
+ """Gatherer State Machine — priority queue with retry and transactional state.
2
+
3
+ Borrowed from Windows Search gatherer
4
+ (C:\\ProgramData\\Microsoft\\Search\\Data\\Applications\\Windows\\Windows-gather.db,
5
+ SystemIndex_Gthr table):
6
+ The Windows Search gatherer tracks per-document crawl state including:
7
+ - Priority (0-255, UNSIGNEDBYTE)
8
+ - FailureUpdateAttempts (retry count with exponential backoff)
9
+ - CrawlNumberCrawled (version counter)
10
+ - TransactionFlags (in-progress, committed, rolled-back, retry-pending)
11
+ - LastRequestedRunTime (prevents re-scheduling)
12
+
13
+ This module implements the same pattern for the harness embedding/indexing
14
+ pipeline: files are enqueued with priority, processed in batches, retried on
15
+ failure with exponential backoff, and tracked with transactional state.
16
+
17
+ Pattern: B31 in ALGO.md.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import time
22
+ from dataclasses import dataclass, field
23
+ from enum import IntFlag
24
+ from typing import Any, Callable
25
+
26
+
27
+ class TransactionFlags(IntFlag):
28
+ NONE = 0
29
+ IN_PROGRESS = 1
30
+ COMMITTED = 2
31
+ ROLLED_BACK = 4
32
+ RETRY_PENDING = 8
33
+
34
+
35
+ @dataclass
36
+ class GathererEntry:
37
+ """A single item in the gatherer queue."""
38
+ path: str
39
+ priority: int = 5 # 0=highest, 255=lowest
40
+ failure_attempts: int = 0
41
+ crawl_number: int = 0
42
+ last_requested_run: float = 0.0
43
+ last_modified: float = 0.0
44
+ transaction_flags: TransactionFlags = TransactionFlags.NONE
45
+ metadata: dict[str, Any] = field(default_factory=dict)
46
+
47
+ MAX_ATTEMPTS: int = 3
48
+
49
+ def should_retry(self) -> bool:
50
+ return self.failure_attempts < self.MAX_ATTEMPTS
51
+
52
+ def next_retry_delay(self) -> float:
53
+ """Exponential backoff: 1s, 2s, 4s, 8s... capped at 300s."""
54
+ return min(2 ** self.failure_attempts, 300)
55
+
56
+ def priority_score(self) -> float:
57
+ """Higher = more urgent. Combines static priority + staleness."""
58
+ staleness = time.time() - self.last_modified if self.last_modified else 0
59
+ return (255 - self.priority) * 100 + min(staleness / 3600, 100)
60
+
61
+ def is_in_progress(self) -> bool:
62
+ return bool(self.transaction_flags & TransactionFlags.IN_PROGRESS)
63
+
64
+ def is_committed(self) -> bool:
65
+ return bool(self.transaction_flags & TransactionFlags.COMMITTED)
66
+
67
+ def is_rolled_back(self) -> bool:
68
+ return bool(self.transaction_flags & TransactionFlags.ROLLED_BACK)
69
+
70
+
71
+ class GathererQueue:
72
+ """Priority queue with retry, backoff, and transactional state.
73
+
74
+ Usage:
75
+ queue = GathererQueue()
76
+ queue.enqueue(GathererEntry(path="foo.py", priority=3))
77
+ batch = queue.next_batch(batch_size=50)
78
+ for entry in batch:
79
+ queue.mark_in_progress(entry)
80
+ try:
81
+ embed_file(entry.path)
82
+ queue.mark_success(entry)
83
+ except Exception:
84
+ queue.mark_failure(entry)
85
+ """
86
+
87
+ def __init__(self) -> None:
88
+ self.entries: dict[str, GathererEntry] = {}
89
+
90
+ # --- enqueue / dequeue ------------------------------------------------
91
+
92
+ def enqueue(self, entry: GathererEntry) -> None:
93
+ """Add or update an entry in the queue."""
94
+ existing = self.entries.get(entry.path)
95
+ if existing:
96
+ # Preserve retry count and crawl number on re-enqueue
97
+ entry.failure_attempts = existing.failure_attempts
98
+ entry.crawl_number = existing.crawl_number
99
+ self.entries[entry.path] = entry
100
+
101
+ def enqueue_many(self, paths: list[str], priority: int = 5) -> None:
102
+ """Bulk enqueue with uniform priority."""
103
+ for path in paths:
104
+ self.enqueue(GathererEntry(path=path, priority=priority))
105
+
106
+ # --- batch selection --------------------------------------------------
107
+
108
+ def next_batch(
109
+ self, batch_size: int = 50, *, respect_backoff: bool = True,
110
+ ) -> list[GathererEntry]:
111
+ """Get next batch sorted by priority score (descending).
112
+
113
+ Filters out:
114
+ - In-progress entries (already being processed)
115
+ - Entries that exceeded max retry attempts (rolled back)
116
+ - Entries whose retry delay hasn't elapsed (unless respect_backoff=False)
117
+ """
118
+ now = time.time()
119
+ eligible: list[GathererEntry] = []
120
+ for entry in self.entries.values():
121
+ if entry.is_in_progress():
122
+ continue
123
+ if entry.is_rolled_back():
124
+ continue
125
+ if not entry.should_retry():
126
+ continue
127
+ # Check retry backoff delay
128
+ if respect_backoff and (entry.transaction_flags & TransactionFlags.RETRY_PENDING):
129
+ if now - entry.last_requested_run < entry.next_retry_delay():
130
+ continue
131
+ eligible.append(entry)
132
+
133
+ eligible.sort(key=lambda e: e.priority_score(), reverse=True)
134
+ return eligible[:batch_size]
135
+
136
+ # --- state transitions ------------------------------------------------
137
+
138
+ def mark_in_progress(self, entry: GathererEntry) -> None:
139
+ entry.transaction_flags = TransactionFlags.IN_PROGRESS
140
+ entry.last_requested_run = time.time()
141
+
142
+ def mark_success(self, entry: GathererEntry) -> None:
143
+ entry.transaction_flags = TransactionFlags.COMMITTED
144
+ entry.failure_attempts = 0
145
+ entry.crawl_number += 1
146
+
147
+ def mark_failure(self, entry: GathererEntry) -> None:
148
+ entry.failure_attempts += 1
149
+ if entry.should_retry():
150
+ entry.transaction_flags = TransactionFlags.RETRY_PENDING
151
+ else:
152
+ entry.transaction_flags = TransactionFlags.ROLLED_BACK
153
+
154
+ def remove(self, path: str) -> None:
155
+ """Remove a completed entry from the queue."""
156
+ self.entries.pop(path, None)
157
+
158
+ # --- queries ----------------------------------------------------------
159
+
160
+ def pending_count(self) -> int:
161
+ """Number of entries not yet committed or rolled back."""
162
+ return sum(
163
+ 1 for e in self.entries.values()
164
+ if not e.is_committed() and not e.is_rolled_back()
165
+ )
166
+
167
+ def committed_count(self) -> int:
168
+ return sum(1 for e in self.entries.values() if e.is_committed())
169
+
170
+ def failed_count(self) -> int:
171
+ return sum(1 for e in self.entries.values() if e.is_rolled_back())
172
+
173
+ def retry_count(self) -> int:
174
+ return sum(
175
+ 1 for e in self.entries.values()
176
+ if e.transaction_flags & TransactionFlags.RETRY_PENDING
177
+ )
178
+
179
+ def stats(self) -> dict[str, int]:
180
+ return {
181
+ "total": len(self.entries),
182
+ "pending": self.pending_count(),
183
+ "committed": self.committed_count(),
184
+ "failed": self.failed_count(),
185
+ "retry": self.retry_count(),
186
+ }
187
+
188
+ # --- processing loop --------------------------------------------------
189
+
190
+ def process(
191
+ self,
192
+ processor: Callable[[GathererEntry], Any],
193
+ *,
194
+ batch_size: int = 50,
195
+ max_rounds: int = 100,
196
+ respect_backoff: bool = True,
197
+ ) -> dict[str, int]:
198
+ """Process the queue until empty or max_rounds reached.
199
+
200
+ Args:
201
+ processor: Function that takes a GathererEntry and processes it.
202
+ Raises on failure.
203
+ batch_size: Max entries per batch.
204
+ max_rounds: Safety limit to prevent infinite loops.
205
+ respect_backoff: If False, skip retry backoff delay (for sync loops).
206
+
207
+ Returns:
208
+ Stats dict with committed/failed/retry counts.
209
+ """
210
+ rounds = 0
211
+ while rounds < max_rounds:
212
+ batch = self.next_batch(batch_size, respect_backoff=respect_backoff)
213
+ if not batch:
214
+ break
215
+ for entry in batch:
216
+ self.mark_in_progress(entry)
217
+ try:
218
+ processor(entry)
219
+ self.mark_success(entry)
220
+ except Exception:
221
+ self.mark_failure(entry)
222
+ rounds += 1
223
+ return self.stats()
@@ -0,0 +1,98 @@
1
+ """B82. Golden Master: Characterization Tests.
2
+
3
+ Capture current behavior before refactoring. Verify no regressions after.
4
+ Source: CCASP pattern.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any, Callable
13
+
14
+
15
+ @dataclass
16
+ class GoldenMaster:
17
+ name: str
18
+ inputs: list[Any] = field(default_factory=list)
19
+ expected_outputs: list[Any] = field(default_factory=list)
20
+ snapshots: dict[str, str] = field(default_factory=dict) # input_hash → output_hash
21
+
22
+ def capture(self, input_data: Any, output: Any) -> None:
23
+ """Capture a golden master snapshot."""
24
+ input_hash = self._hash(input_data)
25
+ output_hash = self._hash(output)
26
+ self.inputs.append(input_data)
27
+ self.expected_outputs.append(output)
28
+ self.snapshots[input_hash] = output_hash
29
+
30
+ def verify(self, input_data: Any, output: Any) -> bool:
31
+ """Verify output matches golden master."""
32
+ input_hash = self._hash(input_data)
33
+ output_hash = self._hash(output)
34
+ return self.snapshots.get(input_hash) == output_hash
35
+
36
+ def verify_all(self, run_fn: Callable[[Any], Any]) -> list[bool]:
37
+ """Verify all captured inputs against current behavior."""
38
+ results: list[bool] = []
39
+ for input_data, expected in zip(self.inputs, self.expected_outputs):
40
+ actual = run_fn(input_data)
41
+ results.append(self.verify(input_data, actual))
42
+ return results
43
+
44
+ @staticmethod
45
+ def _hash(data: Any) -> str:
46
+ if isinstance(data, str):
47
+ return hashlib.sha256(data.encode()).hexdigest()[:16]
48
+ return hashlib.sha256(json.dumps(data, default=str, sort_keys=True).encode()).hexdigest()[:16]
49
+
50
+ def save(self, path: Path) -> None:
51
+ """Save golden master to file."""
52
+ path.write_text(json.dumps({
53
+ "name": self.name,
54
+ "snapshots": self.snapshots,
55
+ }, indent=2), encoding="utf-8")
56
+
57
+ @classmethod
58
+ def load(cls, path: Path) -> "GoldenMaster":
59
+ """Load golden master from file."""
60
+ data = json.loads(path.read_text(encoding="utf-8"))
61
+ gm = cls(name=data["name"])
62
+ gm.snapshots = data.get("snapshots", {})
63
+ return gm
64
+
65
+
66
+ class GoldenMasterRunner:
67
+ """Run golden master characterization tests."""
68
+
69
+ def __init__(self) -> None:
70
+ self._masters: dict[str, GoldenMaster] = {}
71
+
72
+ def create(self, name: str) -> GoldenMaster:
73
+ gm = GoldenMaster(name=name)
74
+ self._masters[name] = gm
75
+ return gm
76
+
77
+ def get(self, name: str) -> GoldenMaster | None:
78
+ return self._masters.get(name)
79
+
80
+ def run_all(self, run_fn: Callable[[Any], Any]) -> dict[str, list[bool]]:
81
+ """Run all golden masters against current behavior."""
82
+ return {name: gm.verify_all(run_fn) for name, gm in self._masters.items()}
83
+
84
+ def regression_report(self, run_fn: Callable[[Any], Any]) -> str:
85
+ """Generate a report of any regressions."""
86
+ lines: list[str] = ["Golden Master Regression Report", ""]
87
+ all_pass = True
88
+ for name, gm in self._masters.items():
89
+ results = gm.verify_all(run_fn)
90
+ passed = sum(results)
91
+ total = len(results)
92
+ status = "PASS" if passed == total else "FAIL"
93
+ if passed != total:
94
+ all_pass = False
95
+ lines.append(f" {name}: {passed}/{total} — {status}")
96
+ lines.append("")
97
+ lines.append("ALL PASS" if all_pass else "REGRESSIONS DETECTED")
98
+ return "\n".join(lines)