pyagentic-core 2.8.1.dev1__tar.gz → 2.9.1.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {pyagentic_core-2.8.1.dev1/pyagentic_core.egg-info → pyagentic_core-2.9.1.dev1}/PKG-INFO +1 -1
  2. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/_app.py +158 -48
  3. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1/pyagentic_core.egg-info}/PKG-INFO +1 -1
  4. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_app.py +21 -0
  5. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_router.py +63 -0
  6. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  7. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  8. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/.github/workflows/docs.yml +0 -0
  9. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/.github/workflows/release.yml +0 -0
  10. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/.github/workflows/testing.yml +0 -0
  11. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/.gitignore +0 -0
  12. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/CHANGELOG.md +0 -0
  13. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/LICENSE +0 -0
  14. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/README.md +0 -0
  15. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/agent-linking.md +0 -0
  16. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/api/creating-an-app.md +0 -0
  17. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/api/deploying.md +0 -0
  18. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/api/index.md +0 -0
  19. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/api/jobs.md +0 -0
  20. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/api/running.md +0 -0
  21. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/declaration.svg +0 -0
  22. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/instantiation.svg +0 -0
  23. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/runtime.svg +0 -0
  24. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/source/README.md +0 -0
  25. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/source/declaration.d2 +0 -0
  26. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/source/instantiation.d2 +0 -0
  27. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/diagrams/source/runtime.d2 +0 -0
  28. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/execution-modes.md +0 -0
  29. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/getting-started.md +0 -0
  30. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/images/langfuse.png +0 -0
  31. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/index.md +0 -0
  32. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/inheritance.md +0 -0
  33. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/observability.md +0 -0
  34. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/phases.md +0 -0
  35. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/policies.md +0 -0
  36. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/reference/architecture.md +0 -0
  37. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/reference/modules.md +0 -0
  38. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/responses.md +0 -0
  39. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/states.md +0 -0
  40. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/structured-output.md +0 -0
  41. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/docs/tools.md +0 -0
  42. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/mkdocs.yml +0 -0
  43. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/__init__.py +0 -0
  44. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/__init__.py +0 -0
  45. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_agent/__init__.py +0 -0
  46. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_agent/_agent.py +0 -0
  47. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  48. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_agent/_agent_state.py +0 -0
  49. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_exceptions.py +0 -0
  50. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_info.py +0 -0
  51. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_mcp.py +0 -0
  52. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_metaclasses.py +0 -0
  53. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_ref.py +0 -0
  54. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_spec.py +0 -0
  55. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_state.py +0 -0
  56. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_tool.py +0 -0
  57. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_base/_validation.py +0 -0
  58. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_utils/_typing.py +0 -0
  59. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_utils/_warnings.py +0 -0
  60. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/_version_scheme.py +0 -0
  61. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/__init__.py +0 -0
  62. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/_config.py +0 -0
  63. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/_docker.py +0 -0
  64. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/_mcp_server.py +0 -0
  65. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/_sessions.py +0 -0
  66. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/__init__.py +0 -0
  67. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/_models.py +0 -0
  68. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/_orchestrator.py +0 -0
  69. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/_routes.py +0 -0
  70. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/backends/__init__.py +0 -0
  71. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/backends/_base.py +0 -0
  72. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/backends/_in_process.py +0 -0
  73. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/store/__init__.py +0 -0
  74. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/store/_base.py +0 -0
  75. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/api/jobs/store/_sqlite.py +0 -0
  76. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/__init__.py +0 -0
  77. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/_anthropic.py +0 -0
  78. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/_gemini.py +0 -0
  79. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/_mock.py +0 -0
  80. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/_openai.py +0 -0
  81. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/_openaiv1.py +0 -0
  82. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/llm/_provider.py +0 -0
  83. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/logging.py +0 -0
  84. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/models/llm.py +0 -0
  85. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/models/response.py +0 -0
  86. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/models/tracing.py +0 -0
  87. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/policies/__init__.py +0 -0
  88. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/policies/_events.py +0 -0
  89. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/policies/_policy.py +0 -0
  90. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/tracing/__init__.py +0 -0
  91. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/tracing/_basic.py +0 -0
  92. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/tracing/_langfuse.py +0 -0
  93. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/tracing/_tracer.py +0 -0
  94. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic/updates.py +0 -0
  95. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic_core.egg-info/SOURCES.txt +0 -0
  96. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  97. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic_core.egg-info/requires.txt +0 -0
  98. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyagentic_core.egg-info/top_level.txt +0 -0
  99. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/pyproject.toml +0 -0
  100. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/setup.cfg +0 -0
  101. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/setup.py +0 -0
  102. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/__init__.py +0 -0
  103. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/__init__.py +0 -0
  104. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_agent.py +0 -0
  105. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_agent_inheritance.py +0 -0
  106. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_agent_linking.py +0 -0
  107. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_agent_provider.py +0 -0
  108. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_params.py +0 -0
  109. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_phases.py +0 -0
  110. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_state.py +0 -0
  111. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_tool.py +0 -0
  112. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/_base/test_validator.py +0 -0
  113. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/__init__.py +0 -0
  114. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/jobs/__init__.py +0 -0
  115. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/jobs/test_orchestrator.py +0 -0
  116. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/jobs/test_routes.py +0 -0
  117. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/jobs/test_store_sqlite.py +0 -0
  118. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_config.py +0 -0
  119. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_docker.py +0 -0
  120. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_mcp_server.py +0 -0
  121. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_metaclass_models.py +0 -0
  122. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/api/test_sessions.py +0 -0
  123. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/conftest.py +0 -0
  124. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/models/test_responses.py +0 -0
  125. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/tracing/__init__.py +0 -0
  126. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/tracing/test_basic_tracer.py +0 -0
  127. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/tracing/test_models.py +0 -0
  128. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/tests/tracing/test_tracer.py +0 -0
  129. {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.1.dev1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.8.1.dev1
3
+ Version: 2.9.1.dev1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -29,7 +29,7 @@ from pydantic import BaseModel
29
29
  from pyagentic._base._agent._agent import BaseAgent
30
30
  from pyagentic.models.llm import LLMResponse
31
31
  from pyagentic.models.response import AgentResponse, ToolResponse
32
- from pyagentic.api._config import AgentsConfig, load_config
32
+ from pyagentic.api._config import AgentsConfig, JobsConfig, load_config
33
33
  from pyagentic.api._sessions import SessionManager
34
34
 
35
35
  # Agents may be passed as a single class, a list of classes, or a
@@ -79,16 +79,21 @@ def _validate_agent_class(cls: type) -> None:
79
79
  )
