quick-agent 0.1.2__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.
@@ -2,38 +2,18 @@
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:
@@ -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
quick_agent/llms.txt CHANGED
@@ -3,7 +3,7 @@
3
3
  **Project Summary**
4
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
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.
6
+ - Execution is deterministic: steps run in order, state stores prior step outputs, and optional handoff can invoke another agent. If `output.file` is set, the final output is written to disk.
7
7
 
8
8
  **Primary Entry Points**
9
9
  - CLI: `quick-agent --agent <id> --input <path>` (module: `quick_agent.cli:main`).
@@ -2,13 +2,148 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
6
+ import re
5
7
  from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ import frontmatter
6
11
 
7
12
  from quick_agent.models.agent_spec import AgentSpec
8
13
 
9
14
 
15
+ logger = logging.getLogger(__name__)
16
+
17
+ SECTION_HEADER_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.MULTILINE)
18
+ STEP_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$")
19
+
20
+
10
21
  @dataclass
11
22
  class LoadedAgentFile:
12
23
  spec: AgentSpec
13
- body: str
24
+ instructions: str
25
+ system_prompt: str
14
26
  step_prompts: dict[str, str] # prompt_section -> markdown chunk
27
+
28
+ def __init__(self, agent: Path | str) -> None:
29
+ post, source_label = load_agent_frontmatter(agent)
30
+ spec = AgentSpec.model_validate(post.metadata)
31
+ sections = parse_agent_sections(post.content)
32
+ if sections.first_section_start is not None and (
33
+ sections.instructions_start is not None or sections.system_prompt_start is not None
34
+ ):
35
+ preamble = post.content[: sections.first_section_start]
36
+ if preamble.strip():
37
+ logger.warning("Ignored text before instructions or system prompt in %s", source_label)
38
+ if not sections.step_prompts and sections.instructions_start is None and sections.system_prompt_start is None:
39
+ raise ValueError("Agent markdown must include instructions, system prompt, or step sections.")
40
+ self.spec = spec
41
+ self.instructions = sections.instructions
42
+ self.system_prompt = sections.system_prompt
43
+ self.step_prompts = sections.step_prompts
44
+
45
+ @classmethod
46
+ def from_parts(
47
+ cls,
48
+ *,
49
+ spec: AgentSpec,
50
+ instructions: str,
51
+ system_prompt: str,
52
+ step_prompts: dict[str, str],
53
+ ) -> "LoadedAgentFile":
54
+ obj = cls.__new__(cls)
55
+ obj.spec = spec
56
+ obj.instructions = instructions
57
+ obj.system_prompt = system_prompt
58
+ obj.step_prompts = step_prompts
59
+ return obj
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ParsedAgentSections:
64
+ instructions: str
65
+ system_prompt: str
66
+ step_prompts: dict[str, str]
67
+ instructions_start: int | None
68
+ system_prompt_start: int | None
69
+ first_section_start: int | None
70
+
71
+
72
+ def load_agent_frontmatter(agent: Path | str) -> tuple[frontmatter.Post, str]:
73
+ if isinstance(agent, Path):
74
+ post = frontmatter.load(str(agent))
75
+ return post, str(agent)
76
+ agent_path = Path(agent)
77
+ if agent_path.exists():
78
+ post = frontmatter.load(str(agent_path))
79
+ return post, str(agent_path)
80
+ post = frontmatter.loads(agent)
81
+ return post, "<inline>"
82
+
83
+
84
+ def normalize_header_text(header_text: str) -> str:
85
+ normalized = header_text.strip().lower().replace("_", " ")
86
+ return re.sub(r"\s+", " ", normalized)
87
+
88
+
89
+ def classify_section_header(header_text: str) -> tuple[str, str] | None:
90
+ header = header_text.strip()
91
+ if ":" in header:
92
+ prefix, step_id = header.split(":", 1)
93
+ if prefix.strip().lower() == "step":
94
+ step_id = step_id.strip()
95
+ if step_id and STEP_ID_RE.match(step_id):
96
+ return ("step", f"step:{step_id}")
97
+ normalized = normalize_header_text(header_text)
98
+ if normalized == "instructions":
99
+ return ("instructions", "instructions")
100
+ if normalized == "system prompt":
101
+ return ("system_prompt", "system_prompt")
102
+ return None
103
+
104
+
105
+ def parse_agent_sections(markdown_body: str) -> ParsedAgentSections:
106
+ matches = list(SECTION_HEADER_RE.finditer(markdown_body))
107
+ recognized: list[tuple[str, str, int, int]] = []
108
+ instructions_start: int | None = None
109
+ system_prompt_start: int | None = None
110
+
111
+ for match in matches:
112
+ header_text = match.group(2).strip()
113
+ classified = classify_section_header(header_text)
114
+ if classified is None:
115
+ continue
116
+ kind, key = classified
117
+ recognized.append((kind, key, match.start(), match.end()))
118
+ if kind == "instructions" and instructions_start is None:
119
+ instructions_start = match.start()
120
+ if kind == "system_prompt" and system_prompt_start is None:
121
+ system_prompt_start = match.start()
122
+
123
+ instructions = ""
124
+ system_prompt = ""
125
+ step_prompts: dict[str, str] = {}
126
+
127
+ for index, (kind, key, _start, end) in enumerate(recognized):
128
+ section_start = end
129
+ next_index = index + 1
130
+ section_end = recognized[next_index][2] if next_index < len(recognized) else len(markdown_body)
131
+ content = markdown_body[section_start:section_end].strip()
132
+ if kind == "instructions":
133
+ if not instructions:
134
+ instructions = content
135
+ elif kind == "system_prompt":
136
+ if not system_prompt:
137
+ system_prompt = content
138
+ else:
139
+ step_prompts[key] = content
140
+
141
+ first_section_start = recognized[0][2] if recognized else None
142
+ return ParsedAgentSections(
143
+ instructions=instructions,
144
+ system_prompt=system_prompt,
145
+ step_prompts=step_prompts,
146
+ instructions_start=instructions_start,
147
+ system_prompt_start=system_prompt_start,
148
+ first_section_start=first_section_start,
149
+ )
@@ -7,4 +7,4 @@ from pydantic import BaseModel
7
7
 
