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.
- ami_survey-1.0.0/.gitignore +3 -0
- ami_survey-1.0.0/LICENSE +33 -0
- ami_survey-1.0.0/PKG-INFO +156 -0
- ami_survey-1.0.0/README.md +127 -0
- ami_survey-1.0.0/ami-survey/ami_survey/__init__.py +7 -0
- ami_survey-1.0.0/ami-survey/ami_survey/adapters/__init__.py +151 -0
- ami_survey-1.0.0/ami-survey/ami_survey/adapters/claude_code.py +440 -0
- ami_survey-1.0.0/ami-survey/ami_survey/adapters/codex.py +513 -0
- ami_survey-1.0.0/ami-survey/ami_survey/categories.py +151 -0
- ami_survey-1.0.0/ami-survey/ami_survey/client.py +187 -0
- ami_survey-1.0.0/ami-survey/ami_survey/config.py +184 -0
- ami_survey-1.0.0/ami-survey/ami_survey/enrol.py +143 -0
- ami_survey-1.0.0/ami-survey/ami_survey/instructions.py +264 -0
- ami_survey-1.0.0/ami-survey/ami_survey/mcp_server.py +914 -0
- ami_survey-1.0.0/ami-survey/ami_survey/runners/__init__.py +8 -0
- ami_survey-1.0.0/ami-survey/ami_survey/runners/agent.py +730 -0
- ami_survey-1.0.0/ami-survey/ami_survey/runners/dialects.py +492 -0
- ami_survey-1.0.0/ami-survey/ami_survey/session_info.py +122 -0
- ami_survey-1.0.0/ami-survey/ami_survey/text.py +41 -0
- ami_survey-1.0.0/ami-survey/ami_survey/timeutil.py +46 -0
- ami_survey-1.0.0/ami-survey/ami_survey/workflow.py +463 -0
- ami_survey-1.0.0/ami-survey/config/workflow_categories.json +55 -0
- ami_survey-1.0.0/ami-survey/skills/ami-make-measurable/SKILL.md +109 -0
- ami_survey-1.0.0/ami-survey/skills/ami-survey/SKILL.md +236 -0
- ami_survey-1.0.0/pyproject.toml +95 -0
- ami_survey-1.0.0/workflows/README.md +159 -0
ami_survey-1.0.0/LICENSE
ADDED
|
@@ -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
|
+
)
|