quick-agent 0.1.1__tar.gz → 0.1.2__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 (57) hide show
  1. {quick_agent-0.1.1 → quick_agent-0.1.2}/PKG-INFO +17 -4
  2. {quick_agent-0.1.1 → quick_agent-0.1.2}/README.md +16 -3
  3. quick_agent-0.1.2/agents/business-extract-structured.md +49 -0
  4. quick_agent-0.1.2/agents/business-extract.md +42 -0
  5. {quick_agent-0.1.1 → quick_agent-0.1.2}/pyproject.toml +2 -2
  6. quick_agent-0.1.2/src/quick_agent/__init__.py +9 -0
  7. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/agent_call_tool.py +22 -5
  8. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/agent_registry.py +2 -2
  9. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/agent_tools.py +3 -2
  10. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/cli.py +19 -5
  11. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/directory_permissions.py +7 -3
  12. quick_agent-0.1.2/src/quick_agent/input_adaptors.py +30 -0
  13. quick_agent-0.1.2/src/quick_agent/llms.txt +239 -0
  14. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/agent_spec.py +3 -0
  15. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/orchestrator.py +15 -8
  16. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/prompting.py +2 -2
  17. quick_agent-0.1.2/src/quick_agent/py.typed +1 -0
  18. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/quick_agent.py +87 -132
  19. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/schemas/outputs.py +6 -0
  20. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent.egg-info/PKG-INFO +17 -4
  21. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent.egg-info/SOURCES.txt +6 -0
  22. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/tests/test_directory_permissions.py +10 -0
  23. quick_agent-0.1.2/src/tests/test_input_adaptors.py +31 -0
  24. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/tests/test_integration.py +134 -1
  25. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/tests/test_orchestrator.py +183 -94
  26. quick_agent-0.1.1/src/quick_agent/__init__.py +0 -6
  27. {quick_agent-0.1.1 → quick_agent-0.1.2}/LICENSE +0 -0
  28. {quick_agent-0.1.1 → quick_agent-0.1.2}/agents/function-spec-validator.md +0 -0
  29. {quick_agent-0.1.1 → quick_agent-0.1.2}/agents/subagent-validate-eval-list.md +0 -0
  30. {quick_agent-0.1.1 → quick_agent-0.1.2}/agents/subagent-validator-contains.md +0 -0
  31. {quick_agent-0.1.1 → quick_agent-0.1.2}/agents/template.md +0 -0
  32. {quick_agent-0.1.1 → quick_agent-0.1.2}/setup.cfg +0 -0
  33. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/io_utils.py +0 -0
  34. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/json_utils.py +0 -0
  35. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/__init__.py +0 -0
  36. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/chain_step_spec.py +0 -0
  37. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/handoff_spec.py +0 -0
  38. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/loaded_agent_file.py +0 -0
  39. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/model_spec.py +0 -0
  40. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/output_spec.py +0 -0
  41. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/run_input.py +0 -0
  42. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/tool_impl_spec.py +0 -0
  43. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/models/tool_json.py +0 -0
  44. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/__init__.py +0 -0
  45. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/filesystem/__init__.py +0 -0
  46. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/filesystem/adapter.py +0 -0
  47. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/filesystem/read_text.py +0 -0
  48. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/filesystem/write_text.py +0 -0
  49. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/filesystem.read_text/tool.json +0 -0
  50. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools/filesystem.write_text/tool.json +0 -0
  51. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent/tools_loader.py +0 -0
  52. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent.egg-info/dependency_links.txt +0 -0
  53. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent.egg-info/entry_points.txt +0 -0
  54. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent.egg-info/requires.txt +0 -0
  55. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/quick_agent.egg-info/top_level.txt +0 -0
  56. {quick_agent-0.1.1 → quick_agent-0.1.2}/src/tests/test_agent.py +0 -0
  57. {quick_agent-0.1.1 → quick_agent-0.1.2}/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.2
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
@@ -707,13 +707,14 @@ Provides-Extra: test
707
707
  Requires-Dist: pytest; extra == "test"
708
708
  Dynamic: license-file
709
709
 
710
- # Simple Agent
710
+ # Quick Agent
711
711
 
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.
712
+ 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
713
 
714
714
  ## Project Goal
715
715
 
