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.
Files changed (35) hide show
  1. quick_agent/__init__.py +4 -1
  2. quick_agent/agent_call_tool.py +22 -5
  3. quick_agent/agent_registry.py +7 -27
  4. quick_agent/agent_tools.py +3 -2
  5. quick_agent/cli.py +19 -5
  6. quick_agent/directory_permissions.py +7 -3
  7. quick_agent/input_adaptors.py +30 -0
  8. quick_agent/llms.txt +239 -0
  9. quick_agent/models/agent_spec.py +3 -0
  10. quick_agent/models/loaded_agent_file.py +136 -1
  11. quick_agent/models/output_spec.py +1 -1
  12. quick_agent/orchestrator.py +15 -8
  13. quick_agent/prompting.py +34 -16
  14. quick_agent/py.typed +1 -0
  15. quick_agent/quick_agent.py +171 -155
  16. quick_agent/schemas/outputs.py +6 -0
  17. quick_agent-0.1.3.data/data/quick_agent/agents/business-extract-structured.md +49 -0
  18. quick_agent-0.1.3.data/data/quick_agent/agents/business-extract.md +42 -0
  19. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/function-spec-validator.md +1 -1
  20. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validate-eval-list.md +1 -1
  21. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/subagent-validator-contains.md +8 -1
  22. {quick_agent-0.1.1.data → quick_agent-0.1.3.data}/data/quick_agent/agents/template.md +12 -1
  23. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/METADATA +21 -4
  24. quick_agent-0.1.3.dist-info/RECORD +52 -0
  25. tests/test_agent.py +273 -9
  26. tests/test_directory_permissions.py +10 -0
  27. tests/test_httpx_tools.py +295 -0
  28. tests/test_input_adaptors.py +31 -0
  29. tests/test_integration.py +134 -1
  30. tests/test_orchestrator.py +525 -111
  31. quick_agent-0.1.1.dist-info/RECORD +0 -45
  32. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/WHEEL +0 -0
  33. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/entry_points.txt +0 -0
  34. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/licenses/LICENSE +0 -0
  35. {quick_agent-0.1.1.dist-info → quick_agent-0.1.3.dist-info}/top_level.txt +0 -0
@@ -43,7 +43,7 @@ handoff:
43
43
  input_mode: "final_output_json"
44
44
  ---
45
45
 
46
- # function_spec_validator
46
+ ## Instructions
47
47
 
48
48
  You are a validation agent that checks a provided function specification.
49
49
  Return a JSON valid result that includes a boolean `valid` field.
@@ -49,7 +49,7 @@ handoff:
49
49
  input_mode: "final_output_json"
50
50
  ---
51
51
 
52
- # subagent-validate-eval-list
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
- # subagent-validator-contains
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
- # doc_pipeline_agent
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.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
- # Simple Agent
712
+ # Quick Agent
711
713
 
712
- Simple 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.
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
- input_path=Path("safe/path/to/input.txt"),
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("do thing", run_input, {"x": 1})
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 (JSON)" in prompt
89
- assert '"x": 1' in prompt
90
- assert "## Step Instructions" in prompt
91
- assert "do thing" in prompt
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, body="", step_prompts={})
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 = agent_registry.load_agent_file(md_path)
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 = agent_registry.load_agent_file(md_path)
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 = agent_registry.load_agent_file(md_path)
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,