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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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)
|