716
716
  Provide a simple, maintainable agent framework that:
717
+
717
718
  - Uses Markdown front matter for agent configuration.
718
719
  - Runs a deterministic chain of steps (text or structured output).
719
720
  - Keeps context handling deliberately limited and predictable.
@@ -887,7 +888,7 @@ def main() -> None:
887
888
  tools=tools,
888
889
  directory_permissions=permissions,
889
890
  agent_id="hello",
890
- input_path=Path("safe/path/to/input.txt"),
891
+ input_data=Path("safe/path/to/input.txt"),
891
892
  extra_tools=None,
892
893
  )
893
894
 
@@ -908,6 +909,17 @@ Agents are stored as Markdown files with YAML front matter and step sections:
908
909
 
909
910
  The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
910
911
 
912
+ ## Nested Output
913
+
914
+ When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
915
+ own `output.file` or return output inline only. Configure this in the parent agent front matter:
916
+
917
+ ```yaml
918
+ nested_output: inline # default, no output file for nested calls
919
+ ```
920
+
921
+ Use `nested_output: file` to allow nested agents to write their configured output files.
922
+
911
923
  ## Documentation
912
924
 
913
925
  See the docs in `docs/`:
@@ -916,3 +928,4 @@ See the docs in `docs/`:
916
928
  - [docs/templates.md](docs/templates.md): Agent template format and examples.
