aip-agents-binary 0.0.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 (447) hide show
  1. aip_agents/__init__.py +66 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1125 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/a2ui/__init__.py +26 -0
  10. aip_agents/a2ui/catalog_provider.py +109 -0
  11. aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
  12. aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
  13. aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
  14. aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
  15. aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
  16. aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
  17. aip_agents/a2ui/middleware.py +754 -0
  18. aip_agents/a2ui/models.py +153 -0
  19. aip_agents/a2ui/public_url.py +138 -0
  20. aip_agents/a2ui/stream_filter.py +67 -0
  21. aip_agents/agent/__init__.py +72 -0
  22. aip_agents/agent/base_agent.py +1437 -0
  23. aip_agents/agent/base_langgraph_agent.py +4048 -0
  24. aip_agents/agent/google_adk_agent.py +932 -0
  25. aip_agents/agent/google_adk_constants.py +11 -0
  26. aip_agents/agent/graph_runtime.py +185 -0
  27. aip_agents/agent/hitl/__init__.py +24 -0
  28. aip_agents/agent/hitl/config.py +28 -0
  29. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  30. aip_agents/agent/hitl/manager.py +532 -0
  31. aip_agents/agent/hitl/models.py +18 -0
  32. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  33. aip_agents/agent/hitl/prompt/base.py +42 -0
  34. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  35. aip_agents/agent/hitl/registry.py +149 -0
  36. aip_agents/agent/interface.py +138 -0
  37. aip_agents/agent/interfaces.py +65 -0
  38. aip_agents/agent/langflow_agent.py +464 -0
  39. aip_agents/agent/langgraph_react_agent.py +3741 -0
  40. aip_agents/agent/model_invocation_adapter.py +346 -0
  41. aip_agents/agent/system_instruction_context.py +34 -0
  42. aip_agents/agent/tool_execution_engine.py +587 -0
  43. aip_agents/audio_interface/__init__.py +30 -0
  44. aip_agents/audio_interface/audio_agent_adapter.py +254 -0
  45. aip_agents/audio_interface/config.py +146 -0
  46. aip_agents/audio_interface/errors.py +15 -0
  47. aip_agents/audio_interface/livekit_audio_session.py +504 -0
  48. aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
  49. aip_agents/audio_interface/session_factory.py +40 -0
  50. aip_agents/clients/__init__.py +10 -0
  51. aip_agents/clients/langflow/__init__.py +10 -0
  52. aip_agents/clients/langflow/client.py +477 -0
  53. aip_agents/clients/langflow/types.py +18 -0
  54. aip_agents/common/__init__.py +5 -0
  55. aip_agents/common/callable_introspection.py +17 -0
  56. aip_agents/constants.py +23 -0
  57. aip_agents/context/__init__.py +5 -0
  58. aip_agents/context/budget.py +237 -0
  59. aip_agents/context/compaction.py +339 -0
  60. aip_agents/context/metadata.py +169 -0
  61. aip_agents/context/offload.py +449 -0
  62. aip_agents/context/preparation.py +196 -0
  63. aip_agents/context/prompt.py +61 -0
  64. aip_agents/context/runtime.py +149 -0
  65. aip_agents/context/session_recall.py +69 -0
  66. aip_agents/context/state.py +48 -0
  67. aip_agents/context/utils.py +44 -0
  68. aip_agents/credentials/manager.py +132 -0
  69. aip_agents/examples/__init__.py +5 -0
  70. aip_agents/examples/audio_livekit_session.py +116 -0
  71. aip_agents/examples/compare_streaming_client.py +783 -0
  72. aip_agents/examples/compare_streaming_server.py +141 -0
  73. aip_agents/examples/gl_connectors_smoke.py +148 -0
  74. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  75. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  76. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  77. aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
  78. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  79. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  80. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  81. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  82. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  83. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
  84. aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
  85. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
  86. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  87. aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
  88. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  89. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  90. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
  91. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  92. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  93. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  94. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  95. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  96. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  97. aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
  98. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
  99. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
  100. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  101. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  102. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
  103. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  104. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
  105. aip_agents/examples/hello_world_filesystem.py +162 -0
  106. aip_agents/examples/hello_world_google_adk.py +41 -0
  107. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  108. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  109. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  110. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  111. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  112. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  113. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  114. aip_agents/examples/hello_world_langchain.py +28 -0
  115. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  116. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  117. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  118. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  119. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  120. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  121. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  122. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  123. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  124. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  125. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  126. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  127. aip_agents/examples/hello_world_langgraph.py +39 -0
  128. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
  129. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  130. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  131. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  132. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  133. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  134. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  135. aip_agents/examples/hello_world_langgraph_skills.py +62 -0
  136. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  137. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  138. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  139. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  140. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  141. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  142. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  143. aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
  144. aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
  145. aip_agents/examples/hello_world_pii_logger.py +21 -0
  146. aip_agents/examples/hello_world_ptc.py +49 -0
  147. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  148. aip_agents/examples/hello_world_sentry.py +133 -0
  149. aip_agents/examples/hello_world_step_limits.py +273 -0
  150. aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
  151. aip_agents/examples/hello_world_tool_output_client.py +55 -0
  152. aip_agents/examples/hello_world_tool_output_server.py +113 -0
  153. aip_agents/examples/hitl_demo.py +724 -0
  154. aip_agents/examples/mcp_configs/configs.py +63 -0
  155. aip_agents/examples/mcp_servers/common.py +76 -0
  156. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  157. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  158. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  159. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  160. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  161. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  162. aip_agents/examples/pii_demo_langgraph_server.py +125 -0
  163. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  164. aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
  165. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  166. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
  167. aip_agents/examples/tools/__init__.py +27 -0
  168. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  169. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  170. aip_agents/examples/tools/data_generator_tool.py +103 -0
  171. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  172. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  173. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  174. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  175. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  176. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  177. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  178. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  179. aip_agents/examples/tools/multiply_tool.py +43 -0
  180. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  181. aip_agents/examples/tools/random_chart_tool.py +142 -0
  182. aip_agents/examples/tools/serper_tool.py +202 -0
  183. aip_agents/examples/tools/stock_tools.py +82 -0
  184. aip_agents/examples/tools/table_generator_tool.py +167 -0
  185. aip_agents/examples/tools/time_tool.py +82 -0
  186. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  187. aip_agents/guardrails/__init__.py +83 -0
  188. aip_agents/guardrails/engines/__init__.py +69 -0
  189. aip_agents/guardrails/engines/base.py +90 -0
  190. aip_agents/guardrails/engines/nemo.py +101 -0
  191. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  192. aip_agents/guardrails/exceptions.py +39 -0
  193. aip_agents/guardrails/manager.py +163 -0
  194. aip_agents/guardrails/middleware.py +199 -0
  195. aip_agents/guardrails/schemas.py +63 -0
  196. aip_agents/guardrails/utils.py +45 -0
  197. aip_agents/integration/__init__.py +43 -0
  198. aip_agents/integration/agent.py +58 -0
  199. aip_agents/integration/guardrails.py +52 -0
  200. aip_agents/integration/hitl.py +71 -0
  201. aip_agents/integration/ptc.py +95 -0
  202. aip_agents/integration/skills.py +37 -0
  203. aip_agents/integration/storage.py +81 -0
  204. aip_agents/integration/version.py +103 -0
  205. aip_agents/mcp/__init__.py +1 -0
  206. aip_agents/mcp/client/__init__.py +60 -0
  207. aip_agents/mcp/client/base_mcp_client.py +377 -0
  208. aip_agents/mcp/client/connection_manager.py +231 -0
  209. aip_agents/mcp/client/errors.py +119 -0
  210. aip_agents/mcp/client/google_adk/__init__.py +45 -0
  211. aip_agents/mcp/client/google_adk/client.py +383 -0
  212. aip_agents/mcp/client/langchain/__init__.py +11 -0
  213. aip_agents/mcp/client/langchain/client.py +265 -0
  214. aip_agents/mcp/client/persistent_session.py +704 -0
  215. aip_agents/mcp/client/session_pool.py +358 -0
  216. aip_agents/mcp/client/transports.py +288 -0
  217. aip_agents/mcp/utils/__init__.py +7 -0
  218. aip_agents/mcp/utils/config_validator.py +139 -0
  219. aip_agents/memory/__init__.py +14 -0
  220. aip_agents/memory/adapters/__init__.py +10 -0
  221. aip_agents/memory/adapters/base_adapter.py +815 -0
  222. aip_agents/memory/adapters/mem0.py +286 -0
  223. aip_agents/memory/base.py +84 -0
  224. aip_agents/memory/constants.py +49 -0
  225. aip_agents/memory/factory.py +86 -0
  226. aip_agents/memory/guidance.py +20 -0
  227. aip_agents/memory/simple_memory.py +47 -0
  228. aip_agents/middleware/__init__.py +65 -0
  229. aip_agents/middleware/backends/__init__.py +72 -0
  230. aip_agents/middleware/backends/in_memory.py +578 -0
  231. aip_agents/middleware/backends/local_disk.py +1203 -0
  232. aip_agents/middleware/backends/protocol.py +353 -0
  233. aip_agents/middleware/backends/sandbox.py +1431 -0
  234. aip_agents/middleware/backends/sandbox_files.py +113 -0
  235. aip_agents/middleware/backends/utils.py +293 -0
  236. aip_agents/middleware/base.py +239 -0
  237. aip_agents/middleware/filesystem.py +690 -0
  238. aip_agents/middleware/manager.py +291 -0
  239. aip_agents/middleware/memory.py +532 -0
  240. aip_agents/middleware/pii_middleware.py +192 -0
  241. aip_agents/middleware/schema.py +19 -0
  242. aip_agents/middleware/skills.py +872 -0
  243. aip_agents/middleware/task_delegation.py +1656 -0
  244. aip_agents/middleware/todolist.py +276 -0
  245. aip_agents/middleware/tools/__init__.py +23 -0
  246. aip_agents/middleware/tools/edit_file.py +70 -0
  247. aip_agents/middleware/tools/execute.py +428 -0
  248. aip_agents/middleware/tools/grep_file.py +79 -0
  249. aip_agents/middleware/tools/ls.py +71 -0
  250. aip_agents/middleware/tools/read_file.py +239 -0
  251. aip_agents/middleware/tools/skill_load.py +193 -0
  252. aip_agents/middleware/tools/skill_resource.py +338 -0
  253. aip_agents/middleware/tools/write_file.py +114 -0
  254. aip_agents/middleware/vision.py +322 -0
  255. aip_agents/ptc/__init__.py +87 -0
  256. aip_agents/ptc/custom_tools.py +511 -0
  257. aip_agents/ptc/custom_tools_payload.py +499 -0
  258. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  259. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
  260. aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
  261. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
  262. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
  263. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  264. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
  265. aip_agents/ptc/doc_gen.py +122 -0
  266. aip_agents/ptc/exceptions.py +57 -0
  267. aip_agents/ptc/executor.py +627 -0
  268. aip_agents/ptc/mcp/__init__.py +45 -0
  269. aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
  270. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  271. aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
  272. aip_agents/ptc/naming.py +430 -0
  273. aip_agents/ptc/payload.py +26 -0
  274. aip_agents/ptc/prompt_builder.py +674 -0
  275. aip_agents/ptc/ptc_helper.py +16 -0
  276. aip_agents/ptc/sandbox_bridge.py +297 -0
  277. aip_agents/ptc/template_utils.py +33 -0
  278. aip_agents/ptc/templates/__init__.py +1 -0
  279. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  280. aip_agents/ptc/tool_def_helpers.py +101 -0
  281. aip_agents/ptc/tool_enrichment.py +163 -0
  282. aip_agents/resilience/__init__.py +10 -0
  283. aip_agents/resilience/base.py +46 -0
  284. aip_agents/resilience/tool.py +1661 -0
  285. aip_agents/sandbox/__init__.py +62 -0
  286. aip_agents/sandbox/defaults.py +220 -0
  287. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
  288. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
  289. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
  290. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
  291. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
  292. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
  293. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
  294. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
  295. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
  296. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
  297. aip_agents/sandbox/e2b_runtime.py +27 -0
  298. aip_agents/sandbox/image_registry.py +98 -0
  299. aip_agents/sandbox/providers/__init__.py +76 -0
  300. aip_agents/sandbox/providers/base.py +392 -0
  301. aip_agents/sandbox/providers/bedrock.py +28 -0
  302. aip_agents/sandbox/providers/e2b.py +230 -0
  303. aip_agents/sandbox/providers/opensandbox.py +163 -0
  304. aip_agents/sandbox/sandbox_runtime.py +967 -0
  305. aip_agents/sandbox/template_builder.py +240 -0
  306. aip_agents/sandbox/types.py +141 -0
  307. aip_agents/sandbox/validation.py +50 -0
  308. aip_agents/schema/__init__.py +106 -0
  309. aip_agents/schema/a2a.py +58 -0
  310. aip_agents/schema/agent.py +117 -0
  311. aip_agents/schema/artifacts.py +75 -0
  312. aip_agents/schema/attachments.py +425 -0
  313. aip_agents/schema/hitl.py +157 -0
  314. aip_agents/schema/langgraph.py +47 -0
  315. aip_agents/schema/model_id.py +103 -0
  316. aip_agents/schema/model_usage.py +50 -0
  317. aip_agents/schema/runtime_metadata.py +78 -0
  318. aip_agents/schema/step_limit.py +108 -0
  319. aip_agents/schema/storage.py +40 -0
  320. aip_agents/schema/tool_payloads.py +68 -0
  321. aip_agents/sentry/__init__.py +11 -0
  322. aip_agents/sentry/sentry.py +267 -0
  323. aip_agents/skills/__init__.py +30 -0
  324. aip_agents/skills/errors.py +110 -0
  325. aip_agents/skills/installer.py +479 -0
  326. aip_agents/skills/models.py +769 -0
  327. aip_agents/skills/stager.py +224 -0
  328. aip_agents/skills/validation.py +53 -0
  329. aip_agents/storage/__init__.py +41 -0
  330. aip_agents/storage/base.py +85 -0
  331. aip_agents/storage/clients/__init__.py +12 -0
  332. aip_agents/storage/clients/minio_client.py +318 -0
  333. aip_agents/storage/config.py +62 -0
  334. aip_agents/storage/providers/__init__.py +15 -0
  335. aip_agents/storage/providers/base.py +106 -0
  336. aip_agents/storage/providers/memory.py +114 -0
  337. aip_agents/storage/providers/object_storage.py +214 -0
  338. aip_agents/tools/__init__.py +65 -0
  339. aip_agents/tools/agent_schedule_tool.py +392 -0
  340. aip_agents/tools/browser_use/__init__.py +83 -0
  341. aip_agents/tools/browser_use/action_parser.py +104 -0
  342. aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
  343. aip_agents/tools/browser_use/llm_config.py +120 -0
  344. aip_agents/tools/browser_use/minio_storage.py +203 -0
  345. aip_agents/tools/browser_use/schemas.py +121 -0
  346. aip_agents/tools/browser_use/session.py +13 -0
  347. aip_agents/tools/browser_use/session_errors.py +132 -0
  348. aip_agents/tools/browser_use/steel_session_recording.py +321 -0
  349. aip_agents/tools/browser_use/streaming.py +263 -0
  350. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  351. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  352. aip_agents/tools/browser_use/types.py +37 -0
  353. aip_agents/tools/code_sandbox/__init__.py +48 -0
  354. aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
  355. aip_agents/tools/code_sandbox/constant.py +13 -0
  356. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
  357. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
  358. aip_agents/tools/computer_use/__init__.py +75 -0
  359. aip_agents/tools/computer_use/computer_use_tool.py +910 -0
  360. aip_agents/tools/computer_use/schemas.py +78 -0
  361. aip_agents/tools/computer_use/streaming.py +198 -0
  362. aip_agents/tools/computer_use/types.py +32 -0
  363. aip_agents/tools/constants.py +177 -0
  364. aip_agents/tools/date_range_tool.py +554 -0
  365. aip_agents/tools/document_loader/__init__.py +44 -0
  366. aip_agents/tools/document_loader/base_reader.py +302 -0
  367. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  368. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  369. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  370. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  371. aip_agents/tools/execute_ptc_code.py +582 -0
  372. aip_agents/tools/gl_connector/__init__.py +5 -0
  373. aip_agents/tools/gl_connector/tool.py +518 -0
  374. aip_agents/tools/gl_connector_tools.py +170 -0
  375. aip_agents/tools/memory_search/__init__.py +29 -0
  376. aip_agents/tools/memory_search/base.py +213 -0
  377. aip_agents/tools/memory_search/mem0.py +424 -0
  378. aip_agents/tools/memory_search/schema.py +81 -0
  379. aip_agents/tools/memory_search_tool.py +34 -0
  380. aip_agents/tools/streaming_common.py +577 -0
  381. aip_agents/tools/time_tool.py +117 -0
  382. aip_agents/tools/tool_config_injector.py +300 -0
  383. aip_agents/tools/web_search/__init__.py +15 -0
  384. aip_agents/tools/web_search/serper_tool.py +187 -0
  385. aip_agents/types/__init__.py +73 -0
  386. aip_agents/types/a2a_events.py +13 -0
  387. aip_agents/utils/__init__.py +91 -0
  388. aip_agents/utils/a2a_connector.py +1758 -0
  389. aip_agents/utils/artifact_helpers.py +542 -0
  390. aip_agents/utils/artifacts/__init__.py +20 -0
  391. aip_agents/utils/artifacts/tracker.py +367 -0
  392. aip_agents/utils/attachment_helpers.py +178 -0
  393. aip_agents/utils/constants.py +22 -0
  394. aip_agents/utils/datetime/__init__.py +34 -0
  395. aip_agents/utils/datetime/normalization.py +231 -0
  396. aip_agents/utils/datetime/timezone.py +206 -0
  397. aip_agents/utils/env_loader.py +27 -0
  398. aip_agents/utils/event_handler_registry.py +58 -0
  399. aip_agents/utils/file_prompt_utils.py +276 -0
  400. aip_agents/utils/file_watchers/__init__.py +33 -0
  401. aip_agents/utils/file_watchers/base.py +46 -0
  402. aip_agents/utils/file_watchers/e2b.py +128 -0
  403. aip_agents/utils/file_watchers/factory.py +39 -0
  404. aip_agents/utils/file_watchers/ls_diff.py +88 -0
  405. aip_agents/utils/file_watchers/sandbox.py +66 -0
  406. aip_agents/utils/file_watchers/watchdog.py +97 -0
  407. aip_agents/utils/final_response_builder.py +211 -0
  408. aip_agents/utils/formatter_llm_client.py +258 -0
  409. aip_agents/utils/langgraph/__init__.py +19 -0
  410. aip_agents/utils/langgraph/converter.py +399 -0
  411. aip_agents/utils/langgraph/delegation_context.py +22 -0
  412. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  413. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  414. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  415. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
  416. aip_agents/utils/langgraph/tool_output_management.py +1047 -0
  417. aip_agents/utils/logger.py +195 -0
  418. aip_agents/utils/metadata/__init__.py +27 -0
  419. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  420. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  421. aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
  422. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  423. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  424. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  425. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  426. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  427. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  428. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  429. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  430. aip_agents/utils/metadata_helper.py +363 -0
  431. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  432. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  433. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  434. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  435. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  436. aip_agents/utils/pii/__init__.py +29 -0
  437. aip_agents/utils/pii/pii_handler.py +555 -0
  438. aip_agents/utils/pii/pii_helper.py +225 -0
  439. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  440. aip_agents/utils/reference_helper.py +273 -0
  441. aip_agents/utils/sse_chunk_transformer.py +836 -0
  442. aip_agents/utils/step_limit_manager.py +265 -0
  443. aip_agents/utils/token_usage_helper.py +531 -0
  444. aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
  445. aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
  446. aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
  447. aip_agents_binary-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,169 @@
