quick-agent 0.1.1__py3-none-any.whl → 0.1.2__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 (31) hide show
  1. quick_agent/__init__.py +4 -1
  2. quick_agent/agent_call_tool.py +22 -5
  3. quick_agent/agent_registry.py +2 -2
  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/orchestrator.py +15 -8
  11. quick_agent/prompting.py +2 -2
  12. quick_agent/py.typed +1 -0
  13. quick_agent/quick_agent.py +87 -132
  14. quick_agent/schemas/outputs.py +6 -0
  15. quick_agent-0.1.2.data/data/quick_agent/agents/business-extract-structured.md +49 -0
  16. quick_agent-0.1.2.data/data/quick_agent/agents/business-extract.md +42 -0
  17. {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/METADATA +17 -4
  18. quick_agent-0.1.2.dist-info/RECORD +51 -0
  19. tests/test_directory_permissions.py +10 -0
  20. tests/test_input_adaptors.py +31 -0
  21. tests/test_integration.py +134 -1
  22. tests/test_orchestrator.py +183 -94
  23. quick_agent-0.1.1.dist-info/RECORD +0 -45
  24. {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/function-spec-validator.md +0 -0
  25. {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/subagent-validate-eval-list.md +0 -0
  26. {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/subagent-validator-contains.md +0 -0
  27. {quick_agent-0.1.1.data → quick_agent-0.1.2.data}/data/quick_agent/agents/template.md +0 -0
  28. {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/WHEEL +0 -0
  29. {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/entry_points.txt +0 -0
  30. {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/licenses/LICENSE +0 -0
  31. {quick_agent-0.1.1.dist-info → quick_agent-0.1.2.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from pathlib import Path
7
- from typing import Any, Type
7
+ from typing import Any, Type, TypeAlias, TypedDict
8
8
 
9
9
  from pydantic import BaseModel, ValidationError
10
10
  from pydantic_ai import Agent
@@ -16,13 +16,24 @@ from pydantic_ai.toolsets import FunctionToolset
16
16
  from quick_agent.agent_registry import AgentRegistry
17
17
  from quick_agent.agent_tools import AgentTools
18
18
  from quick_agent.directory_permissions import DirectoryPermissions
19
- from quick_agent.io_utils import load_input, write_output
19
+ from quick_agent.input_adaptors import FileInput, InputAdaptor, TextInput
20
+ from quick_agent.io_utils import write_output
20
21
  from quick_agent.json_utils import extract_first_json_object
21
22
  from quick_agent.models.loaded_agent_file import LoadedAgentFile
23
+ from quick_agent.models.chain_step_spec import ChainStepSpec
22
24
  from quick_agent.models.model_spec import ModelSpec
25
+ from quick_agent.models.run_input import RunInput
23
26
  from quick_agent.prompting import make_user_prompt
24
27
  from quick_agent.tools_loader import import_symbol
25
28
 
29
+ StepOutput: TypeAlias = str | dict[str, Any]
30
+
31
+
32
+ class ChainState(TypedDict):
33
+ agent_id: str
34
+ steps: dict[str, StepOutput]
35
+ final_output: StepOutput | None
36
+
26
37
 
27
38
  class QuickAgent:
28
39
  def __init__(
@@ -32,29 +43,37 @@ class QuickAgent:
32
43
  tools: AgentTools,
33
44
  directory_permissions: DirectoryPermissions,
34
45
  agent_id: str,
35
- input_path: Path,
46
+ input_data: InputAdaptor | Path,
36
47
  extra_tools: list[str] | None,
48
+ write_output: bool = True,
37
49
  ) -> None:
38
- self._registry = registry
39
- self._tools = tools
40
- self._directory_permissions = directory_permissions
41
- self._agent_id = agent_id
42
- self._input_path = input_path
43
- self._extra_tools = extra_tools
44
-
45
- self.loaded = self._registry.get(self._agent_id)
50
+ self._registry: AgentRegistry = registry
51
+ self._tools: AgentTools = tools
52
+ self._directory_permissions: DirectoryPermissions = directory_permissions
53
+ self._agent_id: str = agent_id
54
+ self._input_data: InputAdaptor | Path = input_data
55
+ self._extra_tools: list[str] | None = extra_tools
56
+ self._write_output_file: bool = write_output
57
+
58
+ self.loaded: LoadedAgentFile = self._registry.get(self._agent_id)
46
59
  safe_dir = self.loaded.spec.safe_dir
47
60
  if safe_dir is not None and Path(safe_dir).is_absolute():
48
61
  raise ValueError("safe_dir must be a relative path.")
49
- self.permissions = self._directory_permissions.scoped(safe_dir)
50
- self.run_input = load_input(self._input_path, self.permissions)
62
+ self.permissions: DirectoryPermissions = self._directory_permissions.scoped(safe_dir)
63
+ if isinstance(self._input_data, InputAdaptor):
64
+ input_adaptor = self._input_data
65
+ else:
66
+ input_adaptor = FileInput(self._input_data, self.permissions)
67
+ self.run_input: RunInput = input_adaptor.load()
51
68
 
52
- self.tool_ids = list(dict.fromkeys((self.loaded.spec.tools or []) + (self._extra_tools or [])))
53
- self.toolset = self._tools.build_toolset(self.tool_ids, self.permissions)
69
+ self.tool_ids: list[str] = list(
70
+ dict.fromkeys((self.loaded.spec.tools or []) + (self._extra_tools or []))
71
+ )
72
+ self.toolset: FunctionToolset[Any] = self._tools.build_toolset(self.tool_ids, self.permissions)
54
73
 
55
- self.model = build_model(self.loaded.spec.model)
56
- self.model_settings_json = self._build_model_settings(self.loaded.spec.model)
57
- self.state = self._init_state(self._agent_id)
74
+ self.model: OpenAIChatModel = build_model(self.loaded.spec.model)
75
+ self.model_settings_json: ModelSettings | None = self._build_model_settings(self.loaded.spec.model)
76
+ self.state: ChainState = self._init_state()
58
77
 
59
78
  async def run(self) -> BaseModel | str:
60
79
  self._tools.maybe_inject_agent_call(
@@ -64,36 +83,33 @@ class QuickAgent:
64
83
  self._run_nested_agent,
65
84
  )
66
85
 
67
- final_output = await self._run_chain(
68
- loaded=self.loaded,
69
- model=self.model,
70
- model_settings_json=self.model_settings_json,
71
- toolset=self.toolset,
72
- run_input=self.run_input,
73
- state=self.state,
74
- )
86
+ final_output = await self._run_chain()
75
87
 
76
- out_path = self._write_final_output(self.loaded, final_output, self.permissions)
88
+ if self._write_output_file:
89
+ self._write_final_output(final_output)
77
90
 
78
- await self._handle_handoff(self.loaded, out_path)
91
+ await self._handle_handoff(final_output)
79
92
 
80
93
  return final_output
81
94
 
82
- async def _run_nested_agent(self, agent_id: str, input_path: Path) -> BaseModel | str:
95
+ async def _run_nested_agent(self, agent_id: str, input_data: InputAdaptor | Path) -> BaseModel | str:
96
+ nested_write_output = self.loaded.spec.nested_output == "file"
83
97
  agent = QuickAgent(
84
98
  registry=self._registry,
85
99
  tools=self._tools,
86
100
  directory_permissions=self._directory_permissions,
87
101
  agent_id=agent_id,
88
- input_path=input_path,
102
+ input_data=input_data,
89
103
  extra_tools=None,
104
+ write_output=nested_write_output,
90
105
  )
91
106
  return await agent.run()
92
107
 
93
- def _init_state(self, agent_id: str) -> dict[str, Any]:
108
+ def _init_state(self) -> ChainState:
94
109
  return {
95
- "agent_id": agent_id,
110
+ "agent_id": self._agent_id,
96
111
  "steps": {},
112
+ "final_output": None,
97
113
  }
98
114
 
99
115
  def _build_model_settings(self, model_spec: ModelSpec) -> ModelSettings | None:
@@ -103,21 +119,15 @@ class QuickAgent:
103
119
  return {"extra_body": {"format": "json"}}
104
120
  return None
105
121
 
106
- def _build_structured_model_settings(
107
- self,
108
- *,
109
- model: OpenAIChatModel,
110
- model_settings_json: ModelSettings | None,
111
- schema_cls: Type[BaseModel],
112
- ) -> ModelSettings | None:
113
- model_settings: ModelSettings | None = model_settings_json
114
- provider = getattr(model, "provider", None)
122
+ def _build_structured_model_settings(self, *, schema_cls: Type[BaseModel]) -> ModelSettings | None:
123
+ model_settings: ModelSettings | None = self.model_settings_json
124
+ provider = getattr(self.model, "provider", None)
115
125
  base_url = getattr(provider, "base_url", None)
116
126
  if base_url == "https://api.openai.com/v1":
117
- if model_settings_json is None:
127
+ if self.model_settings_json is None:
118
128
  model_settings_dict: ModelSettings = {}
119
129
  else:
120
- model_settings_dict = model_settings_json
130
+ model_settings_dict = self.model_settings_json
121
131
  extra_body_obj = model_settings_dict.get("extra_body")
122
132
  extra_body: dict[str, Any] = {}
123
133
  if isinstance(extra_body_obj, dict):
@@ -138,33 +148,16 @@ class QuickAgent:
138
148
  async def _run_step(
139
149
  self,
140
150
  *,
141
- step: Any,
142
- loaded: LoadedAgentFile,
143
- model: OpenAIChatModel,
144
- model_settings_json: ModelSettings | None,
145
- toolset: FunctionToolset[Any],
146
- run_input: Any,
147
- state: dict[str, Any],
148
- ) -> tuple[Any, BaseModel | str]:
151
+ step: ChainStepSpec,
152
+ ) -> tuple[StepOutput, BaseModel | str]:
149
153
  if step.kind == "text":
150
154
  return await self._run_text_step(
151
155
  step=step,
152
- loaded=loaded,
153
- model=model,
154
- toolset=toolset,
155
- run_input=run_input,
156
- state=state,
157
156
  )
158
157
 
159
158
  if step.kind == "structured":
160
159
  return await self._run_structured_step(
161
160
  step=step,
162
- loaded=loaded,
163
- model=model,
164
- model_settings_json=model_settings_json,
165
- toolset=toolset,
166
- run_input=run_input,
167
- state=state,
168
161
  )
169
162
 
170
163
  raise NotImplementedError(f"Unknown step kind: {step.kind}")
@@ -172,37 +165,26 @@ class QuickAgent:
172
165
  def _build_user_prompt(
173
166
  self,
174
167
  *,
175
- step: Any,
176
- loaded: LoadedAgentFile,
177
- run_input: Any,
178
- state: dict[str, Any],
168
+ step: ChainStepSpec,
179
169
  ) -> str:
180
- if step.prompt_section not in loaded.step_prompts:
170
+ if step.prompt_section not in self.loaded.step_prompts:
181
171
  raise KeyError(f"Missing step section {step.prompt_section!r} in agent.md body.")
182
172
 
183
- step_prompt = loaded.step_prompts[step.prompt_section]
184
- return make_user_prompt(step_prompt, run_input, state)
173
+ step_prompt = self.loaded.step_prompts[step.prompt_section]
174
+ return make_user_prompt(step_prompt, self.run_input, self.state)
185
175
 
186
176
  async def _run_text_step(
187
177
  self,
188
178
  *,
189
- step: Any,
190
- loaded: LoadedAgentFile,
191
- model: OpenAIChatModel,
192
- toolset: FunctionToolset[Any],
193
- run_input: Any,
194
- state: dict[str, Any],
195
- ) -> tuple[Any, BaseModel | str]:
179
+ step: ChainStepSpec,
180
+ ) -> tuple[StepOutput, BaseModel | str]:
196
181
  user_prompt = self._build_user_prompt(
197
182
  step=step,
198
- loaded=loaded,
199
- run_input=run_input,
200
- state=state,
201
183
  )
202
184
  agent = Agent(
203
- model,
204
- instructions=loaded.body,
205
- toolsets=[toolset],
185
+ self.model,
186
+ instructions=self.loaded.body,
187
+ toolsets=[self.toolset],
206
188
  output_type=str,
207
189
  )
208
190
  result = await agent.run(user_prompt)
@@ -211,34 +193,21 @@ class QuickAgent:
211
193
  async def _run_structured_step(
212
194
  self,
213
195
  *,
214
- step: Any,
215
- loaded: LoadedAgentFile,
216
- model: OpenAIChatModel,
217
- model_settings_json: ModelSettings | None,
218
- toolset: FunctionToolset[Any],
219
- run_input: Any,
220
- state: dict[str, Any],
221
- ) -> tuple[Any, BaseModel | str]:
196
+ step: ChainStepSpec,
197
+ ) -> tuple[StepOutput, BaseModel | str]:
222
198
  if not step.output_schema:
223
199
  raise ValueError(f"Step {step.id} is structured but missing output_schema.")
224
- schema_cls = resolve_schema(loaded, step.output_schema)
200
+ schema_cls = resolve_schema(self.loaded, step.output_schema)
225
201
 
226
- model_settings = self._build_structured_model_settings(
227
- model=model,
228
- model_settings_json=model_settings_json,
229
- schema_cls=schema_cls,
230
- )
202
+ model_settings = self._build_structured_model_settings(schema_cls=schema_cls)
231
203
 
232
204
  user_prompt = self._build_user_prompt(
233
205
  step=step,
234
- loaded=loaded,
235
- run_input=run_input,
236
- state=state,
237
206
  )
238
207
  agent = Agent(
239
- model,
240
- instructions=loaded.body,
241
- toolsets=[toolset],
208
+ self.model,
209
+ instructions=self.loaded.body,
210
+ toolsets=[self.toolset],
242
211
  output_type=str,
243
212
  model_settings=model_settings,
244
213
  )
@@ -253,49 +222,35 @@ class QuickAgent:
253
222
 
254
223
  async def _run_chain(
255
224
  self,
256
- *,
257
- loaded: LoadedAgentFile,
258
- model: OpenAIChatModel,
259
- model_settings_json: ModelSettings | None,
260
- toolset: FunctionToolset[Any],
261
- run_input: Any,
262
- state: dict[str, Any],
263
225
  ) -> BaseModel | str:
264
226
  final_output: BaseModel | str = ""
265
- for step in loaded.spec.chain:
227
+ for step in self.loaded.spec.chain:
266
228
  step_out, step_final = await self._run_step(
267
229
  step=step,
268
- loaded=loaded,
269
- model=model,
270
- model_settings_json=model_settings_json,
271
- toolset=toolset,
272
- run_input=run_input,
273
- state=state,
274
230
  )
275
- state["steps"][step.id] = step_out
231
+ self.state["steps"][step.id] = step_out
232
+ self.state["final_output"] = step_out
276
233
  final_output = step_final
277
234
  return final_output
278
235
 
279
- def _write_final_output(
280
- self,
281
- loaded: LoadedAgentFile,
282
- final_output: BaseModel | str,
283
- permissions: DirectoryPermissions,
284
- ) -> Path:
285
- out_path = Path(loaded.spec.output.file)
236
+ def _write_final_output(self, final_output: BaseModel | str) -> Path:
237
+ out_path = Path(self.loaded.spec.output.file)
286
238
  if isinstance(final_output, BaseModel):
287
- if loaded.spec.output.format == "json":
288
- write_output(out_path, final_output.model_dump_json(indent=2), permissions)
239
+ if self.loaded.spec.output.format == "json":
240
+ write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
289
241
  else:
290
- write_output(out_path, final_output.model_dump_json(indent=2), permissions)
242
+ write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
291
243
  else:
292
- write_output(out_path, str(final_output), permissions)
244
+ write_output(out_path, str(final_output), self.permissions)
293
245
  return out_path
294
246
 
295
- async def _handle_handoff(self, loaded: LoadedAgentFile, out_path: Path) -> None:
296
- if loaded.spec.handoff.enabled and loaded.spec.handoff.agent_id:
297
- # For a more robust version, generate an intermediate file for handoff input.
298
- await self._run_nested_agent(loaded.spec.handoff.agent_id, out_path)
247
+ async def _handle_handoff(self, final_output: BaseModel | str) -> None:
248
+ if self.loaded.spec.handoff.enabled and self.loaded.spec.handoff.agent_id:
249
+ if isinstance(final_output, BaseModel):
250
+ payload = final_output.model_dump_json(indent=2)
251
+ else:
252
+ payload = str(final_output)
253
+ await self._run_nested_agent(self.loaded.spec.handoff.agent_id, TextInput(payload))
299
254
 
300
255
 
301
256
  def resolve_schema(loaded: LoadedAgentFile, schema_name: str) -> Type[BaseModel]:
@@ -26,6 +26,12 @@ class ValidationResult(BaseModel):
26
26
  summary: str | None = None
27
27
 
28
28
 
29
+ class BusinessSummary(BaseModel):
30
+ company_name: str
31
+ location: str
32
+ summary: str
33
+
34
+
29
35
  class EvalRowResult(BaseModel):
30
36
  index: int = 0
31
37
  agent: str
@@ -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.
@@ -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.
@@ -0,0 +1,51 @@
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=laHxiB3ko2oaQDhN7xG7u7Kc7WTy7j-rPqvfiWdpxEY,2473
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=SNxYm6oFbqvc4mzGsxSpWg1l0iwT-Be0saVw3rtoruQ,6459
11
+ quick_agent/orchestrator.py,sha256=SGa5oeRgbjkKXdFL5kszZT-R6-jIgyOkHEMmjLrDWjc,1321
12
+ quick_agent/prompting.py,sha256=XMlnRut3aU2FEabhXuvb4Z_DXKzi1psZfbbx4iDyg_4,645
13
+ quick_agent/py.typed,sha256=3Q9vdii9v-_pbj9sglgDzJAIelvARaYaDJlB2u-NDqg,38
14
+ quick_agent/quick_agent.py,sha256=3Cok1VPzYZo4Nityyprub2XaKVRBkaoeTm6fs67BRZw,10319
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=rWRxrm0KjgmNIiYfooMM95eohMB1zCZC-eqrp1mf8w0,313
21
+ quick_agent/models/model_spec.py,sha256=spnIZ8BNtXT5t8YRL1nh7cXrgWwNSptt_BMnAB8w1Lg,425
22
+ quick_agent/models/output_spec.py,sha256=bvKFU1ONeza95qethNIiVgrsnT7l1TKMsdHjfXVVJnc,229
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.2.data/data/quick_agent/agents/business-extract-structured.md,sha256=mhieyFbkJ23kjZPvpKguW4xorkTgzwAev1EZCUkcS44,1182
35
+ quick_agent-0.1.2.data/data/quick_agent/agents/business-extract.md,sha256=2SQdD9a4Bg_PXqL5-D5-kJMzZOYAigi0N54XsnoeNCU,996
36
+ quick_agent-0.1.2.data/data/quick_agent/agents/function-spec-validator.md,sha256=Dlc4j1vFpDHj7w2vcWrvC83EEg_sWwwt1GOaRgcsm6Y,3289
37
+ quick_agent-0.1.2.data/data/quick_agent/agents/subagent-validate-eval-list.md,sha256=K83L9BKvGcttPRINwD6VSPopEHnB_aw4tpfSI6KM2u8,4119
38
+ quick_agent-0.1.2.data/data/quick_agent/agents/subagent-validator-contains.md,sha256=3VxtKpyogwcJ4Er5tQFyYNe2nJBtANErb5D-_snBATw,3115
39
+ quick_agent-0.1.2.data/data/quick_agent/agents/template.md,sha256=6IBuVZjK5OPIchI8ruvxao8CdLy0ZOi0meh6d1exI0E,2458
40
+ quick_agent-0.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
41
+ tests/test_agent.py,sha256=tcPpH9WmGPQjMo_jkFJlF8kgOZ3QktIvUHTAJaH1hWA,5533
42
+ tests/test_directory_permissions.py,sha256=-9h7W1VO0LekPLCxEqIAGgebquNks2F8M02BdZDLlQY,2815
43
+ tests/test_input_adaptors.py,sha256=sle9zrumq3CUVGtDPZi2pf0om-Z7v4SI6khnTf7Rz4c,918
44
+ tests/test_integration.py,sha256=XbI6abGeoZm3r_BqpQyhxhF36N7VYtNGcJ9-xNxZysM,9044
45
+ tests/test_orchestrator.py,sha256=fWeZzTF_cQj6nbeEVh0cC5uKwgWP56deDQxkcCO1uNk,31001
46
+ tests/test_tools.py,sha256=Yc6AKCz79XJwHqiRV8dUBE6GVNdspUU0hIeGCXx-Rvc,936
47
+ quick_agent-0.1.2.dist-info/METADATA,sha256=1xL8o4mz4qW2dLuotIPTGJxuHRHm1Uxp42j-w6nsQ-8,47103
48
+ quick_agent-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
49
+ quick_agent-0.1.2.dist-info/entry_points.txt,sha256=bij-xFaQMSrMj7zearzi-bMfcyweum_GvURFMVZGde8,53
50
+ quick_agent-0.1.2.dist-info/top_level.txt,sha256=KT1ID0FVC0OLzQnoBKRIHbXLRNugiU2gcgj_f4DtAsw,18
51
+ quick_agent-0.1.2.dist-info/RECORD,,
@@ -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,
@@ -0,0 +1,31 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from quick_agent.directory_permissions import DirectoryPermissions
6
+ from quick_agent.input_adaptors import FileInput
7
+
8
+
9
+ def test_file_input_checks_permissions_at_creation(tmp_path: Path) -> None:
10
+ safe_root = tmp_path / "safe"
11
+ safe_root.mkdir(parents=True, exist_ok=True)
12
+ input_path = safe_root / "input.txt"
13
+ input_path.write_text("ok", encoding="utf-8")
14
+
15
+ permissions = DirectoryPermissions(safe_root)
16
+
17
+ adaptor = FileInput(input_path, permissions)
18
+
19
+ run_input = adaptor.load()
20
+ assert run_input.text == "ok"
21
+ assert run_input.source_path == str(input_path)
22
+
23
+
24
+ def test_file_input_denies_without_root(tmp_path: Path) -> None:
25
+ input_path = tmp_path / "input.txt"
26
+ input_path.write_text("nope", encoding="utf-8")
27
+
28
+ permissions = DirectoryPermissions(None)
29
+
30
+ with pytest.raises(PermissionError):
31
+ FileInput(input_path, permissions)