8
8
  class OutputSpec(BaseModel):
9
9
  format: str = "json" # "json" or "markdown"
10
- file: str = "out/result.json"
10
+ file: str | None = None
quick_agent/prompting.py CHANGED
@@ -2,27 +2,45 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  from typing import Any, Mapping
7
6
 
7
+ import yaml
8
+
8
9
  from quick_agent.models.run_input import RunInput
9
10
 
10
11
 
11
- def make_user_prompt(step_prompt: str, run_input: RunInput, state: Mapping[str, Any]) -> str:
12
+ def make_user_prompt(run_input: RunInput, state: Mapping[str, Any]) -> str:
12
13
  """
13
14
  Creates a consistent user prompt payload. Consistency helps prefix-caching backends.
14
15
  """
15
16
  # Keep the preamble stable; append variable fields below.
16
- return f"""# Task Input
17
- source_path: {run_input.source_path}
18
- kind: {run_input.kind}
19
-
20
- ## Input Content
21
- {run_input.text}
22
-
23
- ## Chain State (JSON)
24
- {json.dumps(state, indent=2)}
25
-
26
- ## Step Instructions
27
- {step_prompt}
28
- """
17
+ is_inline = run_input.source_path == "inline_input.txt"
18
+ steps_state = state.get("steps") if isinstance(state, Mapping) else None
19
+ has_state = bool(steps_state)
20
+ include_input_header = not (is_inline and not has_state)
21
+
22
+ lines: list[str] = []
23
+ if not is_inline:
24
+ lines.extend(
25
+ [
26
+ "# Task Input",
27
+ f"source_path: {run_input.source_path}",
28
+ f"kind: {run_input.kind}",
29
+ "",
30
+ ]
31
+ )
32
+
33
+ if include_input_header:
34
+ lines.append("## Input Content")
35
+ lines.append(run_input.text)
36
+
37
+ if has_state:
38
+ state_yaml = yaml.safe_dump(
39
+ steps_state,
40
+ allow_unicode=False,
41
+ default_flow_style=False,
42
+ sort_keys=True,
43
+ ).rstrip()
44
+ lines.extend(["", "## Chain State (YAML)", state_yaml])
45
+
46
+ return "\n".join(lines).rstrip() + "\n"
@@ -53,9 +53,9 @@ class QuickAgent:
53
53
  self._agent_id: str = agent_id
54
54
  self._input_data: InputAdaptor | Path = input_data
55
55
  self._extra_tools: list[str] | None = extra_tools
56
- self._write_output_file: bool = write_output
57
-
58
56
  self.loaded: LoadedAgentFile = self._registry.get(self._agent_id)
57
+ output_file = self.loaded.spec.output.file
58
+ self._write_output_file: bool = write_output and bool(output_file)
59
59
  safe_dir = self.loaded.spec.safe_dir
60
60
  if safe_dir is not None and Path(safe_dir).is_absolute():
61
61
  raise ValueError("safe_dir must be a relative path.")