1
+ """Safe compaction metadata contracts for observability.
2
+
3
+ Provides the CompactionMetadata dataclass and helpers to build
4
+ safe A2A/SSE event metadata for compaction lifecycle events.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ _COMPACTION_ACTIVITY_ID = "context_compaction"
17
+ _AGENT_THINKING_STEP_KIND = "agent_thinking_step"
18
+
19
+ _STAGE_MESSAGES = {
20
+ "started": "Compacting context...",
21
+ "completed": "Context compacted",
22
+ "skipped": "Compaction skipped",
23
+ "failed": "Context compaction failed",
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CompactionMetadata:
29
+ """Safe observable metadata for a single compaction cycle.
30
+
31
+ All string fields are safe for logging, traces, and stream events.
32
+ Raw payloads, secrets, signed URLs, and base64 blobs are excluded
33
+ by construction.
34
+
35
+ Attributes:
36
+ summary_id: Opaque identifier for the compaction summary.
37
+ tokens_before: Estimated prompt tokens before compaction.
38
+ tokens_after: Estimated prompt tokens after compaction.
39
+ offloaded_ref_count: Number of payload references offloaded.
40
+ preservation_topic: Optional topic preserved during compaction.
41
+ """
42
+
43
+ summary_id: str | None
44
+ tokens_before: int | None
45
+ tokens_after: int | None
46
+ offloaded_ref_count: int
47
+ preservation_topic: str | None
48
+
49
+
50
+ def build_compaction_event_metadata(
51
+ *,
52
+ stage: str,
53
+ trigger_reason: str | None = None,
54
+ summary_status: str | None = None,
55
+ tokens_before: int | None = None,
56
+ tokens_after: int | None = None,
57
+ offloaded_ref_count: int = 0,
58
+ summary_id: str | None = None,
59
+ preservation_topic: str | None = None,
60
+ ) -> dict[str, Any]:
61
+ """Build safe A2A/SSE metadata dict for a compaction lifecycle event.
62
+
63
+ Args:
64
+ stage: Compaction lifecycle stage (started, completed, skipped, failed).
65
+ trigger_reason: Why compaction was triggered.
66
+ summary_status: Status of the summary generation.
67
+ tokens_before: Estimated tokens before compaction.
68
+ tokens_after: Estimated tokens after compaction.
69
+ offloaded_ref_count: Number of offloaded payload references.
70
+ summary_id: Opaque summary identifier.
71
+ preservation_topic: Optional topic preserved during compaction.
72
+
73
+ Returns:
74
+ Safe metadata dict for A2A/SSE status_update events.
75
+ Guarantees no raw payloads, secrets, signed URLs, or base64 blobs.
76
+ """
77
+ metadata: dict[str, Any] = {
78
+ "kind": _AGENT_THINKING_STEP_KIND,
79
+ "step_type": "context_compaction",
80
+ "stage": stage,
81
+ }
82
+
83
+ if summary_status:
84
+ metadata["summary_status"] = summary_status
85
+ if tokens_before is not None:
86
+ metadata["tokens_before"] = tokens_before
87
+ if tokens_after is not None:
88
+ metadata["tokens_after"] = tokens_after
89
+ if tokens_before is not None and tokens_after is not None:
90
+ metadata["estimated_token_savings"] = max(0, tokens_before - tokens_after)
91
+ if offloaded_ref_count:
92
+ metadata["offloaded_ref_count"] = offloaded_ref_count
93
+ if summary_id:
94
+ metadata["summary_id"] = summary_id
95
+ if trigger_reason:
96
+ metadata["trigger_reason"] = trigger_reason
97
+ if preservation_topic:
98
+ metadata["preservation_topic"] = preservation_topic
99
+
100
+ metadata["thinking_and_activity_info"] = _build_compaction_activity_info(metadata)
101
+ return metadata
102
+
103
+
104
+ def _build_compaction_activity_info(metadata: dict[str, Any]) -> dict[str, str]:
105
+ """Build the existing glchat sidebar activity payload shape."""
106
+ payload: dict[str, Any] = {
107
+ "type": "context_compaction",
108
+ "message": safe_compaction_content(str(metadata.get("stage", ""))),
109
+ "stage": metadata.get("stage", ""),
110
+ }
111
+ for key in (
112
+ "summary_status",
113
+ "trigger_reason",
114
+ "tokens_before",
115
+ "tokens_after",
116
+ "estimated_token_savings",
117
+ "offloaded_ref_count",
118
+ "summary_id",
119
+ "preservation_topic",
120
+ ):
121
+ if key in metadata:
122
+ payload[key] = metadata[key]
123
+
124
+ return {
125
+ "data_type": "activity",
126
+ "id": _COMPACTION_ACTIVITY_ID,
127
+ "data_value": json.dumps(payload, separators=(",", ":")),
128
+ }
129
+
130
+
131
+ def safe_compaction_content(stage: str) -> str:
132
+ """Return a safe, short content string for a compaction stream event.
133
+
134
+ Args:
135
+ stage: Compaction lifecycle stage.
136
+
137
+ Returns:
138
+ Short non-sensitive status text safe for stream event content.
139
+ """
140
+ return _STAGE_MESSAGES.get(stage, "Context compaction")
141
+
142
+
143
+ def build_compaction_metadata(
144
+ state: Any,
145
+ *,
146
+ tokens_before: int | None = None,
147
+ tokens_after: int | None = None,
148
+ preservation_topic: str | None = None,
149
+ summary_id_for_state: str | None = None,
150
+ ) -> CompactionMetadata:
151
+ """Build a CompactionMetadata from a CompactionState and extra params.
152
+
153
+ Args:
154
+ state: CompactionState from the context preparation result.
155
+ tokens_before: Estimated prompt tokens before compaction (overrides state).
156
+ tokens_after: Estimated prompt tokens after compaction (overrides state).
157
+ preservation_topic: Optional topic preserved during compaction.
158
+ summary_id_for_state: Optional summary ID from result.summary.summary_id.
159
+
160
+ Returns:
161
+ CompactionMetadata with safe fields only.
162
+ """
163
+ return CompactionMetadata(
164
+ summary_id=summary_id_for_state or getattr(state, "summary_id", None),
165
+ tokens_before=tokens_before,
166
+ tokens_after=tokens_after,
167
+ offloaded_ref_count=getattr(state, "offloaded_ref_count", 0),
168
+ preservation_topic=preservation_topic,
169
+ )
@@ -0,0 +1,449 @@
1
+ """Helpers for lossless payload offloading and safe context pruning.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import hashlib
11
+ import json
12
+ import re
13
+ import uuid
14
+ from collections.abc import Sequence
15
+ from dataclasses import dataclass
16
+ from typing import TYPE_CHECKING, Any, Literal
17
+
18
+ if TYPE_CHECKING: # pragma: no cover - only for type checking
19
+ from aip_agents.middleware.backends.protocol import BackendProtocol
20
+
21
+
22
+ def _estimate_tokens(text: str, chars_per_token: int = 4) -> int:
23
+ # deferred to #809: Use shared aip_agents.context.utils.estimate_tokens
24
+ if not text:
25
+ return 0
26
+ return max(1, (len(text) + chars_per_token - 1) // chars_per_token)
27
+
28
+
29
+ SourceKind = Literal["tool_input", "tool_result", "subagent_output", "attachment", "artifact"]
30
+
31
+ _SIGNED_URL_PATTERN = re.compile(r"(?i)(x-amz-signature|sig|signature)=")
32
+ _SIGNED_QUERY_KV_PATTERN = re.compile(r"(?i)\b(x-amz-signature|sig|signature)\s*=\s*[^&\s;]+")
33
+ _SECRET_KV_PATTERN = re.compile(r"(?i)(api[_-]?key|token|password|secret)\s*[:=]\s*[^\s]+")
34
+ _LONG_BASE64_PATTERN = re.compile(r"(?=[^+/=]*[+/=])[A-Za-z0-9+/=]{120,}")
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class OffloadedPayloadRef:
39
+ """Reference metadata for an offloaded payload.
40
+
41
+ Attributes:
42
+ ref_id: Stable identifier returned by the offload backend.
43
+ source_kind: Payload origin used for audit and downstream handling.
44
+ backend_uri: Backend-specific location for retrieving the full payload.
45
+ display_name: Safe human-readable name for the payload, if available.
46
+ mime_type: Payload MIME type, if known.
47
+ size_bytes: Original payload size in bytes, if known.
48
+ token_estimate: Estimated token cost of the original payload, if known.
49
+ checksum: Integrity checksum for the original payload, if available.
50
+ preview: Redacted inline preview used in the transformed message.
51
+ redaction_applied: Whether the preview was changed by secret redaction.
52
+ """
53
+
54
+ ref_id: str
55
+ source_kind: SourceKind
56
+ backend_uri: str
57
+ display_name: str | None
58
+ mime_type: str | None
59
+ size_bytes: int | None
60
+ token_estimate: int | None
61
+ checksum: str | None
62
+ preview: str
63
+ redaction_applied: bool
64
+
65
+
66
+ def offload_oversized_payloads(
67
+ messages: Sequence[dict[str, Any]],
68
+ *,
69
+ backend: BackendProtocol | None = None,
70
+ max_inline_chars: int = 4_000,
71
+ preview_chars: int = 400,
72
+ ) -> tuple[list[dict[str, Any]], list[OffloadedPayloadRef]]:
73
+ """Offload oversized or sensitive payload content from messages.
74
+
75
+ Message order and tool-call IDs are preserved.
76
+
77
+ Usage::
78
+
79
+ from aip_agents.context.offload import offload_oversized_payloads
80
+
81
+ messages = [
82
+ {"role": "tool", "name": "search", "content": "x" * 5000},
83
+ ]
84
+ transformed, refs = offload_oversized_payloads(
85
+ messages, backend=backend, max_inline_chars=4000
86
+ )
87
+ # transformed[0]["content"] contains a preview + ref link
88
+ # refs[0] has the OffloadedPayloadRef metadata
89
+ """
90
+ transformed: list[dict[str, Any]] = []
91
+ refs: list[OffloadedPayloadRef] = []
92
+
93
+ for message in messages:
94
+ msg = copy.deepcopy(message)
95
+ content = _extract_text_content(msg)
96
+
97
+ content_ref = _offload_content_field(
98
+ msg,
99
+ content,
100
+ backend=backend,
101
+ max_inline_chars=max_inline_chars,
102
+ preview_chars=preview_chars,
103
+ )
104
+ if content_ref is not None:
105
+ refs.append(content_ref)
106
+
107
+ refs.extend(
108
+ _offload_tool_call_arguments(
109
+ msg,
110
+ backend=backend,
111
+ max_inline_chars=max_inline_chars,
112
+ preview_chars=preview_chars,
113
+ )
114
+ )
115
+ transformed.append(msg)
116
+
117
+ return transformed, refs
118
+
119
+
120
+ def _offload_content_field(
121
+ message: dict[str, Any],
122
+ content: str,
123
+ *,
124
+ backend: BackendProtocol | None,
125
+ max_inline_chars: int,
126
+ preview_chars: int,
127
+ ) -> OffloadedPayloadRef | None:
128
+ if not _should_offload_content(content, max_inline_chars=max_inline_chars):
129
+ return None
130
+ if backend is None:
131
+ return None
132
+
133
+ ref = offload_payload(
134
+ content,
135
+ source_kind=_infer_source_kind(message),
136
+ display_name=_infer_display_name(message),
137
+ backend=backend,
138
+ preview_chars=preview_chars,
139
+ )
140
+ if ref is None:
141
+ return None
142
+ message["content"] = render_offloaded_reference_message(ref)
143
+ message["offloaded_ref_id"] = ref.ref_id
144
+ return ref
145
+
146
+
147
+ def _offload_tool_call_arguments(
148
+ message: dict[str, Any],
149
+ *,
150
+ backend: BackendProtocol | None,
151
+ max_inline_chars: int,
152
+ preview_chars: int,
153
+ ) -> list[OffloadedPayloadRef]:
154
+ tool_calls = message.get("tool_calls")
155
+ if not isinstance(tool_calls, list):
156
+ return []
157
+
158
+ refs: list[OffloadedPayloadRef] = []
159
+ for call in tool_calls:
160
+ if not isinstance(call, dict):
161
+ continue
162
+ arguments = _tool_call_arguments(call)
163
+ if arguments is None or not _should_offload_content(arguments, max_inline_chars=max_inline_chars):
164
+ continue
165
+ if backend is None:
166
+ continue
167
+ ref = offload_payload(
168
+ arguments,
169
+ source_kind="tool_input",
170
+ display_name=_tool_call_name(call) or _infer_display_name(message),
171
+ backend=backend,
172
+ preview_chars=preview_chars,
173
+ )
174
+ if ref is None:
175
+ continue
176
+ _replace_tool_call_arguments(
177
+ call, json.dumps({"context_payload_ref": ref.ref_id, "source": "tool_input", "preview": ref.preview})
178
+ )
179
+ refs.append(ref)
180
+
181
+ if refs:
182
+ message["offloaded_tool_call_ref_ids"] = [ref.ref_id for ref in refs]
183
+ return refs
184
+
185
+
186
+ def _tool_call_arguments(call: dict[str, Any]) -> str | None:
187
+ function = call.get("function")
188
+ if isinstance(function, dict) and isinstance(function.get("arguments"), str):
189
+ return function["arguments"]
190
+ args = call.get("args")
191
+ return args if isinstance(args, str) else None
192
+
193
+
194
+ def _replace_tool_call_arguments(call: dict[str, Any], replacement: str) -> None:
195
+ function = call.get("function")
196
+ if isinstance(function, dict) and isinstance(function.get("arguments"), str):
197
+ function["arguments"] = replacement
198
+ return
199
+ if isinstance(call.get("args"), str):
200
+ call["args"] = replacement
201
+
202
+
203
+ def _tool_call_name(call: dict[str, Any]) -> str | None:
204
+ if isinstance(call.get("name"), str):
205
+ return call["name"]
206
+ function = call.get("function")
207
+ if isinstance(function, dict) and isinstance(function.get("name"), str):
208
+ return function["name"]
209
+ return None
210
+
211
+
212
+ def offload_payload(
213
+ payload_text: str,
214
+ *,
215
+ source_kind: SourceKind,
216
+ display_name: str | None,
217
+ backend: BackendProtocol | None = None,
218
+ preview_chars: int = 400,
219
+ mime_type: str | None = "text/plain",
220
+ ) -> OffloadedPayloadRef | None:
221
+ """Create an offloaded payload reference and optional backend write.
222
+
223
+ Returns None when no backend is provided or the backend write fails.
224
+ """
225
+ ref_id = f"ctx_ref_{uuid.uuid4().hex[:12]}"
226
+ checksum = hashlib.sha256(payload_text.encode("utf-8")).hexdigest()
227
+ size_bytes = len(payload_text.encode("utf-8"))
228
+ preview, redaction_applied = _safe_preview(payload_text, max_chars=preview_chars)
229
+
230
+ if backend is None:
231
+ return None
232
+
233
+ backend_path = f"/context-offloads/{ref_id}.txt"
234
+ try:
235
+ write_result = backend.write(backend_path, payload_text)
236
+ except Exception:
237
+ return None
238
+
239
+ if not getattr(write_result, "success", False):
240
+ return None
241
+
242
+ token_estimate = _estimate_tokens(payload_text)
243
+ return OffloadedPayloadRef(
244
+ ref_id=ref_id,
245
+ source_kind=source_kind,
246
+ backend_uri=backend_path,
247
+ display_name=display_name,
248
+ mime_type=mime_type,
249
+ size_bytes=size_bytes,
250
+ token_estimate=token_estimate,
251
+ checksum=checksum,
252
+ preview=preview,
253
+ redaction_applied=redaction_applied,
254
+ )
255
+
256
+
257
+ def render_offloaded_reference_message(ref: OffloadedPayloadRef) -> str:
258
+ """Render model-visible replacement content for an offloaded payload."""
259
+ return (
260
+ "[Context payload offloaded]\n"
261
+ f"source: {ref.source_kind}\n"
262
+ f"ref: {ref.ref_id}\n"
263
+ f"name: {ref.display_name or 'unknown'}\n"
264
+ f"size: {ref.size_bytes or 0} bytes\n"
265
+ "preview:\n"
266
+ f"{ref.preview}\n"
267
+ "Use the runtime/backend ref to retrieve details if needed."
268
+ )
269
+
270
+
271
+ def prune_duplicate_tool_outputs(
272
+ messages: Sequence[dict[str, Any]],
273
+ *,
274
+ protected_tail_start_index: int,
275
+ ) -> list[dict[str, Any]]:
276
+ """Prune duplicate older tool outputs while preserving provider-valid pairs.
277
+
278
+ Usage::
279
+
280
+ from aip_agents.context.offload import prune_duplicate_tool_outputs
281
+
282
+ # Only tool results at or after protected_tail_start_index are kept;
283
+ # older duplicates are replaced with back-reference placeholders.
284
+ pruned = prune_duplicate_tool_outputs(messages, protected_tail_start_index=10)
285
+ """
286
+ transformed = [copy.deepcopy(message) for message in messages]
287
+ latest_by_fingerprint: dict[str, int] = {}
288
+
289
+ for index in range(len(transformed) - 1, -1, -1):
290
+ _replace_duplicate_tool_output(
291
+ transformed,
292
+ latest_by_fingerprint,
293
+ index,
294
+ protected_tail_start_index=protected_tail_start_index,
295
+ )
296
+
297
+ return transformed
298
+
299
+
300
+ def _replace_duplicate_tool_output(
301
+ messages: list[dict[str, Any]],
302
+ latest_by_fingerprint: dict[str, int],
303
+ index: int,
304
+ *,
305
+ protected_tail_start_index: int,
306
+ ) -> None:
307
+ message = messages[index]
308
+ if not _is_tool_result_message(message):
309
+ return
310
+
311
+ fingerprint = _tool_output_fingerprint(message)
312
+ if index >= protected_tail_start_index:
313
+ latest_by_fingerprint.setdefault(fingerprint, index)
314
+ return
315
+
316
+ newest_index = latest_by_fingerprint.get(fingerprint)
317
+ if newest_index is None:
318
+ latest_by_fingerprint[fingerprint] = index
319
+ return
320
+
321
+ _replace_with_tool_output_back_reference(message, newest_index, messages[newest_index].get("offloaded_ref_id"))
322
+
323
+
324
+ def _replace_with_tool_output_back_reference(message: dict[str, Any], newest_index: int, newest_ref: Any) -> None:
325
+ content = message.get("content")
326
+ if not isinstance(content, str):
327
+ return
328
+ message["content"] = (
329
+ "[Tool output back-reference]\n"
330
+ f"original_index: {newest_index}\n"
331
+ f"ref: {newest_ref or 'none'}\n"
332
+ "Use the newest protected-tail tool output for full details."
333
+ )
334
+
335
+
336
+ def repair_tool_result_pairing(messages: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
337
+ """Repair missing tool-result IDs when a single prior tool call is unambiguous.
338
+
339
+ Usage::
340
+
341
+ from aip_agents.context.offload import repair_tool_result_pairing
342
+
343
+ # After pruning or offloading, tool-call IDs may be missing from
344
+ # tool-result messages. This restores them when one unambiguous call
345
+ # precedes each result.
346
+ repaired = repair_tool_result_pairing(messages)
347
+ """
348
+ repaired = [copy.deepcopy(message) for message in messages]
349
+ recent_tool_call_ids: list[str] = []
350
+
351
+ for message in repaired:
352
+ recent_tool_call_ids = _collect_tool_call_ids(message, recent_tool_call_ids)
353
+ recent_tool_call_ids = _repair_tool_result_id(message, recent_tool_call_ids)
354
+
355
+ return repaired
356
+
357
+
358
+ def _collect_tool_call_ids(message: dict[str, Any], current: list[str]) -> list[str]:
359
+ tool_calls = message.get("tool_calls")
360
+ if not isinstance(tool_calls, list):
361
+ return current
362
+ fresh: list[str] = []
363
+ for call in tool_calls:
364
+ if isinstance(call, dict):
365
+ call_id = call.get("id")
366
+ if isinstance(call_id, str) and call_id:
367
+ fresh.append(call_id)
368
+ return fresh
369
+
370
+
371
+ def _repair_tool_result_id(message: dict[str, Any], recent_tool_call_ids: list[str]) -> list[str]:
372
+ if not _is_tool_result_message(message):
373
+ return recent_tool_call_ids
374
+ tool_call_id = message.get("tool_call_id")
375
+ if isinstance(tool_call_id, str) and tool_call_id:
376
+ return []
377
+ if len(recent_tool_call_ids) == 1:
378
+ message["tool_call_id"] = recent_tool_call_ids[0]
379
+ return []
380
+ return recent_tool_call_ids
381
+
382
+
383
+ def _extract_text_content(message: dict[str, Any]) -> str:
384
+ content = message.get("content")
385
+ if isinstance(content, str):
386
+ return content
387
+ return str(content or "")
388
+
389
+
390
+ def _should_offload_content(content: str, *, max_inline_chars: int) -> bool:
391
+ if len(content) > max_inline_chars:
392
+ return True
393
+ if _SECRET_KV_PATTERN.search(content):
394
+ return True
395
+ if _SIGNED_URL_PATTERN.search(content):
396
+ return True
397
+ if _LONG_BASE64_PATTERN.search(content):
398
+ return True
399
+ return False
400
+
401
+
402
+ def _infer_source_kind(message: dict[str, Any]) -> SourceKind:
403
+ explicit = message.get("source_kind")
404
+ if explicit in {"tool_input", "tool_result", "subagent_output", "attachment", "artifact"}:
405
+ return explicit
406
+
407
+ role = str(message.get("role", "")).lower()
408
+ if role in {"tool", "tool_result"}:
409
+ return "tool_result"
410
+ if role in {"user", "human"}:
411
+ return "tool_input"
412
+ return "artifact"
413
+
414
+
415
+ def _infer_display_name(message: dict[str, Any]) -> str | None:
416
+ for key in ("name", "tool_name", "title"):
417
+ value = message.get(key)
418
+ if isinstance(value, str) and value.strip():
419
+ return value.strip()
420
+ return None
421
+
422
+
423
+ def _safe_preview(
424
+ content: str, *, max_chars: int
425
+ ) -> tuple[str, bool]: # NOSONAR - intentionally redacts sensitive data
426
+ # deferred to #809: Use shared aip_agents.context.utils.safe_preview
427
+ redacted = _SIGNED_QUERY_KV_PATTERN.sub(lambda m: f"{m.group(1)}=[REDACTED]", content)
428
+ redacted = _SECRET_KV_PATTERN.sub(lambda m: f"{m.group(1)}=[REDACTED]", redacted)
429
+ redacted = _LONG_BASE64_PATTERN.sub("[BASE64_REDACTED]", redacted)
430
+ redacted = re.sub(r"(?i)(https?://[^\s?]+)\?[^\s]+", r"\1?[REDACTED_QUERY]", redacted)
431
+ redaction_applied = redacted != content
432
+
433
+ clipped = redacted[:max_chars]
434
+ if len(redacted) > max_chars:
435
+ clipped += "..."
436
+
437
+ return clipped, redaction_applied
438
+
439
+
440
+ def _is_tool_result_message(message: dict[str, Any]) -> bool:
441
+ role = str(message.get("role", "")).lower()
442
+ return role in {"tool", "tool_result"} or "tool_call_id" in message
443
+
444
+
445
+ def _tool_output_fingerprint(message: dict[str, Any]) -> str:
446
+ tool_name = str(message.get("name") or message.get("tool_name") or "")
447
+ content = _extract_text_content(message)
448
+ normalized = f"{tool_name}\n{content}".encode()
449
+ return hashlib.sha256(normalized).hexdigest()