80
80
 
81
81
 
82
+ def _slugify(value: str) -> str:
83
+ """Convert a CamelCase or arbitrary name into a kebab-case URL/route slug."""
84
+ # Insert a hyphen at lower/digit -> upper boundaries (CamelCase -> kebab).
85
+ slug = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", value).lower()
86
+ # Collapse any remaining non-alphanumeric runs into single hyphens.
87
+ slug = re.sub(r"[^a-z0-9]+", "-", slug).strip("-")
88
+ return slug or "agent"
89
+
90
+
82
91
  def _default_prefix(agent_class: type[BaseAgent]) -> str:
83
92
  """Derive a URL prefix from an agent class name (e.g. ResearchAgent -> /research)."""
84
93
  name = agent_class.__name__
85
94
  if name.endswith("Agent") and len(name) > len("Agent"):
86
95
  name = name[: -len("Agent")]
87
- # Insert a hyphen at lower/digit -> upper boundaries (CamelCase -> kebab).
88
- name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", name).lower()
89
- # Collapse any remaining non-alphanumeric runs into single hyphens.
90
- name = re.sub(r"[^a-z0-9]+", "-", name).strip("-")
91
- return "/" + name
96
+ return "/" + _slugify(name)
92
97
 
93
98
 
94
99
  def _store_path_for(base: str, prefix: str, multi: bool) -> str:
@@ -135,33 +140,60 @@ def create_router(
135
140
  model: Optional[str] = None,
136
141
  name: Optional[str] = None,
137
142
  version: str = "0.1.0",
143
+ tags: Optional[list[str]] = None,
144
+ sessions: bool = True,
145
+ jobs: bool = False,
146
+ jobs_config: Optional["JobsConfig"] = None,
138
147
  ) -> APIRouter:
