ami-survey 1.0.0__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,3 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
@@ -0,0 +1,33 @@
1
+ Copyright (c) 2026 Fred Viner. All rights reserved.
2
+
3
+ AMI EVALUATION LICENCE 1.0
4
+
5
+ Permission is granted to any person obtaining a copy of this software to
6
+ install and run it on machines they control, for the sole purpose of
7
+ evaluating it and submitting survey responses to a survey service operated
8
+ by the copyright holder.
9
+
10
+ Permission is also granted to redistribute this software verbatim: complete,
11
+ unmodified, and with this licence included. This is what allows the software
12
+ to be published on a package index and carried by that index's mirrors and
13
+ caches, which copy what they host without being asked.
14
+
15
+ No other permission is granted. In particular this licence does not grant the
16
+ right to sublicense or sell the software; to modify it other than as needed to
17
+ run it for the purpose above; or to create derivative works from it. All rights
18
+ not expressly granted here are reserved.
19
+
20
+ This licence covers only the files in this repository. It grants no rights in
21
+ the survey service the software submits to, in the data collected by that
22
+ service, or in any trade mark or name used by either.
23
+
24
+ Permission ends if the copyright holder gives notice that it has ended, or if
25
+ these terms are breached. Redistribution of verbatim copies already made
26
+ remains permitted.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
32
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
33
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: ami-survey
3
+ Version: 1.0.0
4
+ Summary: Measure what an agent workflow actually cost, from the runtime's own session log.
5
+ Project-URL: Homepage, https://agentbenchmark.dev
6
+ Project-URL: Source, https://github.com/speedofred/ami-survey-client-v1
7
+ Author: Fred Viner
8
+ License-Expression: LicenseRef-AMI-Evaluation-1.0
9
+ License-File: LICENSE
10
+ Keywords: agent,benchmark,claude,codex,mcp,telemetry
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Software Development :: Quality Assurance
25
+ Classifier: Topic :: Software Development :: Testing
26
+ Classifier: Topic :: System :: Benchmark
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+
30
+ # AMI — what did that workflow actually cost?
31
+
32
+ You can find out what a single API call costs. Almost nobody can say what one
33
+ finished piece of work costs — one triaged ticket, one screened CV, one drafted
34
+ reply — across every call, retry and tool round-trip the agent made getting
35
+ there.
36
+
37
+ This measures it, by reading your runtime's own session log after the fact. Ask
38
+ your agent to run it when it finishes something, and you get a scorecard back.
39
+
40
+ Every number comes from the log, not from the agent. An agent asked how many
41
+ tokens it just used will guess, and guess confidently.
42
+
43
+ ## What comes back
44
+
45
+ A real run — six support tickets triaged and answered by Claude Opus 5 in Claude
46
+ Code:
47
+
48
+ ```
49
+ Maturity Index 85.0 Strong (observability 40%, evidence 30%, quality 30%)
50
+ Performance 78.13 Strong confidence Very High
51
+
52
+ quality 80.0 graded Good on ami-quality-v2
53
+ cost 72.73 $0.123226 per ticket ($0.739355 for the run)
54
+ speed 84.47 20.90s per ticket
55
+ evidence 70.0 measured, on a self-issued token
56
+ observability 100.0
57
+
58
+ findings
59
+ weakness Cost is the weakest pillar at 72.73; speed is strongest at 84.47.
60
+ $0.123226 per unit against a $0.01 reference. A cheaper model, or
61
+ fewer calls, moves this; check calls[] for where the tokens went.
62
+
63
+ note Cost and speed were scored against a provisional reference, which is
64
+ a placeholder rather than a measurement. Do not quote them as settled
65
+ yet. The Maturity Index does not use the reference and is unaffected.
66
+ ```
67
+
68
+ **$0.12 per ticket, 21 seconds per ticket.** That is the number this exists to
69
+ produce, and it is the one most teams cannot currently state about their own
70
+ work.
71
+
72
+ The findings are worth reading twice: the scorecard says out loud where its own
73
+ numbers are soft. A cost reference that is still a placeholder is a placeholder
74
+ in your report too, not quietly folded into a score.
75
+
76
+ ## Install
77
+
78
+ Two ways in. The difference between them is whether anything can read your
79
+ runtime's logs, and that decides whether your numbers are **measured** or
80
+ **unmeasured**.
81
+
82
+ ### Measured — one line
83
+
84
+ Add this to your agent's MCP configuration and restart it:
85
+
86
+ ```json
87
+ { "mcpServers": { "ami-survey": { "command": "uvx", "args": ["ami-survey"] } } }
88
+ ```
89
+
90
+ Then ask your agent, after it finishes a piece of work:
91
+
92
+ > Take the AMI survey regarding the ticket triage you just did
93
+
94
+ That is the whole setup. Nothing to clone, nothing to keep updated, and no token
95
+ to paste — the first call that needs one registers this machine and stores it at
96
+ `~/.ami-survey/token`.
97
+
98
+ **`uvx` comes from [uv](https://docs.astral.sh/uv/)** — the same tool the MCP
99
+ docs use for Python servers, so if you have installed one before you already
100
+ have it. If you would rather not, [GETTING-STARTED.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/GETTING-STARTED.md)
101
+ has a `pipx` form and a route that needs neither.
102
+
103
+ Already have a token? Put it in that block's `env` as `AMI_API_TOKEN` and it is
104
+ used instead of registering a new one.
105
+
106
+ ### Unmeasured — a remote connector, nothing installed
107
+
108
+ In claude.ai: Settings → Connectors → Add custom connector, and give it
109
+
110
+ ```
111
+ https://survey.agentbenchmark.dev/mcp
112
+ ```
113
+
114
+ Nothing to install and no token. These runs are recorded as `unmeasured` and are
115
+ never compared against measured ones — a server on the other side of the
116
+ internet cannot read your runtime's logs, so the token counts and cost are
117
+ simply absent rather than guessed.
118
+
119
+ Install the client above when you want those numbers too.
120
+
121
+ ## Licence, up front
122
+
123
+ **This is not open source.** It is an evaluation licence: run it on machines you
124
+ control, redistribute it verbatim if you like, but it may not be modified, sold
125
+ or built upon. Full terms in
126
+ [LICENSE](https://github.com/speedofred/ami-survey-client-v1/blob/main/LICENSE).
127
+
128
+ Said here rather than at the bottom, because finding it at the bottom after
129
+ reading everything else is worse than being told now.
130
+
131
+ ## What leaves your computer
132
+
133
+ Token counts, timings, model names, the stage names your workflow declared, and
134
+ the grade. **Not your files, not your prompts, not your shell commands.**
135
+ [GETTING-STARTED.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/GETTING-STARTED.md)
136
+ sets this out in full.
137
+
138
+ Submissions go to `survey.agentbenchmark.dev` and nowhere else. That destination
139
+ is a constant in the source rather than a setting: a stale environment variable
140
+ cannot redirect your submission onto your own disk, which is the one failure that
141
+ would make a run look successful while collecting nothing.
142
+
143
+ ## Requirements
144
+
145
+ Python 3.9 or newer. No dependencies — the standard library only.
146
+
147
+ ## Everything else
148
+
149
+ - [GETTING-STARTED.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/GETTING-STARTED.md)
150
+ — assumes no prior setup; macOS, Linux and Windows, and what to do when it does
151
+ not work.
152
+ - [COMMANDS.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/COMMANDS.md)
153
+ — the clone-and-run route: benchmarking one workflow across several models on
154
+ your own API key. Not needed to take part.
155
+ - [MAKE-IT-MEASURABLE.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/MAKE-IT-MEASURABLE.md)
156
+ — how to structure a workflow so there is something worth measuring.
@@ -0,0 +1,127 @@
1
+ # AMI — what did that workflow actually cost?
2
+
3
+ You can find out what a single API call costs. Almost nobody can say what one
4
+ finished piece of work costs — one triaged ticket, one screened CV, one drafted
5
+ reply — across every call, retry and tool round-trip the agent made getting
6
+ there.
7
+
8
+ This measures it, by reading your runtime's own session log after the fact. Ask
9
+ your agent to run it when it finishes something, and you get a scorecard back.
10
+
11
+ Every number comes from the log, not from the agent. An agent asked how many
12
+ tokens it just used will guess, and guess confidently.
13
+
14
+ ## What comes back
15
+
16
+ A real run — six support tickets triaged and answered by Claude Opus 5 in Claude
17
+ Code:
18
+
19
+ ```
20
+ Maturity Index 85.0 Strong (observability 40%, evidence 30%, quality 30%)
21
+ Performance 78.13 Strong confidence Very High
22
+
23
+ quality 80.0 graded Good on ami-quality-v2
24
+ cost 72.73 $0.123226 per ticket ($0.739355 for the run)
25
+ speed 84.47 20.90s per ticket
26
+ evidence 70.0 measured, on a self-issued token
27
+ observability 100.0
28
+
29
+ findings
30
+ weakness Cost is the weakest pillar at 72.73; speed is strongest at 84.47.
31
+ $0.123226 per unit against a $0.01 reference. A cheaper model, or
32
+ fewer calls, moves this; check calls[] for where the tokens went.
33
+
34
+ note Cost and speed were scored against a provisional reference, which is
35
+ a placeholder rather than a measurement. Do not quote them as settled
36
+ yet. The Maturity Index does not use the reference and is unaffected.
37
+ ```
38
+
39
+ **$0.12 per ticket, 21 seconds per ticket.** That is the number this exists to
40
+ produce, and it is the one most teams cannot currently state about their own
41
+ work.
42
+
43
+ The findings are worth reading twice: the scorecard says out loud where its own
44
+ numbers are soft. A cost reference that is still a placeholder is a placeholder
45
+ in your report too, not quietly folded into a score.
46
+
47
+ ## Install
48
+
49
+ Two ways in. The difference between them is whether anything can read your
50
+ runtime's logs, and that decides whether your numbers are **measured** or
51
+ **unmeasured**.
52
+
53
+ ### Measured — one line
54
+
55
+ Add this to your agent's MCP configuration and restart it:
56
+
57
+ ```json
58
+ { "mcpServers": { "ami-survey": { "command": "uvx", "args": ["ami-survey"] } } }
59
+ ```
60
+
61
+ Then ask your agent, after it finishes a piece of work:
62
+
63
+ > Take the AMI survey regarding the ticket triage you just did
64
+
65
+ That is the whole setup. Nothing to clone, nothing to keep updated, and no token
66
+ to paste — the first call that needs one registers this machine and stores it at
67
+ `~/.ami-survey/token`.
68
+
69
+ **`uvx` comes from [uv](https://docs.astral.sh/uv/)** — the same tool the MCP
70
+ docs use for Python servers, so if you have installed one before you already
71
+ have it. If you would rather not, [GETTING-STARTED.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/GETTING-STARTED.md)
72
+ has a `pipx` form and a route that needs neither.
73
+
74
+ Already have a token? Put it in that block's `env` as `AMI_API_TOKEN` and it is
75
+ used instead of registering a new one.
76
+
77
+ ### Unmeasured — a remote connector, nothing installed
78
+
79
+ In claude.ai: Settings → Connectors → Add custom connector, and give it
80
+
81
+ ```
82
+ https://survey.agentbenchmark.dev/mcp
83
+ ```
84
+
85
+ Nothing to install and no token. These runs are recorded as `unmeasured` and are
86
+ never compared against measured ones — a server on the other side of the
87
+ internet cannot read your runtime's logs, so the token counts and cost are
88
+ simply absent rather than guessed.
89
+
90
+ Install the client above when you want those numbers too.
91
+
92
+ ## Licence, up front
93
+
94
+ **This is not open source.** It is an evaluation licence: run it on machines you
95
+ control, redistribute it verbatim if you like, but it may not be modified, sold
96
+ or built upon. Full terms in
97
+ [LICENSE](https://github.com/speedofred/ami-survey-client-v1/blob/main/LICENSE).
98
+
99
+ Said here rather than at the bottom, because finding it at the bottom after
100
+ reading everything else is worse than being told now.
101
+
102
+ ## What leaves your computer
103
+
104
+ Token counts, timings, model names, the stage names your workflow declared, and
105
+ the grade. **Not your files, not your prompts, not your shell commands.**
106
+ [GETTING-STARTED.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/GETTING-STARTED.md)
107
+ sets this out in full.
108
+
109
+ Submissions go to `survey.agentbenchmark.dev` and nowhere else. That destination
110
+ is a constant in the source rather than a setting: a stale environment variable
111
+ cannot redirect your submission onto your own disk, which is the one failure that
112
+ would make a run look successful while collecting nothing.
113
+
114
+ ## Requirements
115
+
116
+ Python 3.9 or newer. No dependencies — the standard library only.
117
+
118
+ ## Everything else
119
+
120
+ - [GETTING-STARTED.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/GETTING-STARTED.md)
121
+ — assumes no prior setup; macOS, Linux and Windows, and what to do when it does
122
+ not work.
123
+ - [COMMANDS.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/COMMANDS.md)
124
+ — the clone-and-run route: benchmarking one workflow across several models on
125
+ your own API key. Not needed to take part.
126
+ - [MAKE-IT-MEASURABLE.md](https://github.com/speedofred/ami-survey-client-v1/blob/main/MAKE-IT-MEASURABLE.md)
127
+ — how to structure a workflow so there is something worth measuring.
@@ -0,0 +1,7 @@
1
+ """AMI workflow survey: concrete, evidence-backed collection of the fields in
2
+ Collection_Inventory.csv for any agent workflow run."""
3
+
4
+ #: The one place the version is written. `pyproject.toml` reads it from
5
+ #: here via hatchling, and the MCP handshake reports it, so a release
6
+ #: cannot ship a package and a server claiming different numbers.
7
+ __version__ = "1.0.0"
@@ -0,0 +1,151 @@
1
+ """Runtime telemetry adapters, and the registry that picks one.
2
+
3
+ An adapter turns a runtime's own record of its API calls into the normalised call
4
+ records the survey API ingests. The survey is model-agnostic - the model id and
5
+ its price are read from whatever the runtime recorded - but *measurement* is
6
+ runtime-specific, because each agent harness logs its sessions differently. So
7
+ there is one adapter per harness, not per model, and the agent taking the survey
8
+ never has to say which one it is: `detect()` works that out.
9
+
10
+ Normalised call record:
11
+ call_id, model, start_time, end_time, duration_seconds,
12
+ input_tokens, output_tokens, input_token_breakdown{...},
13
+ tool_calls[{name, command?}], has_text, has_thinking, source, evidence{...}
14
+
15
+ Each adapter module exposes:
16
+ NAME short id, e.g. "claude_code"
17
+ LABEL what goes in `telemetry_adapter`, e.g. "claude_code_transcript"
18
+ locate(cwd, session_id=None, transcript_path=None) -> Path
19
+ the session log this run should be measured from; raises
20
+ TelemetryNotFound if this runtime is not the one in use
21
+ probe(...) runtime identity + proposed measurement window
22
+ collect(...) the full telemetry payload
23
+
24
+ A runtime with no adapter is not stuck: it can post its own usage records
25
+ directly via `ami_record_calls` / POST /runs/{id}/calls. It just has to have them.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ from pathlib import Path
32
+
33
+
34
+ class TelemetryNotFound(RuntimeError):
35
+ """No session log for this runtime - it is probably not the one running."""
36
+
37
+
38
+ def _modules() -> list:
39
+ from . import claude_code, codex
40
+
41
+ return [claude_code, codex]
42
+
43
+
44
+ def available() -> list[str]:
45
+ return [m.NAME for m in _modules()]
46
+
47
+
48
+ def _located(cwd: str | None, session_id: str | None, transcript_path: str | None):
49
+ """Every adapter that can find a session for this run, newest log first."""
50
+ found = []
51
+ for module in _modules():
52
+ # An explicitly supplied log belongs to exactly one runtime; adapters that
53
+ # can recognise their own format get to disown it rather than mis-parse it.
54
+ if transcript_path and hasattr(module, "claims"):
55
+ if not module.claims(transcript_path):
56
+ continue
57
+ try:
58
+ path = module.locate(
59
+ cwd=cwd, session_id=session_id, transcript_path=transcript_path
60
+ )
61
+ except (TelemetryNotFound, OSError):
62
+ continue
63
+ try:
64
+ mtime = Path(path).stat().st_mtime
65
+ except OSError:
66
+ mtime = 0.0
67
+ found.append((mtime, module, path))
68
+ return sorted(found, key=lambda f: f[0], reverse=True)
69
+
70
+
71
+ def _environment_candidates() -> list:
72
+ """Narrow the field using the environment the runtime gave this process.
73
+
74
+ Agent harnesses launch the MCP server as a child process and stamp their own
75
+ variables on it, which identifies the asking runtime exactly. That beats every
76
+ heuristic: two runtimes can have sessions in one directory, and the most
77
+ recently written log is not always the one asking. Adapters that cannot tell
78
+ either way stay in the running.
79
+ """
80
+ verdicts = [(m, m.runs_here() if hasattr(m, "runs_here") else None) for m in _modules()]
81
+ positive = [m for m, v in verdicts if v is True]
82
+ if positive:
83
+ return positive
84
+ undecided = [m for m, v in verdicts if v is not False]
85
+ return undecided or [m for m, _ in verdicts]
86
+
87
+
88
+ def detect(
89
+ cwd: str | None = None,
90
+ session_id: str | None = None,
91
+ transcript_path: str | None = None,
92
+ adapter: str | None = None,
93
+ ):
94
+ """Return the adapter module that should measure this run.
95
+
96
+ Explicit choice wins (argument, then AMI_ADAPTER), then the runtime named by
97
+ this process's environment. Failing both, every remaining adapter is asked
98
+ whether it can find a session for this working directory and the most
99
+ recently written log wins.
100
+ """
101
+ wanted = adapter or os.environ.get("AMI_ADAPTER")
102
+ if wanted:
103
+ for module in _modules():
104
+ if module.NAME == wanted:
105
+ return module
106
+ raise TelemetryNotFound(
107
+ f"Unknown adapter {wanted!r}. Available: {', '.join(available())}"
108
+ )
109
+
110
+ found = _located(cwd, session_id, transcript_path)
111
+ by_environment = {m.NAME for m in _environment_candidates()}
112
+ narrowed = [f for f in found if f[1].NAME in by_environment]
113
+ found = narrowed or found
114
+ if not found:
115
+ raise TelemetryNotFound(
116
+ "No session log found for any known runtime "
117
+ f"({', '.join(available())}) in {cwd or os.getcwd()!r}. If that is not "
118
+ "your workspace directory - the survey server may have been launched "
119
+ "elsewhere - call this again with cwd set to the directory you are "
120
+ "working in. If your runtime has no adapter, post its own usage records "
121
+ "with ami_record_calls instead: the survey does not accept estimates."
122
+ )
123
+ return found[0][1]
124
+
125
+
126
+ def probe(
127
+ cwd: str | None = None,
128
+ session_id: str | None = None,
129
+ transcript_path: str | None = None,
130
+ adapter: str | None = None,
131
+ ) -> dict:
132
+ module = detect(cwd, session_id, transcript_path, adapter)
133
+ return module.probe(cwd=cwd, session_id=session_id, transcript_path=transcript_path)
134
+
135
+
136
+ def collect(
137
+ cwd: str | None = None,
138
+ session_id: str | None = None,
139
+ transcript_path: str | None = None,
140
+ window_start: str | None = None,
141
+ window_end: str | None = None,
142
+ adapter: str | None = None,
143
+ ) -> dict:
144
+ module = detect(cwd, session_id, transcript_path, adapter)
145
+ return module.collect(
146
+ cwd=cwd,
147
+ session_id=session_id,
148
+ transcript_path=transcript_path,
149
+ window_start=window_start,
150
+ window_end=window_end,
151
+ )