quick-agent 0.1.1__tar.gz → 0.1.3__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 (62) hide show
  1. {quick_agent-0.1.1 → quick_agent-0.1.3}/PKG-INFO +21 -4
  2. {quick_agent-0.1.1 → quick_agent-0.1.3}/README.md +18 -3
  3. quick_agent-0.1.3/agents/business-extract-structured.md +49 -0
  4. quick_agent-0.1.3/agents/business-extract.md +42 -0
  5. {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/function-spec-validator.md +1 -1
  6. {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/subagent-validate-eval-list.md +1 -1
  7. {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/subagent-validator-contains.md +8 -1
  8. {quick_agent-0.1.1 → quick_agent-0.1.3}/agents/template.md +12 -1
  9. {quick_agent-0.1.1 → quick_agent-0.1.3}/pyproject.toml +4 -2
  10. quick_agent-0.1.3/src/quick_agent/__init__.py +9 -0
  11. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/agent_call_tool.py +22 -5
  12. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/agent_registry.py +7 -27
  13. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/agent_tools.py +3 -2
  14. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/cli.py +19 -5
  15. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/directory_permissions.py +7 -3
  16. quick_agent-0.1.3/src/quick_agent/input_adaptors.py +30 -0
  17. quick_agent-0.1.3/src/quick_agent/llms.txt +239 -0
  18. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/agent_spec.py +3 -0
  19. quick_agent-0.1.3/src/quick_agent/models/loaded_agent_file.py +149 -0
  20. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/output_spec.py +1 -1
  21. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/orchestrator.py +15 -8
  22. quick_agent-0.1.3/src/quick_agent/prompting.py +46 -0
  23. quick_agent-0.1.3/src/quick_agent/py.typed +1 -0
  24. quick_agent-0.1.3/src/quick_agent/quick_agent.py +329 -0
  25. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/schemas/outputs.py +6 -0
  26. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/PKG-INFO +21 -4
  27. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/SOURCES.txt +7 -0
  28. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/requires.txt +2 -0
  29. quick_agent-0.1.3/src/tests/test_agent.py +460 -0
  30. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_directory_permissions.py +10 -0
  31. quick_agent-0.1.3/src/tests/test_httpx_tools.py +295 -0
  32. quick_agent-0.1.3/src/tests/test_input_adaptors.py +31 -0
  33. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_integration.py +134 -1
  34. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_orchestrator.py +525 -111
  35. quick_agent-0.1.1/src/quick_agent/__init__.py +0 -6
  36. quick_agent-0.1.1/src/quick_agent/models/loaded_agent_file.py +0 -14
  37. quick_agent-0.1.1/src/quick_agent/prompting.py +0 -28
  38. quick_agent-0.1.1/src/quick_agent/quick_agent.py +0 -313
  39. quick_agent-0.1.1/src/tests/test_agent.py +0 -196
  40. {quick_agent-0.1.1 → quick_agent-0.1.3}/LICENSE +0 -0
  41. {quick_agent-0.1.1 → quick_agent-0.1.3}/setup.cfg +0 -0
  42. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/io_utils.py +0 -0
  43. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/json_utils.py +0 -0
  44. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/__init__.py +0 -0
  45. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/chain_step_spec.py +0 -0
  46. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/handoff_spec.py +0 -0
  47. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/model_spec.py +0 -0
  48. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/run_input.py +0 -0
  49. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/tool_impl_spec.py +0 -0
  50. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/models/tool_json.py +0 -0
  51. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/__init__.py +0 -0
  52. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/__init__.py +0 -0
  53. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/adapter.py +0 -0
  54. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/read_text.py +0 -0
  55. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem/write_text.py +0 -0
  56. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem.read_text/tool.json +0 -0
  57. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools/filesystem.write_text/tool.json +0 -0
  58. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent/tools_loader.py +0 -0
  59. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/dependency_links.txt +0 -0
  60. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/entry_points.txt +0 -0
  61. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/quick_agent.egg-info/top_level.txt +0 -0
  62. {quick_agent-0.1.1 → quick_agent-0.1.3}/src/tests/test_tools.py +0 -0
@@ -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.
@@ -1,10 +1,11 @@
1
- # Simple Agent
1
+ # Quick Agent
2
2
 
3
- 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.
3
+ 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.
4
4
 
5
5
  ## Project Goal
6
6
 
7
7
  Provide a simple, maintainable agent framework that:
8
+
8
9
  - Uses Markdown front matter for agent configuration.
9
10
  - Runs a deterministic chain of steps (text or structured output).
10
11
  - Keeps context handling deliberately limited and predictable.
@@ -178,7 +179,7 @@ def main() -> None:
178
179
  tools=tools,
179
180
  directory_permissions=permissions,
180
181
  agent_id="hello",
181
- input_path=Path("safe/path/to/input.txt"),
182
+ input_data=Path("safe/path/to/input.txt"),
182
183
  extra_tools=None,
183
184
  )
184
185
 
@@ -198,6 +199,18 @@ Agents are stored as Markdown files with YAML front matter and step sections:
198
199
  - Body contains `## step:<id>` sections referenced by the chain.
199
200
 
200
201
  The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
202
+ If the agent front matter omits `output.file`, the orchestrator returns the final output without writing a file.
203
+
204
+ ## Nested Output
205
+
206
+ When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
207
+ own `output.file` or return output inline only. Configure this in the parent agent front matter:
208
+
209
+ ```yaml
210
+ nested_output: inline # default, no output file for nested calls
211
+ ```
212
+
213
+ Use `nested_output: file` to allow nested agents to write their configured output files.
201
214
 
202
215
  ## Documentation
203
216
 
@@ -205,5 +218,7 @@ See the docs in `docs/`:
205
218
 
206
219
  - [docs/cli.md](docs/cli.md): Command line usage and options.
207
220
  - [docs/templates.md](docs/templates.md): Agent template format and examples.
221
+ - [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
208
222
  - [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
209
223
  - [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
224
+ - [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: "Business Extract Structured"
3
+ description: "Extract company name, location, and summary into structured JSON."
4
+ model:
5
+ provider: "openai-compatible"
6
+ base_url: "http://localhost:11434/v1"
7
+ api_key_env: "OPENAI_API_KEY"
8
+ model_name: "llama3"
9
+ schemas:
10
+ BusinessSummary: "quick_agent.schemas.outputs:BusinessSummary"
11
+ chain:
12
+ - id: company_name
13
+ kind: text
14
+ prompt_section: step:company_name
15
+ - id: location
16
+ kind: text
17
+ prompt_section: step:location
18
+ - id: summary
19
+ kind: structured
20
+ prompt_section: step:summary
21
+ output_schema: BusinessSummary
22
+ output:
23
+ format: "json"
24
+ file: "out/business_extract_structured.json"
25
+ ---
26
+
27
+ ## Instructions
28
+
29
+ Extract structured details from the input description.
30
+
31
+ ## step:company_name
32
+
33
+ Extract the company name from the input description.
34
+ Return only the company name.
35
+
36
+ ## step:location
37
+
38
+ Extract the location from the input description.
39
+ If a city and region are present, include both.
40
+ Return only the location.
41
+
42
+ ## step:summary
43
+
44
+ Return a JSON object with:
45
+ - `company_name`
46
+ - `location`
47
+ - `summary` (one sentence)
48
+
49
+ Use `state.steps.company_name` and `state.steps.location` for the fields if available.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: "Business Extract"
3
+ description: "Extract company name, location, and a short summary from a business description."
4
+ model:
5
+ provider: "openai-compatible"
6
+ base_url: "http://localhost:11434/v1"
7
+ api_key_env: "OPENAI_API_KEY"
8
+ model_name: "llama3"
9
+ chain:
10
+ - id: company_name
11
+ kind: text
12
+ prompt_section: step:company_name
13
+ - id: location
14
+ kind: text
15
+ prompt_section: step:location
16
+ - id: summary
17
+ kind: text
18
+ prompt_section: step:summary
19
+ output:
20
+ format: "markdown"
21
+ file: "out/business_extract.md"
22
+ ---
23
+
24
+ ## Instructions
25
+
26
+ Extract structured details from the input description.
27
+
28
+ ## step:company_name
29
+
30
+ Extract the company name from the input description.
31
+ Return only the company name.
32
+
33
+ ## step:location
34
+
35
+ Extract the location from the input description.
36
+ If a city and region are present, include both.
37
+ Return only the location.
38
+
39
+ ## step:summary
40
+
41
+ Write one sentence summarizing the business.
42
+ Use `state.steps.company_name` and `state.steps.location` if available.
@@ -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.
@@ -6,13 +6,15 @@ license = {file = "LICENSE"}
6
6
  authors = [
7
7
  {name = "Charles Verge", email = "1906614+charlesverge@users.noreply.github.com"}
8
8
  ]
9
- version = "0.1.1"
9
+ version = "0.1.3"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "anyio",
13
13
  "pydantic",
14
14
  "pydantic-ai",
15
15
  "python-frontmatter",
16
+ "types-PyYAML",
17
+ "PyYAML",
16
18
  ]
17
19
  keywords = ["agents", "llm", "automation", "pydantic", "markdown", "orchestrator"]
18
20
  classifiers = [
@@ -61,7 +63,7 @@ build-backend = "setuptools.build_meta"
61
63
  where = ["src"]
62
64
 
63
65
  [tool.setuptools.package-data]
64
- quick_agent = ["tools/**/tool.json", "agents/**/*.md"]
66
+ quick_agent = ["llms.txt", "py.typed", "tools/**/tool.json", "agents/**/*.md"]
65
67
 
66
68
  [tool.setuptools.data-files]
67
69
  "quick_agent/agents" = ["agents/**/*.md"]
@@ -0,0 +1,9 @@
1
+ """Public package exports."""
2
+
3
+ from quick_agent.input_adaptors import FileInput
4
+ from quick_agent.input_adaptors import InputAdaptor
5
+ from quick_agent.input_adaptors import TextInput
6
+ from quick_agent.orchestrator import Orchestrator
7
+ from quick_agent.quick_agent import QuickAgent
8
+
9
+ __all__ = ["FileInput", "InputAdaptor", "Orchestrator", "QuickAgent", "TextInput"]
@@ -7,11 +7,13 @@ from typing import Any, Awaitable, Callable
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
+ from quick_agent.input_adaptors import InputAdaptor, TextInput
11
+
10
12
 
11
13
  class AgentCallTool:
12
14
  def __init__(
13
15
  self,
14
- call_agent: Callable[[str, Path], Awaitable[BaseModel | str]],
16
+ call_agent: Callable[[str, InputAdaptor | Path], Awaitable[BaseModel | str]],
15
17
  run_input_source_path: str,
16
18
  ) -> None:
17
19
  self._call_agent = call_agent
@@ -32,13 +34,28 @@ class AgentCallTool:
32
34
  path = base_dir / path
33
35
  return path
34
36
 
35
- async def __call__(self, agent: str, input_file: str) -> dict[str, Any]:
37
+ async def __call__(
38
+ self,
39
+ agent: str,
40
+ input_file: str | None = None,
41
+ input_text: str | None = None,
42
+ ) -> dict[str, Any]:
36
43
  """
37
- Call another agent by ID with an input file path.
44
+ Call another agent by ID with an input file path or inline text.
38
45
  Returns JSON-serializable dict output if structured, else {"text": "..."}.
39
46
  """
40
- resolved_input = self._resolve_input_file(input_file)
41
- out = await self._call_agent(agent, resolved_input)
47
+ if input_file and input_text:
48
+ raise ValueError("Provide only one of input_file or input_text.")
49
+ if not input_file and input_text is None:
50
+ raise ValueError("Provide either input_file or input_text.")
51
+ if input_text is not None:
52
+ input_data: InputAdaptor | Path = TextInput(input_text)
53
+ else:
54
+ if input_file is None:
55
+ raise ValueError("Provide either input_file or input_text.")
56
+ resolved_input = self._resolve_input_file(input_file)
57
+ input_data = resolved_input
58
+ out = await self._call_agent(agent, input_data)
42
59
  if isinstance(out, BaseModel):
43
60
  return out.model_dump()
44
61
  return {"text": out}
@@ -2,43 +2,23 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import re
6
5
  from pathlib import Path
7
6
 
8
- import frontmatter
9
-
10
- from quick_agent.models.agent_spec import AgentSpec
11
- from quick_agent.models.loaded_agent_file import LoadedAgentFile
7
+ from quick_agent.models.loaded_agent_file import LoadedAgentFile, parse_agent_sections
12
8
 
13
9
 
14
10
  def split_step_sections(markdown_body: str) -> dict[str, str]:
15
11
  """
16
- Extracts blocks that begin with headings "## step:<id>".
12
+ Extracts blocks that begin with headings like "# step:<id>".
17
13
  Returns mapping: "step:<id>" -> content for that step (excluding heading line).
18
14
  """
19
- pattern = re.compile(r"^##\s+(step:[A-Za-z0-9_\-]+)\s*$", re.MULTILINE)
20
- matches = list(pattern.finditer(markdown_body))
21
- out: dict[str, str] = {}
22
-
23
- for i, m in enumerate(matches):
24
- section_name = m.group(1)
25
- start = m.end()
26
- end = matches[i + 1].start() if (i + 1) < len(matches) else len(markdown_body)
27
- out[section_name] = markdown_body[start:end].strip()
28
-
29
- return out
30
-
31
-
32
- def load_agent_file(path: Path) -> LoadedAgentFile:
33
- post = frontmatter.load(str(path))
34
- spec = AgentSpec.model_validate(post.metadata)
35
- steps = split_step_sections(post.content)
36
- return LoadedAgentFile(spec=spec, body=post.content, step_prompts=steps)
15
+ sections = parse_agent_sections(markdown_body)
16
+ return sections.step_prompts
37
17
 
38
18
 
39
19
  class AgentRegistry:
40
- def __init__(self, agent_roots: list[Path]):
41
- self.agent_roots = agent_roots
20
+ def __init__(self, agent_roots: list[Path]) -> None:
21
+ self.agent_roots: list[Path] = agent_roots
42
22
  self._cache: dict[str, LoadedAgentFile] = {}
43
23
  self._index: dict[str, Path] | None = None
44
24
 
@@ -70,6 +50,6 @@ class AgentRegistry:
70
50
  path = index.get(agent_id)
71
51
  if path is None:
72
52
  raise FileNotFoundError(f"Agent not found: {agent_id} (searched: {self.agent_roots})")
73
- loaded = load_agent_file(path)
53
+ loaded = LoadedAgentFile(path)
74
54
  self._cache[agent_id] = loaded
75
55
  return loaded
@@ -10,12 +10,13 @@ from pydantic_ai.toolsets import FunctionToolset
10
10
 
11
11
  from quick_agent.agent_call_tool import AgentCallTool
12
12
  from quick_agent.directory_permissions import DirectoryPermissions
13
+ from quick_agent.input_adaptors import InputAdaptor
13
14
  from quick_agent.tools_loader import load_tools
14
15
 
15
16
 
16
17
  class AgentTools:
17
18
  def __init__(self, tool_roots: list[Path]) -> None:
18
- self._tool_roots = tool_roots
19
+ self._tool_roots: list[Path] = tool_roots
19
20
 
20
21
  def build_toolset(self, tool_ids: list[str], permissions: DirectoryPermissions) -> FunctionToolset[Any]:
21
22
  tool_ids_for_disk = [tool_id for tool_id in tool_ids if tool_id != "agent.call"]
@@ -28,7 +29,7 @@ class AgentTools:
28
29
  tool_ids: list[str],
29
30
  toolset: FunctionToolset[Any],
30
31
  run_input_source_path: str,
31
- call_agent: Callable[[str, Path], Awaitable[BaseModel | str]],
32
+ call_agent: Callable[[str, InputAdaptor | Path], Awaitable[BaseModel | str]],
32
33
  ) -> None:
33
34
  if "agent.call" not in tool_ids:
34
35
  return
@@ -7,16 +7,28 @@ from pathlib import Path
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
+ from quick_agent.input_adaptors import InputAdaptor, TextInput
10
11
  from quick_agent.orchestrator import Orchestrator
11
12
 
12
13
 
14
+ async def run_agent(
15
+ orch: Orchestrator,
16
+ agent_id: str,
17
+ input_adaptor: InputAdaptor | Path,
18
+ extra_tools: list[str],
19
+ ) -> BaseModel | str:
20
+ return await orch.run(agent_id, input_adaptor, extra_tools=extra_tools)
21
+
22
+
13
23
  def main() -> None:
14
24
  parser = argparse.ArgumentParser()
15
25
  parser.add_argument("--agents-dir", type=str, default="agents")
16
26
  parser.add_argument("--tools-dir", type=str, default="tools")
17
27
  parser.add_argument("--safe-dir", type=str, default="safe")
18
28
  parser.add_argument("--agent", type=str, required=True)
19
- parser.add_argument("--input", type=str, required=True)
29
+ input_group = parser.add_mutually_exclusive_group(required=True)
30
+ input_group.add_argument("--input", type=str, help="Path to an input file")
31
+ input_group.add_argument("--input-text", type=str, help="Raw input text")
20
32
  parser.add_argument("--tool", action="append", default=[], help="Extra tool IDs to add at runtime")
21
33
  args = parser.parse_args()
22
34
 
@@ -30,14 +42,16 @@ def main() -> None:
30
42
  tool_roots = [user_tools_dir, system_tools_dir]
31
43
 
32
44
  orch = Orchestrator(agent_roots, tool_roots, Path(args.safe_dir))
45
+ input_adaptor: InputAdaptor | Path
46
+ if args.input_text is not None:
47
+ input_adaptor = TextInput(args.input_text)
48
+ else:
49
+ input_adaptor = Path(args.input)
33
50
 
34
51
  # Async entrypoint
35
52
  import anyio
36
53
 
37
- async def runner():
38
- return await orch.run(args.agent, Path(args.input), extra_tools=args.tool)
39
-
40
- out = anyio.run(runner)
54
+ out = anyio.run(run_agent, orch, args.agent, input_adaptor, args.tool)
41
55
  if isinstance(out, BaseModel):
42
56
  print(out.model_dump_json(indent=2))
43
57
  else:
@@ -6,14 +6,16 @@ from pathlib import Path
6
6
 
7
7
 
8
8
  class DirectoryPermissions:
9
- def __init__(self, root: Path) -> None:
10
- self._root = root.expanduser().resolve(strict=False)
9
+ def __init__(self, root: Path | None) -> None:
10
+ self._root = root.expanduser().resolve(strict=False) if root is not None else None
11
11
 
12
12
  @property
13
- def root(self) -> Path:
13
+ def root(self) -> Path | None:
14
14
  return self._root
15
15
 
16
16
  def scoped(self, directory: str | None) -> "DirectoryPermissions":
17
+ if self._root is None:
18
+ return self
17
19
  if directory:
18
20
  candidate = (self._root / directory).expanduser().resolve(strict=False)
19
21
  root_resolved = self._root.expanduser().resolve(strict=False)
@@ -23,6 +25,8 @@ class DirectoryPermissions:
23
25
  return self
24
26
 
25
27
  def resolve(self, path: Path, *, for_write: bool) -> Path:
28
+ if self._root is None:
29
+ raise PermissionError("No safe directory configured; reads and writes are denied.")
26
30
  target = path
27
31
  if not target.is_absolute():
28
32
  target = self._root / target
@@ -0,0 +1,30 @@
1
+ """Input adaptors for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from quick_agent.directory_permissions import DirectoryPermissions
8
+ from quick_agent.io_utils import load_input
9
+ from quick_agent.models.run_input import RunInput
10
+
11
+
12
+ class InputAdaptor:
13
+ def load(self) -> RunInput:
14
+ raise NotImplementedError("InputAdaptor.load must be implemented by subclasses.")
15
+
16
+
17
+ class FileInput(InputAdaptor):
18
+ def __init__(self, path: Path, permissions: DirectoryPermissions) -> None:
19
+ self._run_input = load_input(path, permissions)
20
+
21
+ def load(self) -> RunInput:
22
+ return self._run_input
23
+
24
+
25
+ class TextInput(InputAdaptor):
26
+ def __init__(self, text: str) -> None:
27
+ self._text = text
28
+
29
+ def load(self) -> RunInput:
30
+ return RunInput(source_path="inline_input.txt", kind="text", text=self._text, data=None)