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.
- agent_tco-0.0.1/LICENSE +21 -0
- agent_tco-0.0.1/PKG-INFO +145 -0
- agent_tco-0.0.1/README.md +114 -0
- agent_tco-0.0.1/pyproject.toml +26 -0
- agent_tco-0.0.1/src/agent_tco/__init__.py +51 -0
- agent_tco-0.0.1/src/agent_tco/executor.py +25 -0
- agent_tco-0.0.1/src/agent_tco/grid.py +101 -0
- agent_tco-0.0.1/src/agent_tco/model.py +63 -0
- agent_tco-0.0.1/src/agent_tco/pareto.py +45 -0
- agent_tco-0.0.1/src/agent_tco/pricing.py +41 -0
- agent_tco-0.0.1/src/agent_tco/py.typed +0 -0
- agent_tco-0.0.1/src/agent_tco/sensitivity.py +55 -0
- agent_tco-0.0.1/src/agent_tco/simulator.py +105 -0
agent_tco-0.0.1/LICENSE
ADDED
|
@@ -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.
|
agent_tco-0.0.1/PKG-INFO
ADDED
|
@@ -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
|