locus-sdk 0.2.0__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 (217) hide show
  1. locus/__init__.py +241 -0
  2. locus/_oracle_pool_cache.py +196 -0
  3. locus/_sync.py +76 -0
  4. locus/a2a/__init__.py +143 -0
  5. locus/a2a/protocol.py +1310 -0
  6. locus/a2a/protocol_v1.py +556 -0
  7. locus/a2a/spec.py +420 -0
  8. locus/a2a/spec_v1.py +279 -0
  9. locus/agent/__init__.py +37 -0
  10. locus/agent/agent.py +604 -0
  11. locus/agent/composition.py +352 -0
  12. locus/agent/config.py +607 -0
  13. locus/agent/hook_orchestrator.py +167 -0
  14. locus/agent/initializer.py +250 -0
  15. locus/agent/result.py +339 -0
  16. locus/agent/runtime_loop.py +2037 -0
  17. locus/core/__init__.py +178 -0
  18. locus/core/command.py +260 -0
  19. locus/core/config.py +176 -0
  20. locus/core/errors.py +207 -0
  21. locus/core/events.py +261 -0
  22. locus/core/interrupt.py +404 -0
  23. locus/core/messages.py +151 -0
  24. locus/core/protocols.py +348 -0
  25. locus/core/reducers.py +466 -0
  26. locus/core/send.py +325 -0
  27. locus/core/state.py +403 -0
  28. locus/core/structured.py +536 -0
  29. locus/core/termination.py +237 -0
  30. locus/core/warnings.py +41 -0
  31. locus/deepagent/__init__.py +137 -0
  32. locus/deepagent/backends/__init__.py +37 -0
  33. locus/deepagent/backends/filesystem.py +243 -0
  34. locus/deepagent/backends/protocol.py +123 -0
  35. locus/deepagent/backends/state.py +225 -0
  36. locus/deepagent/factory.py +400 -0
  37. locus/deepagent/memory.py +48 -0
  38. locus/deepagent/protocol.py +127 -0
  39. locus/deepagent/subagent.py +153 -0
  40. locus/deepagent/todos.py +122 -0
  41. locus/deepagent/tools/__init__.py +16 -0
  42. locus/deepagent/tools/filesystem.py +160 -0
  43. locus/deepagent/workflow.py +665 -0
  44. locus/evaluation/__init__.py +26 -0
  45. locus/evaluation/framework.py +226 -0
  46. locus/hooks/__init__.py +65 -0
  47. locus/hooks/builtin/__init__.py +64 -0
  48. locus/hooks/builtin/guardrails.py +765 -0
  49. locus/hooks/builtin/logging.py +279 -0
  50. locus/hooks/builtin/retry.py +130 -0
  51. locus/hooks/builtin/steering.py +248 -0
  52. locus/hooks/builtin/telemetry.py +429 -0
  53. locus/hooks/events.py +86 -0
  54. locus/hooks/plugin.py +137 -0
  55. locus/hooks/provider.py +405 -0
  56. locus/hooks/registry.py +398 -0
  57. locus/integrations/__init__.py +18 -0
  58. locus/integrations/fastmcp.py +723 -0
  59. locus/integrations/osv.py +194 -0
  60. locus/loop/__init__.py +54 -0
  61. locus/loop/nodes.py +363 -0
  62. locus/loop/react.py +279 -0
  63. locus/loop/router.py +241 -0
  64. locus/loop/runner.py +307 -0
  65. locus/memory/__init__.py +115 -0
  66. locus/memory/backends/__init__.py +87 -0
  67. locus/memory/backends/_oracle_config.py +89 -0
  68. locus/memory/backends/adapters.py +704 -0
  69. locus/memory/backends/file.py +288 -0
  70. locus/memory/backends/http.py +301 -0
  71. locus/memory/backends/memory.py +247 -0
  72. locus/memory/backends/mysql.py +646 -0
  73. locus/memory/backends/oci_bucket.py +607 -0
  74. locus/memory/backends/opensearch.py +301 -0
  75. locus/memory/backends/oracle.py +617 -0
  76. locus/memory/backends/oracle_sync.py +271 -0
  77. locus/memory/backends/oracle_versioned.py +792 -0
  78. locus/memory/backends/postgresql.py +454 -0
  79. locus/memory/backends/redis.py +129 -0
  80. locus/memory/checkpointer.py +335 -0
  81. locus/memory/compactor.py +329 -0
  82. locus/memory/conversation.py +325 -0
  83. locus/memory/delta.py +546 -0
  84. locus/memory/manager.py +584 -0
  85. locus/memory/managers/__init__.py +47 -0
  86. locus/memory/managers/oracle_agent_memory.py +468 -0
  87. locus/memory/registry.py +221 -0
  88. locus/memory/store.py +935 -0
  89. locus/memory/store_backends/__init__.py +42 -0
  90. locus/memory/store_backends/oracle.py +817 -0
  91. locus/memory/store_backends/oracle_sync.py +167 -0
  92. locus/models/__init__.py +103 -0
  93. locus/models/auxiliary.py +56 -0
  94. locus/models/base.py +121 -0
  95. locus/models/caching.py +85 -0
  96. locus/models/credentials.py +212 -0
  97. locus/models/failover.py +787 -0
  98. locus/models/metadata.py +357 -0
  99. locus/models/native/__init__.py +22 -0
  100. locus/models/native/anthropic.py +418 -0
  101. locus/models/native/ollama.py +222 -0
  102. locus/models/native/openai.py +487 -0
  103. locus/models/pooled.py +212 -0
  104. locus/models/providers/__init__.py +18 -0
  105. locus/models/providers/oci/__init__.py +437 -0
  106. locus/models/providers/oci/_responses_parse.py +295 -0
  107. locus/models/providers/oci/_signing.py +172 -0
  108. locus/models/providers/oci/base.py +130 -0
  109. locus/models/providers/oci/client.py +294 -0
  110. locus/models/providers/oci/models/__init__.py +14 -0
  111. locus/models/providers/oci/models/cohere.py +263 -0
  112. locus/models/providers/oci/models/generic.py +315 -0
  113. locus/models/providers/oci/openai_compat.py +539 -0
  114. locus/models/providers/oci/responses.py +522 -0
  115. locus/models/rate_limits.py +237 -0
  116. locus/models/registry.py +154 -0
  117. locus/multiagent/__init__.py +134 -0
  118. locus/multiagent/functional.py +234 -0
  119. locus/multiagent/graph.py +1671 -0
  120. locus/multiagent/handoff.py +637 -0
  121. locus/multiagent/orchestrator.py +467 -0
  122. locus/multiagent/specialist.py +495 -0
  123. locus/multiagent/swarm.py +701 -0
  124. locus/multiagent/visualize.py +139 -0
  125. locus/observability/__init__.py +67 -0
  126. locus/observability/agent_bridge.py +171 -0
  127. locus/observability/bus_hook.py +175 -0
  128. locus/observability/context.py +137 -0
  129. locus/observability/emit.py +216 -0
  130. locus/observability/event_bus.py +369 -0
  131. locus/observability/router_events.py +231 -0
  132. locus/playbooks/__init__.py +45 -0
  133. locus/playbooks/enforcer.py +403 -0
  134. locus/playbooks/hook.py +161 -0
  135. locus/playbooks/loader.py +271 -0
  136. locus/playbooks/models.py +202 -0
  137. locus/providers/__init__.py +63 -0
  138. locus/providers/image.py +115 -0
  139. locus/providers/speech.py +171 -0
  140. locus/providers/tools.py +217 -0
  141. locus/providers/types.py +42 -0
  142. locus/providers/web_fetch.py +194 -0
  143. locus/providers/web_search.py +129 -0
  144. locus/py.typed +0 -0
  145. locus/rag/__init__.py +180 -0
  146. locus/rag/chunkers/__init__.py +23 -0
  147. locus/rag/chunkers/oracle_indb.py +277 -0
  148. locus/rag/chunkers/oracle_sync.py +97 -0
  149. locus/rag/embeddings/__init__.py +60 -0
  150. locus/rag/embeddings/base.py +219 -0
  151. locus/rag/embeddings/oci.py +441 -0
  152. locus/rag/embeddings/openai.py +207 -0
  153. locus/rag/embeddings/oracle_indb.py +511 -0
  154. locus/rag/embeddings/oracle_sync.py +97 -0
  155. locus/rag/loaders/__init__.py +17 -0
  156. locus/rag/loaders/oracle.py +358 -0
  157. locus/rag/loaders/oracle_sync.py +96 -0
  158. locus/rag/multimodal.py +586 -0
  159. locus/rag/reranker/__init__.py +28 -0
  160. locus/rag/reranker/base.py +57 -0
  161. locus/rag/reranker/cohere_oci.py +225 -0
  162. locus/rag/retriever.py +603 -0
  163. locus/rag/stores/__init__.py +69 -0
  164. locus/rag/stores/_mmr.py +122 -0
  165. locus/rag/stores/_oracle_filter.py +178 -0
  166. locus/rag/stores/base.py +266 -0
  167. locus/rag/stores/memory.py +155 -0
  168. locus/rag/stores/opensearch.py +396 -0
  169. locus/rag/stores/oracle.py +1088 -0
  170. locus/rag/stores/oracle_sync.py +154 -0
  171. locus/rag/stores/pgvector.py +608 -0
  172. locus/rag/tools.py +265 -0
  173. locus/reasoning/__init__.py +79 -0
  174. locus/reasoning/causal.py +727 -0
  175. locus/reasoning/grounding.py +677 -0
  176. locus/reasoning/gsar.py +392 -0
  177. locus/reasoning/gsar_evaluator.py +262 -0
  178. locus/reasoning/gsar_judge.py +336 -0
  179. locus/reasoning/reflexion.py +515 -0
  180. locus/router/__init__.py +115 -0
  181. locus/router/capability.py +133 -0
  182. locus/router/compiler.py +246 -0
  183. locus/router/goal_frame.py +145 -0
  184. locus/router/picker.py +201 -0
  185. locus/router/policy.py +103 -0
  186. locus/router/protocol.py +669 -0
  187. locus/router/runnable.py +267 -0
  188. locus/router/runtime.py +129 -0
  189. locus/router/skill_index.py +80 -0
  190. locus/server/__init__.py +44 -0
  191. locus/server/adapters.py +178 -0
  192. locus/server/app.py +456 -0
  193. locus/skills/__init__.py +37 -0
  194. locus/skills/models.py +255 -0
  195. locus/skills/plugin.py +207 -0
  196. locus/streaming/__init__.py +49 -0
  197. locus/streaming/console.py +373 -0
  198. locus/streaming/handler.py +284 -0
  199. locus/streaming/sse.py +443 -0
  200. locus/streaming/structured.py +144 -0
  201. locus/tools/__init__.py +27 -0
  202. locus/tools/builtins.py +58 -0
  203. locus/tools/context.py +69 -0
  204. locus/tools/decorator.py +197 -0
  205. locus/tools/executor.py +543 -0
  206. locus/tools/oci.py +642 -0
  207. locus/tools/path_safety.py +99 -0
  208. locus/tools/registry.py +87 -0
  209. locus/tools/result_storage.py +183 -0
  210. locus/tools/schema.py +206 -0
  211. locus/tools/url_safety.py +193 -0
  212. locus/tools/watcher.py +304 -0
  213. locus_sdk-0.2.0.dist-info/METADATA +640 -0
  214. locus_sdk-0.2.0.dist-info/RECORD +217 -0
  215. locus_sdk-0.2.0.dist-info/WHEEL +4 -0
  216. locus_sdk-0.2.0.dist-info/licenses/LICENSE +17 -0
  217. locus_sdk-0.2.0.dist-info/licenses/LICENSE.txt +17 -0