@@ -66,22 +66,23 @@ class QuickAgent:
66
66
  input_adaptor = FileInput(self._input_data, self.permissions)
67
67
  self.run_input: RunInput = input_adaptor.load()
68
68
 
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)
69
+ self.tool_ids: list[str] = self._build_tool_ids()
70
+ self.toolset: FunctionToolset[Any] | None = self._build_toolset()
73
71
 
74
72
  self.model: OpenAIChatModel = build_model(self.loaded.spec.model)
75
73
  self.model_settings_json: ModelSettings | None = self._build_model_settings(self.loaded.spec.model)
76
74
  self.state: ChainState = self._init_state()
77
75
 
78
76
  async def run(self) -> BaseModel | str:
79
- self._tools.maybe_inject_agent_call(
80
- self.tool_ids,
81
- self.toolset,
82
- self.run_input.source_path,
83
- self._run_nested_agent,
84
- )
77
+ if self.has_tools():
78
+ if self.toolset is None:
79
+ raise ValueError("Toolset is missing while tools are enabled.")
80
+ self._tools.maybe_inject_agent_call(
81
+ self.tool_ids,
82
+ self.toolset,
83
+ self.run_input.source_path,
84
+ self._run_nested_agent,
85
+ )
85
86
 
86
87
  final_output = await self._run_chain()
87
88
 
@@ -162,34 +163,59 @@ class QuickAgent:
162
163
 
163
164
  raise NotImplementedError(f"Unknown step kind: {step.kind}")
164
165
 
165
- def _build_user_prompt(
166
- self,
167
- *,
168
- step: ChainStepSpec,
169
- ) -> str:
170
- if step.prompt_section not in self.loaded.step_prompts:
171
- raise KeyError(f"Missing step section {step.prompt_section!r} in agent.md body.")
166
+ def _build_user_prompt(self) -> str:
167
+ return make_user_prompt(self.run_input, self.state)
172
168
 
173
- step_prompt = self.loaded.step_prompts[step.prompt_section]
174
- return make_user_prompt(step_prompt, self.run_input, self.state)
169
+ def _build_step_instructions(self, step_prompt: str) -> str:
170
+ if not self.loaded.instructions:
171
+ return step_prompt
172
+ return f"{self.loaded.instructions}{step_prompt}"
173
+
174
+ def _build_single_shot_prompt(self) -> str:
175
+ return make_user_prompt(self.run_input, self.state)
176
+
177
+ def _normalize_agent_text(self, text: str) -> str | None:
178
+ if text:
179
+ return text
180
+ return None
181
+
182
+ def _normalize_system_prompt(self, text: str) -> str | list[str]:
183
+ if text:
184
+ return text
185
+ return []
175
186
 
176
187
  async def _run_text_step(
177
188
  self,
178
189
  *,
179
190
  step: ChainStepSpec,
180
191
  ) -> tuple[StepOutput, BaseModel | str]:
181
- user_prompt = self._build_user_prompt(
182
- step=step,
183
- )
192
+ user_prompt = self._build_user_prompt()
193
+ step_prompt = self.loaded.step_prompts[step.prompt_section]
194
+ step_instructions = self._build_step_instructions(step_prompt)
195
+ toolsets = self._toolsets_for_run()
184
196
  agent = Agent(
185
197
  self.model,
186
- instructions=self.loaded.body,
187
- toolsets=[self.toolset],
198
+ instructions=step_instructions,
199
+ system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
200
+ toolsets=toolsets,
188
201
  output_type=str,
189
202
  )
190
203
  result = await agent.run(user_prompt)
191
204
  return result.output, result.output
192
205
 