139
148
  """Build a FastAPI ``APIRouter`` exposing a single agent's endpoints.
140
149
 
141
- Mount it onto an existing FastAPI app::
150
+ Mount it onto an existing FastAPI app under whatever prefix you like::
142
151
 
143
- app.include_router(create_router(MyAgent), prefix="/agent")
152
+ app.include_router(create_router(MyAgent, name="my-agent"), prefix="/agents/my-agent")
144
153
 
145
154
  The router owns its own :class:`SessionManager`, exposed as
146
155
  ``router.sessions`` so callers can share it (e.g. with :func:`mount_mcp`).
156
+ When ``jobs`` is enabled, the job orchestrator is exposed as
157
+ ``router.orchestrator`` (otherwise ``None``) so callers can wire its
158
+ lifecycle into their app's lifespan.
159
+
160
+ Every route is given a unique ``name`` derived from ``name`` (e.g.
161
+ ``my-agent_chat``), so multiple agents can be mounted on one app without
162
+ colliding and individual routes remain addressable via ``url_path_for``.
147
163
 
148
164
  Routes registered (relative to the mount point):
149
165
  - ``GET /`` agent info, ``GET /health``, ``GET /schema``
150
- - ``POST/GET /sessions``, ``DELETE /sessions/{id}``
151
- - ``POST /sessions/{id}/chat``, ``POST /sessions/{id}/chat/stream``
152
- - ``GET /sessions/{id}/state``
166
+ - when ``sessions``: ``POST/GET /sessions``, ``DELETE /sessions/{id}``,
167
+ ``POST /sessions/{id}/chat``, ``POST /sessions/{id}/chat/stream``,
168
+ ``GET /sessions/{id}/state``
169
+ - when ``jobs``: ``POST/GET /jobs``, ``GET /jobs/{id}``,
170
+ ``GET /jobs/{id}/updates``, ``GET /jobs/{id}/stream``,
171
+ ``POST /jobs/{id}/cancel``
153
172
 
154
173
  Args:
155
174
  agent_class (type[BaseAgent]): The agent class to serve.
156
175
  model (Optional[str]): Default model for sessions. ``None`` falls back to
157
176
  the agent's own default.
158
- name (Optional[str]): Display name for the info route. Defaults to the
159
- class name.
177
+ name (Optional[str]): Display name for the info route and basis for
178
+ route names/tags. Defaults to the class name.
160
179
  version (str): Version string for the info route.
180
+ tags (Optional[list[str]]): OpenAPI tags applied to every route on the
181
+ router (groups them in the docs). ``None`` leaves routes untagged.
182
+ sessions (bool): If True, mount the session-based routes (create/list/
183
+ delete sessions plus synchronous chat, streaming chat, and state).
184
+ If False, those routes are omitted — interaction happens only via
185
+ ``jobs``.
186
+ jobs (bool): If True, mount a durable async job system at ``/jobs`` whose
187
+ orchestrator is exposed as ``router.orchestrator``. The caller is
188
+ responsible for starting/stopping that orchestrator (see
189
+ :func:`create_app`, which does this automatically).
190
+ jobs_config (Optional[JobsConfig]): Configuration for the job system when
191
+ ``jobs`` is True. Defaults to :class:`JobsConfig` defaults.
161
192
 
162
193
  Returns:
163
- APIRouter: A router with all agent routes registered, with its
164
- ``SessionManager`` attached as ``router.sessions``.
194
+ APIRouter: A router with the selected agent routes registered, with its
195
+ ``SessionManager`` attached as ``router.sessions`` and its job
196
+ orchestrator (or ``None``) as ``router.orchestrator``.
165
197
 
166
198
  Raises:
167
199
  TypeError: If agent_class is missing required metaclass attributes.
@@ -169,17 +201,18 @@ def create_router(
169
201
  _validate_agent_class(agent_class)
170
202
 
171
203
  name = name or agent_class.__name__
204
+ slug = _slugify(name)
172
205
  RequestModel = agent_class.__request_model__
173
206
  ResponseModel = agent_class.__response_model__
174
207
  StreamEventModel = agent_class.__stream_event_model__
175
208
  StateModel = agent_class.__state_class__
176
209
 
177
- router = APIRouter()
178
- sessions = SessionManager(agent_class, default_model=model)
210
+ router = APIRouter(tags=tags)
211
+ session_manager = SessionManager(agent_class, default_model=model)
179
212
 
180
213
  # ---- info routes ----
181
214
 
182
- @router.get("/")
215
+ @router.get("/", name=f"{slug}_info")
183
216
  async def agent_info() -> dict:
184
217
  """Return basic agent metadata."""
185
218
  return {
@@ -191,12 +224,12 @@ def create_router(
191
224
  "linked_agents": list(agent_class.__linked_agents__.keys()),
192
225
  }
193
226
 
194
- @router.get("/health")
227
+ @router.get("/health", name=f"{slug}_health")
195
228
  async def health() -> dict:
196
229
  """Liveness probe endpoint."""
197
230
  return {"status": "ok"}
198
231
 
199
- @router.get("/schema")
232
+ @router.get("/schema", name=f"{slug}_schema")
200
233
  async def schema() -> dict:
201
234
  """Return JSON schemas for request, response, stream event, and state models."""
202
235
  return {
@@ -206,9 +239,75 @@ def create_router(
206
239
  "state": StateModel.model_json_schema(),
207
240
  }
208
241
 
242
+ if sessions:
243
+ _register_session_routes(
244
+ router,
245
+ slug,
246
+ session_manager,
247
+ RequestModel,
248
+ ResponseModel,
249
+ StreamEventModel,
250
+ StateModel,
251
+ )
252
+
253
+ # Expose the session manager so callers can share it (e.g. with mount_mcp).
254
+ router.sessions = session_manager
255
+
256
+ orchestrator = None
257
+ if jobs:
258
+ orchestrator = _mount_jobs_router(
259
+ router,
260
+ agent_class,
261
+ session_manager,
262
+ jobs_config or JobsConfig(),
263
+ model,
264
+ tags,
265
+ )
266
+ router.orchestrator = orchestrator
267
+
268
+ return router
269
+
270
+
271
+ def _mount_jobs_router(
272
+ router: APIRouter,
273
+ agent_class: type[BaseAgent],
274
+ sessions: SessionManager,
275
+ jobs_config: "JobsConfig",
276
+ default_model: Optional[str],
277
+ tags: Optional[list[str]],
278
+ ) -> "JobOrchestrator":
279
+ """Build a job orchestrator and include its /jobs router; return the orchestrator."""
280
+ from pyagentic.api.jobs import JobOrchestrator, build_backend, build_store
281
+ from pyagentic.api.jobs._routes import build_jobs_router
282
+
283
+ store = build_store(jobs_config.store)
284
+ backend = build_backend(
285
+ agent_class,
286
+ sessions,
287
+ max_concurrency=jobs_config.max_concurrency,
288
+ default_model=default_model,
289
+ )
290
+ orchestrator = JobOrchestrator(store, backend, jobs_config, sessions)
291
+ router.include_router(
292
+ build_jobs_router(orchestrator, agent_class, sessions), tags=tags
293
+ )
294
+ return orchestrator
295
+
296
+
297
+ def _register_session_routes(
298
+ router: APIRouter,
299
+ slug: str,
300
+ sessions: SessionManager,
301
+ RequestModel: type,
302
+ ResponseModel: type,
303
+ StreamEventModel: type,
304
+ StateModel: type,
305
+ ) -> None:
306
+ """Register the session-based routes (sessions CRUD, chat, stream, state)."""
307
+
209
308
  # ---- session routes ----
210
309
 
211
- @router.post("/sessions", status_code=201)
310
+ @router.post("/sessions", status_code=201, name=f"{slug}_create_session")
212
311
  async def create_session(req: Optional[CreateSessionRequest] = None) -> dict:
213
312
  """Create a new agent session."""
214
313
  model = req.model if req else None
@@ -216,12 +315,12 @@ def create_router(
216
315
  session_id = sessions.create(model=model, api_key=api_key)
217
316
  return {"session_id": session_id}
218
317
 
219
- @router.get("/sessions")
318
+ @router.get("/sessions", name=f"{slug}_list_sessions")
220
319
  async def list_sessions() -> dict:
221
320
  """List all active session IDs."""
222
321
  return {"sessions": sessions.list_sessions()}
223
322
 
224
- @router.delete("/sessions/{session_id}")
323
+ @router.delete("/sessions/{session_id}", name=f"{slug}_delete_session")
225
324
  async def delete_session(session_id: str) -> dict:
226
325
  """Delete an existing session."""
227
326
  try:
@@ -232,7 +331,11 @@ def create_router(
232
331
 
233
332
  # ---- chat routes ----
234
333
 
235
- @router.post("/sessions/{session_id}/chat", response_model=ResponseModel)
334
+ @router.post(
335
+ "/sessions/{session_id}/chat",
336
+ response_model=ResponseModel,
337
+ name=f"{slug}_chat",
338
+ )
236
339
  async def chat(session_id: str, req: RequestModel): # type: ignore[valid-type]
237
340
  """Send a message and receive a complete agent response."""
238
341
  try:
@@ -247,6 +350,7 @@ def create_router(
247
350
  @router.post(
248
351
  "/sessions/{session_id}/chat/stream",
249
352
  response_model=StreamEventModel,
353
+ name=f"{slug}_chat_stream",
250
354
  responses={
251
355
  200: {
252
356
  "description": (
@@ -307,7 +411,11 @@ def create_router(
307
411
 
308
412
  # ---- state route ----
309
413
 
310
- @router.get("/sessions/{session_id}/state", response_model=StateModel)
414
+ @router.get(
415
+ "/sessions/{session_id}/state",
416
+ response_model=StateModel,
417
+ name=f"{slug}_get_state",
418
+ )
311
419
  async def get_state(session_id: str):
312
420
  """Return the current state of the agent for a session."""
313
421
  try:
@@ -316,11 +424,6 @@ def create_router(
316
424
  raise HTTPException(status_code=404, detail="Session not found")
317
425
  return agent.state
318
426
 
319
- # Expose the session manager so callers can share it (e.g. with mount_mcp).
320
- router.sessions = sessions
321
-
322
- return router
323
-
324
427
 
325
428
  def _wire_jobs_lifespan(app: FastAPI, orchestrators: list) -> None:
326
429
  """Start/stop the per-agent job orchestrators inside the app's lifespan.
