quick-agent 0.1.1__py3-none-any.whl → 0.1.3__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.
- quick_agent/__init__.py +4 -1
- quick_agent/agent_call_tool.py +22 -5
- quick_agent/agent_registry.py +7 -27
- quick_agent/agent_tools.py +3 -2
- quick_agent/cli.py +19 -5
- quick_agent/directory_permissions.py +7 -3
- quick_agent/input_adaptors.py +30 -0
- quick_agent/llms.txt +239 -0
- quick_agent/models/agent_spec.py +3 -0
- quick_agent/models/loaded_agent_file.py +136 -1
- quick_agent/models/output_spec.py +1 -1
- quick_agent/orchestrator.py +15 -8
- quick_agent/prompting.py +34 -16
- quick_agent/py.typed +1 -0
- quick_agent/quick_agent.py +171 -155
- quick_agent/schemas/outputs.py +6 -0
- quick_agent-0.1.3.data/data/quick_agent/agents/business-extract-structured.md +49 -0
- quick_agent-0.1.3.data/data/quick_agent/agents/business-extract.md +42 -0
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/function-spec-validator.md +1 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validate-eval-list.md +1 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validator-contains.md +8 -1
- {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/template.md +12 -1
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/METADATA +21 -4
- quick_agent-0.1.3.dist-info/RECORD +52 -0
- tests/test_agent.py +273 -9
- tests/test_directory_permissions.py +10 -0
- tests/test_httpx_tools.py +295 -0
- tests/test_input_adaptors.py +31 -0
- tests/test_integration.py +134 -1
- tests/test_orchestrator.py +525 -111
- quick_agent-0.1.1.dist-info/RECORD +0 -45
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/WHEEL +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/entry_points.txt +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -49,7 +49,7 @@ handoff:
|
|
|
49
49
|
input_mode: "final_output_json"
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
## Instructions
|
|
53
53
|
|
|
54
54
|
You are an EVAL LIST EXECUTOR. Your sole responsibility is reading an eval list file, executing each test agent, validating responses, and producing a results summary. You do NOT perform the tasks yourself—you delegate them entirely.
|
|
55
55
|
|
|
@@ -47,7 +47,7 @@ handoff:
|
|
|
47
47
|
input_mode: "final_output_json"
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Instructions
|
|
51
51
|
|
|
52
52
|
You are a RESPONSE VALIDATOR. Your sole responsibility is comparing a response against expected text patterns defined in an eval.md file. You report PASS or FAIL with detailed results.
|
|
53
53
|
|
|
@@ -56,10 +56,12 @@ You are a RESPONSE VALIDATOR. Your sole responsibility is comparing a response a
|
|
|
56
56
|
### Step 1: Parse Input
|
|
57
57
|
|
|
58
58
|
Extract from the user's request:
|
|
59
|
+
|
|
59
60
|
- Response Text: the text to validate from a provided file path
|
|
60
61
|
- Eval File Path: path to the markdown file containing expected text patterns
|
|
61
62
|
|
|
62
63
|
Input formats accepted:
|
|
64
|
+
|
|
63
65
|
- `validate "{response-file.md}" against {path/to/eval.md}`
|
|
64
66
|
- `check response in {response-file.md} contains {eval.md}`
|
|
65
67
|
- Direct response text followed by eval file path
|
|
@@ -86,26 +88,31 @@ Output a structured validation report and a PASS/FAIL status.
|
|
|
86
88
|
## step:plan
|
|
87
89
|
|
|
88
90
|
Goal:
|
|
91
|
+
|
|
89
92
|
- Identify response path/text and eval file path from the input.
|
|
90
93
|
- Outline the minimal steps.
|
|
91
94
|
|
|
92
95
|
Constraints:
|
|
96
|
+
|
|
93
97
|
- Keep it short.
|
|
94
98
|
|
|
95
99
|
## step:execute
|
|
96
100
|
|
|
97
101
|
Goal:
|
|
102
|
+
|
|
98
103
|
- Read the response file (if a path is provided).
|
|
99
104
|
- Read the eval file.
|
|
100
105
|
- Check for each expected text line in the response.
|
|
101
106
|
- Note missing lines.
|
|
102
107
|
|
|
103
108
|
Constraints:
|
|
109
|
+
|
|
104
110
|
- Do not output JSON in this step.
|
|
105
111
|
|
|
106
112
|
## step:finalize
|
|
107
113
|
|
|
108
114
|
Goal:
|
|
115
|
+
|
|
109
116
|
- Return a `ContainsValidationResult` JSON object with:
|
|
110
117
|
- `status`: PASS if all expected lines are present, otherwise FAIL
|
|
111
118
|
- `checks`: list of per-line results
|
|
@@ -52,7 +52,13 @@ handoff:
|
|
|
52
52
|
input_mode: "final_output_json" # or "final_output_markdown"
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# System Prompt
|
|
56
|
+
|
|
57
|
+
This is a system prompt to included in every run
|
|
58
|
+
|
|
59
|
+
## Instructions
|
|
60
|
+
|
|
61
|
+
Instructions are only included in first run.
|
|
56
62
|
|
|
57
63
|
You are a reliable pipeline agent.
|
|
58
64
|
You must follow the chain steps in order.
|
|
@@ -61,26 +67,31 @@ You may call tools as needed. If you call `agent.call`, wait for the response an
|
|
|
61
67
|
## step:plan
|
|
62
68
|
|
|
63
69
|
Goal:
|
|
70
|
+
|
|
64
71
|
- Read the provided input (a JSON or Markdown/text file) embedded by the orchestrator.
|
|
65
72
|
- Produce a structured **Plan** that lists concrete actions and any tool calls required.
|
|
66
73
|
|
|
67
74
|
Constraints:
|
|
75
|
+
|
|
68
76
|
- Keep steps explicit.
|
|
69
77
|
- If you need another agent, call `agent.call` with a clear request.
|
|
70
78
|
|
|
71
79
|
## step:execute
|
|
72
80
|
|
|
73
81
|
Goal:
|
|
82
|
+
|
|
74
83
|
- Execute the plan.
|
|
75
84
|
- Use the declared tools. You may call tools multiple times.
|
|
76
85
|
|
|
77
86
|
Constraints:
|
|
87
|
+
|
|
78
88
|
- Write intermediate artifacts only if asked.
|
|
79
89
|
- Summarize what you did in plain text.
|
|
80
90
|
|
|
81
91
|
## step:finalize
|
|
82
92
|
|
|
83
93
|
Goal:
|
|
94
|
+
|
|
84
95
|
- Produce a final **FinalResult** object that is valid JSON for the schema.
|
|
85
96
|
- Include references to tools invoked and any sub-agent calls.
|
|
86
97
|
- If anything failed, reflect it in the structured fields rather than “hiding” it in prose.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quick-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Minimal, local-first agent runner using Markdown front matter.
|
|
5
5
|
Author-email: Charles Verge <1906614+charlesverge@users.noreply.github.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -700,6 +700,8 @@ Requires-Dist: anyio
|
|
|
700
700
|
Requires-Dist: pydantic
|
|
701
701
|
Requires-Dist: pydantic-ai
|
|
702
702
|
Requires-Dist: python-frontmatter
|
|
703
|
+
Requires-Dist: types-PyYAML
|
|
704
|
+
Requires-Dist: PyYAML
|
|
703
705
|
Provides-Extra: dev
|
|
704
706
|
Requires-Dist: mypy; extra == "dev"
|
|
705
707
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -707,13 +709,14 @@ Provides-Extra: test
|
|
|
707
709
|
Requires-Dist: pytest; extra == "test"
|
|
708
710
|
Dynamic: license-file
|
|
709
711
|
|
|
710
|
-
#
|
|
712
|
+
# Quick Agent
|
|
711
713
|
|
|
712
|
-
|
|
714
|
+
Quick Agent is a minimal, local-first agent runner that loads agent definitions from Markdown front matter and executes a small chain of steps with limited context handling. It is intentionally small and explicit: you define the model, tools, and steps in a single Markdown file, and the orchestrator runs those steps in order with a bounded prompt preamble.
|
|
713
715
|
|
|
714
716
|
## Project Goal
|
|
715
717
|
|
|
716
718
|
Provide a simple, maintainable agent framework that:
|
|
719
|
+
|
|
717
720
|
- Uses Markdown front matter for agent configuration.
|
|
718
721
|
- Runs a deterministic chain of steps (text or structured output).
|
|
719
722
|
- Keeps context handling deliberately limited and predictable.
|
|
@@ -887,7 +890,7 @@ def main() -> None:
|
|
|
887
890
|
tools=tools,
|
|
888
891
|
directory_permissions=permissions,
|
|
889
892
|
agent_id="hello",
|
|
890
|
-
|
|
893
|
+
input_data=Path("safe/path/to/input.txt"),
|
|
891
894
|
extra_tools=None,
|
|
892
895
|
)
|
|
893
896
|
|
|
@@ -907,6 +910,18 @@ Agents are stored as Markdown files with YAML front matter and step sections:
|
|
|
907
910
|
- Body contains `## step:<id>` sections referenced by the chain.
|
|
908
911
|
|
|
909
912
|
The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
|
|
913
|
+
If the agent front matter omits `output.file`, the orchestrator returns the final output without writing a file.
|
|
914
|
+
|
|
915
|
+
## Nested Output
|
|
916
|
+
|
|
917
|
+
When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
|
|
918
|
+
own `output.file` or return output inline only. Configure this in the parent agent front matter:
|
|
919
|
+
|
|
920
|
+
```yaml
|
|
921
|
+
nested_output: inline # default, no output file for nested calls
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Use `nested_output: file` to allow nested agents to write their configured output files.
|
|
910
925
|
|
|
911
926
|
## Documentation
|
|
912
927
|
|
|
@@ -914,5 +929,7 @@ See the docs in `docs/`:
|
|
|
914
929
|
|
|
915
930
|
- [docs/cli.md](docs/cli.md): Command line usage and options.
|
|
916
931
|
- [docs/templates.md](docs/templates.md): Agent template format and examples.
|
|
932
|
+
- [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
|
|
917
933
|
- [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
|
|
918
934
|
- [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
|
|
935
|
+
- [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
quick_agent/__init__.py,sha256=lgTMjPhbumIZ1Pna33khCaAs4DSjErnYoCkvK_YxBCc,362
|
|
2
|
+
quick_agent/agent_call_tool.py,sha256=yyXpWWpQlhSV8qxkeA1tLdrfcHhYg2cmhf4sxx8FfvA,2195
|
|
3
|
+
quick_agent/agent_registry.py,sha256=VqDFYsH6-kwUwtuiQyHpvmDeWVcSbn62pCvqE8D7IZE,1818
|
|
4
|
+
quick_agent/agent_tools.py,sha256=GwaPkoG_vy1f6hatvRQFoDj0IJ-6nIYV746qc8y-yMg,1475
|
|
5
|
+
quick_agent/cli.py,sha256=nzVF4paXxugaPzmHTMwCBkbQkfkkv9hqGQH4Fh0jJ_U,1973
|
|
6
|
+
quick_agent/directory_permissions.py,sha256=r6Lc56oIrGhVVUVrCoPInue_BCdGZsD5oDgoAHJWiyE,1955
|
|
7
|
+
quick_agent/input_adaptors.py,sha256=ZaK0Mm27cGMGb5y6fsgV0gXRPMW2zmHn9l9dXKzOufA,865
|
|
8
|
+
quick_agent/io_utils.py,sha256=BUmUoZepL4lSYe0JbKxLm4mFFQ6Zt6MZQzprzgoBweU,1200
|
|
9
|
+
quick_agent/json_utils.py,sha256=G6uP9SrdEw5WtA9--dBqdJcTYD2JgdgotPJdleErB_c,1003
|
|
10
|
+
quick_agent/llms.txt,sha256=_jJvgJDdGyuIc-sT3XoadCGxDhzi61Cnb3Fq6zpsE_M,6488
|
|
11
|
+
quick_agent/orchestrator.py,sha256=SGa5oeRgbjkKXdFL5kszZT-R6-jIgyOkHEMmjLrDWjc,1321
|
|
12
|
+
quick_agent/prompting.py,sha256=ZxKZ-qKfbStj7160xi84RYJWB5BxhYD01-ruiTN4iQA,1317
|
|
13
|
+
quick_agent/py.typed,sha256=3Q9vdii9v-_pbj9sglgDzJAIelvARaYaDJlB2u-NDqg,38
|
|
14
|
+
quick_agent/quick_agent.py,sha256=P5qlJrdTwwYAafkm_ML5DJT3w49QRtEvUpF3fiJjR6Q,12850
|
|
15
|
+
quick_agent/tools_loader.py,sha256=oveR-EGAZo3Q7NPJ2aWKU3d8xd8kIpibOrjInpUZwQg,2642
|
|
16
|
+
quick_agent/models/__init__.py,sha256=YseFAVWLarh0wZCVUL0fmkjVgA-FnYJLEiVinG1-6Tk,737
|
|
17
|
+
quick_agent/models/agent_spec.py,sha256=YsCrs9DSKJJJBsqnMAxb_19zG6UuNHtXwiuTZjbFg5U,882
|
|
18
|
+
quick_agent/models/chain_step_spec.py,sha256=NcdYFmgJhA5ODIwCTgTb2fmmJJesAZCVAIy3cGC5dhs,342
|
|
19
|
+
quick_agent/models/handoff_spec.py,sha256=46Pt4JGU8-6f-cdbDR0oqWuJbg5ENJcXrFN1IK3chvs,280
|
|
20
|
+
quick_agent/models/loaded_agent_file.py,sha256=kOOrzfoYqCgoifcnOtn2us_MNPSVg4kRWgmyeb21BEY,5159
|
|
21
|
+
quick_agent/models/model_spec.py,sha256=spnIZ8BNtXT5t8YRL1nh7cXrgWwNSptt_BMnAB8w1Lg,425
|
|
22
|
+
quick_agent/models/output_spec.py,sha256=ZGYmHXAh82SoD5g8VPJRu_2uTjL_W_NM0gCXrAfiYYI,223
|
|
23
|
+
quick_agent/models/run_input.py,sha256=WpV-QPlOPXMmZR6eIvP7vJOuCeBt3xWe4j4ngEbmSxg,282
|
|
24
|
+
quick_agent/models/tool_impl_spec.py,sha256=7B161m8nFZCNjslb4VZdSi-mdAZM0jhp4kV4uaq8X1s,216
|
|
25
|
+
quick_agent/models/tool_json.py,sha256=eYFBPCqxmRfyo7GYHBXCfog7kUzpjginfjt9grQy4ic,274
|
|
26
|
+
quick_agent/schemas/outputs.py,sha256=8FTIczUV079imbygbFU3h9tyLWSH8aQZCPBTxM8QVtk,1527
|
|
27
|
+
quick_agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
quick_agent/tools/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
quick_agent/tools/filesystem/adapter.py,sha256=aYcX1qjI9wRSANSNSB3KWbKi7uDIYo3EpJotpUxnQUM,1027
|
|
30
|
+
quick_agent/tools/filesystem/read_text.py,sha256=owpZxwCFoYbljABhwpNKU4g7-8xDPJDu0kZYF7bPLfs,478
|
|
31
|
+
quick_agent/tools/filesystem/write_text.py,sha256=lwsMCk_p8xurVxychM2UBieXDp0SgN3C6rufLBUzDxo,584
|
|
32
|
+
quick_agent/tools/filesystem.read_text/tool.json,sha256=UtkOxcpi6qYd611tJx-zFXVUjOwx2ZbmxEWsF4cAVpo,246
|
|
33
|
+
quick_agent/tools/filesystem.write_text/tool.json,sha256=EcGmB_M_KDTqOrnxk_NVhKRoW_KADFFksgJ7fmw472s,275
|
|
34
|
+
quick_agent-0.1.3.data/data/quick_agent/agents/business-extract-structured.md,sha256=yeNIZFw60-2u93ZdI6nnQnTOGrllCN8SQLJPApvqXdU,1168
|
|
35
|
+
quick_agent-0.1.3.data/data/quick_agent/agents/business-extract.md,sha256=SZAgpRI8Z4HnkMBAwvMLrErkNQ5HkHqerVMSisn-gNU,993
|
|
36
|
+
quick_agent-0.1.3.data/data/quick_agent/agents/function-spec-validator.md,sha256=ls3r0ejswgM_9SljMLxMjk75x9XJna7ceqHX3pWcgnk,3279
|
|
37
|
+
quick_agent-0.1.3.data/data/quick_agent/agents/subagent-validate-eval-list.md,sha256=IxFwHGIsnGbnaCK-j7znrp4llShepRfRlhTJ5sjqe-g,4105
|
|
38
|
+
quick_agent-0.1.3.data/data/quick_agent/agents/subagent-validator-contains.md,sha256=1yY3Z5copRoCIvq_taVSjzvp8prFab49s1-ybaDcdpk,3107
|
|
39
|
+
quick_agent-0.1.3.data/data/quick_agent/agents/template.md,sha256=4_ldsHj46qmDcer4SCfwfxRJqoesZsp87XHSAimtaLo,2571
|
|
40
|
+
quick_agent-0.1.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
41
|
+
tests/test_agent.py,sha256=1FQteGMCftv40bC-i_iNETaLR-7hmUz9kIlcJYvnazU,10999
|
|
42
|
+
tests/test_directory_permissions.py,sha256=-9h7W1VO0LekPLCxEqIAGgebquNks2F8M02BdZDLlQY,2815
|
|
43
|
+
tests/test_httpx_tools.py,sha256=xqIQ376STyNGwd5YA8VZPZS4nsr1EsSDYCrrcoOuXBE,10500
|
|
44
|
+
tests/test_input_adaptors.py,sha256=sle9zrumq3CUVGtDPZi2pf0om-Z7v4SI6khnTf7Rz4c,918
|
|
45
|
+
tests/test_integration.py,sha256=XbI6abGeoZm3r_BqpQyhxhF36N7VYtNGcJ9-xNxZysM,9044
|
|
46
|
+
tests/test_orchestrator.py,sha256=Tc1iynzPMf_ghbHitlcKDS9dks91t-ojbG3l7qInsbA,41716
|
|
47
|
+
tests/test_tools.py,sha256=Yc6AKCz79XJwHqiRV8dUBE6GVNdspUU0hIeGCXx-Rvc,936
|
|
48
|
+
quick_agent-0.1.3.dist-info/METADATA,sha256=x_ButLyAVP8hACb2fwZsHzPAQbYQQ6Li-TgsUP55hpE,47339
|
|
49
|
+
quick_agent-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
50
|
+
quick_agent-0.1.3.dist-info/entry_points.txt,sha256=bij-xFaQMSrMj7zearzi-bMfcyweum_GvURFMVZGde8,53
|
|
51
|
+
quick_agent-0.1.3.dist-info/top_level.txt,sha256=KT1ID0FVC0OLzQnoBKRIHbXLRNugiU2gcgj_f4DtAsw,18
|
|
52
|
+
quick_agent-0.1.3.dist-info/RECORD,,
|
tests/test_agent.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
import sys
|
|
3
4
|
import types
|
|
4
5
|
from pathlib import Path
|
|
@@ -79,16 +80,24 @@ def test_write_output_creates_parent(tmp_path: Path) -> None:
|
|
|
79
80
|
|
|
80
81
|
def test_make_user_prompt_contains_sections() -> None:
|
|
81
82
|
run_input = RunInput(source_path="file.txt", kind="text", text="hi", data=None)
|
|
82
|
-
prompt = prompting.make_user_prompt(
|
|
83
|
+
prompt = prompting.make_user_prompt(run_input, {"steps": {"x": 1}})
|
|
83
84
|
|
|
84
85
|
assert "# Task Input" in prompt
|
|
85
86
|
assert "source_path: file.txt" in prompt
|
|
86
87
|
assert "## Input Content" in prompt
|
|
87
88
|
assert "hi" in prompt
|
|
88
|
-
assert "## Chain State (
|
|
89
|
-
assert
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
assert "## Chain State (YAML)" in prompt
|
|
90
|
+
assert "x: 1" in prompt
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_make_user_prompt_inline_without_state_hides_headers() -> None:
|
|
94
|
+
run_input = RunInput(source_path="inline_input.txt", kind="text", text="hello", data=None)
|
|
95
|
+
prompt = prompting.make_user_prompt(run_input, {})
|
|
96
|
+
|
|
97
|
+
assert "# Task Input" not in prompt
|
|
98
|
+
assert "## Input Content" not in prompt
|
|
99
|
+
assert "## Chain State" not in prompt
|
|
100
|
+
assert prompt.strip() == "hello"
|
|
92
101
|
|
|
93
102
|
|
|
94
103
|
def test_resolve_schema_valid_missing_and_invalid() -> None:
|
|
@@ -110,7 +119,7 @@ def test_resolve_schema_valid_missing_and_invalid() -> None:
|
|
|
110
119
|
chain=[ChainStepSpec(id="s1", kind="text", prompt_section="step:one")],
|
|
111
120
|
schemas={"Good": "schemas.tmp:GoodSchema", "Bad": "schemas.tmp:NotSchema"},
|
|
112
121
|
)
|
|
113
|
-
loaded = LoadedAgentFile(spec=spec,
|
|
122
|
+
loaded = LoadedAgentFile.from_parts(spec=spec, instructions="", system_prompt="", step_prompts={})
|
|
114
123
|
|
|
115
124
|
try:
|
|
116
125
|
assert resolve_schema(loaded, "Good") is GoodSchema
|
|
@@ -137,6 +146,10 @@ chain:
|
|
|
137
146
|
prompt_section: step:one
|
|
138
147
|
---
|
|
139
148
|
|
|
149
|
+
## Instructions
|
|
150
|
+
|
|
151
|
+
system.
|
|
152
|
+
|
|
140
153
|
## step:one
|
|
141
154
|
|
|
142
155
|
body.
|
|
@@ -144,8 +157,9 @@ body.
|
|
|
144
157
|
md_path = tmp_path / "agent.md"
|
|
145
158
|
md_path.write_text(md, encoding="utf-8")
|
|
146
159
|
|
|
147
|
-
loaded =
|
|
160
|
+
loaded = LoadedAgentFile(md_path)
|
|
148
161
|
assert loaded.spec.name == "Test"
|
|
162
|
+
assert loaded.instructions == "system."
|
|
149
163
|
assert loaded.step_prompts["step:one"] == "body."
|
|
150
164
|
|
|
151
165
|
|
|
@@ -166,7 +180,7 @@ body.
|
|
|
166
180
|
md_path = tmp_path / "agent.md"
|
|
167
181
|
md_path.write_text(md, encoding="utf-8")
|
|
168
182
|
|
|
169
|
-
loaded =
|
|
183
|
+
loaded = LoadedAgentFile(md_path)
|
|
170
184
|
assert loaded.spec.model.base_url == "https://api.openai.com/v1"
|
|
171
185
|
assert loaded.spec.model.api_key_env == "OPENAI_API_KEY"
|
|
172
186
|
assert loaded.spec.model.model_name == "gpt-5.2"
|
|
@@ -189,8 +203,258 @@ body.
|
|
|
189
203
|
md_path = tmp_path / "agent.md"
|
|
190
204
|
md_path.write_text(md, encoding="utf-8")
|
|
191
205
|
|
|
192
|
-
loaded =
|
|
206
|
+
loaded = LoadedAgentFile(md_path)
|
|
193
207
|
assert loaded.spec.model.base_url == "https://api.openai.com/v1"
|
|
194
208
|
assert loaded.spec.model.api_key_env == "OPENAI_API_KEY"
|
|
195
209
|
assert loaded.spec.model.model_name == "gpt-5.2"
|
|
196
210
|
assert loaded.spec.model.provider == "openai-compatible"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_load_agent_file_parses_instructions_and_system_prompt(tmp_path: Path) -> None:
|
|
214
|
+
md = """---
|
|
215
|
+
name: Sections
|
|
216
|
+
model:
|
|
217
|
+
base_url: http://localhost
|
|
218
|
+
model_name: test
|
|
219
|
+
chain: []
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## instructions
|
|
223
|
+
|
|
224
|
+
Use the tool.
|
|
225
|
+
|
|
226
|
+
## Notes
|
|
227
|
+
|
|
228
|
+
ignored.
|
|
229
|
+
|
|
230
|
+
## System prompt
|
|
231
|
+
|
|
232
|
+
You are concise.
|
|
233
|
+
"""
|
|
234
|
+
md_path = tmp_path / "agent.md"
|
|
235
|
+
md_path.write_text(md, encoding="utf-8")
|
|
236
|
+
|
|
237
|
+
loaded = LoadedAgentFile(md_path)
|
|
238
|
+
assert "Use the tool." in loaded.instructions
|
|
239
|
+
assert "## Notes" in loaded.instructions
|
|
240
|
+
assert "ignored." in loaded.instructions
|
|
241
|
+
assert loaded.system_prompt == "You are concise."
|
|
242
|
+
assert loaded.step_prompts == {}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.parametrize(
|
|
246
|
+
("header", "expected"),
|
|
247
|
+
[
|
|
248
|
+
("## Instructions", "Do it."),
|
|
249
|
+
("## instructions", "Do it."),
|
|
250
|
+
("## INSTRUCTIONS", "Do it."),
|
|
251
|
+
],
|
|
252
|
+
)
|
|
253
|
+
def test_load_agent_file_accepts_instruction_header_case(
|
|
254
|
+
tmp_path: Path, header: str, expected: str
|
|
255
|
+
) -> None:
|
|
256
|
+
md = f"""---
|
|
257
|
+
name: Case Instructions
|
|
258
|
+
model:
|
|
259
|
+
base_url: http://localhost
|
|
260
|
+
model_name: test
|
|
261
|
+
chain: []
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
{header}
|
|
265
|
+
|
|
266
|
+
{expected}
|
|
267
|
+
"""
|
|
268
|
+
md_path = tmp_path / "agent.md"
|
|
269
|
+
md_path.write_text(md, encoding="utf-8")
|
|
270
|
+
|
|
271
|
+
loaded = LoadedAgentFile(md_path)
|
|
272
|
+
assert loaded.instructions == expected
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.mark.parametrize(
|
|
276
|
+
("header", "expected"),
|
|
277
|
+
[
|
|
278
|
+
("# System prompt", "Be brief."),
|
|
279
|
+
("## System prompt", "Be brief."),
|
|
280
|
+
("### System prompt", "Be brief."),
|
|
281
|
+
("## system prompt", "Be brief."),
|
|
282
|
+
("## SYSTEM PROMPT", "Be brief."),
|
|
283
|
+
("## system_prompt", "Be brief."),
|
|
284
|
+
],
|
|
285
|
+
)
|
|
286
|
+
def test_load_agent_file_accepts_system_prompt_header_case(
|
|
287
|
+
tmp_path: Path, header: str, expected: str
|
|
288
|
+
) -> None:
|
|
289
|
+
md = f"""---
|
|
290
|
+
name: Case System
|
|
291
|
+
model:
|
|
292
|
+
base_url: http://localhost
|
|
293
|
+
model_name: test
|
|
294
|
+
chain: []
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
{header}
|
|
298
|
+
|
|
299
|
+
{expected}
|
|
300
|
+
"""
|
|
301
|
+
md_path = tmp_path / "agent.md"
|
|
302
|
+
md_path.write_text(md, encoding="utf-8")
|
|
303
|
+
|
|
304
|
+
loaded = LoadedAgentFile(md_path)
|
|
305
|
+
assert loaded.system_prompt == expected
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@pytest.mark.parametrize(
|
|
309
|
+
"header",
|
|
310
|
+
[
|
|
311
|
+
"## step:one",
|
|
312
|
+
"## STEP:one",
|
|
313
|
+
"## Step:one",
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
def test_load_agent_file_accepts_step_header_case(tmp_path: Path, header: str) -> None:
|
|
317
|
+
md = f"""---
|
|
318
|
+
name: Case Step
|
|
319
|
+
model:
|
|
320
|
+
base_url: http://localhost
|
|
321
|
+
model_name: test
|
|
322
|
+
chain:
|
|
323
|
+
- id: one
|
|
324
|
+
kind: text
|
|
325
|
+
prompt_section: step:one
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
{header}
|
|
329
|
+
|
|
330
|
+
Say hi.
|
|
331
|
+
"""
|
|
332
|
+
md_path = tmp_path / "agent.md"
|
|
333
|
+
md_path.write_text(md, encoding="utf-8")
|
|
334
|
+
|
|
335
|
+
loaded = LoadedAgentFile(md_path)
|
|
336
|
+
assert loaded.step_prompts["step:one"] == "Say hi."
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_load_agent_file_allows_instructions_only_no_steps(tmp_path: Path) -> None:
|
|
340
|
+
md = """---
|
|
341
|
+
name: No Steps
|
|
342
|
+
model:
|
|
343
|
+
base_url: http://localhost
|
|
344
|
+
model_name: test
|
|
345
|
+
chain: []
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Instructions
|
|
349
|
+
|
|
350
|
+
Just answer.
|
|
351
|
+
"""
|
|
352
|
+
md_path = tmp_path / "agent.md"
|
|
353
|
+
md_path.write_text(md, encoding="utf-8")
|
|
354
|
+
|
|
355
|
+
loaded = LoadedAgentFile(md_path)
|
|
356
|
+
assert loaded.instructions == "Just answer."
|
|
357
|
+
assert loaded.step_prompts == {}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_load_agent_file_raises_without_sections(tmp_path: Path) -> None:
|
|
361
|
+
md = """---
|
|
362
|
+
name: Empty
|
|
363
|
+
model:
|
|
364
|
+
base_url: http://localhost
|
|
365
|
+
model_name: test
|
|
366
|
+
chain: []
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
just text.
|
|
370
|
+
"""
|
|
371
|
+
md_path = tmp_path / "agent.md"
|
|
372
|
+
md_path.write_text(md, encoding="utf-8")
|
|
373
|
+
|
|
374
|
+
with pytest.raises(ValueError, match="instructions, system prompt, or step sections"):
|
|
375
|
+
LoadedAgentFile(md_path)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_load_agent_file_warns_on_preamble_before_instructions(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
|
379
|
+
md = """---
|
|
380
|
+
name: Preamble
|
|
381
|
+
model:
|
|
382
|
+
base_url: http://localhost
|
|
383
|
+
model_name: test
|
|
384
|
+
chain: []
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
Preamble text.
|
|
388
|
+
|
|
389
|
+
## Instructions
|
|
390
|
+
|
|
391
|
+
Do the thing.
|
|
392
|
+
"""
|
|
393
|
+
md_path = tmp_path / "agent.md"
|
|
394
|
+
md_path.write_text(md, encoding="utf-8")
|
|
395
|
+
|
|
396
|
+
with caplog.at_level(logging.WARNING):
|
|
397
|
+
LoadedAgentFile(md_path)
|
|
398
|
+
|
|
399
|
+
assert "Ignored text before instructions or system prompt" in caplog.text
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_load_agent_file_preserves_subsections_in_all_sections(tmp_path: Path) -> None:
|
|
403
|
+
md = """---
|
|
404
|
+
name: Subsections
|
|
405
|
+
model:
|
|
406
|
+
base_url: http://localhost
|
|
407
|
+
model_name: test
|
|
408
|
+
chain:
|
|
409
|
+
- id: one
|
|
410
|
+
kind: text
|
|
411
|
+
prompt_section: step:one
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Instructions
|
|
415
|
+
|
|
416
|
+
Intro line.
|
|
417
|
+
|
|
418
|
+
## Constraints
|
|
419
|
+
|
|
420
|
+
- Be concise.
|
|
421
|
+
- Keep scope tight.
|
|
422
|
+
|
|
423
|
+
## System prompt
|
|
424
|
+
|
|
425
|
+
System intro.
|
|
426
|
+
|
|
427
|
+
## Rules
|
|
428
|
+
|
|
429
|
+
Always follow the rules.
|
|
430
|
+
|
|
431
|
+
## step:one
|
|
432
|
+
|
|
433
|
+
Step intro.
|
|
434
|
+
|
|
435
|
+
## Details
|
|
436
|
+
|
|
437
|
+
Explain details here.
|
|
438
|
+
|
|
439
|
+
## step:two
|
|
440
|
+
|
|
441
|
+
Step intro two.
|
|
442
|
+
|
|
443
|
+
## Details
|
|
444
|
+
|
|
445
|
+
Explain details here.
|
|
446
|
+
"""
|
|
447
|
+
md_path = tmp_path / "agent.md"
|
|
448
|
+
md_path.write_text(md, encoding="utf-8")
|
|
449
|
+
|
|
450
|
+
loaded = LoadedAgentFile(md_path)
|
|
451
|
+
assert "Intro line." in loaded.instructions
|
|
452
|
+
assert "## Constraints" in loaded.instructions
|
|
453
|
+
assert "Be concise." in loaded.instructions
|
|
454
|
+
assert "System intro." in loaded.system_prompt
|
|
455
|
+
assert "## Rules" in loaded.system_prompt
|
|
456
|
+
assert "Always follow the rules." in loaded.system_prompt
|
|
457
|
+
assert "Step intro." in loaded.step_prompts["step:one"]
|
|
458
|
+
assert "## Details" in loaded.step_prompts["step:one"]
|
|
459
|
+
assert "Explain details here." in loaded.step_prompts["step:one"]
|
|
460
|
+
assert "## Details" in loaded.step_prompts["step:two"]
|
|
@@ -45,6 +45,16 @@ def test_directory_permissions_can_read_write(tmp_path: Path) -> None:
|
|
|
45
45
|
assert perms.can_write(Path("../nope.txt")) is False
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def test_directory_permissions_without_root_denies_all() -> None:
|
|
49
|
+
perms = DirectoryPermissions(None)
|
|
50
|
+
|
|
51
|
+
with pytest.raises(PermissionError):
|
|
52
|
+
perms.resolve(Path("anything.txt"), for_write=False)
|
|
53
|
+
|
|
54
|
+
assert perms.can_read(Path("anything.txt")) is False
|
|
55
|
+
assert perms.can_write(Path("anything.txt")) is False
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
def test_agent_cannot_write_outside_scoped_directory(
|
|
49
59
|
tmp_path: Path,
|
|
50
60
|
monkeypatch: pytest.MonkeyPatch,
|