agent-tco 0.0.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chinmay Kakatkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.3
2
+ Name: agent-tco
3
+ Version: 0.0.1
4
+ Summary: agent-tco: Pareto-optimal configuration of agentic workflows.
5
+ Author: Chinmay Kakatkar
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Chinmay Kakatkar
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Requires-Dist: scipy>=1.10
28
+ Requires-Python: >=3.12
29
+ Project-URL: Homepage, https://github.com/ckstash/agent-tco
30
+ Description-Content-Type: text/markdown
31
+
32
+ # agent-tco
33
+
34
+ **agent-tco** is a Python library for Pareto-optimal configuration of agentic workflows.
35
+
36
+ ---
37
+
38
+ ## Features
39
+
40
+ - **Workflow simulator** with six-class scenario taxonomy (i1–i6): happy path, retry-resolved, HITL success, HITL partial, compound recovery, aborted.
41
+ - **Grid search** — evaluates all candidate configurations with Beta credible intervals on scenario probabilities.
42
+ - **Pareto frontier** — identifies non-dominated configurations on the ACR vs. E[TCO] plane.
43
+ - **Critical step sensitivity analysis** — Proposition 1: ∂ACR/∂q_k prioritizes which step to upgrade first.
44
+ - **Tier pricing utilities** — built-in `TIER_A`, `TIER_B`, `TIER_C` constants covering frontier, mid-range, and lightweight models. Pass your own `TierCost(tier_id, cost_per_1k_tokens)` to use current pricing.
45
+ - **`StepExecutor` protocol** — plug in real LLM calls or a `Mock` for testing, no SDK dependency.
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install agent-tco
53
+ ```
54
+
55
+ Built for Python 3.12 or above.
56
+
57
+ ---
58
+
59
+ ## Quick Start
60
+
61
+ ### 1. Define configurations and run the grid search
62
+
63
+ ```python
64
+ from agent_tco import (
65
+ CostParams, GridSearch,
66
+ step_config_from_tier, WorkflowConfig,
67
+ TIER_A, TIER_B, TIER_C,
68
+ )
69
+
70
+ cost_params = CostParams(
71
+ labor_rate=60.0,
72
+ review_time_hours=0.25,
73
+ manual_process_cost=50.0,
74
+ )
75
+
76
+ # j3: all Tier C
77
+ j3 = WorkflowConfig("j3", [
78
+ step_config_from_tier(k, TIER_C, acr=0.78 if k == 2 else 0.92, hitl_prob=0.20 if k == 4 else 0.05)
79
+ for k in range(1, 6)
80
+ ])
81
+
82
+ # j9: Tier B at step 2 (the bottleneck), Tier C elsewhere
83
+ j9 = WorkflowConfig("j9", [
84
+ step_config_from_tier(k, TIER_B if k == 2 else TIER_C,
85
+ acr=0.88 if k == 2 else 0.92,
86
+ hitl_prob=0.08 if k == 2 else (0.20 if k == 4 else 0.05))
87
+ for k in range(1, 6)
88
+ ])
89
+
90
+ gs = GridSearch(cost_params=cost_params, n_runs=300, seed=42)
91
+ results = gs.evaluate_all([j3, j9])
92
+ for r in results:
93
+ print(f"{r.config.config_id}: ACR={r.acr:.2f} E[TCO]=${r.expected_tco:.2f} M=${r.executive_metric:.2f}")
94
+ ```
95
+
96
+ ### 2. Compute the Pareto frontier and identify the recommended configuration
97
+
98
+ ```python
99
+ from agent_tco import pareto_frontier, rank_by_executive_metric
100
+
101
+ frontier = pareto_frontier(results)
102
+ ranked = rank_by_executive_metric(frontier)
103
+ best = ranked[0]
104
+ print(f"Recommended: {best.config.config_id} — ${best.executive_metric:.2f} per autonomous outcome")
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Interpreting the Executive Metric
110
+
111
+ The executive metric `M = E[TCO] / ACR` is the expected cost per autonomously delivered outcome. When `M < C_manual` (the cost of the manual process), the agent is economically viable at this configuration. If `M > C_manual` for every configuration on the Pareto frontier, no configuration is cost-effective and the business case does not hold.
112
+
113
+ ---
114
+
115
+ ## Using a real LLM executor
116
+
117
+ The `StepExecutor` protocol accepts any callable `(step, context) -> (completed, hitl_triggered)`:
118
+
119
+ ```python
120
+ from agent_tco import WorkflowSimulator, CostParams
121
+
122
+ def my_llm_executor(step, context):
123
+ # call your LLM here
124
+ completed = run_llm_step(step, context)
125
+ hitl = should_escalate(step, context)
126
+ return completed, hitl
127
+
128
+ sim = WorkflowSimulator(cost_params=cost_params, executor=my_llm_executor)
129
+ run = sim.simulate_run(j9)
130
+ print(run.scenario, run.total_cost)
131
+ ```
132
+
133
+ In tests, replace `my_llm_executor` with `Mock(return_value=(True, False))`.
134
+
135
+ ---
136
+
137
+ ## API Reference
138
+
139
+ See [API.md](API.md).
140
+
141
+ ---
142
+
143
+ ## License
144
+
145
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,114 @@
1
+ # agent-tco
2
+
3
+ **agent-tco** is a Python library for Pareto-optimal configuration of agentic workflows.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - **Workflow simulator** with six-class scenario taxonomy (i1–i6): happy path, retry-resolved, HITL success, HITL partial, compound recovery, aborted.
10
+ - **Grid search** — evaluates all candidate configurations with Beta credible intervals on scenario probabilities.
11
+ - **Pareto frontier** — identifies non-dominated configurations on the ACR vs. E[TCO] plane.
12
+ - **Critical step sensitivity analysis** — Proposition 1: ∂ACR/∂q_k prioritizes which step to upgrade first.
13
+ - **Tier pricing utilities** — built-in `TIER_A`, `TIER_B`, `TIER_C` constants covering frontier, mid-range, and lightweight models. Pass your own `TierCost(tier_id, cost_per_1k_tokens)` to use current pricing.
14
+ - **`StepExecutor` protocol** — plug in real LLM calls or a `Mock` for testing, no SDK dependency.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install agent-tco
22
+ ```
23
+
24
+ Built for Python 3.12 or above.
25
+
26
+ ---
27
+
28
+ ## Quick Start
29
+
30
+ ### 1. Define configurations and run the grid search
31
+
32
+ ```python
33
+ from agent_tco import (
34
+ CostParams, GridSearch,
35
+ step_config_from_tier, WorkflowConfig,
36
+ TIER_A, TIER_B, TIER_C,
37
+ )
38
+
39
+ cost_params = CostParams(
40
+ labor_rate=60.0,
41
+ review_time_hours=0.25,
42
+ manual_process_cost=50.0,
43
+ )
44
+
45
+ # j3: all Tier C
46
+ j3 = WorkflowConfig("j3", [
47
+ step_config_from_tier(k, TIER_C, acr=0.78 if k == 2 else 0.92, hitl_prob=0.20 if k == 4 else 0.05)
48
+ for k in range(1, 6)
49
+ ])
50
+
51
+ # j9: Tier B at step 2 (the bottleneck), Tier C elsewhere
52
+ j9 = WorkflowConfig("j9", [
53
+ step_config_from_tier(k, TIER_B if k == 2 else TIER_C,
54
+ acr=0.88 if k == 2 else 0.92,
55
+ hitl_prob=0.08 if k == 2 else (0.20 if k == 4 else 0.05))
56
+ for k in range(1, 6)
57
+ ])
58
+
59
+ gs = GridSearch(cost_params=cost_params, n_runs=300, seed=42)
60
+ results = gs.evaluate_all([j3, j9])
61
+ for r in results:
62
+ print(f"{r.config.config_id}: ACR={r.acr:.2f} E[TCO]=${r.expected_tco:.2f} M=${r.executive_metric:.2f}")
63
+ ```
64
+
65
+ ### 2. Compute the Pareto frontier and identify the recommended configuration
66
+
67
+ ```python
68
+ from agent_tco import pareto_frontier, rank_by_executive_metric
69
+
70
+ frontier = pareto_frontier(results)
71
+ ranked = rank_by_executive_metric(frontier)
72
+ best = ranked[0]
73
+ print(f"Recommended: {best.config.config_id} — ${best.executive_metric:.2f} per autonomous outcome")
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Interpreting the Executive Metric
79
+
80
+ The executive metric `M = E[TCO] / ACR` is the expected cost per autonomously delivered outcome. When `M < C_manual` (the cost of the manual process), the agent is economically viable at this configuration. If `M > C_manual` for every configuration on the Pareto frontier, no configuration is cost-effective and the business case does not hold.
81
+
82
+ ---
83
+
84
+ ## Using a real LLM executor
85
+
86
+ The `StepExecutor` protocol accepts any callable `(step, context) -> (completed, hitl_triggered)`:
87
+
88
+ ```python
89
+ from agent_tco import WorkflowSimulator, CostParams
90
+
91
+ def my_llm_executor(step, context):
92
+ # call your LLM here
93
+ completed = run_llm_step(step, context)
94
+ hitl = should_escalate(step, context)
95
+ return completed, hitl
96
+
97
+ sim = WorkflowSimulator(cost_params=cost_params, executor=my_llm_executor)
98
+ run = sim.simulate_run(j9)
99
+ print(run.scenario, run.total_cost)
100
+ ```
101
+
102
+ In tests, replace `my_llm_executor` with `Mock(return_value=(True, False))`.
103
+
104
+ ---
105
+
106
+ ## API Reference
107
+
108
+ See [API.md](API.md).
109
+
110
+ ---
111
+
112
+ ## License
113
+
114
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "agent-tco"
3
+ version = "0.0.1"
4
+ license = { file = "LICENSE" }
5
+ description = "agent-tco: Pareto-optimal configuration of agentic workflows."
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "Chinmay Kakatkar" }
9
+ ]
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "scipy>=1.10",
13
+ ]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/ckstash/agent-tco"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.8.11,<0.9.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pydoc-markdown>=4.8.2",
25
+ "pytest>=9.1.1",
26
+ ]
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from agent_tco.executor import StepExecutor
4
+ from agent_tco.grid import GridResult, GridSearch, ScenarioStats
5
+ from agent_tco.model import CostParams, Scenario, SimulationRun, StepConfig, WorkflowConfig
6
+ from agent_tco.pareto import (
7
+ dominates,
8
+ executive_metric,
9
+ pareto_frontier,
10
+ rank_by_executive_metric,
11
+ )
12
+ from agent_tco.pricing import (
13
+ TIER_A,
14
+ TIER_B,
15
+ TIER_C,
16
+ TierCost,
17
+ inference_cost,
18
+ step_config_from_tier,
19
+ )
20
+ from agent_tco.sensitivity import (
21
+ critical_step_sensitivity,
22
+ marginal_acr_gain,
23
+ sample_complexity,
24
+ )
25
+ from agent_tco.simulator import WorkflowSimulator
26
+
27
+ __all__ = [
28
+ "Scenario",
29
+ "StepConfig",
30
+ "WorkflowConfig",
31
+ "CostParams",
32
+ "SimulationRun",
33
+ "StepExecutor",
34
+ "WorkflowSimulator",
35
+ "ScenarioStats",
36
+ "GridResult",
37
+ "GridSearch",
38
+ "dominates",
39
+ "pareto_frontier",
40
+ "executive_metric",
41
+ "rank_by_executive_metric",
42
+ "marginal_acr_gain",
43
+ "critical_step_sensitivity",
44
+ "sample_complexity",
45
+ "TierCost",
46
+ "TIER_A",
47
+ "TIER_B",
48
+ "TIER_C",
49
+ "inference_cost",
50
+ "step_config_from_tier",
51
+ ]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from agent_tco.model import StepConfig
6
+
7
+
8
+ class StepExecutor(Protocol):
9
+ """Protocol for executing one LLM step in a workflow.
10
+
11
+ Any callable matching (step: StepConfig, context: dict) -> tuple[bool, bool]
12
+ satisfies this protocol. The return value is (completed, hitl_triggered).
13
+
14
+ In production, implement this with a real LLM call.
15
+ In tests, pass a lambda or unittest.mock.Mock to control outcomes without
16
+ any network call.
17
+ """
18
+
19
+ def __call__(
20
+ self,
21
+ step: StepConfig,
22
+ context: dict,
23
+ ) -> tuple[bool, bool]:
24
+ """Execute the step and return (completed, hitl_triggered)."""
25
+ ...
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from scipy.stats import beta as beta_dist
6
+
7
+ from agent_tco.model import CostParams, Scenario, WorkflowConfig
8
+ from agent_tco.simulator import WorkflowSimulator
9
+
10
+
11
+ @dataclass
12
+ class ScenarioStats:
13
+ """Statistics for one scenario class across N simulation runs."""
14
+
15
+ scenario: Scenario
16
+ probability: float # empirical P_i = count_i / N
17
+ ci_lower: float # 95% Beta credible interval lower bound
18
+ ci_upper: float # 95% Beta credible interval upper bound
19
+ mean_cost: float # empirical mean cost within this scenario (0.0 if no runs)
20
+
21
+
22
+ @dataclass
23
+ class GridResult:
24
+ """Aggregated results for one WorkflowConfig after N simulation runs."""
25
+
26
+ config: WorkflowConfig
27
+ n_runs: int
28
+ scenario_stats: dict[Scenario, ScenarioStats]
29
+ acr: float # P(i1) + P(i2)
30
+ expected_tco: float # sum_i P_i * mean_cost_i
31
+ executive_metric: float # expected_tco / acr; inf if acr == 0
32
+ pareto_dominated: bool = False
33
+
34
+
35
+ def _beta_ci(k: int, n: int) -> tuple[float, float]:
36
+ """Return the 95% Beta posterior credible interval for k successes in n trials."""
37
+ lower = float(beta_dist.ppf(0.025, k + 1, n - k + 1))
38
+ upper = float(beta_dist.ppf(0.975, k + 1, n - k + 1))
39
+ return lower, upper
40
+
41
+
42
+ class GridSearch:
43
+ """Evaluate agentic workflow configurations over N simulation runs each."""
44
+
45
+ def __init__(
46
+ self,
47
+ cost_params: CostParams,
48
+ n_runs: int = 300,
49
+ seed: int = 42,
50
+ ) -> None:
51
+ """Initialize the grid search with cost parameters and simulation budget."""
52
+ self._cost_params = cost_params
53
+ self._n_runs = n_runs
54
+ self._seed = seed
55
+
56
+ def evaluate(self, config: WorkflowConfig) -> GridResult:
57
+ """Run n_runs simulations for one config and return a GridResult."""
58
+ sim = WorkflowSimulator(cost_params=self._cost_params, seed=self._seed)
59
+ runs = [sim.simulate_run(config, run_index=i) for i in range(self._n_runs)]
60
+ return self._aggregate(config, runs)
61
+
62
+ def evaluate_all(self, configs: list[WorkflowConfig]) -> list[GridResult]:
63
+ """Evaluate all configs sequentially and return one GridResult per config."""
64
+ return [self.evaluate(c) for c in configs]
65
+
66
+ def _aggregate(self, config: WorkflowConfig, runs: list) -> GridResult:
67
+ """Compute scenario statistics and composite metrics from a list of SimulationRun."""
68
+ n = len(runs)
69
+ scenario_stats: dict[Scenario, ScenarioStats] = {}
70
+
71
+ for scenario in Scenario:
72
+ matching = [r for r in runs if r.scenario == scenario]
73
+ k = len(matching)
74
+ probability = k / n
75
+ ci_lower, ci_upper = _beta_ci(k, n)
76
+ mean_cost = sum(r.total_cost for r in matching) / k if k > 0 else 0.0
77
+ scenario_stats[scenario] = ScenarioStats(
78
+ scenario=scenario,
79
+ probability=probability,
80
+ ci_lower=ci_lower,
81
+ ci_upper=ci_upper,
82
+ mean_cost=mean_cost,
83
+ )
84
+
85
+ acr = (
86
+ scenario_stats[Scenario.HAPPY_PATH].probability
87
+ + scenario_stats[Scenario.RETRY_RESOLVED].probability
88
+ )
89
+ expected_tco = sum(
90
+ s.probability * s.mean_cost for s in scenario_stats.values()
91
+ )
92
+ exec_metric = expected_tco / acr if acr > 0 else float("inf")
93
+
94
+ return GridResult(
95
+ config=config,
96
+ n_runs=n,
97
+ scenario_stats=scenario_stats,
98
+ acr=acr,
99
+ expected_tco=expected_tco,
100
+ executive_metric=exec_metric,
101
+ )
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ class Scenario(Enum):
8
+ """The six mutually exclusive trajectory outcome classes."""
9
+
10
+ HAPPY_PATH = "i1" # r=0, h=0, s=success
11
+ RETRY_RESOLVED = "i2" # r>=1, h=0, s=success
12
+ HITL_SUCCESS = "i3" # h=1, r=0, s=success
13
+ HITL_PARTIAL = "i4" # h=1, s=partial
14
+ COMPOUND_RECOVERY = "i5" # r>=1, h=1, s=success
15
+ ABORTED = "i6" # s=aborted (any r, h)
16
+
17
+
18
+ @dataclass
19
+ class StepConfig:
20
+ """Per-step configuration for one LLM touchpoint in a workflow."""
21
+
22
+ step_index: int # 1-based position in the workflow
23
+ model_tier: str # informational label, e.g. "A", "B", "C"
24
+ acr: float # per-step autonomous completion rate in [0, 1]
25
+ hitl_prob: float # probability of HITL trigger in [0, 1]
26
+ inference_cost: float # cost per LLM call for this step (dollars)
27
+
28
+
29
+ @dataclass
30
+ class WorkflowConfig:
31
+ """A complete workflow configuration: one StepConfig per touchpoint."""
32
+
33
+ config_id: str
34
+ steps: list[StepConfig]
35
+
36
+ @property
37
+ def n_steps(self) -> int:
38
+ """Return the number of steps in this configuration."""
39
+ return len(self.steps)
40
+
41
+
42
+ @dataclass
43
+ class CostParams:
44
+ """Cost parameters that are independent of the workflow configuration."""
45
+
46
+ labor_rate: float # dollars per hour
47
+ review_time_hours: float # hours per HITL review event
48
+ manual_process_cost: float # opportunity cost applied when a run is aborted
49
+
50
+ @property
51
+ def hitl_cost_per_event(self) -> float:
52
+ """Return the cost of one HITL review event in dollars."""
53
+ return self.labor_rate * self.review_time_hours
54
+
55
+
56
+ @dataclass
57
+ class SimulationRun:
58
+ """Result of one simulated workflow run."""
59
+
60
+ scenario: Scenario
61
+ total_cost: float
62
+ retry_count: int
63
+ hitl_triggered: bool
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from agent_tco.grid import GridResult
4
+
5
+
6
+ def dominates(a: GridResult, b: GridResult) -> bool:
7
+ """Return True if a Pareto-dominates b.
8
+
9
+ a dominates b when a.acr >= b.acr and a.expected_tco <= b.expected_tco,
10
+ with at least one strict inequality.
11
+ """
12
+ better_acr = a.acr >= b.acr
13
+ better_tco = a.expected_tco <= b.expected_tco
14
+ strictly_better = (a.acr > b.acr) or (a.expected_tco < b.expected_tco)
15
+ return better_acr and better_tco and strictly_better
16
+
17
+
18
+ def pareto_frontier(results: list[GridResult]) -> list[GridResult]:
19
+ """Return the non-dominated subset of results.
20
+
21
+ Sets pareto_dominated=True on every dominated GridResult in-place.
22
+ """
23
+ for r in results:
24
+ r.pareto_dominated = False
25
+
26
+ frontier = []
27
+ for candidate in results:
28
+ dominated = any(dominates(other, candidate) for other in results if other is not candidate)
29
+ if dominated:
30
+ candidate.pareto_dominated = True
31
+ else:
32
+ frontier.append(candidate)
33
+ return frontier
34
+
35
+
36
+ def executive_metric(expected_tco: float, acr: float) -> float:
37
+ """Return expected_tco / acr; float('inf') if acr == 0."""
38
+ if acr == 0:
39
+ return float("inf")
40
+ return expected_tco / acr
41
+
42
+
43
+ def rank_by_executive_metric(results: list[GridResult]) -> list[GridResult]:
44
+ """Return results sorted ascending by executive_metric (lowest cost-per-outcome first)."""
45
+ return sorted(results, key=lambda r: r.executive_metric)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from agent_tco.model import StepConfig
6
+
7
+
8
+ @dataclass
9
+ class TierCost:
10
+ """Per-1K-output-token cost for one model tier."""
11
+
12
+ tier_id: str
13
+ cost_per_1k_tokens: float # dollars
14
+
15
+
16
+ # Built-in tier cost defaults. Override with your own TierCost if pricing has changed.
17
+ TIER_A = TierCost("A", 0.003) # frontier model (e.g. GPT-4o / Claude Sonnet class)
18
+ TIER_B = TierCost("B", 0.0008) # mid-range model (e.g. GPT-4o Mini / Claude Haiku class)
19
+ TIER_C = TierCost("C", 0.00015) # lightweight model
20
+
21
+
22
+ def inference_cost(output_tokens: int, tier: TierCost) -> float:
23
+ """Return the inference cost in dollars for output_tokens at tier pricing."""
24
+ return (output_tokens / 1000) * tier.cost_per_1k_tokens
25
+
26
+
27
+ def step_config_from_tier(
28
+ step_index: int,
29
+ tier: TierCost,
30
+ acr: float,
31
+ hitl_prob: float,
32
+ avg_output_tokens: int = 500,
33
+ ) -> StepConfig:
34
+ """Construct a StepConfig from a TierCost and estimated average token usage."""
35
+ return StepConfig(
36
+ step_index=step_index,
37
+ model_tier=tier.tier_id,
38
+ acr=acr,
39
+ hitl_prob=hitl_prob,
40
+ inference_cost=inference_cost(avg_output_tokens, tier),
41
+ )
File without changes
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ from agent_tco.model import WorkflowConfig
6
+
7
+
8
+ def marginal_acr_gain(config: WorkflowConfig, step_index: int) -> float:
9
+ """Return the marginal ACR gain from improving step step_index by one unit.
10
+
11
+ ∂ACR/∂q_k = product of q_j for all j != step_index.
12
+ step_index is 1-based. Uses the no-retry independence assumption.
13
+ """
14
+ indices = [s.step_index for s in config.steps]
15
+ if step_index not in indices:
16
+ raise ValueError(
17
+ f"step_index {step_index} not found in config steps {indices}"
18
+ )
19
+ result = 1.0
20
+ for step in config.steps:
21
+ if step.step_index != step_index:
22
+ result *= step.acr
23
+ return result
24
+
25
+
26
+ def critical_step_sensitivity(config: WorkflowConfig) -> list[tuple[int, float]]:
27
+ """Return (step_index, marginal_acr_gain) pairs sorted descending by gain.
28
+
29
+ The first entry is the bottleneck step — the highest-priority upgrade target.
30
+ """
31
+ gains = [
32
+ (step.step_index, marginal_acr_gain(config, step.step_index))
33
+ for step in config.steps
34
+ ]
35
+ return sorted(gains, key=lambda x: x[1], reverse=True)
36
+
37
+
38
+ def sample_complexity(
39
+ c_max: float,
40
+ epsilon: float,
41
+ delta: float = 0.05,
42
+ ) -> int:
43
+ """Return the minimum N for E[TCO] estimation within epsilon at confidence 1-delta.
44
+
45
+ Uses the Hoeffding bound:
46
+ N >= ceil(c_max^2 * log(6 / delta) / (2 * epsilon^2))
47
+ """
48
+ if epsilon <= 0:
49
+ raise ValueError(f"epsilon must be positive; got {epsilon}")
50
+ if not (0 < delta < 1):
51
+ raise ValueError(f"delta must be in (0, 1); got {delta}")
52
+ if c_max <= 0:
53
+ raise ValueError(f"c_max must be positive; got {c_max}")
54
+ n = (c_max ** 2 * math.log(6 / delta)) / (2 * epsilon ** 2)
55
+ return math.ceil(n)
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from typing import Callable
5
+
6
+ from agent_tco.model import (
7
+ CostParams,
8
+ Scenario,
9
+ SimulationRun,
10
+ StepConfig,
11
+ WorkflowConfig,
12
+ )
13
+
14
+
15
+ class WorkflowSimulator:
16
+ """Simulate one run of a workflow under a given configuration.
17
+
18
+ Implements the single-retry model: if a step does not complete on the first
19
+ attempt, one retry is made. If the retry also fails, the run is aborted
20
+ (scenario i6) and the opportunity cost is applied.
21
+
22
+ Pass an optional executor callable to drive steps with real LLM calls instead
23
+ of Bernoulli sampling. The executor must match StepExecutor protocol:
24
+ (step: StepConfig, context: dict) -> tuple[bool, bool] (completed, hitl_triggered).
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ cost_params: CostParams,
30
+ seed: int = 42,
31
+ executor: Callable | None = None,
32
+ ) -> None:
33
+ """Initialize the simulator with cost parameters and optional LLM executor."""
34
+ self._cost_params = cost_params
35
+ self._seed = seed
36
+ self._executor = executor
37
+
38
+ def simulate_run(
39
+ self,
40
+ config: WorkflowConfig,
41
+ run_index: int = 0,
42
+ ) -> SimulationRun:
43
+ """Simulate one workflow run and return the classified outcome."""
44
+ rng = random.Random(self._seed + run_index)
45
+ cost = 0.0
46
+ retry_count = 0
47
+ hitl_triggered = False
48
+ terminal = "success"
49
+ context: dict = {}
50
+
51
+ for step in config.steps:
52
+ cost += step.inference_cost # first-attempt inference cost
53
+
54
+ if self._executor is not None:
55
+ completed, hitl = self._executor(step, context)
56
+ else:
57
+ hitl = rng.random() < step.hitl_prob
58
+ completed = rng.random() < step.acr
59
+
60
+ if hitl:
61
+ hitl_triggered = True
62
+ cost += self._cost_params.hitl_cost_per_event
63
+
64
+ if not completed:
65
+ # Single retry
66
+ retry_count += 1
67
+ cost += step.inference_cost # retry inference cost
68
+ if self._executor is not None:
69
+ completed, _ = self._executor(step, context)
70
+ else:
71
+ completed = rng.random() < step.acr
72
+
73
+ if not completed:
74
+ cost += self._cost_params.manual_process_cost
75
+ terminal = "aborted"
76
+ break
77
+
78
+ scenario = self.classify_scenario(retry_count, hitl_triggered, terminal)
79
+ return SimulationRun(
80
+ scenario=scenario,
81
+ total_cost=cost,
82
+ retry_count=retry_count,
83
+ hitl_triggered=hitl_triggered,
84
+ )
85
+
86
+ def classify_scenario(
87
+ self,
88
+ retry_count: int,
89
+ hitl_triggered: bool,
90
+ terminal_state: str,
91
+ ) -> Scenario:
92
+ """Classify a run outcome into one of the six scenario classes."""
93
+ if terminal_state == "aborted":
94
+ return Scenario.ABORTED
95
+ if terminal_state == "partial":
96
+ return Scenario.HITL_PARTIAL # h=1 implied when partial
97
+ # terminal_state == "success"
98
+ if not hitl_triggered and retry_count == 0:
99
+ return Scenario.HAPPY_PATH
100
+ if not hitl_triggered and retry_count >= 1:
101
+ return Scenario.RETRY_RESOLVED
102
+ if hitl_triggered and retry_count == 0:
103
+ return Scenario.HITL_SUCCESS
104
+ # hitl_triggered and retry_count >= 1
105
+ return Scenario.COMPOUND_RECOVERY