@@ -360,6 +463,7 @@ def create_app(
360
463
  description: Optional[str] = None,
361
464
  model: Optional[str] = None,
362
465
  mcp: bool = False,
466
+ sessions: bool = True,
363
467
  jobs: bool = False,
364
468
  ) -> FastAPI:
365
469
  """Build a standalone FastAPI app serving one or several agents.
@@ -388,6 +492,10 @@ def create_app(
388
492
  model (Optional[str]): Default model for all agents' sessions. Overrides
389
493
  config.
390
494
  mcp (bool): If True, mount an MCP endpoint per agent at ``<prefix>/mcp``.
495
+ sessions (bool): If True (default), mount the session-based routes
496
+ (sessions CRUD plus synchronous/streaming chat and state) for each
497
+ agent. If False, those routes are omitted — pair with ``jobs`` to
498
+ serve agents purely through the async job API.
391
499
  jobs (bool): If True (or ``[jobs].enabled`` in config), mount a durable
392
500
  async job system per agent at ``<prefix>/jobs`` — runs submitted there
393
501
  execute in the background and survive client timeouts/reconnects.
@@ -420,7 +528,24 @@ def create_app(
420
528
  index = []
421
529
  orchestrators = []
422
530
  for prefix, agent_class in mapping.items():
423
- router = create_router(agent_class, model=model, name=name, version=version)
531
+ # The root agent inherits the app's display name; agents under a prefix
532
+ # take a name derived from that prefix, so each router's route names stay
533
+ # unique across the app.
534
+ agent_name = name if prefix == "" else prefix.strip("/").replace("/", "-")
535
+ jobs_cfg = None
536
+ if jobs_enabled:
537
+ jobs_cfg = config.jobs.model_copy(
538
+ update={"store": _store_path_for(config.jobs.store, prefix, multi)}
539
+ )
540
+ router = create_router(
541
+ agent_class,
542
+ model=model,
543
+ name=agent_name,
544
+ version=version,
545
+ sessions=sessions,
546
+ jobs=jobs_enabled,
547
+ jobs_config=jobs_cfg,
548
+ )
424
549
  app.include_router(router, prefix=prefix)
425
550
  index.append({"agent_class": agent_class.__name__, "prefix": prefix or "/"})
426
551
  if mcp:
@@ -432,23 +557,8 @@ def create_app(
432
557
  sessions=router.sessions,
433
558
  path=(prefix + "/mcp"),
434
559
  )
435
- if jobs_enabled:
436
- from pyagentic.api.jobs import JobOrchestrator, build_backend, build_store
437
- from pyagentic.api.jobs._routes import build_jobs_router
438
-
439
- store = build_store(_store_path_for(config.jobs.store, prefix, multi))
440
- backend = build_backend(
441
- agent_class,
442
- router.sessions,
443
- max_concurrency=config.jobs.max_concurrency,
444
- default_model=model,
445
- )
446
- orchestrator = JobOrchestrator(store, backend, config.jobs, router.sessions)
447
- app.include_router(
448
- build_jobs_router(orchestrator, agent_class, router.sessions),
449
- prefix=prefix,
450
- )
451
- orchestrators.append(orchestrator)
560
+ if router.orchestrator is not None:
561
+ orchestrators.append(router.orchestrator)
452
562
 
453
563
  if orchestrators:
454
564
  _wire_jobs_lifespan(app, orchestrators)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.8.1.dev1
3
+ Version: 2.9.1.dev1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -150,3 +150,24 @@ def test_jobs_enabled_multi_agent_prefixed():
150
150
  paths = {getattr(r, "path", None) for r in app.routes}
151
151
  assert "/app-test/jobs" in paths
152
152
  assert "/writer/jobs" in paths
153
+
154
+
155
+ # ---- sessions opt-out ----
156
+
157
+
158
+ def test_sessions_disabled():
159
+ """sessions=False omits the session routes from the app."""
160
+ app = create_app(_AppTestAgent, model="_mock::test-model", sessions=False)
161
+ paths = {getattr(r, "path", None) for r in app.routes}
162
+ assert "/sessions" not in paths
163
+ assert "/schema" in paths # info routes remain
164
+
165
+
166
+ def test_sessions_off_jobs_on():
167
+ """An async-only app: no /sessions, but /jobs is served."""
168
+ app = create_app(
169
+ _AppTestAgent, model="_mock::test-model", sessions=False, jobs=True
170
+ )
171
+ paths = {getattr(r, "path", None) for r in app.routes}
172
+ assert "/sessions" not in paths
173
+ assert "/jobs" in paths
@@ -79,3 +79,66 @@ def test_two_routers_isolated_sessions():
79
79
  sid = client.post("/a/sessions").json()["session_id"]
80
80
  assert sid in client.get("/a/sessions").json()["sessions"]
81
81
  assert sid not in client.get("/b/sessions").json()["sessions"]
82
+
83
+
84
+ def test_routes_are_named():
85
+ """Each route gets a name derived from the agent name, for url reversal."""
86
+ router = create_router(_RouterTestAgent, name="my-agent", model="_mock::test-model")
87
+ names = {route.name for route in router.routes}
88
+ assert {"my-agent_info", "my-agent_chat", "my-agent_get_state"} <= names
89
+
90
+
91
+ def test_tags_applied():
92
+ """The tags arg is attached to every route on the router."""
93
+ router = create_router(
94
+ _RouterTestAgent, model="_mock::test-model", tags=["bots"]
95
+ )
96
+ assert all("bots" in route.tags for route in router.routes)
97
+
98
+
99
+ def test_named_routes_unique_across_mounts():
100
+ """Distinct names keep route names unique when mounted on one app."""
101
+ app = FastAPI()
102
+ app.include_router(
103
+ create_router(_RouterTestAgent, name="a", model="_mock::test-model"), prefix="/a"
104
+ )
105
+ app.include_router(
106
+ create_router(_RouterTestAgent, name="b", model="_mock::test-model"), prefix="/b"
107
+ )
108
+ # url_path_for resolves each agent's chat route by its unique name.
109
+ assert app.url_path_for("a_chat", session_id="x") == "/a/sessions/x/chat"
110
+ assert app.url_path_for("b_chat", session_id="x") == "/b/sessions/x/chat"
111
+
112
+
113
+ def test_sessions_disabled_omits_session_routes():
114
+ """sessions=False drops the /sessions routes but keeps info/health/schema."""
115
+ app = FastAPI()
116
+ app.include_router(
117
+ create_router(_RouterTestAgent, model="_mock::test-model", sessions=False),
118
+ prefix=PREFIX,
119
+ )
120
+ client = TestClient(app)
121
+ assert client.get(f"{PREFIX}/health").json() == {"status": "ok"}
122
+ assert client.post(f"{PREFIX}/sessions").status_code == 404
123
+ assert client.get(f"{PREFIX}/sessions").status_code == 404
124
+
125
+
126
+ def test_jobs_router_and_orchestrator_exposed():
127
+ """jobs=True mounts /jobs and exposes the orchestrator on the router."""
128
+ router = create_router(_RouterTestAgent, model="_mock::test-model", jobs=True)
129
+ assert router.orchestrator is not None
130
+ paths = {getattr(r, "path", None) for r in router.routes}
131
+ assert "/jobs" in paths
132
+ # Without jobs, no orchestrator.
133
+ plain = create_router(_RouterTestAgent, model="_mock::test-model")
134
+ assert plain.orchestrator is None
135
+
136
+
137
+ def test_sessions_off_jobs_on():
138
+ """sessions=False with jobs=True serves an async-only agent."""
139
+ router = create_router(
140
+ _RouterTestAgent, model="_mock::test-model", sessions=False, jobs=True
141
+ )
142
+ paths = {getattr(r, "path", None) for r in router.routes}
143
+ assert "/jobs" in paths
144
+ assert "/sessions" not in paths