206
+ async def _run_single_shot(self) -> BaseModel | str:
207
+ user_prompt = self._build_single_shot_prompt()
208
+ toolsets = self._toolsets_for_run()
209
+ agent = Agent(
210
+ self.model,
211
+ instructions=self._normalize_agent_text(self.loaded.instructions),
212
+ system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
213
+ toolsets=toolsets,
214
+ output_type=str,
215
+ )
216
+ result = await agent.run(user_prompt)
217
+ return result.output
218
+
193
219
  async def _run_structured_step(
194
220
  self,
195
221
  *,
@@ -201,28 +227,37 @@ class QuickAgent:
201
227
 
202
228
  model_settings = self._build_structured_model_settings(schema_cls=schema_cls)
203
229
 
204
- user_prompt = self._build_user_prompt(
205
- step=step,
206
- )
230
+ user_prompt = self._build_user_prompt()
231
+ step_prompt = self.loaded.step_prompts[step.prompt_section]
232
+ step_instructions = self._build_step_instructions(step_prompt)
233
+ toolsets = self._toolsets_for_run()
207
234
  agent = Agent(
208
235
  self.model,
209
- instructions=self.loaded.body,
210
- toolsets=[self.toolset],
211
- output_type=str,
236
+ instructions=step_instructions,
237
+ system_prompt=self._normalize_system_prompt(self.loaded.system_prompt),
238
+ toolsets=toolsets,
239
+ output_type=schema_cls,
212
240
  model_settings=model_settings,
213
241
  )
214
242
  result = await agent.run(user_prompt)
215
243
  raw_output = result.output
216
- try:
217
- parsed = schema_cls.model_validate_json(raw_output)
218
- except ValidationError:
219
- extracted = extract_first_json_object(raw_output)
220
- parsed = schema_cls.model_validate_json(extracted)
244
+ if isinstance(raw_output, BaseModel):
245
+ parsed = raw_output
246
+ elif isinstance(raw_output, dict):
247
+ parsed = schema_cls.model_validate(raw_output)
248
+ else:
249
+ try:
250
+ parsed = schema_cls.model_validate_json(raw_output)
251
+ except ValidationError:
252
+ extracted = extract_first_json_object(raw_output)
253
+ parsed = schema_cls.model_validate_json(extracted)
221
254
  return parsed.model_dump(), parsed
222
255
 
223
256
  async def _run_chain(
224
257
  self,
225
258
  ) -> BaseModel | str:
259
+ if not self.loaded.spec.chain:
260
+ return await self._run_single_shot()
226
261
  final_output: BaseModel | str = ""
227
262
  for step in self.loaded.spec.chain:
228
263
  step_out, step_final = await self._run_step(
@@ -233,8 +268,34 @@ class QuickAgent:
233
268
  final_output = step_final
234
269
  return final_output
235
270
 
271
+ def has_tools(self) -> bool:
272
+ if not self.tool_ids:
273
+ return False
274
+ return True
275
+
276
+ def _build_tool_ids(self) -> list[str]:
277
+ if not self.loaded.spec.tools:
278
+ return []
279
+ return list(dict.fromkeys((self.loaded.spec.tools or []) + (self._extra_tools or [])))
280
+
281
+ def _build_toolset(self) -> FunctionToolset[Any] | None:
282
+ if not self.has_tools():
283
+ return None
284
+ return self._tools.build_toolset(self.tool_ids, self.permissions)
285
+
286
+ def _toolsets_for_run(self) -> list[FunctionToolset[Any]]:
287
+ if not self.has_tools():
288
+ return []
289
+ toolset = self.toolset
290
+ if toolset is None:
291
+ return []
292
+ return [toolset]
293
+
236
294
  def _write_final_output(self, final_output: BaseModel | str) -> Path:
237
- out_path = Path(self.loaded.spec.output.file)
295
+ output_file = self.loaded.spec.output.file
296
+ if not output_file:
297
+ raise ValueError("Output file is not configured.")
298
+ out_path = Path(output_file)
238
299
  if isinstance(final_output, BaseModel):
239
300
  if self.loaded.spec.output.format == "json":
240
301
  write_output(out_path, final_output.model_dump_json(indent=2), self.permissions)
@@ -24,7 +24,7 @@ output:
24
24
  file: "out/business_extract_structured.json"
25
25
  ---
26
26
 
27
- # Business Extract Structured
27
+ ## Instructions
28
28
 
29
29
  Extract structured details from the input description.
30
30
 
@@ -21,7 +21,7 @@ output:
21
21
  file: "out/business_extract.md"
22
22
  ---
23
23
 
24
- # Business Extract
24
+ ## Instructions
25
25
 
26
26
  Extract structured details from the input description.
27
27
 
@@ -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.2
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"
@@ -908,6 +910,7 @@ Agents are stored as Markdown files with YAML front matter and step sections:
908
910
  - Body contains `## step:<id>` sections referenced by the chain.
909
911
 
910
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.
911
914
 
912
915
  ## Nested Output
913
916
 
@@ -926,6 +929,7 @@ See the docs in `docs/`:
926
929
 
927
930
  - [docs/cli.md](docs/cli.md): Command line usage and options.
928
931
  - [docs/templates.md](docs/templates.md): Agent template format and examples.
932
+ - [docs/outputs.md](docs/outputs.md): Output configuration and behavior.
929
933
  - [docs/python.md](docs/python.md): Embedding the orchestrator in scripts.
930
934
  - [docs/python.md#inter-agent-calls](docs/python.md#inter-agent-calls): Example of one agent calling another.
931
935
  - [src/quick_agent/llms.txt](src/quick_agent/llms.txt): LLM-oriented project summary and examples.