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.
Files changed (54) hide show
  1. {ciagent-0.7.0 → ciagent-0.8.0}/CHANGELOG.md +42 -1
  2. {ciagent-0.7.0 → ciagent-0.8.0}/PKG-INFO +45 -1
  3. {ciagent-0.7.0 → ciagent-0.8.0}/README.md +44 -0
  4. {ciagent-0.7.0 → ciagent-0.8.0}/pyproject.toml +3 -2
  5. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/capture.py +35 -15
  6. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/cli.py +73 -9
  7. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/mock_runner.py +22 -4
  8. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/reporter.py +10 -0
  9. ciagent-0.8.0/src/agentci/examples/__init__.py +3 -0
  10. ciagent-0.8.0/src/agentci/examples/demo_spec.yaml +73 -0
  11. {ciagent-0.7.0 → ciagent-0.8.0}/.gitignore +0 -0
  12. {ciagent-0.7.0 → ciagent-0.8.0}/LICENSE +0 -0
  13. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/__init__.py +0 -0
  14. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/__init__.py +0 -0
  15. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/base.py +0 -0
  16. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/generic.py +0 -0
  17. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/langgraph.py +0 -0
  18. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/adapters/openai_agents.py +0 -0
  19. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/assertions.py +0 -0
  20. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/baselines.py +0 -0
  21. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/config.py +0 -0
  22. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/cost.py +0 -0
  23. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/diff_engine.py +0 -0
  24. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/__init__.py +0 -0
  25. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/check_generator.py +0 -0
  26. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/correctness.py +0 -0
  27. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/cost.py +0 -0
  28. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/cost_estimator.py +0 -0
  29. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/diff.py +0 -0
  30. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/judge.py +0 -0
  31. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/judge_audit.py +0 -0
  32. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/metrics.py +0 -0
  33. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/parallel.py +0 -0
  34. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/path.py +0 -0
  35. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/results.py +0 -0
  36. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/runner.py +0 -0
  37. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/span_assertions.py +0 -0
  38. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/engine/stability.py +0 -0
  39. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/exceptions.py +0 -0
  40. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/loader.py +0 -0
  41. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/mocks.py +0 -0
  42. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/models.py +0 -0
  43. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/py.typed +0 -0
  44. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/pytest_plugin.py +0 -0
  45. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/report.py +0 -0
  46. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/runner.py +0 -0
  47. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/__init__.py +0 -0
  48. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/agentci_spec.schema.json +0 -0
  49. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/generate_schema.py +0 -0
  50. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/schema/spec_models.py +0 -0
  51. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/__init__.py +0 -0
  52. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/github_action.yml.j2 +0 -0
  53. {ciagent-0.7.0 → ciagent-0.8.0}/src/agentci/templates/pre_push_hook.sh.j2 +0 -0
  54. {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
- ## [Unreleased]
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.7.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.7.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
- _active_trace.set(self.trace)
69
- _active_span.set(root_span)
70
-
71
- # Apply monkey patches
72
- self._patch_openai()
73
- self._patch_anthropic()
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
- # Clear context
93
- _active_trace.set(None)
94
- _active_span.set(None)
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
- def bootstrap(queries, agent, runner, output, baseline_dir):
1631
- """Zero-to-Golden interactive bootstrapper."""
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
- trace = runner_fn(q)
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 Confirm.ask("Accept this trace as golden? [Y/n]", default=True):
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
- mock_flaky = os.environ.get("AGENTCI_MOCK_FLAKY", "").lower() in ("1", "true", "yes")
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(spec, run_index=run_index, flaky=mock_flaky)
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(spec, run_index: int = 0, flaky: bool = False, **_kwargs) -> dict[str, Trace]:
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
- flaky_break = flaky and (i % 2 == 0) and (run_index % 2 == 1)
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,3 @@
1
+ # Copyright 2025-2026 The AgentCI Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Bundled example specs shipped with the package (used by the zero-key demo)."""
@@ -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