python-yama 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-yama
3
+ Version: 0.2.0
4
+ Summary: A reproducible evaluation runner for tool-using Agent skills
5
+ Project-URL: Repository, https://github.com/world-sim-dev/yama
6
+ Project-URL: Issues, https://github.com/world-sim-dev/yama/issues
7
+ Author-email: Farmer Sun <podpodiumapp@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent,evaluation,llm,skill,testing
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: fastapi>=0.139.0
19
+ Requires-Dist: jinja2>=3.1
20
+ Requires-Dist: litellm<2.0,>=1.80
21
+ Requires-Dist: orjson>=3.11.9
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: rich<15.0,>=14.1
24
+ Description-Content-Type: text/markdown
25
+
26
+ # yama
27
+
28
+ English | [简体中文](https://github.com/world-sim-dev/yama/blob/main/README.zh-CN.md)
29
+
30
+ `yama` is a reproducible evaluation framework for tool-using Agent Skills: a declarative YAML Case describes what the model sees — the system prompt, Skill metadata, tool schemas, and multi-turn user messages — then yama calls the LLM through LiteLLM, drives the tool loop, and scores the transcript with deterministic hard checks or an LLM judge. See the [design doc](https://github.com/world-sim-dev/yama/blob/main/docs/agent-skill-test-framework-design.md) (Chinese) for the full Case schema and execution contract.
31
+
32
+ ## Quick start
33
+
34
+ ```bash
35
+ # Run from a plugin root; collects __evals__/cases/**/*.yaml by default
36
+ cd dingding-simple
37
+ OPENAI_API_KEY=... uv run --project yama yama
38
+
39
+ # Or, from a workspace root (a directory with yama.toml), pick a plugin by name
40
+ OPENAI_API_KEY=... uv run --project yama yama --plugin dingding-simple
41
+ ```
42
+
43
+ ## Writing a Case
44
+
45
+ A Case is a YAML file organized as `context` (what the model sees) → `mocks` (how tool calls get executed) → `steps` (user turns sent in order, each with assertions) → `outcome` (the pass bar for the whole Case):
46
+
47
+ ```yaml
48
+ context:
49
+ system_prompt: { default: true } # use SYSTEM.md from the plugin root
50
+
51
+ tools:
52
+ - file: tools/read-dsl.yaml # tool schema, resolved relative to __evals__
53
+
54
+ mocks:
55
+ tools:
56
+ read_dsl:
57
+ respond:
58
+ result:
59
+ visualStyle: { name: Vintage Film }
60
+
61
+ steps:
62
+ - id: request-directions
63
+ user: Give me a few creative directions
64
+ assert:
65
+ hard:
66
+ - tool_called: { name: read_dsl }
67
+ - assistant_contains: creative direction
68
+
69
+ outcome:
70
+ require:
71
+ hard_checks: all_pass
72
+ ```
73
+
74
+ - `context.tools` is the tool schema sent to the model; `mocks.tools` is how the runner responds when a tool call arrives. The two sets of tool names must match exactly.
75
+ - To use a Skill, declare it by name under `context.skills` and also declare `{builtin: skill}` under `context.tools`; the model reads the Skill body via `skill(name, file?)`.
76
+ - To simulate command-line tools, declare `{builtin: bash}` and configure per-command output under `mocks.cli`.
77
+ - `steps[].assert.hard` are deterministic checks (nine types, including `tool_called`, `tool_arguments`, and `assistant_contains`); add `assert.judge` for LLM scoring.
78
+
79
+ For the full schema (every Skill/tool/mock form, the `bash` sandbox, message injection, judge configuration, and more), see the [design doc](https://github.com/world-sim-dev/yama/blob/main/docs/agent-skill-test-framework-design.md) (Chinese).
80
+
81
+ ## CLI usage
82
+
83
+ ```bash
84
+ uv run --project yama yama --plugin dingding-simple --report
85
+ ```
86
+
87
+ | Flag | Meaning |
88
+ | --- | --- |
89
+ | `paths` (positional, repeatable) | Explicit Case YAML paths to run |
90
+ | `--plugin-root PATH` | Use the given path as the single plugin root |
91
+ | `--plugin NAME` (repeatable) | Select plugins by name from `yama.toml` |
92
+ | `--all-plugins` | Run every plugin configured in `yama.toml` |
93
+ | `--result-dir PATH` | Override the artifact root (default `<plugin_root>/.yama/runs`) |
94
+ | `--report [PATH]` | Also generate a single-file HTML report (default `.yama/reports/latest.html`) |
95
+ | `--list` | Only print the collected Cases, without running them |
96
+ | `--no-artifacts` | Write no artifact files at all |
97
+ | `--json` | Emit machine-readable JSON instead of Rich tables |
98
+
99
+ `--plugin`, `--all-plugins`, and `--plugin-root` are mutually exclusive. When none of them is given, yama walks up from the current directory to the nearest directory containing `yama.toml` and uses it as the workspace root (falling back to the cwd), running it as a single plugin root.
@@ -1,12 +1,12 @@
1
1
  yama/__init__.py,sha256=UYhoZTSu1HiMT54LOZCdvLKMQ_L0qec8RT8jDfde_cY,1156
2
2
  yama/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
3
  yama/assertions.py,sha256=0x1r82W2dtAY1mTk4LC86VulD5Q9blS7QeRCTysb4kE,21736
4
- yama/cli.py,sha256=0RsIDhv767oJ42-1FfkAxttJXqardA2UG33bSHDQH6w,11718
5
- yama/llm.py,sha256=RrgU2xO8J_AfiXwOJn1Dmb3OzjbwWFpW7mpQ6dlHYiQ,13352
6
- yama/mocks.py,sha256=v94SFa5VyTB3cvMMcuHMkiSL8HS962-0OmY5iWq9mxs,14256
4
+ yama/cli.py,sha256=RJcffqSX2DTUU8i6PB2ZyPMXuACCpBuj_Ifm-uQ4j9g,11736
5
+ yama/llm.py,sha256=XqGE4aKPOimy9n3oNF5rUvqWdJlpi0EVzMlD-FBim6A,13570
6
+ yama/mocks.py,sha256=YyiU6Y7NgttxHYJBsqGPILKYlTsM4IMTIilMNRkUuzs,14973
7
7
  yama/models.py,sha256=-ry0QUI2e0ialL6A9NUEAM0QpwCQg8oXEfPBXn3CvNI,3964
8
- yama/report.py,sha256=qnLpp8yA2TdZw1UWHN_XtJWMVa0v9Z7f0ez9z9HViY0,37563
9
- yama/runner.py,sha256=GRg3VWZBw4bupDW6qMQO7TjE2VHjwo1hV8HYYw0_Mks,20661
8
+ yama/report.py,sha256=bjp_aev2KHx3i2pqYRGE1VHTuBmiakXgbqYjN3RAGEg,37558
9
+ yama/runner.py,sha256=dJi-hxXxpUawZ3g14Hk9NFZy7HQNvcGpxpxvj8QKbX8,21694
10
10
  yama/workspace.py,sha256=rHQ9kQUkAo32gJpqRf7pSd5JhBc05xgxA1wPRC4bOQ8,3618
11
11
  yama/loaders/__init__.py,sha256=DIwfa8QlIlKt6NpFMZJ_OcqbJPPrmKLBzd9_e-BCuBs,174
12
12
  yama/loaders/case.py,sha256=gas5J9jtPuaqo43IO5gk-4uVJ49JWc7sNMXFI8I_7AA,14255
@@ -17,11 +17,11 @@ yama/tools/base.py,sha256=ONFuGzTSWVsA0k4-UUREncqAHmyQI2uHpVOU2Dh_XiQ,446
17
17
  yama/tools/bash/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  yama/tools/bash/_cli_shim.py,sha256=f3Rom5UipW0dA0KRYavqIa9HJDKT336ssDZGWeVmkvo,2494
19
19
  yama/tools/bash/bash.py,sha256=4GlFvmqTUS4k6DgJ5Io91YF5fB6wF6e3e7p2P-pmbEQ,10358
20
- yama/tools/bash/fs.py,sha256=CMPPAEfubt2kkaccxQaIYriJ497J2_CDfZm5l3evnaY,20674
20
+ yama/tools/bash/fs.py,sha256=ncLZf4_gErRP3HHXinLEQeN6chrf7JaSiaTuadVl8MY,21314
21
21
  yama/tools/skill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  yama/tools/skill/skill.py,sha256=P2rDz6VqRLfhiabWjOU3ZBaWLw30Eywf3o_OxVfFbPI,1992
23
- python_yama-0.1.0.dist-info/METADATA,sha256=klpVw1LaW-EsoGJQtvzH950HF4vLA5QmOehOX2sxWPI,4299
24
- python_yama-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
25
- python_yama-0.1.0.dist-info/entry_points.txt,sha256=N5se3y1FflUcDU3TmUrH2Tu9dzsrBBo7sUP-Gu_2cVs,39
26
- python_yama-0.1.0.dist-info/licenses/LICENSE,sha256=cMAJJAs7R1lemJPPc7qMNv4w_sqn5QncFf4aXKYyEc4,1067
27
- python_yama-0.1.0.dist-info/RECORD,,
23
+ python_yama-0.2.0.dist-info/METADATA,sha256=TuKyzkSIHUvjSVNTBj6YYErZjK91mznGMs6gFBFLCmg,4598
24
+ python_yama-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
25
+ python_yama-0.2.0.dist-info/entry_points.txt,sha256=N5se3y1FflUcDU3TmUrH2Tu9dzsrBBo7sUP-Gu_2cVs,39
26
+ python_yama-0.2.0.dist-info/licenses/LICENSE,sha256=cMAJJAs7R1lemJPPc7qMNv4w_sqn5QncFf4aXKYyEc4,1067
27
+ python_yama-0.2.0.dist-info/RECORD,,
yama/cli.py CHANGED
@@ -19,7 +19,7 @@ from .workspace import (
19
19
  load_workspace_config,
20
20
  resolve_plugin_root,
21
21
  )
22
- from .llm import LLMError, LiteLLMClient, ScriptedLLMClient
22
+ from .llm import LLMError, LiteLLMClient
23
23
  from .loaders import CollectionError, resolve_case
24
24
  from .report import write_html_report
25
25
  from .runner import YamaRunner
@@ -190,9 +190,6 @@ def _parser() -> argparse.ArgumentParser:
190
190
  parser.add_argument(
191
191
  "--all-plugins", action="store_true", help="Run all configured plugins"
192
192
  )
193
- parser.add_argument(
194
- "--response-script", type=Path, help="Use deterministic scripted LLM responses"
195
- )
196
193
  parser.add_argument("--result-dir", type=Path, help="Override artifact directory")
197
194
  parser.add_argument(
198
195
  "--report",
@@ -255,11 +252,7 @@ async def _run(args: argparse.Namespace) -> int:
255
252
  Console(stderr=True).print("[bold red]No eval cases collected.[/bold red]")
256
253
  return 2
257
254
 
258
- llm = (
259
- ScriptedLLMClient.from_file(args.response_script.resolve())
260
- if args.response_script
261
- else LiteLLMClient()
262
- )
255
+ llm = LiteLLMClient()
263
256
  all_results = []
264
257
  result_roots = []
265
258
  for case in collected:
@@ -275,8 +268,6 @@ async def _run(args: argparse.Namespace) -> int:
275
268
  write_artifacts=not args.no_artifacts,
276
269
  )
277
270
  all_results.append(await runner.run_case(case))
278
- if isinstance(llm, ScriptedLLMClient):
279
- llm.assert_consumed()
280
271
  report_target = args.report
281
272
  if report_target == DEFAULT_REPORT:
282
273
  report_target = result_roots[0].parent / "reports" / "latest.html"
@@ -309,10 +300,18 @@ def main(argv: list[str] | None = None) -> int:
309
300
  args = _parser().parse_args(argv)
310
301
  try:
311
302
  return asyncio.run(_run(args))
312
- except (ConfigurationError, CollectionError, LLMError, ValueError) as exc:
303
+ except (ConfigurationError, CollectionError, ValueError) as exc:
313
304
  Console(stderr=True).print(
314
305
  Text.assemble(("Collection error: ", "bold red"), (str(exc), "red"))
315
306
  )
316
307
  return 2
308
+ except LLMError as exc:
309
+ # Not a collection problem: cases were already collected (and possibly
310
+ # executed) when the LLM layer failed -- label it accordingly so the
311
+ # user debugs the right layer.
312
+ Console(stderr=True).print(
313
+ Text.assemble(("LLM error: ", "bold red"), (str(exc), "red"))
314
+ )
315
+ return 2
317
316
  except KeyboardInterrupt:
318
317
  return 130
yama/llm.py CHANGED
@@ -337,7 +337,10 @@ class ScriptedLLMClient(LLMClient):
337
337
  if not isinstance(expectation, dict):
338
338
  raise LLMError("script expect must be an object")
339
339
  if "tools" in expectation:
340
- actual = [tool["function"]["name"] for tool in request["tools"]]
340
+ # A request with no tools omits the "tools" key entirely (see
341
+ # _litellm_call_arguments), so expect: {tools: []} must read it
342
+ # as an empty list instead of raising KeyError.
343
+ actual = [tool["function"]["name"] for tool in request.get("tools", [])]
341
344
  if actual != expectation["tools"]:
342
345
  raise LLMError(
343
346
  f"script expected tools {expectation['tools']}, got {actual}"
yama/mocks.py CHANGED
@@ -178,6 +178,27 @@ def _matches(actual: Any, expected: Any) -> bool:
178
178
  _MISSING = object()
179
179
 
180
180
 
181
+ def result_is_error(result: Any) -> bool:
182
+ """Semantic error status of a tool result envelope.
183
+
184
+ A result is an error only when it carries a non-null ``error`` payload,
185
+ or when it is a bash-style ``{stdout, stderr, exit_code}`` envelope with a
186
+ non-zero exit code. A literal ``error: null`` field is a successful result.
187
+ """
188
+ if not isinstance(result, dict):
189
+ return False
190
+ if result.get("error") is not None:
191
+ return True
192
+ if (
193
+ isinstance(result.get("stdout"), str)
194
+ and isinstance(result.get("stderr"), str)
195
+ and isinstance(result.get("exit_code"), int)
196
+ and not isinstance(result.get("exit_code"), bool)
197
+ ):
198
+ return result["exit_code"] != 0
199
+ return False
200
+
201
+
181
202
  def _state_diff(before: Any, after: Any, path: str = "$") -> list[dict[str, Any]]:
182
203
  if before == after:
183
204
  return []
@@ -353,7 +374,7 @@ class MockToolRegistry:
353
374
  "match_failed": match_failed,
354
375
  "arguments": arguments,
355
376
  "result": result,
356
- "is_error": is_error or (isinstance(result, dict) and "error" in result),
377
+ "is_error": is_error or result_is_error(result),
357
378
  "duration_ms": round((time.perf_counter() - started) * 1000, 3),
358
379
  "state_diff": _state_diff(before, after),
359
380
  }
yama/report.py CHANGED
@@ -418,7 +418,7 @@ def _render_step(
418
418
  "</span>"
419
419
  "</summary>"
420
420
  '<div class="step-body">'
421
- '<details class="subsection timeline-section" open>'
421
+ '<details class="subsection timeline-section">'
422
422
  '<summary><span>Execution timeline</span>'
423
423
  f'<span class="muted">{len(events)} events</span></summary>'
424
424
  f'<div class="timeline">{"".join(events)}</div></details>'
yama/runner.py CHANGED
@@ -14,9 +14,15 @@ from typing import Any
14
14
  import yaml
15
15
 
16
16
  from .assertions import run_hard_assertions, run_judge
17
- from .tools.bash.fs import cleanup_session
17
+ from .tools.bash.fs import case_workspace_root, cleanup_session
18
18
  from .llm import LLMClient
19
- from .mocks import MockToolRegistry, ToolMockContext, _matches, load_python_callable
19
+ from .mocks import (
20
+ MockToolRegistry,
21
+ ToolMockContext,
22
+ _matches,
23
+ load_python_callable,
24
+ result_is_error,
25
+ )
20
26
  from .models import ResolvedCase, StepResult, ToolCallRecord, sanitize_case_key
21
27
 
22
28
 
@@ -109,6 +115,7 @@ class YamaRunner:
109
115
  repeat = int(case.execution.get("repeat", 1))
110
116
  if repeat < 1:
111
117
  raise ValueError("execution.repeat must be at least 1")
118
+ self._clean_case_dirs(case)
112
119
  runs = [
113
120
  await self._run_once(case, run_index) for run_index in range(1, repeat + 1)
114
121
  ]
@@ -351,7 +358,7 @@ class YamaRunner:
351
358
  call_index=call_index,
352
359
  call=call,
353
360
  result=result,
354
- is_error=isinstance(result, dict) and "error" in result,
361
+ is_error=result_is_error(result),
355
362
  )
356
363
  records.append(record)
357
364
  messages.append(
@@ -452,12 +459,32 @@ class YamaRunner:
452
459
  return False
453
460
  return True
454
461
 
462
+ def _clean_case_dirs(self, case: ResolvedCase) -> None:
463
+ """Drop leftovers of earlier executions before the first run starts.
464
+
465
+ Both the bash-sandbox workspaces (`.yama/fs-runs/<case>`) and the run
466
+ artifacts (`.yama/runs/<case>`) are keyed by run index, which restarts
467
+ at 1 on every execution -- without this wipe a later execution would
468
+ reuse stale workspaces (old .mockbin scripts, files written by a
469
+ previous model) and leave orphaned run-00N artifact dirs behind when
470
+ execution.repeat shrinks.
471
+ """
472
+ fs_root = case_workspace_root(case.plugin_root, case.case_key)
473
+ if fs_root.exists():
474
+ shutil.rmtree(fs_root)
475
+ if self.write_artifacts:
476
+ case_dir = self._case_artifact_root(case)
477
+ if case_dir.exists():
478
+ shutil.rmtree(case_dir)
479
+
480
+ def _case_artifact_root(self, case: ResolvedCase) -> Path:
481
+ root = self.result_root or case.plugin_root / ".yama" / "runs"
482
+ return root / sanitize_case_key(case.case_key)
483
+
455
484
  def _artifact_dir(self, case: ResolvedCase, run_index: int) -> Path | None:
456
485
  if not self.write_artifacts:
457
486
  return None
458
- root = self.result_root or case.plugin_root / ".yama" / "runs"
459
- key = sanitize_case_key(case.case_key)
460
- return root / key / f"run-{run_index:03d}"
487
+ return self._case_artifact_root(case) / f"run-{run_index:03d}"
461
488
 
462
489
  def _write_artifacts(self, case: ResolvedCase, run: RunResult) -> None:
463
490
  assert run.artifact_dir is not None
yama/tools/bash/fs.py CHANGED
@@ -463,6 +463,15 @@ class ShellSession:
463
463
  pass
464
464
 
465
465
 
466
+ def case_workspace_root(plugin_root: Path, case_key: str) -> Path:
467
+ """Parent directory of all bash-sandbox workspaces of one case.
468
+
469
+ The runner wipes it before a case's first run so a fresh execution never
470
+ reuses stale workspaces from an earlier one.
471
+ """
472
+ return plugin_root / ".yama" / "fs-runs" / sanitize_case_key(case_key)
473
+
474
+
466
475
  def workspace_dir(context: ToolMockContext) -> Path:
467
476
  """Bash-sandbox workspace directory of the current run.
468
477
 
@@ -471,10 +480,7 @@ def workspace_dir(context: ToolMockContext) -> Path:
471
480
  first `bash` call actually materializes the directory.
472
481
  """
473
482
  return (
474
- context.plugin_root
475
- / ".yama"
476
- / "fs-runs"
477
- / sanitize_case_key(context.case_key)
483
+ case_workspace_root(context.plugin_root, context.case_key)
478
484
  / f"run-{context.run_index:03d}"
479
485
  )
480
486
 
@@ -485,7 +491,12 @@ async def _get_session(
485
491
  session = _SESSIONS.get(id(context.state))
486
492
  if session is None or session.closed:
487
493
  workspace = workspace_dir(context)
488
- if not workspace.exists():
494
+ # Keyed on the per-run state marker, not directory existence: a
495
+ # leftover directory from an earlier execution (or one pre-created by
496
+ # a python handler) must still get this run's files and .mockbin
497
+ # scripts, while a session restart mid-run must not clobber what the
498
+ # model already wrote.
499
+ if "__fs__" not in context.state:
489
500
  materialize_workspace(workspace, fs_config)
490
501
  session = ShellSession(workspace, fs_config)
491
502
  await session.start()
@@ -1,98 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-yama
3
- Version: 0.1.0
4
- Summary: A reproducible evaluation runner for tool-using Agent skills
5
- Project-URL: Repository, https://github.com/world-sim-dev/yama
6
- Project-URL: Issues, https://github.com/world-sim-dev/yama/issues
7
- Author-email: Farmer Sun <podpodiumapp@gmail.com>
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Keywords: agent,evaluation,llm,skill,testing
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Topic :: Software Development :: Testing
17
- Requires-Python: >=3.12
18
- Requires-Dist: fastapi>=0.139.0
19
- Requires-Dist: jinja2>=3.1
20
- Requires-Dist: litellm<2.0,>=1.80
21
- Requires-Dist: orjson>=3.11.9
22
- Requires-Dist: pyyaml>=6.0
23
- Requires-Dist: rich<15.0,>=14.1
24
- Description-Content-Type: text/markdown
25
-
26
- # yama
27
-
28
- `yama` 是一个可复现的 Agent Skill 评测框架:用声明式 YAML Case 描述模型看到的 System Prompt、Skill metadata、Tool Schema 与多轮 User Message,通过 LiteLLM 统一调用 LLM 并驱动 Tool Loop,再对 Transcript 做确定性 Hard Check 或 LLM Judge 评分。完整 Case Schema 与执行契约见[设计文档](docs/agent-skill-test-framework-design.md)。
29
-
30
- ## 快速开始
31
-
32
- ```bash
33
- # 在 Plugin 根目录下直接运行,默认收集 __evals__/cases/**/*.yaml
34
- cd dingding-simple
35
- OPENAI_API_KEY=... uv run --project yama yama
36
-
37
- # 在 Workspace 根目录(存在 yama.toml)按名字指定 Plugin
38
- OPENAI_API_KEY=... uv run --project yama yama --plugin dingding-simple
39
- ```
40
-
41
- ## 编写一个 Case
42
-
43
- 一个 Case 是一份 YAML 文件,按 `context`(模型看到什么)→ `mocks`(Tool 怎么被执行)→ `steps`(依次发送的用户回合与断言)→ `outcome`(整个 Case 的通过门槛)的结构组织:
44
-
45
- ```yaml
46
- context:
47
- system_prompt: { default: true } # 使用 Plugin 根目录下的 SYSTEM.md
48
-
49
- tools:
50
- - file: tools/read-dsl.yaml # Tool Schema,相对 __evals__ 目录解析
51
-
52
- mocks:
53
- tools:
54
- read_dsl:
55
- respond:
56
- result:
57
- visualStyle: { name: 复古胶片 }
58
-
59
- steps:
60
- - id: request-directions
61
- user: 给我几个创意方向
62
- assert:
63
- hard:
64
- - tool_called: { name: read_dsl }
65
- - assistant_contains: 创意方向
66
-
67
- outcome:
68
- require:
69
- hard_checks: all_pass
70
- ```
71
-
72
- - `context.tools` 是发给模型的 Tool Schema,`mocks.tools` 是 Runner 收到 Tool Call 后如何返回结果;两者的 Tool 名字集合必须完全一致。
73
- - 需要 Skill 时在 `context.skills` 中按名字声明,并在 `context.tools` 中同时声明 `{builtin: skill}`;模型通过 `skill(name, file?)` 读取正文。
74
- - 需要模拟命令行工具时声明 `{builtin: bash}`,在 `mocks.cli` 中按命令配置输出。
75
- - `steps[].assert.hard` 是确定性检查(`tool_called`、`tool_arguments`、`assistant_contains` 等九种类型);还可以加 `assert.judge` 做 LLM 打分。
76
-
77
- 完整 Schema(Skill/Tool/Mock 的全部写法、`bash` 沙箱、Message Injection、Judge 配置等)见[设计文档](docs/agent-skill-test-framework-design.md)。
78
-
79
- ## 命令行使用
80
-
81
- ```bash
82
- uv run --project yama yama --plugin dingding-simple --report
83
- ```
84
-
85
- | 参数 | 含义 |
86
- | --- | --- |
87
- | `paths`(位置参数,可多个) | 显式指定要跑的 Case YAML 路径 |
88
- | `--plugin-root PATH` | 以指定路径作为单一 Plugin Root |
89
- | `--plugin NAME`(可重复) | 按 `yama.toml` 中的 Plugin 名字选择 |
90
- | `--all-plugins` | 运行 `yama.toml` 中配置的全部 Plugin |
91
- | `--response-script PATH` | 用脚本化 LLM 响应回放,不调用真实模型 |
92
- | `--result-dir PATH` | 覆盖产物根目录(默认 `<plugin_root>/.yama/runs`) |
93
- | `--report [PATH]` | 额外生成单文件 HTML 报告(默认 `.yama/reports/latest.html`) |
94
- | `--list` | 只打印匹配到的 Case,不运行 |
95
- | `--no-artifacts` | 不写任何产物文件 |
96
- | `--json` | 输出机器可读 JSON 而不是 Rich 表格 |
97
-
98
- `--plugin`/`--all-plugins`/`--plugin-root` 三者互斥;都不提供时默认用当前目录向上搜索到的 `yama.toml` 所在目录作为 Workspace Root(找不到则用 cwd),把该目录当作单一 Plugin Root 运行。