ciagent 0.7.0__tar.gz → 0.8.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.
- {ciagent-0.7.0 → ciagent-0.8.0}/CHANGELOG.md +42 -1
- {ciagent-0.7.0 → ciagent-0.8.0}/PKG-INFO +45 -1
- {ciagent-0.7.0 → ciagent-0.8.0}/README.md +44 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/pyproject.toml +3 -2
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/capture.py +35 -15
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/cli.py +73 -9
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/mock_runner.py +22 -4
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/reporter.py +10 -0
- ciagent-0.8.0/src/agentci/examples/__init__.py +3 -0
- ciagent-0.8.0/src/agentci/examples/demo_spec.yaml +73 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/.gitignore +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/LICENSE +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/__init__.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/__init__.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/base.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/generic.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/langgraph.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/openai_agents.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/assertions.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/baselines.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/config.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/cost.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/diff_engine.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/__init__.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/check_generator.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/correctness.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/cost.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/cost_estimator.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/diff.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/judge.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/judge_audit.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/metrics.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/parallel.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/path.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/results.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/runner.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/span_assertions.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/stability.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/exceptions.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/loader.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/mocks.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/models.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/py.typed +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/pytest_plugin.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/report.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/runner.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/__init__.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/agentci_spec.schema.json +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/generate_schema.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/spec_models.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/__init__.py +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/github_action.yml.j2 +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/pre_push_hook.sh.j2 +0 -0
- {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/report.html.j2 +0 -0
|
@@ -5,7 +5,48 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.8.0] - 2026-07-06
|
|
9
|
+
|
|
10
|
+
### Added — Claude Code plugin (F5: your coding agent onboards and operates AgentCI)
|
|
11
|
+
- The repo is now a Claude Code plugin marketplace
|
|
12
|
+
(`/plugin marketplace add suniel12/AgentCI`, `/plugin install agentci@agentci`)
|
|
13
|
+
with two skills: **onboard** (write the runner, record goldens via
|
|
14
|
+
`bootstrap --yes`, generate + tighten the spec, verify with `test --runs 3`)
|
|
15
|
+
and **check** (run the right check after agent changes; route failures by
|
|
16
|
+
flip source; never weaken a correct check to go green)
|
|
17
|
+
- `agentci bootstrap --yes`: fully non-interactive golden recording (requires
|
|
18
|
+
`--queries`); bootstrap now runs the runner through the same TraceContext
|
|
19
|
+
capture path as `agentci test`, so **runners returning plain strings work**
|
|
20
|
+
(previously crashed on anything but a Trace)
|
|
21
|
+
- `--format json` output now includes the agent's `answer` text per query
|
|
22
|
+
(eng-review work item: JSON consumers need what the agent said, not just
|
|
23
|
+
verdicts; also unblocks JSON as a judge-audit answers source)
|
|
24
|
+
- Tests: skills are lint-gated — every `agentci` command and flag a skill
|
|
25
|
+
teaches is asserted to exist in the CLI, so skill docs cannot rot silently
|
|
26
|
+
|
|
27
|
+
### Fixed — from Codex cross-model review of this branch
|
|
28
|
+
- **Nested `TraceContext` no longer double-records or loses capture**: a
|
|
29
|
+
Trace-returning runner that uses `TraceContext` internally, wrapped again by
|
|
30
|
+
the executor, used to stack SDK patches (every LLM call recorded once per
|
|
31
|
+
wrapper) and its exit cleared the outer context entirely. Patches now install
|
|
32
|
+
only in the outermost context, and exits restore the enclosing context via
|
|
33
|
+
contextvar tokens. Affected `agentci test` as well as the new bootstrap path
|
|
34
|
+
- JSON `answer` field now uses the correctness layer's extractor (metadata →
|
|
35
|
+
last span output), so trace shapes the evaluator can grade also serialize
|
|
36
|
+
their answer instead of `null`
|
|
37
|
+
|
|
38
|
+
### Added — zero-key demo (`uvx ciagent test --mock --runs 3`)
|
|
39
|
+
- `agentci test --mock` with no `agentci_spec.yaml` in the working directory now
|
|
40
|
+
falls back to a bundled demo spec (8 synthetic support-agent queries), clearly
|
|
41
|
+
labeled as demo mode with synthetic data. An explicitly passed `--config` that
|
|
42
|
+
is missing remains an error — the fallback only applies to the default path
|
|
43
|
+
- Demo multi-run sessions simulate a pseudo-flaky agent by default with the new
|
|
44
|
+
`"spread"` style (one query breaks per run), so the aggregate score stays
|
|
45
|
+
constant while individual verdicts flip — the report the demo exists to show.
|
|
46
|
+
`AGENTCI_MOCK_FLAKY=0` turns the simulation off; non-demo specs are unaffected
|
|
47
|
+
(flakiness stays env-var opt-in, `"alternate"` style unchanged)
|
|
48
|
+
- Missing spec without `--mock` now exits with a hint pointing at both
|
|
49
|
+
`agentci init` and the zero-key demo command
|
|
9
50
|
|
|
10
51
|
### Changed — stability report hardening (pre-launch fixes from eng review)
|
|
11
52
|
- **Flip attribution now compares per-layer sub-verdicts first**: if every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ciagent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Continuous Integration for AI Agents. Catch cost spikes and logic regressions before production.
|
|
5
5
|
Project-URL: Documentation, https://github.com/suniel12/AgentCI#readme
|
|
6
6
|
Project-URL: Repository, https://github.com/suniel12/AgentCI.git
|
|
@@ -54,6 +54,34 @@ You changed a prompt. Your agent broke in production. Three days later, a user c
|
|
|
54
54
|
|
|
55
55
|
Works with OpenAI, Anthropic, and LangGraph. Runs inside pytest.
|
|
56
56
|
|
|
57
|
+
## See It in 30 Seconds
|
|
58
|
+
|
|
59
|
+
No install, no API keys, no config — one command runs a bundled demo suite three times on synthetic traces:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uvx ciagent test --mock --runs 3
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Run 1/3: 7/8 passed
|
|
67
|
+
Run 2/3: 7/8 passed
|
|
68
|
+
Run 3/3: 7/8 passed
|
|
69
|
+
|
|
70
|
+
────────────────────────────────────────────────────────────
|
|
71
|
+
Stability Report
|
|
72
|
+
────────────────────────────────────────────────────────────
|
|
73
|
+
Suite score across 3 runs: 88% / 88% / 88% ← looks stable
|
|
74
|
+
|
|
75
|
+
⚠️ FLAKY — 3/8 queries flipped verdicts across runs: ← is not
|
|
76
|
+
"What's your return window?" ❌✅✅ pass_rate=0.67 source: agent-variance
|
|
77
|
+
"Do you ship internationally?" ✅❌✅ pass_rate=0.67 source: agent-variance
|
|
78
|
+
"How do I reset my password?" ✅✅❌ pass_rate=0.67 source: agent-variance
|
|
79
|
+
|
|
80
|
+
Flip sources: 3 agent-variance (fix the agent) │ 0 judge-flake (fix the eval)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The aggregate score is identical every run. Three of the eight verdicts underneath it flipped. A single-run eval score would never tell you — the stability report does, and attributes every flip to the layer that caused it. (The demo simulates a flaky agent; point it at your own with the spec below.)
|
|
84
|
+
|
|
57
85
|
## Add to Your Project
|
|
58
86
|
|
|
59
87
|
```bash
|
|
@@ -213,6 +241,22 @@ It extracts hard facts (prices, rates, SKUs, "30 days") as variant-set assertion
|
|
|
213
241
|
would fail a known-good answer is rejected before you ever see it. One LLM call at
|
|
214
242
|
authoring time; the checks run free forever. Details: [docs/generate-checks.md](docs/generate-checks.md).
|
|
215
243
|
|
|
244
|
+
## Let your coding agent set it up
|
|
245
|
+
|
|
246
|
+
AgentCI ships as a Claude Code plugin. Two skills: **onboard** (writes the runner,
|
|
247
|
+
records golden baselines, generates the spec, verifies it) and **check** (runs the
|
|
248
|
+
right test after every change to your agent and routes failures by flip source).
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
/plugin marketplace add suniel12/AgentCI
|
|
252
|
+
/plugin install agentci@agentci
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Then ask your coding agent to "set up AgentCI for this repo." It records goldens with
|
|
256
|
+
`agentci bootstrap --yes` and verifies with `agentci test --runs 3` — no human CLI use
|
|
257
|
+
needed. The runner it writes is one function: `(query: str) -> str`; trace capture is
|
|
258
|
+
automatic.
|
|
259
|
+
|
|
216
260
|
## Demo
|
|
217
261
|
|
|
218
262
|
Here's a RAG agent demo where someone "optimizes for latency" by reducing retriever docs from 8 to 1. AgentCI catches the correctness regression:
|
|
@@ -11,6 +11,34 @@ You changed a prompt. Your agent broke in production. Three days later, a user c
|
|
|
11
11
|
|
|
12
12
|
Works with OpenAI, Anthropic, and LangGraph. Runs inside pytest.
|
|
13
13
|
|
|
14
|
+
## See It in 30 Seconds
|
|
15
|
+
|
|
16
|
+
No install, no API keys, no config — one command runs a bundled demo suite three times on synthetic traces:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uvx ciagent test --mock --runs 3
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Run 1/3: 7/8 passed
|
|
24
|
+
Run 2/3: 7/8 passed
|
|
25
|
+
Run 3/3: 7/8 passed
|
|
26
|
+
|
|
27
|
+
────────────────────────────────────────────────────────────
|
|
28
|
+
Stability Report
|
|
29
|
+
────────────────────────────────────────────────────────────
|
|
30
|
+
Suite score across 3 runs: 88% / 88% / 88% ← looks stable
|
|
31
|
+
|
|
32
|
+
⚠️ FLAKY — 3/8 queries flipped verdicts across runs: ← is not
|
|
33
|
+
"What's your return window?" ❌✅✅ pass_rate=0.67 source: agent-variance
|
|
34
|
+
"Do you ship internationally?" ✅❌✅ pass_rate=0.67 source: agent-variance
|
|
35
|
+
"How do I reset my password?" ✅✅❌ pass_rate=0.67 source: agent-variance
|
|
36
|
+
|
|
37
|
+
Flip sources: 3 agent-variance (fix the agent) │ 0 judge-flake (fix the eval)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The aggregate score is identical every run. Three of the eight verdicts underneath it flipped. A single-run eval score would never tell you — the stability report does, and attributes every flip to the layer that caused it. (The demo simulates a flaky agent; point it at your own with the spec below.)
|
|
41
|
+
|
|
14
42
|
## Add to Your Project
|
|
15
43
|
|
|
16
44
|
```bash
|
|
@@ -170,6 +198,22 @@ It extracts hard facts (prices, rates, SKUs, "30 days") as variant-set assertion
|
|
|
170
198
|
would fail a known-good answer is rejected before you ever see it. One LLM call at
|
|
171
199
|
authoring time; the checks run free forever. Details: [docs/generate-checks.md](docs/generate-checks.md).
|
|
172
200
|
|
|
201
|
+
## Let your coding agent set it up
|
|
202
|
+
|
|
203
|
+
AgentCI ships as a Claude Code plugin. Two skills: **onboard** (writes the runner,
|
|
204
|
+
records golden baselines, generates the spec, verifies it) and **check** (runs the
|
|
205
|
+
right test after every change to your agent and routes failures by flip source).
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
/plugin marketplace add suniel12/AgentCI
|
|
209
|
+
/plugin install agentci@agentci
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Then ask your coding agent to "set up AgentCI for this repo." It records goldens with
|
|
213
|
+
`agentci bootstrap --yes` and verifies with `agentci test --runs 3` — no human CLI use
|
|
214
|
+
needed. The runner it writes is one function: `(query: str) -> str`; trace capture is
|
|
215
|
+
automatic.
|
|
216
|
+
|
|
173
217
|
## Demo
|
|
174
218
|
|
|
175
219
|
Here's a RAG agent demo where someone "optimizes for latency" by reducing retriever docs from 8 to 1. AgentCI catches the correctness regression:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ciagent"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Continuous Integration for AI Agents. Catch cost spikes and logic regressions before production."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "Apache-2.0"}
|
|
@@ -64,10 +64,11 @@ include = [
|
|
|
64
64
|
[tool.hatch.build]
|
|
65
65
|
include = [
|
|
66
66
|
"src/agentci/templates/**/*",
|
|
67
|
+
"src/agentci/examples/**/*",
|
|
67
68
|
]
|
|
68
69
|
|
|
69
70
|
[tool.setuptools.package-data]
|
|
70
|
-
agentci = ["templates/*.j2"]
|
|
71
|
+
agentci = ["templates/*.j2", "examples/*.yaml"]
|
|
71
72
|
|
|
72
73
|
[tool.pytest.ini_options]
|
|
73
74
|
pythonpath = ["src"]
|
|
@@ -25,6 +25,12 @@ _active_trace: contextvars.ContextVar[Trace | None] = contextvars.ContextVar(
|
|
|
25
25
|
_active_span: contextvars.ContextVar[Span | None] = contextvars.ContextVar(
|
|
26
26
|
'_active_span', default=None
|
|
27
27
|
)
|
|
28
|
+
# Nesting depth of active TraceContexts. SDK patches are installed only by the
|
|
29
|
+
# outermost context — stacked patches would record every LLM call once per
|
|
30
|
+
# wrapper into the same active span.
|
|
31
|
+
_patch_depth: contextvars.ContextVar[int] = contextvars.ContextVar(
|
|
32
|
+
'_patch_depth', default=0
|
|
33
|
+
)
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
class TraceContext:
|
|
@@ -58,23 +64,31 @@ class TraceContext:
|
|
|
58
64
|
self.trace = Trace(agent_name=agent_name, test_name=test_name)
|
|
59
65
|
self._patches = []
|
|
60
66
|
self._start_time = 0.0
|
|
61
|
-
|
|
67
|
+
self._trace_token = None
|
|
68
|
+
self._span_token = None
|
|
69
|
+
|
|
62
70
|
def __enter__(self):
|
|
63
71
|
# Create root span
|
|
64
72
|
root_span = Span(kind=SpanKind.AGENT, name=self.trace.agent_name)
|
|
65
73
|
self.trace.spans.append(root_span)
|
|
66
|
-
|
|
67
|
-
# Set context vars
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
|
|
75
|
+
# Set context vars, keeping reset tokens so a nested context restores
|
|
76
|
+
# the enclosing context on exit instead of clearing it
|
|
77
|
+
self._trace_token = _active_trace.set(self.trace)
|
|
78
|
+
self._span_token = _active_span.set(root_span)
|
|
79
|
+
|
|
80
|
+
# Apply monkey patches only in the outermost context: stacked patches
|
|
81
|
+
# would record every LLM call once per wrapper into the active span
|
|
82
|
+
# (e.g. a Trace-returning runner that uses TraceContext itself, wrapped
|
|
83
|
+
# again by _run_with_retry)
|
|
84
|
+
if _patch_depth.get() == 0:
|
|
85
|
+
self._patch_openai()
|
|
86
|
+
self._patch_anthropic()
|
|
87
|
+
_patch_depth.set(_patch_depth.get() + 1)
|
|
88
|
+
|
|
75
89
|
self._start_time = time.perf_counter()
|
|
76
90
|
return self
|
|
77
|
-
|
|
91
|
+
|
|
78
92
|
def __exit__(self, *args):
|
|
79
93
|
# Compute duration
|
|
80
94
|
self.trace.total_duration_ms = (time.perf_counter() - self._start_time) * 1000
|
|
@@ -85,13 +99,19 @@ class TraceContext:
|
|
|
85
99
|
# Auto-extract final output if not manually set
|
|
86
100
|
self._auto_extract_final_output()
|
|
87
101
|
|
|
88
|
-
# Remove patches
|
|
102
|
+
# Remove patches (only the outermost context installed any)
|
|
103
|
+
_patch_depth.set(max(_patch_depth.get() - 1, 0))
|
|
89
104
|
for restore_fn in self._patches:
|
|
90
105
|
restore_fn()
|
|
106
|
+
self._patches = []
|
|
91
107
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
# Restore the enclosing context (None only when outermost)
|
|
109
|
+
if self._trace_token is not None:
|
|
110
|
+
_active_trace.reset(self._trace_token)
|
|
111
|
+
self._trace_token = None
|
|
112
|
+
if self._span_token is not None:
|
|
113
|
+
_active_span.reset(self._span_token)
|
|
114
|
+
self._span_token = None
|
|
95
115
|
|
|
96
116
|
def _auto_extract_final_output(self) -> None:
|
|
97
117
|
"""Auto-extract the agent's final output from the trace.
|
|
@@ -1627,15 +1627,26 @@ queries:
|
|
|
1627
1627
|
@click.option('--runner', required=True, help='Import path for runner function, e.g. myagent.run:run')
|
|
1628
1628
|
@click.option('--output', default='agentci_spec.yaml', help='Output spec file')
|
|
1629
1629
|
@click.option('--baseline-dir', default='./baselines', help='Directory for baselines')
|
|
1630
|
-
|
|
1631
|
-
|
|
1630
|
+
@click.option('--yes', '-y', is_flag=True,
|
|
1631
|
+
help='Accept every captured trace as golden without prompting '
|
|
1632
|
+
'(for CI and coding agents; requires --queries)')
|
|
1633
|
+
def bootstrap(queries, agent, runner, output, baseline_dir, yes):
|
|
1634
|
+
"""Zero-to-Golden bootstrapper: run queries, record golden baselines, write a spec.
|
|
1635
|
+
|
|
1636
|
+
The runner may return an agentci.models.Trace or a plain string — string
|
|
1637
|
+
returns get automatic LLM/tool capture, same as `agentci test`.
|
|
1638
|
+
"""
|
|
1632
1639
|
import yaml
|
|
1633
1640
|
import json
|
|
1634
1641
|
import re
|
|
1635
1642
|
from datetime import datetime, timezone
|
|
1636
1643
|
from pathlib import Path
|
|
1637
1644
|
from rich.prompt import Prompt, Confirm
|
|
1638
|
-
from .engine.parallel import resolve_runner
|
|
1645
|
+
from .engine.parallel import resolve_runner, _run_with_retry
|
|
1646
|
+
|
|
1647
|
+
if yes and not queries:
|
|
1648
|
+
console.print("[bold red]--yes requires --queries[/] (nothing to confirm interactively)")
|
|
1649
|
+
sys.exit(2)
|
|
1639
1650
|
|
|
1640
1651
|
try:
|
|
1641
1652
|
runner_fn = resolve_runner(runner)
|
|
@@ -1678,8 +1689,13 @@ def bootstrap(queries, agent, runner, output, baseline_dir):
|
|
|
1678
1689
|
for i, q in enumerate(query_list):
|
|
1679
1690
|
console.print(f"\n[bold cyan]Running Query {i+1}/{len(query_list)}:[/] {q}")
|
|
1680
1691
|
try:
|
|
1681
|
-
|
|
1682
|
-
|
|
1692
|
+
# Same execution path as `agentci test`: TraceContext capture plus
|
|
1693
|
+
# coercion, so string-returning runners work here too.
|
|
1694
|
+
trace = _run_with_retry(runner_fn, q, retry_count=0, backoff=1.0, agent_name=agent)
|
|
1695
|
+
if trace is None:
|
|
1696
|
+
console.print(" [yellow]Runner returned nothing — skipping this query.[/]")
|
|
1697
|
+
continue
|
|
1698
|
+
|
|
1683
1699
|
# Print Tier 1 summary
|
|
1684
1700
|
console.print(f" Duration: {trace.total_duration_ms:.1f}ms")
|
|
1685
1701
|
console.print(f" Cost: ${trace.total_cost_usd:.4f}")
|
|
@@ -1687,8 +1703,11 @@ def bootstrap(queries, agent, runner, output, baseline_dir):
|
|
|
1687
1703
|
console.print(f" Tool Calls: {len(trace.tool_call_sequence)}")
|
|
1688
1704
|
if trace.tool_call_sequence:
|
|
1689
1705
|
console.print(f" Path: {' → '.join(trace.tool_call_sequence)}")
|
|
1690
|
-
|
|
1691
|
-
if
|
|
1706
|
+
answer_preview = (trace.metadata.get("final_output") or "")[:120]
|
|
1707
|
+
if answer_preview:
|
|
1708
|
+
console.print(f" Answer: {answer_preview}")
|
|
1709
|
+
|
|
1710
|
+
if yes or Confirm.ask("Accept this trace as golden? [Y/n]", default=True):
|
|
1692
1711
|
# Generates assertions
|
|
1693
1712
|
expected_tools = trace.tool_call_sequence
|
|
1694
1713
|
max_tool_calls = len(trace.tool_call_sequence) + 1
|
|
@@ -2779,6 +2798,8 @@ def test_cmd(config, tags, fmt, output, baseline_dir, workers, sample_ensemble,
|
|
|
2779
2798
|
evaluates all three layers (Correctness / Path / Cost), and reports results.
|
|
2780
2799
|
|
|
2781
2800
|
Use --mock to validate your spec with synthetic traces (zero API cost).
|
|
2801
|
+
With no agentci_spec.yaml present, --mock runs a bundled demo spec —
|
|
2802
|
+
try `agentci test --mock --runs 3` in an empty directory (zero API keys).
|
|
2782
2803
|
|
|
2783
2804
|
Use --runs N to run the whole suite N times: a stable aggregate score can
|
|
2784
2805
|
hide per-query verdict flips. Each flip is attributed to its source —
|
|
@@ -2806,6 +2827,25 @@ def test_cmd(config, tags, fmt, output, baseline_dir, workers, sample_ensemble,
|
|
|
2806
2827
|
from .engine.runner import evaluate_spec
|
|
2807
2828
|
from .exceptions import ConfigError
|
|
2808
2829
|
|
|
2830
|
+
# ── Zero-key demo fallback ────────────────────────────────────────────────
|
|
2831
|
+
# Only when the user did not pass --config themselves: an explicitly named
|
|
2832
|
+
# spec that is missing must stay an error, never silently become the demo.
|
|
2833
|
+
demo_mode = False
|
|
2834
|
+
config_source = click.get_current_context().get_parameter_source("config")
|
|
2835
|
+
if config_source == click.core.ParameterSource.DEFAULT and not Path(config).exists():
|
|
2836
|
+
if mock:
|
|
2837
|
+
from importlib.resources import files
|
|
2838
|
+
|
|
2839
|
+
config = str(files("agentci").joinpath("examples", "demo_spec.yaml"))
|
|
2840
|
+
demo_mode = True
|
|
2841
|
+
else:
|
|
2842
|
+
console.print(
|
|
2843
|
+
"[bold red]No agentci_spec.yaml found in this directory.[/]\n\n"
|
|
2844
|
+
" [cyan]agentci init[/] scaffold a spec for your own agent\n"
|
|
2845
|
+
" [cyan]agentci test --mock --runs 3[/] try the bundled demo (synthetic traces, zero API keys)"
|
|
2846
|
+
)
|
|
2847
|
+
sys.exit(2)
|
|
2848
|
+
|
|
2809
2849
|
try:
|
|
2810
2850
|
spec = load_spec(config)
|
|
2811
2851
|
except ConfigError as e:
|
|
@@ -2849,14 +2889,33 @@ def test_cmd(config, tags, fmt, output, baseline_dir, workers, sample_ensemble,
|
|
|
2849
2889
|
)
|
|
2850
2890
|
console.print("[dim]Running with synthetic traces — zero API cost[/]\n")
|
|
2851
2891
|
|
|
2892
|
+
if demo_mode:
|
|
2893
|
+
console.print(
|
|
2894
|
+
"[yellow]Demo mode:[/] no agentci_spec.yaml found — using the bundled "
|
|
2895
|
+
"demo spec ([bold]synthetic data[/], simulated agent).\n"
|
|
2896
|
+
"[dim]Test your own agent instead: agentci init[/]\n"
|
|
2897
|
+
)
|
|
2898
|
+
|
|
2852
2899
|
# AGENTCI_MOCK_FLAKY=1 simulates agent-variance across runs so the
|
|
2853
2900
|
# stability report (and its tests) can be exercised without API keys.
|
|
2854
|
-
|
|
2901
|
+
# Demo mode defaults the simulated flakiness ON for multi-run sessions —
|
|
2902
|
+
# the whole point of the demo is showing a stable score hiding verdict
|
|
2903
|
+
# flips — but an explicit env value always wins.
|
|
2904
|
+
flaky_env = os.environ.get("AGENTCI_MOCK_FLAKY")
|
|
2905
|
+
if flaky_env is None:
|
|
2906
|
+
mock_flaky = demo_mode and runs > 1
|
|
2907
|
+
else:
|
|
2908
|
+
mock_flaky = flaky_env.lower() in ("1", "true", "yes")
|
|
2855
2909
|
|
|
2856
2910
|
try:
|
|
2857
2911
|
run_results = []
|
|
2858
2912
|
for run_index in range(runs):
|
|
2859
|
-
traces = run_mock_spec(
|
|
2913
|
+
traces = run_mock_spec(
|
|
2914
|
+
spec,
|
|
2915
|
+
run_index=run_index,
|
|
2916
|
+
flaky=mock_flaky,
|
|
2917
|
+
flaky_style="spread" if demo_mode else "alternate",
|
|
2918
|
+
)
|
|
2860
2919
|
results = evaluate_spec(spec, traces, None)
|
|
2861
2920
|
run_results.append(results)
|
|
2862
2921
|
if runs > 1 and fmt in ("console", "github"):
|
|
@@ -2875,6 +2934,11 @@ def test_cmd(config, tags, fmt, output, baseline_dir, workers, sample_ensemble,
|
|
|
2875
2934
|
)
|
|
2876
2935
|
|
|
2877
2936
|
exit_code = report_results(run_results[0], format=fmt, spec_file=config)
|
|
2937
|
+
if demo_mode and fmt == "console":
|
|
2938
|
+
console.print(
|
|
2939
|
+
"\n[dim]Tip: agentci test --mock --runs 3 runs the demo suite three times "
|
|
2940
|
+
"and shows the stability report with flip-source attribution.[/]"
|
|
2941
|
+
)
|
|
2878
2942
|
sys.exit(exit_code)
|
|
2879
2943
|
|
|
2880
2944
|
# ── Check for runner ──────────────────────────────────────────────────────
|
|
@@ -82,7 +82,13 @@ def mock_run(query: str, query_spec: dict, flaky_break: bool = False) -> Trace:
|
|
|
82
82
|
return trace
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def run_mock_spec(
|
|
85
|
+
def run_mock_spec(
|
|
86
|
+
spec,
|
|
87
|
+
run_index: int = 0,
|
|
88
|
+
flaky: bool = False,
|
|
89
|
+
flaky_style: str = "alternate",
|
|
90
|
+
**_kwargs,
|
|
91
|
+
) -> dict[str, Trace]:
|
|
86
92
|
"""Run all queries in a spec using mock traces.
|
|
87
93
|
|
|
88
94
|
Parameters
|
|
@@ -92,11 +98,20 @@ def run_mock_spec(spec, run_index: int = 0, flaky: bool = False, **_kwargs) -> d
|
|
|
92
98
|
run_index : int
|
|
93
99
|
Which run this is (0-based) in a multi-run stability session.
|
|
94
100
|
flaky : bool
|
|
95
|
-
Simulate deterministic pseudo-flakiness for stability testing
|
|
96
|
-
even-indexed queries produce a broken answer on odd run indices
|
|
101
|
+
Simulate deterministic pseudo-flakiness for stability testing
|
|
97
102
|
(agent-variance). Judge-flake cannot be simulated here — identical
|
|
98
103
|
answers cannot flip deterministic checks by construction; that path
|
|
99
104
|
is covered by unit tests constructing results directly.
|
|
105
|
+
flaky_style : str
|
|
106
|
+
How simulated flakiness is distributed across runs:
|
|
107
|
+
|
|
108
|
+
- ``"alternate"`` (default): even-indexed queries break on odd run
|
|
109
|
+
indices. Half the suite flips at once, so the aggregate score
|
|
110
|
+
visibly dips on flaky runs.
|
|
111
|
+
- ``"spread"``: exactly one of the first three queries breaks per
|
|
112
|
+
run (query ``run_index % 3``). The aggregate score stays constant
|
|
113
|
+
across runs while individual verdicts flip — the "stable score,
|
|
114
|
+
unstable system" shape the bundled demo exists to show.
|
|
100
115
|
|
|
101
116
|
Returns
|
|
102
117
|
-------
|
|
@@ -106,6 +121,9 @@ def run_mock_spec(spec, run_index: int = 0, flaky: bool = False, **_kwargs) -> d
|
|
|
106
121
|
traces: dict[str, Trace] = {}
|
|
107
122
|
for i, q in enumerate(spec.queries):
|
|
108
123
|
query_dict = q.model_dump() if hasattr(q, "model_dump") else {}
|
|
109
|
-
|
|
124
|
+
if flaky_style == "spread":
|
|
125
|
+
flaky_break = flaky and i < 3 and run_index % 3 == i
|
|
126
|
+
else:
|
|
127
|
+
flaky_break = flaky and (i % 2 == 0) and (run_index % 2 == 1)
|
|
110
128
|
traces[q.query] = mock_run(q.query, query_dict, flaky_break=flaky_break)
|
|
111
129
|
return traces
|
|
@@ -528,8 +528,18 @@ def _serialize_stability(report: "StabilityReport") -> dict[str, Any]:
|
|
|
528
528
|
|
|
529
529
|
|
|
530
530
|
def _serialize_result(r: QueryResult) -> dict[str, Any]:
|
|
531
|
+
# Answer text included so JSON consumers (coding agents, judge-audit
|
|
532
|
+
# answer sources) can see what the agent said, not just the verdicts.
|
|
533
|
+
# Same extractor the correctness layer uses, so any trace shape the
|
|
534
|
+
# evaluator can grade also serializes its answer.
|
|
535
|
+
answer = None
|
|
536
|
+
if r.trace is not None:
|
|
537
|
+
from .runner import _extract_answer
|
|
538
|
+
|
|
539
|
+
answer = _extract_answer(r.trace) or None
|
|
531
540
|
return {
|
|
532
541
|
"query": r.query,
|
|
542
|
+
"answer": answer,
|
|
533
543
|
"hard_fail": r.hard_fail,
|
|
534
544
|
"has_warnings": r.has_warnings,
|
|
535
545
|
"correctness": {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# AgentCI bundled demo spec — SYNTHETIC DATA ONLY.
|
|
2
|
+
#
|
|
3
|
+
# This spec backs the zero-API-key demo:
|
|
4
|
+
#
|
|
5
|
+
# uvx ciagent test --mock --runs 3
|
|
6
|
+
#
|
|
7
|
+
# When `agentci test --mock` runs with no agentci_spec.yaml in the working
|
|
8
|
+
# directory, the CLI falls back to this file. Traces are synthesized by the
|
|
9
|
+
# mock runner; in multi-run demo sessions a deterministic pseudo-flaky agent
|
|
10
|
+
# is simulated so the stability report shows verdict flips with
|
|
11
|
+
# flip-source attribution. Nothing here touches a real agent or API.
|
|
12
|
+
#
|
|
13
|
+
# To test your own agent instead: `agentci init`
|
|
14
|
+
|
|
15
|
+
version: 1
|
|
16
|
+
agent: demo-support-agent
|
|
17
|
+
|
|
18
|
+
queries:
|
|
19
|
+
- query: "What's your return window?"
|
|
20
|
+
description: "Policy lookup — returns"
|
|
21
|
+
correctness:
|
|
22
|
+
expected_in_answer: ["30 days"]
|
|
23
|
+
path:
|
|
24
|
+
expected_tools: [search_kb]
|
|
25
|
+
|
|
26
|
+
- query: "Do you ship internationally?"
|
|
27
|
+
description: "Policy lookup — shipping"
|
|
28
|
+
correctness:
|
|
29
|
+
expected_in_answer: ["international shipping"]
|
|
30
|
+
path:
|
|
31
|
+
expected_tools: [search_kb]
|
|
32
|
+
|
|
33
|
+
- query: "How do I reset my password?"
|
|
34
|
+
description: "Account help — password reset"
|
|
35
|
+
correctness:
|
|
36
|
+
expected_in_answer: ["reset link"]
|
|
37
|
+
path:
|
|
38
|
+
expected_tools: [search_kb]
|
|
39
|
+
|
|
40
|
+
- query: "What payment methods do you accept?"
|
|
41
|
+
description: "Policy lookup — payments"
|
|
42
|
+
correctness:
|
|
43
|
+
expected_in_answer: ["credit card"]
|
|
44
|
+
path:
|
|
45
|
+
expected_tools: [search_kb]
|
|
46
|
+
|
|
47
|
+
- query: "Can I change my delivery address after ordering?"
|
|
48
|
+
description: "Order management — address change"
|
|
49
|
+
correctness:
|
|
50
|
+
expected_in_answer: ["before it ships"]
|
|
51
|
+
path:
|
|
52
|
+
expected_tools: [search_kb, lookup_order]
|
|
53
|
+
|
|
54
|
+
- query: "How do I cancel my subscription?"
|
|
55
|
+
description: "Account help — cancellation"
|
|
56
|
+
correctness:
|
|
57
|
+
expected_in_answer: ["account settings"]
|
|
58
|
+
path:
|
|
59
|
+
expected_tools: [search_kb]
|
|
60
|
+
|
|
61
|
+
- query: "Do you offer student discounts?"
|
|
62
|
+
description: "Policy lookup — discounts"
|
|
63
|
+
correctness:
|
|
64
|
+
expected_in_answer: ["10%"]
|
|
65
|
+
path:
|
|
66
|
+
expected_tools: [search_kb]
|
|
67
|
+
|
|
68
|
+
- query: "Where can I download my invoice?"
|
|
69
|
+
description: "Account help — invoices"
|
|
70
|
+
correctness:
|
|
71
|
+
expected_in_answer: ["billing page"]
|
|
72
|
+
path:
|
|
73
|
+
expected_tools: [search_kb]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|