917
929
  - [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
918
930
  - [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
931
+ - [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
 
@@ -199,6 +200,17 @@ Agents are stored as Markdown files with YAML front matter and step sections:
199
200
 
200
201
  The orchestrator loads the agent, builds the tools, and executes each step in order, writing the final output to disk.
201
202
 
203
+ ## Nested Output
204
+
205
+ When an agent invokes another agent via `agent_call` or `handoff`, the nested agent can either write its
206
+ own `output.file` or return output inline only. Configure this in the parent agent front matter:
207
+
208
+ ```yaml
209
+ nested_output: inline # default, no output file for nested calls
210
+ ```
211
+
212
+ Use `nested_output: file` to allow nested agents to write their configured output files.
213
+
202
214
  ## Documentation
203
215
 
204
216
  See the docs in `docs/`:
@@ -207,3 +219,4 @@ See the docs in `docs/`:
207
219
  - [docs/templates.md](docs/templates.md): Agent template format and examples.
208
220
  - [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
209
221
  - [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
222
+ - [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
+ # Business Extract Structured
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
+ # Business Extract
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.
@@ -6,7 +6,7 @@ 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.2"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "anyio",
@@ -61,7 +61,7 @@ build-backend = "setuptools.build_meta"
61
61
  where = ["src"]
62
62
 
63
63
  [tool.setuptools.package-data]
64
- quick_agent = ["tools/**/tool.json", "agents/**/*.md"]
64
+ quick_agent = ["llms.txt", "py.typed", "tools/**/tool.json", "agents/**/*.md"]
65
65
 
66
66
  [tool.setuptools.data-files]
67
67
  "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}
@@ -37,8 +37,8 @@ def load_agent_file(path: Path) -> LoadedAgentFile:
37
37
 
38
38
 
39
39
  class AgentRegistry:
40
- def __init__(self, agent_roots: list[Path]):
41
- self.agent_roots = agent_roots
40
+ def __init__(self, agent_roots: list[Path]) -> None:
41
+ self.agent_roots: list[Path] = agent_roots
42
42
  self._cache: dict[str, LoadedAgentFile] = {}
43
43
  self._index: dict[str, Path] | None = None
44
44
 
@@ -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)
@@ -0,0 +1,239 @@
1
+ # Quick Agent (llms.txt)
2
+
3
+ **Project Summary**
4
+ - 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 bounded context.
5
+ - Agents are Markdown files with YAML front matter for model/tools/chain and `## step:<id>` sections for prompts.
6
+ - Execution is deterministic: steps run in order, state stores prior step outputs, final output is written to disk, and optional handoff can invoke another agent.
7
+
8
+ **Primary Entry Points**
9
+ - CLI: `quick-agent --agent <id> --input <path>` (module: `quick_agent.cli:main`).
10
+ - Python API: `quick_agent.orchestrator.Orchestrator` and `quick_agent.quick_agent.QuickAgent`.
11
+
12
+ **How It Works (High Level)**
13
+ - Agent files are loaded by `AgentRegistry` from user and packaged directories.
14
+ - `QuickAgent` loads the agent spec, scopes file permissions, builds the toolset, then runs each chain step (text or structured).
15
+ - Structured steps validate JSON output against a Pydantic schema and attempt extraction if the raw output contains extra text.
16
+ - Outputs are written via `io_utils.write_output` and constrained by `DirectoryPermissions`.
17
+ - Input handling uses adaptors: file inputs are permission-checked at creation, and text inputs bypass filesystem access.
18
+
19
+ **Key Files**
20
+ - `src/quick_agent/quick_agent.py`: core execution engine and step handling.
21
+ - `src/quick_agent/orchestrator.py`: convenience wrapper for running agents.
22
+ - `src/quick_agent/agent_registry.py`: loads agent Markdown files.
23
+ - `src/quick_agent/agent_tools.py`: loads tools and builds the toolset.
24
+ - `src/quick_agent/directory_permissions.py`: enforces safe directory access.
25
+ - `src/quick_agent/tools/**/tool.json`: tool definitions (id, module, function).
26
+ - `agents/`: example agents shipped with the repo.
27
+ - `docs/`: CLI, template, and Python usage documentation.
28
+
29
+ **Agent File Format (Essentials)**
30
+ - Required fields: `name`, `model.base_url`, `model.model_name`, `chain`.
31
+ - Each `chain` step must reference a matching `## step:<id>` section in the body.
32
+ - Tools are referenced by id and resolved from `tool.json` definitions.
33
+ - `safe_dir` must be relative and further scopes file access inside the CLI safe root.
34
+
35
+ **Safety and Permissions**
36
+ - File reads and writes are restricted to a safe directory configured by `--safe-dir` (default `safe`).
37
+ - Agent-level `safe_dir` can further restrict access to a subdirectory.
38
+ - If no safe directory is configured, all reads and writes are denied.
39
+
40
+ **Tests**
41
+ - Tests live in `src/tests`.
42
+ - Run with `pytest` (requires optional dependency group `test`).
43
+
44
+ **Useful Docs**
45
+ - `docs/cli.md`: CLI usage and options.
46
+ - `docs/templates.md`: agent template format and examples.
47
+ - `docs/python.md`: embedding the orchestrator and inter-agent calls.
48
+ - `docs/state.md`: how chain state is stored and used.
49
+
50
+ **Mini Example Agent**
51
+ ```markdown
52
+ ---
53
+ name: "Hello Agent"
54
+ description: "Minimal example"
55
+ model:
56
+ provider: "openai-compatible"
57
+ base_url: "http://localhost:11434/v1"
58
+ api_key_env: "OPENAI_API_KEY"
59
+ model_name: "llama3"
60
+ chain:
61
+ - id: hello
62
+ kind: text
63
+ prompt_section: step:hello
64
+ output:
65
+ format: json
66
+ file: out/hello.json
67
+ ---
68
+
69
+ ## step:hello
70
+
71
+ Say hello to the input.
72
+ ```
73
+
74
+ **Mini Example (Structured Output)**
75
+ ```markdown
76
+ ---
77
+ name: "Structured Agent"
78
+ model:
79
+ provider: "openai-compatible"
80
+ base_url: "http://localhost:11434/v1"
81
+ api_key_env: "OPENAI_API_KEY"
82
+ model_name: "llama3"
83
+ schemas:
84
+ Summary: "quick_agent.schemas.outputs:SummaryOutput"
85
+ chain:
86
+ - id: summarize
87
+ kind: structured
88
+ prompt_section: step:summarize
89
+ output_schema: Summary
90
+ output:
91
+ format: json
92
+ file: out/summary.json
93
+ ---
94
+
95
+ ## step:summarize
96
+
97
+ Summarize the input into a short title and 2 bullet points.
98
+ ```
99
+
100
+ **Schema Snippet (for Structured Output)**
101
+ ```python
102
+ from pydantic import BaseModel
103
+
104
+
105
+ class SummaryOutput(BaseModel):
106
+ title: str
107
+ bullets: list[str]
108
+ ```
109
+
110
+ **Input Adaptors (Python)**
111
+ ```python
112
+ from pathlib import Path
113
+
114
+ from quick_agent import FileInput
115
+ from quick_agent import Orchestrator
116
+ from quick_agent import TextInput
117
+
118
+ orchestrator = Orchestrator([Path("agents")], [Path("tools")], safe_dir=Path("safe"))
119
+
120
+ file_input = FileInput(Path("safe/input.txt"), orchestrator.directory_permissions)
121
+ text_input = TextInput("hello from memory")
122
+
123
+ result_from_file = await orchestrator.run("example", file_input)
124
+ result_from_text = await orchestrator.run("example", text_input)
125
+ ```
126
+
127
+ **Multi-Step Example (State + Structured Output)**
128
+ ```markdown
129
+ ---
130
+ name: "Draft And Summarize"
131
+ model:
132
+ provider: "openai-compatible"
133
+ base_url: "http://localhost:11434/v1"
134
+ api_key_env: "OPENAI_API_KEY"
135
+ model_name: "llama3"
136
+ schemas:
137
+ Summary: "quick_agent.schemas.outputs:SummaryOutput"
138
+ chain:
139
+ - id: draft
140
+ kind: text
141
+ prompt_section: step:draft
142
+ - id: summarize
143
+ kind: structured
144
+ prompt_section: step:summarize
145
+ output_schema: Summary
146
+ output:
147
+ format: json
148
+ file: out/draft_summary.json
149
+ ---
150
+
151
+ ## step:draft
152
+
153
+ Write a short draft based on the input.
154
+
155
+ ## step:summarize
156
+
157
+ Summarize the draft from state as a title and 2 bullets.
158
+ Use the value at state.steps.draft.
159
+ ```
160
+
161
+ **Inter-Agent Call Example (agent.call)**
162
+ ```markdown
163
+ ---
164
+ name: "Parent Agent"
165
+ tools:
166
+ - "agent.call"
167
+ nested_output: inline
168
+ chain:
169
+ - id: invoke_child
170
+ kind: text
171
+ prompt_section: step:invoke_child
172
+ output:
173
+ format: json
174
+ file: out/parent.json
175
+ ---
176
+
177
+ ## step:invoke_child
178
+
179
+ Call agent_call with agent "child" and input_file "{base_directory}/child_input.txt".
180
+ Then respond with only the returned text value.
181
+ ```
182
+
183
+ ```markdown
184
+ ---
185
+ name: "Child Agent"
186
+ chain:
187
+ - id: respond
188
+ kind: text
189
+ prompt_section: step:respond
190
+ output:
191
+ format: json
192
+ file: out/child.json
193
+ ---
194
+
195
+ ## step:respond
196
+
197
+ Reply with exactly: pong
198
+ ```
199
+
200
+ Nested calls default to `nested_output: inline`, so the child agent above will not write
201
+ `out/child.json` unless the parent sets `nested_output: file`.
202
+
203
+ **Handoff Example (Run Another Agent After Output)**
204
+ ```markdown
205
+ ---
206
+ name: "Writer With Handoff"
207
+ chain:
208
+ - id: write
209
+ kind: text
210
+ prompt_section: step:write
211
+ output:
212
+ format: json
213
+ file: out/writer.json
214
+ handoff:
215
+ enabled: true
216
+ agent_id: "reviewer"
217
+ ---
218
+
219
+ ## step:write
220
+
221
+ Write a short answer to the input.
222
+ ```
223
+
224
+ ```markdown
225
+ ---
226
+ name: "Reviewer"
227
+ chain:
228
+ - id: review
229
+ kind: text
230
+ prompt_section: step:review
231
+ output:
232
+ format: json
233
+ file: out/reviewer.json
234
+ ---
235
+
236
+ ## step:review
237
+
238
+ Review the inline output produced by the writer and provide a brief critique.
239
+ ```
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Literal
6
+
5
7
  from pydantic import BaseModel, Field
6
8
 
7
9
  from quick_agent.models.chain_step_spec import ChainStepSpec
@@ -19,4 +21,5 @@ class AgentSpec(BaseModel):
19
21
  chain: list[ChainStepSpec]
20
22
  output: OutputSpec = Field(default_factory=OutputSpec)
21
23
  handoff: HandoffSpec = Field(default_factory=HandoffSpec)
24
+ nested_output: Literal["inline", "file"] = "inline"
22
25
  safe_dir: str | None = None