quick-agent 0.1.1__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 (45) hide show
  1. quick_agent/__init__.py +6 -0
  2. quick_agent/agent_call_tool.py +44 -0
  3. quick_agent/agent_registry.py +75 -0
  4. quick_agent/agent_tools.py +41 -0
  5. quick_agent/cli.py +44 -0
  6. quick_agent/directory_permissions.py +49 -0
  7. quick_agent/io_utils.py +36 -0
  8. quick_agent/json_utils.py +37 -0
  9. quick_agent/models/__init__.py +23 -0
  10. quick_agent/models/agent_spec.py +22 -0
  11. quick_agent/models/chain_step_spec.py +14 -0
  12. quick_agent/models/handoff_spec.py +13 -0
  13. quick_agent/models/loaded_agent_file.py +14 -0
  14. quick_agent/models/model_spec.py +14 -0
  15. quick_agent/models/output_spec.py +10 -0
  16. quick_agent/models/run_input.py +14 -0
  17. quick_agent/models/tool_impl_spec.py +11 -0
  18. quick_agent/models/tool_json.py +14 -0
  19. quick_agent/orchestrator.py +35 -0
  20. quick_agent/prompting.py +28 -0
  21. quick_agent/quick_agent.py +313 -0
  22. quick_agent/schemas/outputs.py +55 -0
  23. quick_agent/tools/__init__.py +0 -0
  24. quick_agent/tools/filesystem/__init__.py +0 -0
  25. quick_agent/tools/filesystem/adapter.py +26 -0
  26. quick_agent/tools/filesystem/read_text.py +16 -0
  27. quick_agent/tools/filesystem/write_text.py +19 -0
  28. quick_agent/tools/filesystem.read_text/tool.json +10 -0
  29. quick_agent/tools/filesystem.write_text/tool.json +10 -0
  30. quick_agent/tools_loader.py +76 -0
  31. quick_agent-0.1.1.data/data/quick_agent/agents/function-spec-validator.md +109 -0
  32. quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validate-eval-list.md +122 -0
  33. quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validator-contains.md +115 -0
  34. quick_agent-0.1.1.data/data/quick_agent/agents/template.md +88 -0
  35. quick_agent-0.1.1.dist-info/METADATA +918 -0
  36. quick_agent-0.1.1.dist-info/RECORD +45 -0
  37. quick_agent-0.1.1.dist-info/WHEEL +5 -0
  38. quick_agent-0.1.1.dist-info/entry_points.txt +2 -0
  39. quick_agent-0.1.1.dist-info/licenses/LICENSE +674 -0
  40. quick_agent-0.1.1.dist-info/top_level.txt +2 -0
  41. tests/test_agent.py +196 -0
  42. tests/test_directory_permissions.py +89 -0
  43. tests/test_integration.py +221 -0
  44. tests/test_orchestrator.py +797 -0
  45. tests/test_tools.py +25 -0
