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
tests/test_integration.py CHANGED
@@ -1,5 +1,5 @@
1
- from pathlib import Path
2
1
  import os
2
+ from pathlib import Path
3
3
 
4
4
  import pytest
5
5
  from quick_agent.orchestrator import Orchestrator
@@ -154,6 +154,9 @@ Use the extracted JSON from the chain state as the ContactInfo object.
154
154
  assert output.summary
155
155
  assert "Avery" in output.summary
156
156
  assert "Acme" in output.summary
157
+ assert output_path.exists()
158
+ file_output = ContactSummary.model_validate_json(output_path.read_text(encoding="utf-8"))
159
+ assert file_output.model_dump() == output.model_dump()
157
160
 
158
161
 
159
162
  def test_orchestrator_allows_agent_call_tool(tmp_path: Path) -> None:
@@ -187,6 +190,7 @@ Reply with exactly: pong
187
190
  name: Parent Agent
188
191
  tools:
189
192
  - "agent.call"
193
+ nested_output: inline
190
194
  chain:
191
195
  - id: invoke
192
196
  kind: text
@@ -219,3 +223,132 @@ Then respond with only the returned text value.
219
223
 
220
224
  output = anyio.run(_run_agent, orchestrator, "parent", parent_input)
221
225
  assert output == "pong"
226
+ assert not child_output.exists()
227
+
228
+
229
+ def test_orchestrator_allows_agent_call_tool_with_inline_text(tmp_path: Path) -> None:
230
+ _require_env("OPENAI_API_KEY")
231
+ safe_root = tmp_path / "safe"
232
+ safe_root.mkdir(parents=True, exist_ok=True)
233
+
234
+ agents_dir = tmp_path / "agents"
235
+ agents_dir.mkdir(parents=True)
236
+
237
+ child_output = safe_root / "out" / "child.json"
238
+ child_md = f"""---
239
+ name: Child Agent
240
+ chain:
241
+ - id: respond
242
+ kind: text
243
+ prompt_section: step:respond
244
+ output:
245
+ format: json
246
+ file: {child_output}
247
+ ---
248
+
249
+ ## step:respond
250
+
251
+ Reply with exactly: pong
252
+ """
253
+ (agents_dir / "child.md").write_text(child_md, encoding="utf-8")
254
+
255
+ parent_output = safe_root / "out" / "parent.json"
256
+ parent_md = f"""---
257
+ name: Parent Agent
258
+ tools:
259
+ - "agent.call"
260
+ nested_output: inline
261
+ chain:
262
+ - id: invoke
263
+ kind: text
264
+ prompt_section: step:invoke
265
+ output:
266
+ format: json
267
+ file: {parent_output}
268
+ ---
269
+
270
+ ## step:invoke
271
+
272
+ Call agent_call with agent "child" and input_text "hello from memory".
273
+ Then respond with only the returned text value.
274
+ """
275
+ (agents_dir / "parent.md").write_text(parent_md, encoding="utf-8")
276
+
277
+ parent_input = safe_root / "parent_input.txt"
278
+ parent_input.write_text("call child", encoding="utf-8")
279
+
280
+ orchestrator = Orchestrator(
281
+ [agents_dir],
282
+ [tmp_path / "tools"],
283
+ safe_dir=safe_root,
284
+ )
285
+
286
+ import anyio
287
+
288
+ output = anyio.run(_run_agent, orchestrator, "parent", parent_input)
289
+ assert output == "pong"
290
+ assert not child_output.exists()
291
+
292
+
293
+ def test_orchestrator_allows_nested_output_file(tmp_path: Path) -> None:
294
+ _require_env("OPENAI_API_KEY")
295
+ safe_root = tmp_path / "safe"
296
+ safe_root.mkdir(parents=True, exist_ok=True)
297
+
298
+ agents_dir = tmp_path / "agents"
299
+ agents_dir.mkdir(parents=True)
300
+
301
+ child_output = safe_root / "out" / "child.json"
302
+ child_md = f"""---
303
+ name: Child Agent
304
+ chain:
305
+ - id: respond
306
+ kind: text
307
+ prompt_section: step:respond
308
+ output:
309
+ format: json
310
+ file: {child_output}
311
+ ---
312
+
313
+ ## step:respond
314
+
315
+ Reply with exactly: pong
316
+ """
317
+ (agents_dir / "child.md").write_text(child_md, encoding="utf-8")
318
+
319
+ parent_output = safe_root / "out" / "parent.json"
320
+ parent_md = f"""---
321
+ name: Parent Agent
322
+ tools:
323
+ - "agent.call"
324
+ nested_output: file
325
+ chain:
326
+ - id: invoke
327
+ kind: text
328
+ prompt_section: step:invoke
329
+ output:
330
+ format: json
331
+ file: {parent_output}
332
+ ---
333
+
334
+ ## step:invoke
335
+
336
+ Call agent_call with agent "child" and input_text "hello from memory".
337
+ Then respond with only the returned text value.
338
+ """
339
+ (agents_dir / "parent.md").write_text(parent_md, encoding="utf-8")
340
+
341
+ parent_input = safe_root / "parent_input.txt"
342
+ parent_input.write_text("call child", encoding="utf-8")
343
+
344
+ orchestrator = Orchestrator(
345
+ [agents_dir],
346
+ [tmp_path / "tools"],
347
+ safe_dir=safe_root,
348
+ )
349
+
350
+ import anyio
351
+
352
+ output = anyio.run(_run_agent, orchestrator, "parent", parent_input)
353
+ assert output == "pong"
354
+ assert child_output.exists()
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
  import types