locus/__init__.py ADDED
@@ -0,0 +1,241 @@
1
+ # Copyright (c) 2025, 2026 Oracle and/or its affiliates.
2
+ # Licensed under the Universal Permissive License v1.0 as shown at
3
+ # https://oss.oracle.com/licenses/upl/
4
+
5
+ """
6
+ Locus - A zero-LangChain agentic SDK.
7
+
8
+ Built-in Reflexion, Grounding Evaluation, and production-grade orchestration.
9
+ 100% Pydantic. No magic.
10
+
11
+ Usage:
12
+ from locus import Agent, tool
13
+
14
+ @tool
15
+ def search(query: str) -> str:
16
+ '''Search the knowledge base.'''
17
+ return "results..."
18
+
19
+ agent = Agent(
20
+ model="openai:gpt-4o", # or oci:cohere.command-r-plus
21
+ tools=[search],
22
+ system_prompt="You are a helpful assistant.",
23
+ )
24
+
25
+ async for event in agent.run("Find information about X"):
26
+ print(event)
27
+ """
28
+
29
+ from locus.core.config import LocusSettings
30
+ from locus.core.errors import LocusError
31
+ from locus.core.events import (
32
+ GroundingEvent,
33
+ LocusEvent,
34
+ ReflectEvent,
35
+ TerminateEvent,
36
+ ThinkEvent,
37
+ ToolCompleteEvent,
38
+ ToolStartEvent,
39
+ )
40
+ from locus.core.messages import Message, Role, ToolCall
41
+ from locus.core.state import AgentState
42
+ from locus.tools.context import ToolContext
43
+ from locus.tools.decorator import tool
44
+
45
+
46
+ # Lazy import mapping for optional dependencies
47
+ _LAZY_IMPORTS = {
48
+ "Agent": ("locus.agent.agent", "Agent"),
49
+ "AgentConfig": ("locus.agent.config", "AgentConfig"),
50
+ "AgentResult": ("locus.agent.result", "AgentResult"),
51
+ # Composition primitives — graph-free orchestration shapes.
52
+ "SequentialPipeline": ("locus.agent.composition", "SequentialPipeline"),
53
+ "ParallelPipeline": ("locus.agent.composition", "ParallelPipeline"),
54
+ "LoopAgent": ("locus.agent.composition", "LoopAgent"),
55
+ # Multi-agent primitives — graph + handoff + orchestrator/specialist.
56
+ "StateGraph": ("locus.multiagent.graph", "StateGraph"),
57
+ "GraphConfig": ("locus.multiagent.graph", "GraphConfig"),
58
+ "START": ("locus.multiagent.graph", "START"),
59
+ "END": ("locus.multiagent.graph", "END"),
60
+ "Send": ("locus.core.send", "Send"),
61
+ "Handoff": ("locus.multiagent.handoff", "Handoff"),
62
+ "HandoffContext": ("locus.multiagent.handoff", "HandoffContext"),
63
+ "HandoffReason": ("locus.multiagent.handoff", "HandoffReason"),
64
+ "create_handoff_agent": ("locus.multiagent.handoff", "create_handoff_agent"),
65
+ "create_handoff_manager": ("locus.multiagent.handoff", "create_handoff_manager"),
66
+ "Orchestrator": ("locus.multiagent.orchestrator", "Orchestrator"),
67
+ "RoutingDecision": ("locus.multiagent.orchestrator", "RoutingDecision"),
68
+ "create_orchestrator": ("locus.multiagent.orchestrator", "create_orchestrator"),
69
+ "Specialist": ("locus.multiagent.specialist", "Specialist"),
70
+ # Public name "Reflexion" maps to the actual class "Reflector". The
71
+ # mismatch was an import error on locus 0.1.0 — keep the alias so existing
72
+ # docs / code that does ``from locus import Reflexion`` keeps working.
73
+ "Reflexion": ("locus.reasoning.reflexion", "Reflector"),
74
+ "Reflector": ("locus.reasoning.reflexion", "Reflector"),
75
+ "GroundingEvaluator": ("locus.reasoning.grounding", "GroundingEvaluator"),
76
+ "CausalChain": ("locus.reasoning.causal", "CausalChain"),
77
+ "HookProvider": ("locus.hooks.provider", "HookProvider"),
78
+ "HookRegistry": ("locus.hooks.registry", "HookRegistry"),
79
+ # RAG
80
+ "RAGRetriever": ("locus.rag.retriever", "RAGRetriever"),
81
+ "OCIEmbeddings": ("locus.rag.embeddings.oci", "OCIEmbeddings"),
82
+ "OracleVectorStore": ("locus.rag.stores.oracle", "OracleVectorStore"),
83
+ # PRISM router — bounded-graph generation atop locus primitives.
84
+ "Router": ("locus.router.runtime", "Router"),
85
+ "GoalFrame": ("locus.router.goal_frame", "GoalFrame"),
86
+ "TaskType": ("locus.router.goal_frame", "TaskType"),
87
+ "Risk": ("locus.router.goal_frame", "Risk"),
88
+ "Complexity": ("locus.router.goal_frame", "Complexity"),
89
+ "Capability": ("locus.router.capability", "Capability"),
90
+ "CapabilityIndex": ("locus.router.capability", "CapabilityIndex"),
91
+ "Protocol": ("locus.router.protocol", "Protocol"),
92
+ "ProtocolRegistry": ("locus.router.protocol", "ProtocolRegistry"),
93
+ "PolicyGate": ("locus.router.policy", "PolicyGate"),
94
+ "PolicyVerdict": ("locus.router.policy", "PolicyVerdict"),
95
+ "CognitiveCompiler": ("locus.router.compiler", "CognitiveCompiler"),
96
+ "RunnableResult": ("locus.router.runnable", "RunnableResult"),
97
+ "SkillIndex": ("locus.router.skill_index", "SkillIndex"),
98
+ "builtin_protocols": ("locus.router.protocol", "builtin_protocols"),
99
+ # Deep research — research-shaped Agent factory + provider protocol.
100
+ # Submodule is ``locus.deepagent`` (Pythonic path-name convention).
101
+ # Factory is ``create_deepagent`` (matches ``create_orchestrator``,
102
+ # ``create_handoff_agent`` — the existing locus naming for "build me
103
+ # a configured X").
104
+ "create_deepagent": ("locus.deepagent", "create_deepagent"),
105
+ "create_research_workflow": ("locus.deepagent.workflow", "create_research_workflow"),
106
+ "make_execute_node": ("locus.deepagent.workflow", "make_execute_node"),
107
+ "make_causal_inference_node": ("locus.deepagent.workflow", "make_causal_inference_node"),
108
+ "make_summarize_node": ("locus.deepagent.workflow", "make_summarize_node"),
109
+ "make_grounding_eval_node": ("locus.deepagent.workflow", "make_grounding_eval_node"),
110
+ "make_regenerate_summary_node": ("locus.deepagent.workflow", "make_regenerate_summary_node"),
111
+ "make_replan_node": ("locus.deepagent.workflow", "make_replan_node"),
112
+ "route_after_grounding": ("locus.deepagent.workflow", "route_after_grounding"),
113
+ # Research workflow state keys
114
+ "KEY_PROMPT": ("locus.deepagent.workflow", "KEY_PROMPT"),
115
+ "KEY_EXECUTE_PROMPT": ("locus.deepagent.workflow", "KEY_EXECUTE_PROMPT"),
116
+ "KEY_EVIDENCE": ("locus.deepagent.workflow", "KEY_EVIDENCE"),
117
+ "KEY_GROUNDING_FACTS": ("locus.deepagent.workflow", "KEY_GROUNDING_FACTS"),
118
+ "KEY_CAUSAL_CHAIN": ("locus.deepagent.workflow", "KEY_CAUSAL_CHAIN"),
119
+ "KEY_CAUSAL_HYPOTHESIS": ("locus.deepagent.workflow", "KEY_CAUSAL_HYPOTHESIS"),
120
+ "KEY_CAUSAL_CONFIDENCE": ("locus.deepagent.workflow", "KEY_CAUSAL_CONFIDENCE"),
121
+ "KEY_SUMMARY": ("locus.deepagent.workflow", "KEY_SUMMARY"),
122
+ "KEY_STRUCTURED_OUTPUT": ("locus.deepagent.workflow", "KEY_STRUCTURED_OUTPUT"),
123
+ "KEY_GROUNDING_SCORE": ("locus.deepagent.workflow", "KEY_GROUNDING_SCORE"),
124
+ "KEY_UNGROUNDED_CLAIMS": ("locus.deepagent.workflow", "KEY_UNGROUNDED_CLAIMS"),
125
+ "KEY_REPLAN_COUNT": ("locus.deepagent.workflow", "KEY_REPLAN_COUNT"),
126
+ "KEY_REGENERATION_COUNT": ("locus.deepagent.workflow", "KEY_REGENERATION_COUNT"),
127
+ "KEY_STOP_REASON": ("locus.deepagent.workflow", "KEY_STOP_REASON"),
128
+ "KnowledgeProvider": ("locus.deepagent", "KnowledgeProvider"),
129
+ "KnowledgeRow": ("locus.deepagent", "KnowledgeRow"),
130
+ "ItemRef": ("locus.deepagent", "ItemRef"),
131
+ "Grounding": ("locus.deepagent", "Grounding"),
132
+ }
133
+
134
+
135
+ def __getattr__(name: str) -> object:
136
+ """Lazy import for Agent and model classes."""
137
+ if name in _LAZY_IMPORTS:
138
+ module_path, attr_name = _LAZY_IMPORTS[name]
139
+ import importlib
140
+
141
+ module = importlib.import_module(module_path)
142
+ return getattr(module, attr_name)
143
+
144
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
145
+
146
+
147
+ __version__ = "0.2.0"
148
+ __all__ = [
149
+ "Agent",
150
+ "AgentConfig",
151
+ "AgentResult",
152
+ "AgentState",
153
+ "CausalChain",
154
+ "END",
155
+ "GraphConfig",
156
+ "GroundingEvaluator",
157
+ "GroundingEvent",
158
+ "Handoff",
159
+ "HandoffContext",
160
+ "HandoffReason",
161
+ "HookProvider",
162
+ "HookRegistry",
163
+ "LocusError",
164
+ "LocusEvent",
165
+ "LocusSettings",
166
+ "LoopAgent",
167
+ "Message",
168
+ "Orchestrator",
169
+ "ParallelPipeline",
170
+ "ReflectEvent",
171
+ "Reflector",
172
+ "Reflexion",
173
+ "Role",
174
+ "RoutingDecision",
175
+ "START",
176
+ "Send",
177
+ "SequentialPipeline",
178
+ "Specialist",
179
+ "StateGraph",
180
+ "TerminateEvent",
181
+ "ThinkEvent",
182
+ "ToolCall",
183
+ "ToolCompleteEvent",
184
+ "ToolContext",
185
+ "ToolStartEvent",
186
+ "__version__",
187
+ "create_handoff_agent",
188
+ "create_handoff_manager",
189
+ "create_orchestrator",
190
+ "tool",
191
+ # RAG (lazy)
192
+ "RAGRetriever",
193
+ "OCIEmbeddings",
194
+ "OracleVectorStore",
195
+ # PRISM router (lazy)
196
+ "Capability",
197
+ "CapabilityIndex",
198
+ "CognitiveCompiler",
199
+ "Complexity",
200
+ "GoalFrame",
201
+ "PolicyGate",
202
+ "PolicyVerdict",
203
+ "Protocol",
204
+ "ProtocolRegistry",
205
+ "Risk",
206
+ "Router",
207
+ "RunnableResult",
208
+ "SkillIndex",
209
+ "TaskType",
210
+ "builtin_protocols",
211
+ # Deep research — agent factory (lazy)
212
+ "create_deepagent",
213
+ "KnowledgeProvider",
214
+ "KnowledgeRow",
215
+ "ItemRef",
216
+ "Grounding",
217
+ # Deep research — research workflow primitives (lazy)
218
+ "create_research_workflow",
219
+ "make_execute_node",
220
+ "make_causal_inference_node",
221
+ "make_summarize_node",
222
+ "make_grounding_eval_node",
223
+ "make_regenerate_summary_node",
224
+ "make_replan_node",
225
+ "route_after_grounding",
226
+ # Research workflow state keys (lazy)
227
+ "KEY_PROMPT",
228
+ "KEY_EXECUTE_PROMPT",
229
+ "KEY_EVIDENCE",
230
+ "KEY_GROUNDING_FACTS",
231
+ "KEY_CAUSAL_CHAIN",
232
+ "KEY_CAUSAL_HYPOTHESIS",
233
+ "KEY_CAUSAL_CONFIDENCE",
234
+ "KEY_SUMMARY",
235
+ "KEY_STRUCTURED_OUTPUT",
236
+ "KEY_GROUNDING_SCORE",
237
+ "KEY_UNGROUNDED_CLAIMS",
238
+ "KEY_REPLAN_COUNT",
239
+ "KEY_REGENERATION_COUNT",
240
+ "KEY_STOP_REASON",
241
+ ]
@@ -0,0 +1,196 @@
1
+ # Copyright (c) 2025, 2026 Oracle and/or its affiliates.
2
+ # Licensed under the Universal Permissive License v1.0 as shown at
3
+ # https://oss.oracle.com/licenses/upl/
4
+
5
+ """Per-event-loop ``oracledb`` async pool cache.
6
+
7
+ ``oracledb`` async pools are bound to the asyncio event loop that
8
+ creates them. The moment a caller invokes :meth:`Agent.run_sync` a
9
+ second time (each call spins a fresh loop via :func:`asyncio.run`),
10
+ or FastAPI's lifespan / anyio's BlockingPortal opens a different
11
+ loop, the cached pool is attached to a dead loop and the next
12
+ ``acquire`` raises::
13
+
14
+ RuntimeError: Task ... got Future attached to a different loop
15
+
16
+ The fix is to detect a loop mismatch and rebuild. This helper does
17
+ that for every Oracle-touching backend in locus so the per-class
18
+ copies stay one line.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import contextlib
25
+ from collections.abc import AsyncIterator, Awaitable, Callable
26
+ from typing import Any, Protocol
27
+
28
+
29
+ class _PoolHolder(Protocol):
30
+ """Duck-typed surface every Oracle backend already exposes.
31
+
32
+ ``_pool`` is the cached pool; ``_pool_loop`` is the event loop it
33
+ was created on. Backends declare these as instance attributes (or
34
+ Pydantic private attrs) and call :func:`get_pool` to read /
35
+ refresh.
36
+ """
37
+
38
+ _pool: Any
39
+ _pool_loop: Any
40
+
41
+
42
+ async def get_pool(
43
+ holder: _PoolHolder,
44
+ builder: Callable[[], Any] | Callable[[], Awaitable[Any]],
45
+ ) -> Any:
46
+ """Return a pool bound to the current event loop, building if needed.
47
+
48
+ Args:
49
+ holder: any object with mutable ``_pool`` / ``_pool_loop``
50
+ attributes. Most locus backends already satisfy this.
51
+ builder: zero-arg callable that produces a fresh
52
+ ``oracledb`` async pool. May be sync (the common case —
53
+ ``oracledb.create_pool_async`` returns the pool directly,
54
+ not a coroutine) or async.
55
+
56
+ The cached pool is invalidated when the running loop changes;
57
+ the dropped pool is left for ``oracledb`` to GC.
58
+ """
59
+ try:
60
+ running = asyncio.get_running_loop()
61
+ except RuntimeError: # pragma: no cover — backend methods are async
62
+ running = None
63
+
64
+ if holder._pool is not None and holder._pool_loop is not running:
65
+ # Cross-loop call — drop the dead reference, reinitialise table
66
+ # provisioning state, and rebuild below.
67
+ holder._pool = None
68
+ # ``_initialized`` is a sibling flag on most backends that gates
69
+ # CREATE TABLE checks; reset it so the new pool re-verifies
70
+ # against the database.
71
+ if hasattr(holder, "_initialized"):
72
+ holder._initialized = False
73
+
74
+ if holder._pool is None:
75
+ pool = builder()
76
+ if asyncio.iscoroutine(pool):
77
+ pool = await pool
78
+ holder._pool = pool
79
+ holder._pool_loop = running
80
+
81
+ return holder._pool
82
+
83
+
84
+ # Oracle network-layer errors that mean the underlying TCP connection
85
+ # is dead — Autonomous Database aggressively closes idle sockets, and
86
+ # the pool's cached connections may have been killed since the last
87
+ # use. The fix is the same in every case: drop the pool, rebuild on the
88
+ # next call, and retry the operation once.
89
+ _RECONNECT_PREFIXES: tuple[str, ...] = (
90
+ "DPY-4011", # the database or network closed the connection
91
+ "DPY-1001", # not connected to the database
92
+ "DPY-6000", # cannot connect to database (broken connection)
93
+ "ORA-12537", # TNS:connection closed
94
+ "ORA-03113", # end-of-file on communication channel
95
+ "ORA-03114", # not connected to ORACLE
96
+ "ORA-03138", # connection terminated due to security policy violation
97
+ "ORA-03146", # invalid buffer length for TTC field (protocol desync)
98
+ "ORA-12541", # TNS:no listener
99
+ "ORA-12170", # TNS:Connect timeout occurred
100
+ )
101
+
102
+
103
+ def is_reconnectable(exc: BaseException) -> bool:
104
+ """True when *exc* names a network-layer disconnect we can recover from.
105
+
106
+ Matches against the error string rather than the exception class
107
+ because ``oracledb.DatabaseError`` wraps every database-side
108
+ condition; the ORA / DPY prefix tells reconnectable apart from
109
+ application-level errors (ORA-00001 etc.) we should not retry.
110
+ """
111
+ msg = str(exc)
112
+ return any(p in msg for p in _RECONNECT_PREFIXES)
113
+
114
+
115
+ async def with_reconnect(
116
+ holder: _PoolHolder,
117
+ op: Callable[[Any], Awaitable[Any]],
118
+ builder: Callable[[], Any] | Callable[[], Awaitable[Any]],
119
+ ) -> Any:
120
+ """Run *op(pool)* with a one-shot rebuild on a network-layer error.
121
+
122
+ The retry path drops the pool and any sibling ``_initialized`` flag
123
+ so the second attempt walks the full provisioning path again. We
124
+ retry exactly once — a persistent disconnect (DB shutdown, wrong
125
+ DSN) should surface to the caller rather than loop.
126
+ """
127
+ pool = await get_pool(holder, builder)
128
+ try:
129
+ return await op(pool)
130
+ except BaseException as exc: # noqa: BLE001
131
+ if not is_reconnectable(exc):
132
+ raise
133
+ # Drop the dead pool + sibling provisioning state so the next
134
+ # ``get_pool`` rebuilds cleanly. Don't await ``pool.close()`` —
135
+ # the broken socket would just raise again.
136
+ holder._pool = None
137
+ if hasattr(holder, "_initialized"):
138
+ holder._initialized = False
139
+ pool = await get_pool(holder, builder)
140
+ return await op(pool)
141
+
142
+
143
+ @contextlib.asynccontextmanager
144
+ async def safe_acquire(holder: _PoolHolder, pool: Any) -> AsyncIterator[Any]:
145
+ """Drop-in replacement for ``async with pool.acquire() as conn``.
146
+
147
+ Behaves like ``pool.acquire()`` for the body of the block — yields a
148
+ connection, no surprises. The wrapper exists for the *close* side:
149
+ Autonomous Database aggressively closes idle TCP connections, so a
150
+ pooled connection's socket may be dead by the time we're done with
151
+ it. When that close-time handshake fails with DPY-4011 (or a sibling
152
+ network-layer error) we:
153
+
154
+ 1. Swallow the exception — the work inside the block already
155
+ committed, surfacing a close-time disconnect would mask a
156
+ successful save with a false error.
157
+ 2. Invalidate the pool by setting ``holder._pool = None`` so the
158
+ next operation rebuilds it. Stale connections in the pool
159
+ likely share the same fate.
160
+
161
+ Errors raised *inside* the block propagate untouched — pair with
162
+ :func:`with_reconnect` if you also need body-time retry.
163
+
164
+ Implementation note: ``pool.acquire()`` in ``oracledb`` returns the
165
+ connection object directly (synchronously) and the connection
166
+ itself implements the async context-manager protocol. We drive
167
+ ``__aenter__`` / ``__aexit__`` manually so close-time errors can
168
+ be caught separately from body errors.
169
+ """
170
+ ctx = pool.acquire()
171
+ conn = await ctx.__aenter__()
172
+ body_exc: BaseException | None = None
173
+ try:
174
+ yield conn
175
+ except BaseException as exc:
176
+ body_exc = exc
177
+ raise
178
+ finally:
179
+ try:
180
+ if body_exc is None:
181
+ await ctx.__aexit__(None, None, None)
182
+ else:
183
+ await ctx.__aexit__(type(body_exc), body_exc, body_exc.__traceback__)
184
+ except BaseException as close_exc: # noqa: BLE001
185
+ if is_reconnectable(close_exc):
186
+ # Socket's dead — drop the pool so the next call rebuilds.
187
+ # Suppress the close-time error: the body either succeeded
188
+ # already (no point surfacing a false failure) or has its
189
+ # own ``body_exc`` that's about to propagate.
190
+ holder._pool = None
191
+ if hasattr(holder, "_initialized"):
192
+ holder._initialized = False
193
+ elif body_exc is None:
194
+ # Real close-time failure with no body error to propagate.
195
+ raise
196
+ # else: body_exc is about to propagate; don't mask it.
locus/_sync.py ADDED
@@ -0,0 +1,76 @@
1
+ # Copyright (c) 2025, 2026 Oracle and/or its affiliates.
2
+ # Licensed under the Universal Permissive License v1.0 as shown at
3
+ # https://oss.oracle.com/licenses/upl/
4
+
5
+ """Run an async coroutine from sync code, regardless of loop state.
6
+
7
+ Single shared helper used by every ``Sync*`` wrapper class shipped
8
+ alongside the async Oracle primitives (checkpointer / store / vector
9
+ store / loader / chunker / embedder). Mirrors the same trick
10
+ ``langgraph-oracledb``'s ``OracleSaver`` uses to bridge sync callers
11
+ into an asyncpg-style driver — without taking any langchain or
12
+ langgraph dependency.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ from collections.abc import AsyncIterator, Coroutine
19
+ from typing import Any, TypeVar
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ def run_sync(coro: Coroutine[Any, Any, T]) -> T:
26
+ """Drive a coroutine to completion from synchronous context.
27
+
28
+ Handles two cases:
29
+
30
+ 1. **No running loop** (script / Jupyter cell with ``await``
31
+ disabled): use :func:`asyncio.run`.
32
+ 2. **Inside a running loop** (Jupyter ``await``, FastAPI
33
+ handler, etc.): we can't reuse the active loop — instead we
34
+ run the coroutine to completion on a *fresh* event loop
35
+ hosted on a background thread, and block the calling thread
36
+ until that completes.
37
+
38
+ This is the same trick ``langgraph-oracledb``'s ``OracleSaver``
39
+ uses to bridge sync callers into an asyncpg-style driver.
40
+ """
41
+ try:
42
+ asyncio.get_running_loop()
43
+ except RuntimeError:
44
+ # No running loop in this thread — safe to spin one up.
45
+ return asyncio.run(coro)
46
+
47
+ import threading
48
+
49
+ result: list[Any] = []
50
+ error: list[BaseException] = []
51
+
52
+ def runner() -> None:
53
+ new_loop = asyncio.new_event_loop()
54
+ try:
55
+ result.append(new_loop.run_until_complete(coro))
56
+ except BaseException as exc: # noqa: BLE001 — re-raised in caller thread
57
+ error.append(exc)
58
+ finally:
59
+ new_loop.close()
60
+
61
+ t = threading.Thread(target=runner, daemon=True)
62
+ t.start()
63
+ t.join()
64
+ if error:
65
+ raise error[0]
66
+ return result[0] # type: ignore[no-any-return]
67
+
68
+
69
+ async def drain(agen: AsyncIterator[T]) -> list[T]:
70
+ """Drain an async iterator into a list.
71
+
72
+ Tiny helper used by every sync wrapper that needs to surface an
73
+ async generator as a plain ``list[...]`` to its synchronous
74
+ caller. Kept here so each wrapper module doesn't redefine it.
75
+ """
76
+ return [x async for x in agen]
locus/a2a/__init__.py ADDED
@@ -0,0 +1,143 @@
1
+ # Copyright (c) 2025, 2026 Oracle and/or its affiliates.
2
+ # Licensed under the Universal Permissive License v1.0 as shown at
3
+ # https://oss.oracle.com/licenses/upl/
4
+
5
+ """Agent-to-Agent (A2A) protocol — spec-compliant cross-framework interop.
6
+
7
+ Implements the public A2A protocol (https://a2aproject.github.io/A2A/)
8
+ so Locus agents can talk to peers from other frameworks (Strands, ADK,
9
+ Google A2A SDKs) without a translation shim.
10
+
11
+ Wire surface served by :class:`A2AServer`:
12
+
13
+ - ``GET /.well-known/agent-card.json`` — public Agent Card (spec §5.5)
14
+ - ``POST /`` — JSON-RPC 2.0 method dispatch
15
+ for A2A v1.0 methods (``SendMessage``, ``SendStreamingMessage``,
16
+ ``GetTask``, ``ListTasks``, ``CancelTask``, ``SubscribeToTask``)
17
+ and compatibility methods (``message/send``, ``message/stream``,
18
+ ``tasks/get``, ``tasks/cancel``)
19
+ - Backwards-compat: ``GET /agent-card``, ``POST /a2a/{invoke,stream}``
20
+
21
+ Re-exports the spec models so consumers can do ``from locus.a2a import
22
+ AgentCard, AgentSkill, Message, TextPart, Task`` directly. The
23
+ ``A2AV1*`` types are the canonical A2A v1.0 wire models and are stable
24
+ public API for cross-framework interoperability.
25
+ """
26
+
27
+ from locus.a2a.protocol import (
28
+ A2AClient,
29
+ A2AMessage,
30
+ A2ARequest,
31
+ A2AResponse,
32
+ A2AServer,
33
+ )
34
+ from locus.a2a.spec import (
35
+ AgentCapabilities,
36
+ AgentCard,
37
+ AgentInterface,
38
+ AgentProvider,
39
+ AgentSkill,
40
+ Artifact,
41
+ DataPart,
42
+ FilePart,
43
+ FileWithBytes,
44
+ FileWithUri,
45
+ JsonRpcError,
46
+ JsonRpcErrorResponse,
47
+ JsonRpcRequest,
48
+ JsonRpcSuccessResponse,
49
+ Message,
50
+ MessageSendConfiguration,
51
+ MessageSendParams,
52
+ Part,
53
+ PushNotificationAuthenticationInfo,
54
+ PushNotificationConfig,
55
+ Task,
56
+ TaskArtifactUpdateEvent,
57
+ TaskIdParams,
58
+ TaskPushNotificationConfig,
59
+ TaskQueryParams,
60
+ TaskState,
61
+ TaskStatus,
62
+ TaskStatusUpdateEvent,
63
+ TextPart,
64
+ )
65
+ from locus.a2a.spec_v1 import (
66
+ A2A_V1_PROTOCOL_VERSION,
67
+ A2AV1Artifact,
68
+ A2AV1CancelTaskRequest,
69
+ A2AV1GetTaskRequest,
70
+ A2AV1ListTasksRequest,
71
+ A2AV1ListTasksResponse,
72
+ A2AV1Message,
73
+ A2AV1Part,
74
+ A2AV1Role,
75
+ A2AV1SendMessageConfiguration,
76
+ A2AV1SendMessageRequest,
77
+ A2AV1SendMessageResponse,
78
+ A2AV1StreamResponse,
79
+ A2AV1Task,
80
+ A2AV1TaskArtifactUpdateEvent,
81
+ A2AV1TaskState,
82
+ A2AV1TaskStatus,
83
+ A2AV1TaskStatusUpdateEvent,
84
+ )
85
+
86
+
87
+ __all__ = [
88
+ "A2A_V1_PROTOCOL_VERSION",
89
+ "A2AV1Artifact",
90
+ "A2AV1CancelTaskRequest",
91
+ "A2AV1GetTaskRequest",
92
+ "A2AV1ListTasksRequest",
93
+ "A2AV1ListTasksResponse",
94
+ "A2AV1Message",
95
+ "A2AV1Part",
96
+ "A2AV1Role",
97
+ "A2AV1SendMessageConfiguration",
98
+ "A2AV1SendMessageRequest",
99
+ "A2AV1SendMessageResponse",
100
+ "A2AV1StreamResponse",
101
+ "A2AV1Task",
102
+ "A2AV1TaskArtifactUpdateEvent",
103
+ "A2AV1TaskState",
104
+ "A2AV1TaskStatus",
105
+ "A2AV1TaskStatusUpdateEvent",
106
+ # Server / client.
107
+ "A2AClient",
108
+ "A2AServer",
109
+ # Spec models.
110
+ "AgentCapabilities",
111
+ "AgentCard",
112
+ "AgentInterface",
113
+ "AgentProvider",
114
+ "AgentSkill",
115
+ "Artifact",
116
+ "DataPart",
117
+ "FilePart",
118
+ "FileWithBytes",
119
+ "FileWithUri",
120
+ "JsonRpcError",
121
+ "JsonRpcErrorResponse",
122
+ "JsonRpcRequest",
123
+ "JsonRpcSuccessResponse",
124
+ "Message",
125
+ "MessageSendConfiguration",
126
+ "MessageSendParams",
127
+ "Part",
128
+ "PushNotificationAuthenticationInfo",
129
+ "PushNotificationConfig",
130
+ "Task",
131
+ "TaskArtifactUpdateEvent",
132
+ "TaskIdParams",
133
+ "TaskPushNotificationConfig",
134
+ "TaskQueryParams",
135
+ "TaskState",
136
+ "TaskStatus",
137
+ "TaskStatusUpdateEvent",
138
+ "TextPart",
139
+ # Legacy flat models (kept for the pre-spec wire surface).
140
+ "A2AMessage",
141
+ "A2ARequest",
142
+ "A2AResponse",
143
+ ]