@@ -0,0 +1,313 @@
1
+ """Agent execution logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Type
8
+
9
+ from pydantic import BaseModel, ValidationError
10
+ from pydantic_ai import Agent
11
+ from pydantic_ai.models.openai import OpenAIChatModel
12
+ from pydantic_ai.providers.openai import OpenAIProvider
13
+ from pydantic_ai.settings import ModelSettings
14
+ from pydantic_ai.toolsets import FunctionToolset
15
+
16
+ from quick_agent.agent_registry import AgentRegistry
17
+ from quick_agent.agent_tools import AgentTools
18
+ from quick_agent.directory_permissions import DirectoryPermissions
19
+ from quick_agent.io_utils import load_input, write_output
20
+ from quick_agent.json_utils import extract_first_json_object
21
+ from quick_agent.models.loaded_agent_file import LoadedAgentFile
22
+ from quick_agent.models.model_spec import ModelSpec
23
+ from quick_agent.prompting import make_user_prompt
24
+ from quick_agent.tools_loader import import_symbol
25
+
26
+
27
+ class QuickAgent:
28
+ def __init__(
29
+ self,
30
+ *,
31
+ registry: AgentRegistry,
32
+ tools: AgentTools,
33
+ directory_permissions: DirectoryPermissions,
34
+ agent_id: str,
35
+ input_path: Path,
36
+ extra_tools: list[str] | None,
37
+ ) -> 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)
46
+ safe_dir = self.loaded.spec.safe_dir
47
+ if safe_dir is not None and Path(safe_dir).is_absolute():
48
+ 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)
51
+
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)
54
+
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)
58
+
59
+ async def run(self) -> BaseModel | str:
60
+ self._tools.maybe_inject_agent_call(
61
+ self.tool_ids,
62
+ self.toolset,
63
+ self.run_input.source_path,
64
+ self._run_nested_agent,
65
+ )
66
+
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
+ )
75
+
76
+ out_path = self._write_final_output(self.loaded, final_output, self.permissions)
77
+
78
+ await self._handle_handoff(self.loaded, out_path)
79
+
80
+ return final_output
81
+
82
+ async def _run_nested_agent(self, agent_id: str, input_path: Path) -> BaseModel | str:
83
+ agent = QuickAgent(
84
+ registry=self._registry,
85
+ tools=self._tools,
86
+ directory_permissions=self._directory_permissions,
87
+ agent_id=agent_id,
88
+ input_path=input_path,
89
+ extra_tools=None,
90
+ )
91
+ return await agent.run()
92
+
93
+ def _init_state(self, agent_id: str) -> dict[str, Any]:
94
+ return {
95
+ "agent_id": agent_id,
96
+ "steps": {},
97
+ }
98
+
99
+ def _build_model_settings(self, model_spec: ModelSpec) -> ModelSettings | None:
100
+ if model_spec.provider == "openai-compatible":
101
+ # Ollama OpenAI-compatible API uses "format": "json" to force JSON output.
102
+ if model_spec.base_url != "https://api.openai.com/v1":
103
+ return {"extra_body": {"format": "json"}}
104
+ return None
105
+
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)
115
+ base_url = getattr(provider, "base_url", None)
116
+ if base_url == "https://api.openai.com/v1":
117
+ if model_settings_json is None:
118
+ model_settings_dict: ModelSettings = {}
119
+ else:
120
+ model_settings_dict = model_settings_json
121
+ extra_body_obj = model_settings_dict.get("extra_body")
122
+ extra_body: dict[str, Any] = {}
123
+ if isinstance(extra_body_obj, dict):
124
+ extra_body = dict(extra_body_obj)
125
+ if "response_format" not in extra_body:
126
+ extra_body["response_format"] = {
127
+ "type": "json_schema",
128
+ "json_schema": {
129
+ "name": schema_cls.__name__,
130
+ "schema": schema_cls.model_json_schema(),
131
+ "strict": True,
132
+ },
133
+ }
134
+ model_settings_dict["extra_body"] = extra_body
135
+ model_settings = model_settings_dict
136
+ return model_settings
137
+
138
+ async def _run_step(
139
+ self,
140
+ *,
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]:
149
+ if step.kind == "text":
150
+ return await self._run_text_step(
151
+ step=step,
152
+ loaded=loaded,
153
+ model=model,
154
+ toolset=toolset,
155
+ run_input=run_input,
156
+ state=state,
157
+ )
158
+
159
+ if step.kind == "structured":
160
+ return await self._run_structured_step(
161
+ 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
+ )
169
+
170
+ raise NotImplementedError(f"Unknown step kind: {step.kind}")
171
+
172
+ def _build_user_prompt(
173
+ self,
174
+ *,
175
+ step: Any,
176
+ loaded: LoadedAgentFile,
177
+ run_input: Any,
178
+ state: dict[str, Any],
179
+ ) -> str:
180
+ if step.prompt_section not in loaded.step_prompts:
181
+ raise KeyError(f"Missing step section {step.prompt_section!r} in agent.md body.")
182
+
183
+ step_prompt = loaded.step_prompts[step.prompt_section]
184
+ return make_user_prompt(step_prompt, run_input, state)
185
+
186
+ async def _run_text_step(
187
+ self,
188
+ *,
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]:
196
+ user_prompt = self._build_user_prompt(
197
+ step=step,
198
+ loaded=loaded,
199
+ run_input=run_input,
200
+ state=state,
201
+ )
202
+ agent = Agent(
203
+ model,
204
+ instructions=loaded.body,
205
+ toolsets=[toolset],
206
+ output_type=str,
207
+ )
208
+ result = await agent.run(user_prompt)
209
+ return result.output, result.output
210
+
211
+ async def _run_structured_step(
212
+ self,
213
+ *,
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]:
222
+ if not step.output_schema:
223
+ raise ValueError(f"Step {step.id} is structured but missing output_schema.")
224
+ schema_cls = resolve_schema(loaded, step.output_schema)
225
+
226
+ model_settings = self._build_structured_model_settings(
227
+ model=model,
228
+ model_settings_json=model_settings_json,
229
+ schema_cls=schema_cls,
230
+ )
231
+
232
+ user_prompt = self._build_user_prompt(
233
+ step=step,
234
+ loaded=loaded,
235
+ run_input=run_input,
236
+ state=state,
237
+ )
238
+ agent = Agent(
239
+ model,
240
+ instructions=loaded.body,
241
+ toolsets=[toolset],
242
+ output_type=str,
243
+ model_settings=model_settings,
244
+ )
245
+ result = await agent.run(user_prompt)
246
+ raw_output = result.output
247
+ try:
248
+ parsed = schema_cls.model_validate_json(raw_output)
249
+ except ValidationError:
250
+ extracted = extract_first_json_object(raw_output)
251
+ parsed = schema_cls.model_validate_json(extracted)
252
+ return parsed.model_dump(), parsed
253
+
254
+ async def _run_chain(
255
+ 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
+ ) -> BaseModel | str:
264
+ final_output: BaseModel | str = ""
265
+ for step in loaded.spec.chain:
266
+ step_out, step_final = await self._run_step(
267
+ 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
+ )
275
+ state["steps"][step.id] = step_out
276
+ final_output = step_final
277
+ return final_output
278
+
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)
286
+ 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)
289
+ else:
290
+ write_output(out_path, final_output.model_dump_json(indent=2), permissions)
291
+ else:
292
+ write_output(out_path, str(final_output), permissions)
293
+ return out_path
294
+
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)
299
+
300
+
301
+ def resolve_schema(loaded: LoadedAgentFile, schema_name: str) -> Type[BaseModel]:
302
+ if schema_name not in loaded.spec.schemas:
303
+ raise KeyError(f"Schema {schema_name!r} not registered in agent.md schemas.")
304
+ cls = import_symbol(loaded.spec.schemas[schema_name])
305
+ if not isinstance(cls, type) or not issubclass(cls, BaseModel):
306
+ raise TypeError(f"Schema {schema_name!r} must be a Pydantic BaseModel subclass.")
307
+ return cls
308
+
309
+
310
+ def build_model(model_spec: ModelSpec) -> OpenAIChatModel:
311
+ api_key = os.environ.get(model_spec.api_key_env, "noop")
312
+ provider = OpenAIProvider(base_url=model_spec.base_url, api_key=api_key)
313
+ return OpenAIChatModel(model_spec.model_name, provider=provider)
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field, AliasChoices
6
+
7
+
8
+ class Plan(BaseModel):
9
+ # LLMs sometimes emit structured steps; allow both strings and dicts.
10
+ steps: list[str | dict[str, Any]] = Field(default_factory=list)
11
+ tool_calls: list[str | dict[str, Any]] = Field(default_factory=list)
12
+
13
+
14
+ class ValidationIssue(BaseModel):
15
+ field: str
16
+ reason: str
17
+
18
+
19
+ class ValidationResult(BaseModel):
20
+ valid: bool
21
+ function_name: str | None = None
22
+ parameters_count: int | None = None
23
+ return_type: str | None = None
24
+ target_path: str | None = None
25
+ issues: list[ValidationIssue] = Field(default_factory=list)
26
+ summary: str | None = None
27
+
28
+
29
+ class EvalRowResult(BaseModel):
30
+ index: int = 0
31
+ agent: str
32
+ command_file: str
33
+ result: str = Field(default="", validation_alias=AliasChoices("result", "results")) # "PASS" or "FAIL"
34
+ note: str | None = None
35
+
36
+
37
+ class EvalListResult(BaseModel):
38
+ total: int
39
+ passed: int
40
+ failed: int
41
+ results_path: str
42
+ rows: list[EvalRowResult] = Field(default_factory=list)
43
+
44
+
45
+ class ContainsCheck(BaseModel):
46
+ text: str
47
+ found: bool
48
+
49
+
50
+ class ContainsValidationResult(BaseModel):
51
+ status: str # "PASS" or "FAIL"
52
+ checks: list[ContainsCheck] = Field(default_factory=list)
53
+ missing: list[str] = Field(default_factory=list)
54
+ eval_path: str | None = None
55
+ response_path: str | None = None
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ """Filesystem tool adapter with directory permissions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from quick_agent.directory_permissions import DirectoryPermissions
8
+
9
+
10
+ class FilesystemToolAdapter:
11
+ def __init__(self, permissions: DirectoryPermissions) -> None:
12
+ self._permissions = permissions
13
+
14
+ def read_text(self, path: str) -> str:
15
+ if not self._permissions.can_read(Path(path)):
16
+ raise PermissionError(f"Read access denied for {path}.")
17
+ safe_path = self._permissions.resolve(Path(path), for_write=False)
18
+ return safe_path.read_text(encoding="utf-8")
19
+
20
+ def write_text(self, path: str, content: str) -> str:
21
+ if not self._permissions.can_write(Path(path)):
22
+ raise PermissionError(f"Write access denied for {path}.")
23
+ safe_path = self._permissions.resolve(Path(path), for_write=True)
24
+ safe_path.parent.mkdir(parents=True, exist_ok=True)
25
+ safe_path.write_text(content, encoding="utf-8")
26
+ return str(safe_path)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from quick_agent.directory_permissions import DirectoryPermissions
6
+
7
+
8
+ def read_text(
9
+ path: str,
10
+ permissions: DirectoryPermissions,
11
+ ) -> str:
12
+ """Read UTF-8 text from a file path."""
13
+ if not permissions.can_read(Path(path)):
14
+ raise PermissionError(f"Read access denied for {path}.")
15
+ safe_path = permissions.resolve(Path(path), for_write=False)
16
+ return safe_path.read_text(encoding="utf-8")
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from quick_agent.directory_permissions import DirectoryPermissions
6
+
7
+
8
+ def write_text(
9
+ path: str,
10
+ content: str,
11
+ permissions: DirectoryPermissions,
12
+ ) -> str:
13
+ """Write UTF-8 text to a file path. Returns the written path."""
14
+ if not permissions.can_write(Path(path)):
15
+ raise PermissionError(f"Write access denied for {path}.")
16
+ out = permissions.resolve(Path(path), for_write=True)
17
+ out.parent.mkdir(parents=True, exist_ok=True)
18
+ out.write_text(content, encoding="utf-8")
19
+ return str(out)
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "filesystem.read_text",
3
+ "name": "filesystem.read_text",
4
+ "description": "Read a UTF-8 text file from disk.",
5
+ "impl": {
6
+ "kind": "python",
7
+ "module": "quick_agent.tools.filesystem.read_text",
8
+ "function": "read_text"
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "filesystem.write_text",
3
+ "name": "filesystem.write_text",
4
+ "description": "Write a UTF-8 text file to disk. Returns the written path.",
5
+ "impl": {
6
+ "kind": "python",
7
+ "module": "quick_agent.tools.filesystem.write_text",
8
+ "function": "write_text"
9
+ }
10
+ }
@@ -0,0 +1,76 @@
1
+ """Tool discovery and loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from pathlib import Path
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ from pydantic_ai.toolsets import FunctionToolset
11
+
12
+ from quick_agent.directory_permissions import DirectoryPermissions
13
+ from quick_agent.models.tool_json import ToolJson
14
+ from quick_agent.tools.filesystem.adapter import FilesystemToolAdapter
15
+
16
+
17
+ def import_symbol(path: str) -> Any:
18
+ """
19
+ Imports a symbol given "package.module:SymbolName".
20
+ """
21
+ if ":" not in path:
22
+ raise ValueError(f"Expected import path 'module:Symbol', got {path!r}")
23
+ mod, sym = path.split(":", 1)
24
+ module = importlib.import_module(mod)
25
+ return getattr(module, sym)
26
+
27
+
28
+ def _discover_tool_index(tool_roots: list[Path]) -> dict[str, Path]:
29
+ index: dict[str, Path] = {}
30
+ for root in tool_roots:
31
+ if not root.exists():
32
+ continue
33
+ for tool_json_path in root.rglob("tool.json"):
34
+ tool_obj = ToolJson.model_validate_json(tool_json_path.read_text(encoding="utf-8"))
35
+ if tool_obj.id in index:
36
+ continue
37
+ index[tool_obj.id] = tool_json_path
38
+ return index
39
+
40
+
41
+ def load_tools(
42
+ tool_roots: list[Path],
43
+ tool_ids: list[str],
44
+ permissions: DirectoryPermissions,
45
+ ) -> FunctionToolset[Any]:
46
+ """
47
+ Minimal approach: load local python functions and register them into a FunctionToolset.
48
+ """
49
+ toolset = FunctionToolset()
50
+
51
+ tool_index = _discover_tool_index(tool_roots)
52
+ fs_adapter = FilesystemToolAdapter(permissions)
53
+
54
+ for tool_id in tool_ids:
55
+ tool_json_path = tool_index.get(tool_id)
56
+ if tool_json_path is None:
57
+ raise FileNotFoundError(f"Missing tool.json for tool {tool_id} in roots: {tool_roots}")
58
+
59
+ tool_obj = ToolJson.model_validate_json(tool_json_path.read_text(encoding="utf-8"))
60
+ if tool_obj.impl.kind != "python":
61
+ raise NotImplementedError("Skeleton supports python tools only. Add MCP support next.")
62
+
63
+ func: Callable[..., Any]
64
+ if tool_id == "filesystem.read_text":
65
+ func = fs_adapter.read_text
66
+ elif tool_id == "filesystem.write_text":
67
+ func = fs_adapter.write_text
68
+ else:
69
+ func = import_symbol(f"{tool_obj.impl.module}:{tool_obj.impl.function}")
70
+
71
+ # Register function as a tool.
72
+ # The FunctionToolset will derive schema from type hints / docstring.
73
+ # You can enforce consistency with tool.json by adding checks here.
74
+ toolset.add_function(func=func, name=tool_obj.name, description=tool_obj.description)
75
+
76
+ return toolset
@@ -0,0 +1,109 @@
1
+ ---
2
+ # Agent identity
3
+ name: "function_spec_validator"
4
+ description: "Validates function specifications meet required standards before creation."
5
+
6
+ # Model configuration: OpenAI-compatible endpoint (e.g., SGLang server)
7
+ model:
8
+ provider: "openai-compatible"
9
+ base_url: "http://localhost:11434/v1"
10
+ api_key_env: "OLLAMA_API_KEY"
11
+ model_name: "gpt-oss:20b"
12
+ temperature: 0.2
13
+ max_tokens: 2048
14
+
15
+ # Tools available to this agent (tool IDs resolved by the orchestrator)
16
+ tools: []
17
+
18
+ # Schema registry: symbolic names -> import paths
19
+ schemas:
20
+ Plan: "simple_agent.schemas.outputs:Plan"
21
+ ValidationResult: "simple_agent.schemas.outputs:ValidationResult"
22
+
23
+ # Prompt-chaining steps (ordered). Each step references a markdown section below.
24
+ chain:
25
+ - id: "execute"
26
+ kind: "text"
27
+ prompt_section: "step:execute"
28
+
29
+ - id: "finalize"
30
+ kind: "structured"
31
+ output_schema: "ValidationResult"
32
+ prompt_section: "step:finalize"
33
+
34
+ # Output settings
35
+ output:
36
+ format: "json"
37
+ file: "out/function_spec_validation.json"
38
+
39
+ # Optional: handoff to another agent after producing final output
40
+ handoff:
41
+ enabled: false
42
+ agent_id: null
43
+ input_mode: "final_output_json"
44
+ ---
45
+
46
+ # function_spec_validator
47
+
48
+ You are a validation agent that checks a provided function specification.
49
+ Return a JSON valid result that includes a boolean `valid` field.
50
+ Follow the chain steps in order.
51
+ Do not create comments outside of the structured output.
52
+
53
+ ## Required Fields
54
+
55
+ 1. Function name - must be provided, snake_case format
56
+ 2. Parameters with types - each parameter must have a type annotation
57
+ 3. Return type - must be specified (use `None` if no return)
58
+ 4. Behavior description - must describe what the function does
59
+ 5. Target file path - must be a valid Python file path (.py extension)
60
+
61
+ ## Validation Rules
62
+
63
+ ### Function Name
64
+ - Must be non-empty
65
+ - Must use snake_case (lowercase with underscores)
66
+ - Must not start with a number
67
+ - Must not be a Python reserved keyword
68
+
69
+ ### Parameters with Types
70
+ - Each parameter must follow format: `name: Type`
71
+ - Type must be a valid Python type or imported type
72
+ - Optional parameters must specify default values
73
+ - If there are no parameters, the following is accepted: None, an empty list, or no content
74
+
75
+ ### Return Type
76
+ - Must be explicitly stated
77
+ - Use `None` for functions with no return value
78
+ - Complex types must be properly annotated (e.g., `List[str]`, `Dict[str, int]`)
79
+
80
+ ### Behavior Description
81
+ - Minimum 10 characters
82
+ - Must describe the function's purpose
83
+ - Should mention expected inputs and outputs
84
+
85
+ ### Target File Path
86
+ - Must end with `.py`
87
+ - Must be an relative path
88
+ - Project Root level files or within subdirectories are acceptable
89
+ - Project Root level files will not have a leading slash
90
+
91
+ ## step:execute
92
+
93
+ Goal:
94
+ - Perform the validation checks on all required fields.
95
+ - Summarize findings in plain text and list any missing or invalid fields.
96
+
97
+ Constraints:
98
+ - Do not output JSON in this step.
99
+
100
+ ## step:finalize
101
+
102
+ Goal:
103
+ - Return a `ValidationResult` JSON object.
104
+ - The object must include `valid` and should include any issues.
105
+ - If invalid, include the list of issues with `field` and `reason`.
106
+ - No comments on the json code is needed
107
+ - Remove any comments on the json output
108
+
109
+ Return only the structured object required by the schema (no additional commentary).