3
3
  from pathlib import Path
4
- from typing import Any, cast
4
+ from typing import Any, Literal, cast
5
5
 
6
6
  import pytest
7
7
  from pydantic import BaseModel
@@ -11,6 +11,7 @@ from pydantic_ai.toolsets import FunctionToolset
11
11
 
12
12
  from quick_agent import quick_agent as qa_module
13
13
  from quick_agent import agent_tools as tools_module
14
+ from quick_agent import input_adaptors as input_adaptors_module
14
15
  from quick_agent.agent_call_tool import AgentCallTool
15
16
  from quick_agent.agent_registry import AgentRegistry
16
17
  from quick_agent.agent_tools import AgentTools
@@ -157,10 +158,10 @@ class RecordingQuickAgent(QuickAgent):
157
158
 
158
159
  class HandoffQuickAgent(QuickAgent):
159
160
  def __init__(self) -> None:
160
- self.calls: list[tuple[str, Path]] = []
161
+ self.calls: list[tuple[str, input_adaptors_module.InputAdaptor | Path]] = []
161
162
 
162
- async def _run_nested_agent(self, agent_id: str, input_path: Path) -> str:
163
- self.calls.append((agent_id, input_path))
163
+ async def _run_nested_agent(self, agent_id: str, input_data: input_adaptors_module.InputAdaptor | Path) -> str:
164
+ self.calls.append((agent_id, input_data))
164
165
  return "ok"
165
166
 
166
167
 
@@ -306,12 +307,41 @@ def test_maybe_inject_agent_call_tool_skips_when_missing() -> None:
306
307
  assert toolset.add_calls == []
307
308
 
308
309
 
310
+ @pytest.mark.anyio
311
+ async def test_agent_call_tool_accepts_input_text() -> None:
312
+ recorder = AsyncCallRecorder(return_value="ok")
313
+ tool = AgentCallTool(recorder, "run/input.json")
314
+
315
+ result = await tool(agent="child", input_text="hello")
316
+
317
+ assert result == {"text": "ok"}
318
+ assert len(recorder.calls) == 1
319
+ args = recorder.calls[0]["args"]
320
+ assert args[0] == "child"
321
+ assert isinstance(args[1], input_adaptors_module.TextInput)
322
+ run_input = args[1].load()
323
+ assert run_input.kind == "text"
324
+ assert run_input.text == "hello"
325
+
326
+
327
+ @pytest.mark.anyio
328
+ async def test_agent_call_tool_rejects_missing_or_duplicate_input() -> None:
329
+ recorder = AsyncCallRecorder(return_value="ok")
330
+ tool = AgentCallTool(recorder, "run/input.json")
331
+
332
+ with pytest.raises(ValueError):
333
+ await tool(agent="child")
334
+ with pytest.raises(ValueError):
335
+ await tool(agent="child", input_file="a.txt", input_text="hi")
336
+
337
+
309
338
  def test_init_state_contains_agent_id_and_steps() -> None:
310
339
  qa = object.__new__(QuickAgent)
311
340
 
312
- state = qa._init_state("agent-1")
341
+ qa._agent_id = "agent-1"
342
+ state = qa._init_state()
313
343
 
314
- assert state == {"agent_id": "agent-1", "steps": {}}
344
+ assert state == {"agent_id": "agent-1", "steps": {}, "final_output": None}
315
345
 
316
346
 
317
347
  def test_build_model_settings_openai_compatible() -> None:
@@ -348,14 +378,11 @@ def test_build_model_settings_other_provider() -> None:
348
378
  def test_build_structured_model_settings_non_openai_passthrough() -> None:
349
379
  qa = object.__new__(QuickAgent)
350
380
  schema = ExampleSchema
351
- model = cast(OpenAIChatModel, DummyOpenAIModel("http://localhost"))
352
381
  settings: ModelSettings = {"extra_body": {"format": "json"}}
382
+ qa.model = cast(OpenAIChatModel, DummyOpenAIModel("http://localhost"))
383
+ qa.model_settings_json = settings
353
384
 
354
- result = qa._build_structured_model_settings(
355
- model=model,
356
- model_settings_json=settings,
357
- schema_cls=schema,
358
- )
385
+ result = qa._build_structured_model_settings(schema_cls=schema)
359
386
 
360
387
  assert result == settings
361
388
 
@@ -363,13 +390,10 @@ def test_build_structured_model_settings_non_openai_passthrough() -> None:
363
390
  def test_build_structured_model_settings_openai_injects_schema() -> None:
364
391
  qa = object.__new__(QuickAgent)
365
392
  schema = ExampleSchema
366
- model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
393
+ qa.model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
394
+ qa.model_settings_json = None
367
395
 
368
- result = qa._build_structured_model_settings(
369
- model=model,
370
- model_settings_json=None,
371
- schema_cls=schema,
372
- )
396
+ result = qa._build_structured_model_settings(schema_cls=schema)
373
397
 
374
398
  assert result is not None
375
399
  extra_body_obj = result.get("extra_body")
@@ -390,8 +414,13 @@ def test_build_user_prompt_raises_for_missing_section() -> None:
390
414
  run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
391
415
 
392
416
  qa = object.__new__(QuickAgent)
417
+ qa.loaded = loaded
418
+ qa.run_input = run_input
419
+ qa.state = {"agent_id": "agent-1", "steps": {}, "final_output": None}
393
420
  with pytest.raises(KeyError):
394
- qa._build_user_prompt(step=step, loaded=loaded, run_input=run_input, state={"steps": {}})
421
+ qa._build_user_prompt(
422
+ step=step,
423
+ )
395
424
 
396
425
 
397
426
  def test_build_user_prompt_uses_prompting(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -402,10 +431,20 @@ def test_build_user_prompt_uses_prompting(monkeypatch: pytest.MonkeyPatch) -> No
402
431
  monkeypatch.setattr(qa_module, "make_user_prompt", recorder)
403
432
 
404
433
  qa = object.__new__(QuickAgent)
405
- result = qa._build_user_prompt(step=step, loaded=loaded, run_input=run_input, state={"steps": {}})
434
+ qa.loaded = loaded
435
+ qa.run_input = run_input
436
+ qa.state = {"agent_id": "agent-1", "steps": {}, "final_output": None}
437
+ result = qa._build_user_prompt(
438
+ step=step,
439
+ )
406
440
 
407
441
  assert result == "prompt"
408
- assert recorder.calls == [((loaded.step_prompts["step:one"], run_input, {"steps": {}}), {})]
442
+ assert recorder.calls == [
443
+ (
444
+ (loaded.step_prompts["step:one"], run_input, {"agent_id": "agent-1", "steps": {}, "final_output": None}),
445
+ {},
446
+ )
447
+ ]
409
448
 
410
449
 
411
450
  @pytest.mark.anyio
@@ -418,14 +457,14 @@ async def test_run_step_text_returns_output(monkeypatch: pytest.MonkeyPatch) ->
418
457
  run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
419
458
 
420
459
  qa = object.__new__(QuickAgent)
460
+ qa.loaded = loaded
461
+ qa.model = cast(OpenAIChatModel, object())
462
+ qa.model_settings_json = None
463
+ qa.toolset = RecordingToolset()
464
+ qa.run_input = run_input
465
+ qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
421
466
  output, final = await qa._run_step(
422
467
  step=step,
423
- loaded=loaded,
424
- model=cast(OpenAIChatModel, object()),
425
- model_settings_json=None,
426
- toolset=RecordingToolset(),
427
- run_input=run_input,
428
- state={"agent_id": "a", "steps": {}},
429
468
  )
430
469
 
431
470
  assert output == "hello"
@@ -456,14 +495,14 @@ async def test_run_step_structured_parses_json_with_fallback(monkeypatch: pytest
456
495
 
457
496
  try:
458
497
  qa = object.__new__(QuickAgent)
498
+ qa.loaded = loaded
499
+ qa.model = cast(OpenAIChatModel, object())
500
+ qa.model_settings_json = {"extra_body": {"format": "json"}}
501
+ qa.toolset = RecordingToolset()
502
+ qa.run_input = run_input
503
+ qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
459
504
  output, final = await qa._run_step(
460
505
  step=step,
461
- loaded=loaded,
462
- model=cast(OpenAIChatModel, object()),
463
- model_settings_json={"extra_body": {"format": "json"}},
464
- toolset=RecordingToolset(),
465
- run_input=run_input,
466
- state={"agent_id": "a", "steps": {}},
467
506
  )
468
507
  finally:
469
508
  sys.modules.pop("schemas.struct", None)
@@ -481,15 +520,15 @@ async def test_run_step_unknown_kind_raises(monkeypatch: pytest.MonkeyPatch) ->
481
520
  run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
482
521
 
483
522
  qa = object.__new__(QuickAgent)
523
+ qa.loaded = loaded
524
+ qa.model = cast(OpenAIChatModel, object())
525
+ qa.model_settings_json = None
526
+ qa.toolset = RecordingToolset()
527
+ qa.run_input = run_input
528
+ qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
484
529
  with pytest.raises(NotImplementedError):
485
530
  await qa._run_step(
486
531
  step=step,
487
- loaded=loaded,
488
- model=cast(OpenAIChatModel, object()),
489
- model_settings_json=None,
490
- toolset=RecordingToolset(),
491
- run_input=run_input,
492
- state={"agent_id": "a", "steps": {}},
493
532
  )
494
533
 
495
534
 
@@ -502,15 +541,14 @@ async def test_run_text_step_uses_build_user_prompt(monkeypatch: pytest.MonkeyPa
502
541
  run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
503
542
 
504
543
  qa = object.__new__(QuickAgent)
544
+ qa.loaded = loaded
545
+ qa.model = cast(OpenAIChatModel, object())
546
+ qa.toolset = RecordingToolset()
547
+ qa.run_input = run_input
505
548
  monkeypatch.setattr(qa, "_build_user_prompt", SyncCallRecorder(return_value="prompt"))
506
549
 
507
550
  output, final = await qa._run_text_step(
508
551
  step=step,
509
- loaded=loaded,
510
- model=cast(OpenAIChatModel, object()),
511
- toolset=RecordingToolset(),
512
- run_input=run_input,
513
- state={"agent_id": "a", "steps": {}},
514
552
  )
515
553
 
516
554
  assert output == "ok"
@@ -525,15 +563,14 @@ async def test_run_structured_step_missing_schema_raises() -> None:
525
563
  run_input = RunInput(source_path="in.json", kind="json", text="{}", data={})
526
564
 
527
565
  qa = object.__new__(QuickAgent)
566
+ qa.loaded = loaded
567
+ qa.model = cast(OpenAIChatModel, object())
568
+ qa.model_settings_json = None
569
+ qa.toolset = RecordingToolset()
570
+ qa.run_input = run_input
528
571
  with pytest.raises(ValueError):
529
572
  await qa._run_structured_step(
530
573
  step=step,
531
- loaded=loaded,
532
- model=cast(OpenAIChatModel, object()),
533
- model_settings_json=None,
534
- toolset=RecordingToolset(),
535
- run_input=run_input,
536
- state={"agent_id": "a", "steps": {}},
537
574
  )
538
575
 
539
576
 
@@ -558,14 +595,14 @@ async def test_run_structured_step_parses_json(monkeypatch: pytest.MonkeyPatch)
558
595
 
559
596
  try:
560
597
  qa = object.__new__(QuickAgent)
598
+ qa.loaded = loaded
599
+ qa.model = cast(OpenAIChatModel, object())
600
+ qa.model_settings_json = None
601
+ qa.toolset = RecordingToolset()
602
+ qa.run_input = run_input
603
+ qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
561
604
  output, final = await qa._run_structured_step(
562
605
  step=step,
563
- loaded=loaded,
564
- model=cast(OpenAIChatModel, object()),
565
- model_settings_json=None,
566
- toolset=RecordingToolset(),
567
- run_input=run_input,
568
- state={"agent_id": "a", "steps": {}},
569
606
  )
570
607
  finally:
571
608
  sys.modules.pop("schemas.struct2", None)
@@ -595,14 +632,14 @@ async def test_run_structured_step_adds_json_schema_for_openai(monkeypatch: pyte
595
632
 
596
633
  try:
597
634
  qa = object.__new__(QuickAgent)
635
+ qa.loaded = loaded
636
+ qa.model = cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1"))
637
+ qa.model_settings_json = None
638
+ qa.toolset = RecordingToolset()
639
+ qa.run_input = run_input
640
+ qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
598
641
  await qa._run_structured_step(
599
642
  step=step,
600
- loaded=loaded,
601
- model=cast(OpenAIChatModel, DummyOpenAIModel("https://api.openai.com/v1")),
602
- model_settings_json=None,
603
- toolset=RecordingToolset(),
604
- run_input=run_input,
605
- state={"agent_id": "a", "steps": {}},
606
643
  )
607
644
  finally:
608
645
  sys.modules.pop("schemas.struct3", None)
@@ -623,19 +660,18 @@ async def test_run_chain_updates_state_and_returns_last() -> None:
623
660
  loaded = _make_loaded_with_chain([step1, step2])
624
661
 
625
662
  qa = RecordingQuickAgent(outputs=[({"a": 1}, "first"), ("b", "second")])
626
- state = {"agent_id": "a", "steps": {}}
627
-
628
- final = await qa._run_chain(
629
- loaded=loaded,
630
- model=cast(OpenAIChatModel, object()),
631
- model_settings_json=None,
632
- toolset=RecordingToolset(),
633
- run_input=RunInput(source_path="in.txt", kind="text", text="hi", data=None),
634
- state=state,
635
- )
663
+ qa.loaded = loaded
664
+ qa.model = cast(OpenAIChatModel, object())
665
+ qa.model_settings_json = None
666
+ qa.toolset = RecordingToolset()
667
+ qa.run_input = RunInput(source_path="in.txt", kind="text", text="hi", data=None)
668
+ qa.state = {"agent_id": "a", "steps": {}, "final_output": None}
669
+
670
+ final = await qa._run_chain()
636
671
 
637
672
  assert final == "second"
638
- assert state["steps"] == {"s1": {"a": 1}, "s2": "b"}
673
+ assert qa.state["steps"] == {"s1": {"a": 1}, "s2": "b"}
674
+ assert qa.state["final_output"] == "b"
639
675
  assert qa.calls == ["s1", "s2"]
640
676
 
641
677
 
@@ -648,7 +684,9 @@ def test_write_final_output_serializes_model(tmp_path: Path) -> None:
648
684
 
649
685
  permissions = DirectoryPermissions(safe_root)
650
686
  qa = object.__new__(QuickAgent)
651
- result_path = qa._write_final_output(loaded, OutputSchema(msg="hi"), permissions)
687
+ qa.loaded = loaded
688
+ qa.permissions = permissions
689
+ result_path = qa._write_final_output(OutputSchema(msg="hi"))
652
690
 
653
691
  assert result_path == out_path
654
692
  assert "\"msg\": \"hi\"" in out_path.read_text(encoding="utf-8")
@@ -663,7 +701,9 @@ def test_write_final_output_writes_text(tmp_path: Path) -> None:
663
701
 
664
702
  permissions = DirectoryPermissions(safe_root)
665
703
  qa = object.__new__(QuickAgent)
666
- result_path = qa._write_final_output(loaded, "hello", permissions)
704
+ qa.loaded = loaded
705
+ qa.permissions = permissions
706
+ result_path = qa._write_final_output("hello")
667
707
 
668
708
  assert result_path == out_path
669
709
  assert out_path.read_text(encoding="utf-8") == "hello"
@@ -671,26 +711,49 @@ def test_write_final_output_writes_text(tmp_path: Path) -> None:
671
711
 
672
712
  @pytest.mark.anyio
673
713
  async def test_handle_handoff_runs_followup() -> None:
674
- out_path = Path("/tmp/out.json")
675
714
  handoff = HandoffSpec(enabled=True, agent_id="next")
676
715
  step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
677
716
  loaded = _make_loaded_with_chain([step], handoff=handoff)
678
717
 
679
718
  qa = HandoffQuickAgent()
680
- await qa._handle_handoff(loaded, out_path)
719
+ qa.loaded = loaded
720
+ await qa._handle_handoff("hello")
721
+
722
+ assert len(qa.calls) == 1
723
+ agent_id, input_data = qa.calls[0]
724
+ assert agent_id == "next"
725
+ assert isinstance(input_data, input_adaptors_module.TextInput)
726
+ run_input = input_data.load()
727
+ assert run_input.kind == "text"
728
+ assert run_input.text == "hello"
729
+
730
+
731
+ @pytest.mark.anyio
732
+ async def test_handle_handoff_serializes_structured_output() -> None:
733
+ handoff = HandoffSpec(enabled=True, agent_id="next")
734
+ step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
735
+ loaded = _make_loaded_with_chain([step], handoff=handoff)
736
+
737
+ qa = HandoffQuickAgent()
738
+ qa.loaded = loaded
739
+ await qa._handle_handoff(OutputSchema(msg="hi"))
681
740
 
682
- assert qa.calls == [("next", out_path)]
741
+ assert len(qa.calls) == 1
742
+ _, input_data = qa.calls[0]
743
+ assert isinstance(input_data, input_adaptors_module.TextInput)
744
+ run_input = input_data.load()
745
+ assert "\"msg\": \"hi\"" in run_input.text
683
746
 
684
747
 
685
748
  @pytest.mark.anyio
686
749
  async def test_handle_handoff_skips_when_disabled() -> None:
687
- out_path = Path("/tmp/out.json")
688
750
  handoff = HandoffSpec(enabled=False, agent_id="next")
689
751
  step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
690
752
  loaded = _make_loaded_with_chain([step], handoff=handoff)
691
753
 
692
754
  qa = HandoffQuickAgent()
693
- await qa._handle_handoff(loaded, out_path)
755
+ qa.loaded = loaded
756
+ await qa._handle_handoff("ignored")
694
757
 
695
758
  assert qa.calls == []
696
759
 
@@ -722,7 +785,7 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
722
785
  write_output_recorder = SyncCallRecorder(return_value=out_path)
723
786
  handoff_recorder = AsyncCallRecorder(return_value=None)
724
787
 
725
- monkeypatch.setattr(qa_module, "load_input", load_input_recorder)
788
+ monkeypatch.setattr(input_adaptors_module, "load_input", load_input_recorder)
726
789
  monkeypatch.setattr(qa_module, "build_model", build_model_recorder)
727
790
  monkeypatch.setattr(QuickAgent, "_build_model_settings", build_settings_recorder)
728
791
  monkeypatch.setattr(QuickAgent, "_run_chain", run_chain_recorder)
@@ -739,7 +802,7 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
739
802
  tools=tools,
740
803
  directory_permissions=_permissions(tmp_path),
741
804
  agent_id="agent-1",
742
- input_path=tmp_path / "input.json",
805
+ input_data=tmp_path / "input.json",
743
806
  extra_tools=["tool.b"],
744
807
  )
745
808
 
@@ -779,19 +842,45 @@ async def test_run_agent_wires_dependencies(monkeypatch: pytest.MonkeyPatch, tmp
779
842
  assert callable(maybe_args[3])
780
843
 
781
844
  assert run_chain_recorder.calls
782
- run_chain_kwargs = run_chain_recorder.calls[0]["kwargs"]
783
- assert run_chain_kwargs["loaded"] is loaded
784
- assert run_chain_kwargs["model"] is model
785
- assert run_chain_kwargs["model_settings_json"] is settings
786
- assert run_chain_kwargs["toolset"] is toolset
787
- assert run_chain_kwargs["run_input"] is run_input
788
- assert run_chain_kwargs["state"]["agent_id"] == "agent-1"
845
+ assert run_chain_recorder.calls[0]["kwargs"] == {}
789
846
 
790
847
  assert write_output_recorder.calls
791
848
  write_args, write_kwargs = write_output_recorder.calls[0]
792
849
  assert write_kwargs == {}
793
- assert write_args[0] is loaded
794
- assert write_args[1] == "final"
795
- assert isinstance(write_args[2], DirectoryPermissions)
796
- assert write_args[2].root == _permissions(tmp_path).root
797
- assert handoff_recorder.calls == [{"args": (loaded, out_path), "kwargs": {}}]
850
+ assert write_args[0] == "final"
851
+ assert handoff_recorder.calls == [{"args": ("final",), "kwargs": {}}]
852
+
853
+
854
+ @pytest.mark.anyio
855
+ @pytest.mark.parametrize(
856
+ ("nested_output", "expected_write_output"),
857
+ [
858
+ ("inline", False),
859
+ ("file", True),
860
+ ],
861
+ )
862
+ async def test_run_nested_agent_respects_nested_output(
863
+ monkeypatch: pytest.MonkeyPatch,
864
+ nested_output: Literal["inline", "file"],
865
+ expected_write_output: bool,
866
+ ) -> None:
867
+ qa = object.__new__(QuickAgent)
868
+ qa._registry = cast(AgentRegistry, object())
869
+ qa._tools = cast(AgentTools, object())
870
+ qa._directory_permissions = cast(DirectoryPermissions, object())
871
+
872
+ step = ChainStepSpec(id="s1", kind="text", prompt_section="step:one")
873
+ loaded = _make_loaded_with_chain([step])
874
+ loaded.spec.nested_output = nested_output
875
+ qa.loaded = loaded
876
+
877
+ init_recorder = SyncCallRecorder(return_value=None)
878
+ run_recorder = AsyncCallRecorder(return_value="ok")
879
+ monkeypatch.setattr(QuickAgent, "__init__", init_recorder)
880
+ monkeypatch.setattr(QuickAgent, "run", run_recorder)
881
+
882
+ await qa._run_nested_agent("child", Path("input.txt"))
883
+
884
+ assert len(init_recorder.calls) == 1
885
+ _, kwargs = init_recorder.calls[0]
886
+ assert kwargs["write_output"] is expected_write_output
@@ -1,45 +0,0 @@
1
- quick_agent/__init__.py,sha256=RdySRuttQey8SWqmhQD0HQ387LpGOikh-vEwvZnwSW0,170
2
- quick_agent/agent_call_tool.py,sha256=EdLsNTTfgeoiLGC5Pu9lJ-KNStzrwQ9cAS8KvsQ07og,1509
3
- quick_agent/agent_registry.py,sha256=SWO5cs634BuA_vxjm3_EEX_yM7wT1FyK6WRjsiNVu1w,2453
4
- quick_agent/agent_tools.py,sha256=N5oit0LiHeURU_Fge0TF9wlfaP57Y_ggX_IZxWcGTxM,1396
5
- quick_agent/cli.py,sha256=7e_UPrXfU9JG-khC9tC9DzDkXUJ4Fm9weCEqiIEYmY0,1395
6
- quick_agent/directory_permissions.py,sha256=3dp0AoNvmkyuR6BSxiePki__2eVpXLgHGfP7stT7lo0,1729
7
- quick_agent/io_utils.py,sha256=BUmUoZepL4lSYe0JbKxLm4mFFQ6Zt6MZQzprzgoBweU,1200
8
- quick_agent/json_utils.py,sha256=G6uP9SrdEw5WtA9--dBqdJcTYD2JgdgotPJdleErB_c,1003
9
- quick_agent/orchestrator.py,sha256=liN5SElPJ8m879tsxL2tLJKAb7wCtByXxXQ9OC6-was,1081
10
- quick_agent/prompting.py,sha256=MQxrIJTMj0s_aNOJoqLPDv-_hcTlZOIzFdvXuFTrS3A,633
11
- quick_agent/quick_agent.py,sha256=H2ypcWFniEHcUk2Kql7fPDC2ePavCpF78uFS7AVJwnE,11188
12
- quick_agent/tools_loader.py,sha256=oveR-EGAZo3Q7NPJ2aWKU3d8xd8kIpibOrjInpUZwQg,2642
13
- quick_agent/models/__init__.py,sha256=YseFAVWLarh0wZCVUL0fmkjVgA-FnYJLEiVinG1-6Tk,737
14
- quick_agent/models/agent_spec.py,sha256=K02ZbqMRbkdbk2f75ETfrGI55OUFiVQH562qsg1r5AQ,798
15
- quick_agent/models/chain_step_spec.py,sha256=NcdYFmgJhA5ODIwCTgTb2fmmJJesAZCVAIy3cGC5dhs,342
16
- quick_agent/models/handoff_spec.py,sha256=46Pt4JGU8-6f-cdbDR0oqWuJbg5ENJcXrFN1IK3chvs,280
17
- quick_agent/models/loaded_agent_file.py,sha256=rWRxrm0KjgmNIiYfooMM95eohMB1zCZC-eqrp1mf8w0,313
18
- quick_agent/models/model_spec.py,sha256=spnIZ8BNtXT5t8YRL1nh7cXrgWwNSptt_BMnAB8w1Lg,425
19
- quick_agent/models/output_spec.py,sha256=bvKFU1ONeza95qethNIiVgrsnT7l1TKMsdHjfXVVJnc,229
20
- quick_agent/models/run_input.py,sha256=WpV-QPlOPXMmZR6eIvP7vJOuCeBt3xWe4j4ngEbmSxg,282
21
- quick_agent/models/tool_impl_spec.py,sha256=7B161m8nFZCNjslb4VZdSi-mdAZM0jhp4kV4uaq8X1s,216
22
- quick_agent/models/tool_json.py,sha256=eYFBPCqxmRfyo7GYHBXCfog7kUzpjginfjt9grQy4ic,274
23
- quick_agent/schemas/outputs.py,sha256=4yQigYus0cqu2_AWSPi566i2AT18jtKURkaqEQc7U6E,1434
24
- quick_agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- quick_agent/tools/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- quick_agent/tools/filesystem/adapter.py,sha256=aYcX1qjI9wRSANSNSB3KWbKi7uDIYo3EpJotpUxnQUM,1027
27
- quick_agent/tools/filesystem/read_text.py,sha256=owpZxwCFoYbljABhwpNKU4g7-8xDPJDu0kZYF7bPLfs,478
28
- quick_agent/tools/filesystem/write_text.py,sha256=lwsMCk_p8xurVxychM2UBieXDp0SgN3C6rufLBUzDxo,584
29
- quick_agent/tools/filesystem.read_text/tool.json,sha256=UtkOxcpi6qYd611tJx-zFXVUjOwx2ZbmxEWsF4cAVpo,246
30
- quick_agent/tools/filesystem.write_text/tool.json,sha256=EcGmB_M_KDTqOrnxk_NVhKRoW_KADFFksgJ7fmw472s,275
31
- quick_agent-0.1.1.data/data/quick_agent/agents/function-spec-validator.md,sha256=Dlc4j1vFpDHj7w2vcWrvC83EEg_sWwwt1GOaRgcsm6Y,3289
32
- quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validate-eval-list.md,sha256=K83L9BKvGcttPRINwD6VSPopEHnB_aw4tpfSI6KM2u8,4119
33
- quick_agent-0.1.1.data/data/quick_agent/agents/subagent-validator-contains.md,sha256=3VxtKpyogwcJ4Er5tQFyYNe2nJBtANErb5D-_snBATw,3115
34
- quick_agent-0.1.1.data/data/quick_agent/agents/template.md,sha256=6IBuVZjK5OPIchI8ruvxao8CdLy0ZOi0meh6d1exI0E,2458
35
- quick_agent-0.1.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
36
- tests/test_agent.py,sha256=tcPpH9WmGPQjMo_jkFJlF8kgOZ3QktIvUHTAJaH1hWA,5533
37
- tests/test_directory_permissions.py,sha256=RvehzVQeOYC2y4-aEHzdQVMz0yJfqhyUnfgAUNGu9RY,2489
38
- tests/test_integration.py,sha256=MT_ANbQZ54NbcOtwXn6ZiYLiAa9byT_RDZXmHxV7fW4,5872
39
- tests/test_orchestrator.py,sha256=CGbo6Q6Y4iSXzt2F9V1PYx34WzV8eI6aL2Olmr2zDOw,28118
40
- tests/test_tools.py,sha256=Yc6AKCz79XJwHqiRV8dUBE6GVNdspUU0hIeGCXx-Rvc,936
41
- quick_agent-0.1.1.dist-info/METADATA,sha256=Khq2FvREwc_kLjOQrKRshoUsZ545Wdr07E4t7hTuBxY,46615
42
- quick_agent-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
43
- quick_agent-0.1.1.dist-info/entry_points.txt,sha256=bij-xFaQMSrMj7zearzi-bMfcyweum_GvURFMVZGde8,53
44
- quick_agent-0.1.1.dist-info/top_level.txt,sha256=KT1ID0FVC0OLzQnoBKRIHbXLRNugiU2gcgj_f4DtAsw,18
45
- quick_agent-0.1.1.dist-info/RECORD,,