polyrob 0.5.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 (745) hide show
  1. agents/README.md +453 -0
  2. agents/__init__.py +164 -0
  3. agents/base_agent.py +743 -0
  4. agents/personality/character.py +119 -0
  5. agents/personality/character_manager.py +309 -0
  6. agents/personality/characters/rob.character.json +56 -0
  7. agents/personality/characters/trump.character.json +63 -0
  8. agents/personality/persona_render.py +78 -0
  9. agents/personality/persona_resolver.py +115 -0
  10. agents/prompt/__init__.py +58 -0
  11. agents/prompt/base_prompt.py +92 -0
  12. agents/prompt/system.py +417 -0
  13. agents/task/README_PATH_MANAGEMENT.md +141 -0
  14. agents/task/__init__.py +162 -0
  15. agents/task/agent/__init__.py +70 -0
  16. agents/task/agent/agent_state.py +408 -0
  17. agents/task/agent/async_delegation.py +274 -0
  18. agents/task/agent/autonomy_state.py +275 -0
  19. agents/task/agent/conversation.py +75 -0
  20. agents/task/agent/core/__init__.py +6 -0
  21. agents/task/agent/core/aux_metering.py +61 -0
  22. agents/task/agent/core/background_review.py +157 -0
  23. agents/task/agent/core/construction.py +1179 -0
  24. agents/task/agent/core/conversational_exit.py +49 -0
  25. agents/task/agent/core/correspondent_gate.py +227 -0
  26. agents/task/agent/core/curator.py +246 -0
  27. agents/task/agent/core/episodic_digest.py +103 -0
  28. agents/task/agent/core/error_recovery.py +480 -0
  29. agents/task/agent/core/history_io.py +257 -0
  30. agents/task/agent/core/llm_provisioning.py +287 -0
  31. agents/task/agent/core/llm_runner.py +547 -0
  32. agents/task/agent/core/logging_io.py +276 -0
  33. agents/task/agent/core/loop_detection.py +262 -0
  34. agents/task/agent/core/memory_prefetch.py +325 -0
  35. agents/task/agent/core/memory_writer.py +667 -0
  36. agents/task/agent/core/model_introspection.py +82 -0
  37. agents/task/agent/core/model_swap.py +177 -0
  38. agents/task/agent/core/next_action_internal.py +1331 -0
  39. agents/task/agent/core/output_validation.py +303 -0
  40. agents/task/agent/core/project_context.py +272 -0
  41. agents/task/agent/core/resources.py +93 -0
  42. agents/task/agent/core/result_processing.py +389 -0
  43. agents/task/agent/core/run_loop.py +661 -0
  44. agents/task/agent/core/safety_lifecycle.py +228 -0
  45. agents/task/agent/core/secret_guard.py +293 -0
  46. agents/task/agent/core/self_wake.py +280 -0
  47. agents/task/agent/core/session_metadata.py +104 -0
  48. agents/task/agent/core/step.py +781 -0
  49. agents/task/agent/core/step_execution.py +398 -0
  50. agents/task/agent/core/step_telemetry.py +222 -0
  51. agents/task/agent/core/test_aux_metering.py +50 -0
  52. agents/task/agent/core/test_billing_failover.py +68 -0
  53. agents/task/agent/core/test_fatal_error_classifier.py +38 -0
  54. agents/task/agent/core/test_judge_validation.py +275 -0
  55. agents/task/agent/core/test_model_swap.py +190 -0
  56. agents/task/agent/core/test_reflection_offload.py +54 -0
  57. agents/task/agent/core/turn_input.py +8 -0
  58. agents/task/agent/core/untrusted_wrap.py +103 -0
  59. agents/task/agent/core/user_ingress.py +228 -0
  60. agents/task/agent/hitl_manager.py +396 -0
  61. agents/task/agent/log_sanitize.py +36 -0
  62. agents/task/agent/message_manager/config.py +97 -0
  63. agents/task/agent/message_manager/service.py +758 -0
  64. agents/task/agent/message_manager/tests.py +305 -0
  65. agents/task/agent/message_manager/tool_call_builder.py +539 -0
  66. agents/task/agent/message_manager/tool_message_repair.py +364 -0
  67. agents/task/agent/message_manager/views.py +143 -0
  68. agents/task/agent/messages/__init__.py +8 -0
  69. agents/task/agent/messages/builders.py +309 -0
  70. agents/task/agent/messages/compactor.py +611 -0
  71. agents/task/agent/messages/context_references.py +271 -0
  72. agents/task/agent/messages/filters.py +333 -0
  73. agents/task/agent/messages/guidance.py +328 -0
  74. agents/task/agent/messages/persistence.py +448 -0
  75. agents/task/agent/messages/retrieval.py +506 -0
  76. agents/task/agent/messages/sqlite_persistence.py +85 -0
  77. agents/task/agent/messages/test_compaction_upgrade.py +349 -0
  78. agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
  79. agents/task/agent/messages/test_sqlite_persistence.py +54 -0
  80. agents/task/agent/messages/token_counter.py +532 -0
  81. agents/task/agent/orchestrator.py +776 -0
  82. agents/task/agent/profile_manager.py +105 -0
  83. agents/task/agent/profile_registry.py +173 -0
  84. agents/task/agent/prompts.py +1090 -0
  85. agents/task/agent/scenario_registry.py +226 -0
  86. agents/task/agent/service.py +509 -0
  87. agents/task/agent/session.py +984 -0
  88. agents/task/agent/skill_discovery.py +110 -0
  89. agents/task/agent/skill_frontmatter.py +94 -0
  90. agents/task/agent/skill_manager.py +1201 -0
  91. agents/task/agent/skill_store.py +627 -0
  92. agents/task/agent/skill_validation.py +56 -0
  93. agents/task/agent/skill_writer.py +492 -0
  94. agents/task/agent/sub_agent_manager.py +1137 -0
  95. agents/task/agent/test_session_disk_recovery.py +34 -0
  96. agents/task/agent/tests.py +513 -0
  97. agents/task/agent/tool_call_tracker.py +511 -0
  98. agents/task/agent/views.py +469 -0
  99. agents/task/config.py +432 -0
  100. agents/task/constants.py +1384 -0
  101. agents/task/flag_defaults.py +63 -0
  102. agents/task/goals/__init__.py +1 -0
  103. agents/task/goals/autonomy_marker.py +28 -0
  104. agents/task/goals/board.py +690 -0
  105. agents/task/goals/completion_judge.py +247 -0
  106. agents/task/goals/context.py +62 -0
  107. agents/task/goals/dispatcher.py +848 -0
  108. agents/task/goals/escalation.py +102 -0
  109. agents/task/goals/planner.py +114 -0
  110. agents/task/logging_config.py +155 -0
  111. agents/task/path.py +991 -0
  112. agents/task/robust_parse_config.py +400 -0
  113. agents/task/runtime/__init__.py +0 -0
  114. agents/task/runtime/run_as_session.py +130 -0
  115. agents/task/runtime_safety.py +308 -0
  116. agents/task/session/__init__.py +5 -0
  117. agents/task/session/browser_pool.py +130 -0
  118. agents/task/session/cleanup.py +612 -0
  119. agents/task/session/execution.py +397 -0
  120. agents/task/session/feed.py +200 -0
  121. agents/task/session/hitl_ingress.py +201 -0
  122. agents/task/session/hooks.py +83 -0
  123. agents/task/session/multi_agent.py +243 -0
  124. agents/task/session/workspace.py +91 -0
  125. agents/task/session_registry.py +91 -0
  126. agents/task/session_route.py +35 -0
  127. agents/task/sqlite_session_registry.py +261 -0
  128. agents/task/surface_config.py +185 -0
  129. agents/task/telemetry/__init__.py +58 -0
  130. agents/task/telemetry/event_log.py +180 -0
  131. agents/task/telemetry/formatters.py +642 -0
  132. agents/task/telemetry/manager.py +571 -0
  133. agents/task/telemetry/memory_events.py +66 -0
  134. agents/task/telemetry/self_events.py +53 -0
  135. agents/task/telemetry/sequence.py +160 -0
  136. agents/task/telemetry/service.py +1265 -0
  137. agents/task/telemetry/views.py +882 -0
  138. agents/task/templates.py +98 -0
  139. agents/task/test_aux_chain.py +151 -0
  140. agents/task/test_aux_router.py +41 -0
  141. agents/task/tests/test_parse_robustness.py +170 -0
  142. agents/task/tool_defaults.py +110 -0
  143. agents/task/utils.py +762 -0
  144. agents/task/utils_json.py +768 -0
  145. agents/task/utils_webview.py +86 -0
  146. agents/task/workspace_context.py +229 -0
  147. agents/task_agent_lite.py +2398 -0
  148. api/README.md +683 -0
  149. api/TASK_API_DOCS.md +151 -0
  150. api/__init__.py +0 -0
  151. api/a2a/__init__.py +37 -0
  152. api/a2a/agent_card.py +337 -0
  153. api/a2a/client.py +657 -0
  154. api/a2a/endpoints.py +375 -0
  155. api/a2a/models.py +268 -0
  156. api/a2a/streaming.py +419 -0
  157. api/a2a/task_handler.py +929 -0
  158. api/admin_endpoints.py +1584 -0
  159. api/app.py +923 -0
  160. api/auth_constants.py +28 -0
  161. api/auth_endpoints.py +402 -0
  162. api/auth_state.py +36 -0
  163. api/chat_via_task.py +42 -0
  164. api/conversation_manager.py +231 -0
  165. api/dependencies.py +186 -0
  166. api/eip8004_endpoints.py +452 -0
  167. api/hyperliquid_models.py +174 -0
  168. api/hyperliquid_routes.py +450 -0
  169. api/interfaces.py +207 -0
  170. api/jwt_middleware.py +83 -0
  171. api/kb/__init__.py +1 -0
  172. api/kb/endpoints.py +314 -0
  173. api/mcp_models.py +279 -0
  174. api/mcp_routes.py +438 -0
  175. api/middleware.py +526 -0
  176. api/models.py +176 -0
  177. api/openai_compat/__init__.py +0 -0
  178. api/openai_compat/model_map.py +25 -0
  179. api/openai_compat/models.py +59 -0
  180. api/openai_compat/router.py +140 -0
  181. api/payment_endpoints.py +444 -0
  182. api/payment_verification.py +138 -0
  183. api/polymarket_models.py +155 -0
  184. api/polymarket_routes.py +501 -0
  185. api/pricing_endpoints.py +89 -0
  186. api/session_routing.py +54 -0
  187. api/skill_endpoints.py +453 -0
  188. api/task_http_api.py +1856 -0
  189. api/webhooks.py +52 -0
  190. api/x402_endpoints.py +280 -0
  191. avatar/README.md +44 -0
  192. avatar/__init__.py +1 -0
  193. avatar/config/rob.json +7 -0
  194. avatar/mindprint.js +395 -0
  195. avatar/renders/rob.meta.json +20 -0
  196. avatar/renders/rob.png +0 -0
  197. avatar/studio.html +373 -0
  198. avatar/webview/avatar-live.js +44 -0
  199. cli/README.md +89 -0
  200. cli/__init__.py +0 -0
  201. cli/cli_surface.py +67 -0
  202. cli/commands/__init__.py +0 -0
  203. cli/commands/_bootstrap.py +49 -0
  204. cli/commands/_errors.py +53 -0
  205. cli/commands/chat.py +973 -0
  206. cli/commands/config.py +100 -0
  207. cli/commands/dashboard.py +103 -0
  208. cli/commands/doctor.py +222 -0
  209. cli/commands/email.py +174 -0
  210. cli/commands/gateway.py +346 -0
  211. cli/commands/goals.py +509 -0
  212. cli/commands/init.py +276 -0
  213. cli/commands/journey.py +30 -0
  214. cli/commands/kb.py +280 -0
  215. cli/commands/model.py +107 -0
  216. cli/commands/owner.py +431 -0
  217. cli/commands/pfp.py +232 -0
  218. cli/commands/run.py +414 -0
  219. cli/commands/serve.py +41 -0
  220. cli/commands/session.py +684 -0
  221. cli/commands/skill_install.py +786 -0
  222. cli/commands/skills.py +128 -0
  223. cli/commands/subagents.py +100 -0
  224. cli/commands/surface.py +81 -0
  225. cli/commands/telegram.py +185 -0
  226. cli/commands/todos.py +209 -0
  227. cli/commands/tools.py +102 -0
  228. cli/commands/update.py +316 -0
  229. cli/commands/whatsapp.py +175 -0
  230. cli/config_store.py +276 -0
  231. cli/gitignore.py +43 -0
  232. cli/inventory.py +46 -0
  233. cli/keys.py +101 -0
  234. cli/persona.py +17 -0
  235. cli/polyrob.py +137 -0
  236. cli/ui/__init__.py +64 -0
  237. cli/ui/activity.py +141 -0
  238. cli/ui/app.py +538 -0
  239. cli/ui/banner.py +233 -0
  240. cli/ui/blocks.py +534 -0
  241. cli/ui/bootstrap_notice.py +47 -0
  242. cli/ui/commands/__init__.py +41 -0
  243. cli/ui/commands/h_cron.py +64 -0
  244. cli/ui/commands/h_journey.py +164 -0
  245. cli/ui/commands/h_kb.py +121 -0
  246. cli/ui/commands/h_learn.py +100 -0
  247. cli/ui/commands/h_mcp.py +215 -0
  248. cli/ui/commands/h_self.py +113 -0
  249. cli/ui/commands/h_skills.py +314 -0
  250. cli/ui/commands/handlers.py +1672 -0
  251. cli/ui/commands/registry.py +426 -0
  252. cli/ui/dialog.py +425 -0
  253. cli/ui/event_registry.py +110 -0
  254. cli/ui/event_specs.py +45 -0
  255. cli/ui/events.py +433 -0
  256. cli/ui/identity.py +15 -0
  257. cli/ui/lifecycle.py +159 -0
  258. cli/ui/live_hooks.py +43 -0
  259. cli/ui/model_selector.py +532 -0
  260. cli/ui/persistent_loop.py +181 -0
  261. cli/ui/pick.py +113 -0
  262. cli/ui/plain_renderer.py +351 -0
  263. cli/ui/renderer.py +384 -0
  264. cli/ui/rich_renderer.py +533 -0
  265. cli/ui/secrets.py +115 -0
  266. cli/ui/state.py +376 -0
  267. cli/ui/statusbar.py +231 -0
  268. cli/ui/streaming.py +254 -0
  269. cli/ui/terminal_render.py +105 -0
  270. cli/ui/theme.py +132 -0
  271. cli/update/__init__.py +6 -0
  272. cli/update/context.py +121 -0
  273. cli/update/detect.py +176 -0
  274. cli/update/engine.py +95 -0
  275. cli/update/migrate_guarded.py +55 -0
  276. cli/update/process_guard.py +215 -0
  277. cli/update/runners.py +78 -0
  278. cli/update/snapshot.py +270 -0
  279. cli/update/versions.py +205 -0
  280. core/README.md +377 -0
  281. core/__init__.py +174 -0
  282. core/assets.py +87 -0
  283. core/async_bridge.py +76 -0
  284. core/autonomy_runtime.py +259 -0
  285. core/base_component.py +302 -0
  286. core/bootstrap.py +681 -0
  287. core/bot.py +310 -0
  288. core/config.py +833 -0
  289. core/constants.py +126 -0
  290. core/container.py +462 -0
  291. core/db_manifest.py +101 -0
  292. core/embedding.py +57 -0
  293. core/env.py +39 -0
  294. core/exceptions.py +287 -0
  295. core/flags.py +144 -0
  296. core/flags_catalog.py +345 -0
  297. core/home_migration.py +62 -0
  298. core/identity.py +118 -0
  299. core/initialization.py +1004 -0
  300. core/instance.py +537 -0
  301. core/interactive_gate.py +96 -0
  302. core/logging.py +436 -0
  303. core/owner_doc_writer.py +176 -0
  304. core/pairing.py +193 -0
  305. core/path_safety.py +20 -0
  306. core/paths.py +27 -0
  307. core/payment_config.py +21 -0
  308. core/permissions.py +322 -0
  309. core/runtime_config.py +96 -0
  310. core/runtime_paths.py +135 -0
  311. core/seams.py +35 -0
  312. core/secret_scan.py +25 -0
  313. core/secret_scrub.py +71 -0
  314. core/secrets.py +35 -0
  315. core/security_logging_filter.py +148 -0
  316. core/self_context_writer.py +333 -0
  317. core/self_evolution.py +294 -0
  318. core/session_context.py +47 -0
  319. core/sqlite_util.py +59 -0
  320. core/surfaces/__init__.py +16 -0
  321. core/surfaces/access.py +118 -0
  322. core/surfaces/binding.py +88 -0
  323. core/surfaces/bootstrap.py +78 -0
  324. core/surfaces/circuit.py +168 -0
  325. core/surfaces/continuity.py +77 -0
  326. core/surfaces/correspondents.py +228 -0
  327. core/surfaces/dispatcher.py +216 -0
  328. core/surfaces/envelopes.py +75 -0
  329. core/surfaces/gc.py +30 -0
  330. core/surfaces/idempotency.py +57 -0
  331. core/surfaces/inbound_webhook.py +124 -0
  332. core/surfaces/media.py +32 -0
  333. core/surfaces/message_router.py +91 -0
  334. core/surfaces/outbound_allowlist.py +80 -0
  335. core/surfaces/outbound_dispatcher.py +110 -0
  336. core/surfaces/outbound_mirror.py +37 -0
  337. core/surfaces/outbound_queue.py +107 -0
  338. core/surfaces/outbound_target.py +12 -0
  339. core/surfaces/owner_admin.py +29 -0
  340. core/surfaces/proactive.py +78 -0
  341. core/surfaces/progress.py +108 -0
  342. core/surfaces/rate_bucket.py +21 -0
  343. core/surfaces/registry.py +51 -0
  344. core/surfaces/rendering.py +53 -0
  345. core/surfaces/send_policy.py +14 -0
  346. core/surfaces/serialize.py +24 -0
  347. core/surfaces/session_chat_registry.py +112 -0
  348. core/surfaces/session_policy.py +60 -0
  349. core/surfaces/surface.py +288 -0
  350. core/surfaces/transcription.py +88 -0
  351. core/surfaces/voice_echo.py +26 -0
  352. core/surfaces/voice_guard.py +23 -0
  353. core/tickers.py +174 -0
  354. core/tool_catalog.py +192 -0
  355. core/version.py +75 -0
  356. core/wallet/__init__.py +5 -0
  357. core/wallet/agent_wallet.py +59 -0
  358. core/wallet/audit_sink.py +61 -0
  359. core/wallet/config.py +70 -0
  360. core/wallet/factory.py +87 -0
  361. core/wallet/policy.py +104 -0
  362. core/wallet/signer.py +51 -0
  363. cron/README.md +65 -0
  364. cron/__init__.py +0 -0
  365. cron/delivery.py +277 -0
  366. cron/digest.py +124 -0
  367. cron/jobs.py +207 -0
  368. cron/runner.py +297 -0
  369. cron/schedule.py +194 -0
  370. cron/scheduler.py +201 -0
  371. cron/service.py +57 -0
  372. cron/wake_gate.py +223 -0
  373. data/__init__.py +1 -0
  374. data/prompts/autov2_prompts.json +87 -0
  375. data/prompts/skills/browser-automation/SKILL.md +43 -0
  376. data/prompts/skills/coding-workflow/SKILL.md +36 -0
  377. data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
  378. data/prompts/skills/document-writing/SKILL.md +33 -0
  379. data/prompts/skills/email-comms/SKILL.md +33 -0
  380. data/prompts/skills/file-data-ops/SKILL.md +35 -0
  381. data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
  382. data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
  383. data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
  384. data/prompts/skills/lead-research/SKILL.md +86 -0
  385. data/prompts/skills/market-research-brief/SKILL.md +104 -0
  386. data/prompts/skills/person-analyzer/SKILL.md +124 -0
  387. data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
  388. data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
  389. data/prompts/skills/polymarket-trading/SKILL.md +55 -0
  390. data/prompts/skills/presentation-creator/SKILL.md +36 -0
  391. data/prompts/skills/project-analyzer/SKILL.md +169 -0
  392. data/prompts/skills/rules.json +268 -0
  393. data/prompts/skills/secret-handling/SKILL.md +20 -0
  394. data/prompts/skills/skill-authoring/SKILL.md +31 -0
  395. data/prompts/skills/skill-security-review/SKILL.md +27 -0
  396. data/prompts/skills/social-discovery/SKILL.md +57 -0
  397. data/prompts/skills/task-planning/SKILL.md +33 -0
  398. data/prompts/skills/web-research/SKILL.md +39 -0
  399. data/prompts/skills/web-scraping/SKILL.md +73 -0
  400. data/prompts/skills/x-engagement/SKILL.md +69 -0
  401. data/prompts/system_prompts.json +4 -0
  402. data/subscription_channels.json +5 -0
  403. modules/README.md +503 -0
  404. modules/__init__.py +124 -0
  405. modules/auth/__init__.py +13 -0
  406. modules/auth/api_key_manager.py +157 -0
  407. modules/auth/identity_mapper.py +380 -0
  408. modules/auth/siwe_auth.py +273 -0
  409. modules/auth/tier_manager.py +173 -0
  410. modules/base_module.py +190 -0
  411. modules/credits/__init__.py +29 -0
  412. modules/credits/balance_manager.py +237 -0
  413. modules/credits/cost_utils.py +129 -0
  414. modules/credits/pricing.py +139 -0
  415. modules/credits/unified_ledger.py +148 -0
  416. modules/credits/usage_meter.py +310 -0
  417. modules/credits/usage_tracker.py +674 -0
  418. modules/database/__init__.py +33 -0
  419. modules/database/audit_log.py +322 -0
  420. modules/database/auth_tables.py +276 -0
  421. modules/database/connection.py +644 -0
  422. modules/database/connection_pool.py +305 -0
  423. modules/database/conversation_contexts.py +446 -0
  424. modules/database/database_manager.py +237 -0
  425. modules/database/hyperliquid.py +375 -0
  426. modules/database/polymarket.py +574 -0
  427. modules/database/user_mcp_servers.py +728 -0
  428. modules/database/user_profiles.py +562 -0
  429. modules/database/utils.py +107 -0
  430. modules/database/x402_tables.py +92 -0
  431. modules/eip8004/README.md +1069 -0
  432. modules/eip8004/__init__.py +65 -0
  433. modules/eip8004/contracts.py +498 -0
  434. modules/eip8004/models.py +271 -0
  435. modules/eip8004/registration.py +148 -0
  436. modules/eip8004/reputation.py +367 -0
  437. modules/eip8004/validation.py +277 -0
  438. modules/llm/__init__.py +97 -0
  439. modules/llm/adapters.py +963 -0
  440. modules/llm/anthropic_client.py +831 -0
  441. modules/llm/available_models.py +106 -0
  442. modules/llm/brain_scrubber.py +137 -0
  443. modules/llm/cache_hints.py +162 -0
  444. modules/llm/deepseek_client.py +562 -0
  445. modules/llm/gemini_client.py +1367 -0
  446. modules/llm/llm_client.py +1139 -0
  447. modules/llm/llm_client_registry.py +188 -0
  448. modules/llm/llm_factory.py +237 -0
  449. modules/llm/llm_manager.py +1036 -0
  450. modules/llm/messages.py +374 -0
  451. modules/llm/model_registry.py +1935 -0
  452. modules/llm/nvidia_client.py +125 -0
  453. modules/llm/openai_client.py +800 -0
  454. modules/llm/openrouter_client.py +969 -0
  455. modules/llm/profiles.py +178 -0
  456. modules/llm/test_anthropic_cache_capture.py +35 -0
  457. modules/llm/test_cached_pricing_derivation.py +24 -0
  458. modules/llm/think_scrubber.py +444 -0
  459. modules/llm/token_counter.py +268 -0
  460. modules/memory/__init__.py +35 -0
  461. modules/memory/backend_factory.py +83 -0
  462. modules/memory/cache_manager.py +186 -0
  463. modules/memory/episodic.py +108 -0
  464. modules/memory/local_vector_memory_provider.py +552 -0
  465. modules/memory/memory_manager.py +225 -0
  466. modules/memory/models.py +312 -0
  467. modules/memory/provider.py +210 -0
  468. modules/memory/registry.py +205 -0
  469. modules/memory/sqlite_memory_provider.py +767 -0
  470. modules/memory/task/__init__.py +52 -0
  471. modules/memory/task/compaction_manager.py +157 -0
  472. modules/memory/task/context_retriever.py +809 -0
  473. modules/memory/task/hierarchical_memory.py +1002 -0
  474. modules/memory/task/lexical_retriever.py +71 -0
  475. modules/memory/task/null_context_manager.py +213 -0
  476. modules/memory/task/phase_manager.py +625 -0
  477. modules/memory/task/reflection_service.py +157 -0
  478. modules/memory/task/semantic_retriever.py +218 -0
  479. modules/memory/task/task_context_manager.py +1165 -0
  480. modules/memory/task/test_forgetting_engages.py +24 -0
  481. modules/memory/task/test_reflection_llm.py +39 -0
  482. modules/memory/task/test_threat_scan.py +53 -0
  483. modules/memory/task/threat_scan.py +90 -0
  484. modules/memory/test_sqlite_memory_provider.py +106 -0
  485. modules/memory/user_profile_manager.py +527 -0
  486. modules/payments/__init__.py +11 -0
  487. modules/payments/deposit_monitor.py +478 -0
  488. modules/payments/price_oracle.py +70 -0
  489. modules/payments/treasury_sweeper.py +401 -0
  490. modules/payments/wallet_generator.py +145 -0
  491. modules/pfp/__init__.py +1 -0
  492. modules/pfp/config.py +92 -0
  493. modules/pfp/mesh.py +565 -0
  494. modules/pfp/push.py +69 -0
  495. modules/pfp/renderer.py +85 -0
  496. modules/pfp/store.py +82 -0
  497. modules/skills/__init__.py +1 -0
  498. modules/skills/skill_usage.py +248 -0
  499. modules/transcription/__init__.py +24 -0
  500. modules/transcription/base.py +20 -0
  501. modules/transcription/faster_whisper_transcriber.py +55 -0
  502. modules/x402/README.md +249 -0
  503. modules/x402/__init__.py +22 -0
  504. modules/x402/invoicing.py +416 -0
  505. modules/x402/middleware.py +364 -0
  506. modules/x402/settlement_watcher.py +145 -0
  507. modules/x402/x402_integration.py +344 -0
  508. polyrob-0.5.0.dist-info/METADATA +482 -0
  509. polyrob-0.5.0.dist-info/RECORD +745 -0
  510. polyrob-0.5.0.dist-info/WHEEL +5 -0
  511. polyrob-0.5.0.dist-info/entry_points.txt +2 -0
  512. polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
  513. polyrob-0.5.0.dist-info/top_level.txt +12 -0
  514. surfaces/README.md +70 -0
  515. surfaces/__init__.py +0 -0
  516. surfaces/email/__init__.py +0 -0
  517. surfaces/email/dedup.py +44 -0
  518. surfaces/email/harness.py +187 -0
  519. surfaces/email/inbound.py +159 -0
  520. surfaces/email/seed.py +60 -0
  521. surfaces/email/surface.py +105 -0
  522. surfaces/telegram/__init__.py +0 -0
  523. surfaces/telegram/dedup.py +83 -0
  524. surfaces/telegram/harness.py +886 -0
  525. surfaces/telegram/inbound.py +145 -0
  526. surfaces/telegram/interactive_tools.py +49 -0
  527. surfaces/telegram/markdown.py +347 -0
  528. surfaces/telegram/rate_limit.py +167 -0
  529. surfaces/telegram/surface.py +187 -0
  530. surfaces/telegram/voice.py +52 -0
  531. surfaces/whatsapp/__init__.py +0 -0
  532. surfaces/whatsapp/client.py +51 -0
  533. surfaces/whatsapp/harness.py +62 -0
  534. surfaces/whatsapp/inbound.py +132 -0
  535. surfaces/whatsapp/surface.py +64 -0
  536. surfaces/whatsapp/window.py +34 -0
  537. tools/README.md +578 -0
  538. tools/__init__.py +424 -0
  539. tools/alchemy/__init__.py +5 -0
  540. tools/alchemy/alchemy_tool.py +389 -0
  541. tools/anysite/__init__.py +17 -0
  542. tools/anysite/client.py +88 -0
  543. tools/anysite/tool.py +89 -0
  544. tools/base_tool.py +468 -0
  545. tools/browser/__init__.py +22 -0
  546. tools/browser/actions.py +118 -0
  547. tools/browser/browser.py +1457 -0
  548. tools/browser/browser_manager.py +690 -0
  549. tools/browser/context.py +1748 -0
  550. tools/browser/playwright_utils.py +137 -0
  551. tools/browser/views.py +56 -0
  552. tools/code_exec/SANDBOX_SECURITY.md +144 -0
  553. tools/code_exec/__init__.py +178 -0
  554. tools/code_exec/backend.py +73 -0
  555. tools/code_exec/backends/__init__.py +1 -0
  556. tools/code_exec/backends/docker.py +779 -0
  557. tools/code_exec/backends/local_subprocess.py +154 -0
  558. tools/code_exec/env_policy.py +45 -0
  559. tools/code_exec/result.py +42 -0
  560. tools/code_exec/sandbox_guard.py +59 -0
  561. tools/code_exec/tool.py +227 -0
  562. tools/coding/__init__.py +50 -0
  563. tools/coding/edit.py +107 -0
  564. tools/coding/search.py +113 -0
  565. tools/coding/tool.py +353 -0
  566. tools/collabland/__init__.py +5 -0
  567. tools/collabland/collabland_tool.py +471 -0
  568. tools/controller/__init__.py +0 -0
  569. tools/controller/_helpers.py +165 -0
  570. tools/controller/action_registration.py +1765 -0
  571. tools/controller/approval.py +244 -0
  572. tools/controller/approval_interactive.py +90 -0
  573. tools/controller/delegation.py +195 -0
  574. tools/controller/execution.py +797 -0
  575. tools/controller/execution_context.py +141 -0
  576. tools/controller/hooks.py +135 -0
  577. tools/controller/introspection.py +344 -0
  578. tools/controller/mcp_registrar.py +190 -0
  579. tools/controller/message_send.py +31 -0
  580. tools/controller/registry/__init__.py +4 -0
  581. tools/controller/registry/schema_generators.py +395 -0
  582. tools/controller/registry/schema_sanitizer.py +406 -0
  583. tools/controller/registry/service.py +1266 -0
  584. tools/controller/registry/views.py +151 -0
  585. tools/controller/service.py +409 -0
  586. tools/controller/tool_management.py +334 -0
  587. tools/controller/types.py +28 -0
  588. tools/controller/views.py +450 -0
  589. tools/cronjob_tools.py +166 -0
  590. tools/crypto_trade_gate.py +59 -0
  591. tools/descriptors.py +452 -0
  592. tools/dom/__init__.py +0 -0
  593. tools/dom/history_tree_processor/service.py +244 -0
  594. tools/dom/history_tree_processor/view.py +99 -0
  595. tools/dom/service.py +471 -0
  596. tools/dom/views.py +346 -0
  597. tools/email_tool.py +403 -0
  598. tools/exceptions.py +169 -0
  599. tools/filesystem.py +1038 -0
  600. tools/filesystem_docproc.py +331 -0
  601. tools/filesystem_pdf.py +755 -0
  602. tools/git/__init__.py +42 -0
  603. tools/git/tool.py +258 -0
  604. tools/github/__init__.py +33 -0
  605. tools/github/client.py +94 -0
  606. tools/github/tool.py +237 -0
  607. tools/goal_tools.py +309 -0
  608. tools/hyperliquid/__init__.py +56 -0
  609. tools/hyperliquid/models.py +233 -0
  610. tools/hyperliquid/service.py +1680 -0
  611. tools/knowledge_ingest.py +950 -0
  612. tools/mcp/README.md +535 -0
  613. tools/mcp/__init__.py +52 -0
  614. tools/mcp/catalog.py +115 -0
  615. tools/mcp/config.py +337 -0
  616. tools/mcp/mcp_tool.py +1415 -0
  617. tools/mcp/param_coercion.py +289 -0
  618. tools/mcp/protocol.py +1520 -0
  619. tools/mcp/rate_limit.py +50 -0
  620. tools/mcp/security.py +424 -0
  621. tools/mcp/self_install.py +111 -0
  622. tools/mcp/server_manager.py +829 -0
  623. tools/mcp/subscriptions.py +72 -0
  624. tools/mcp/user_mcp_service.py +899 -0
  625. tools/mcp/validation_tracker.py +89 -0
  626. tools/mcp/views.py +346 -0
  627. tools/oauth/__init__.py +17 -0
  628. tools/oauth/manager.py +75 -0
  629. tools/oauth/provider.py +69 -0
  630. tools/oauth/providers/__init__.py +1 -0
  631. tools/oauth/providers/generic_oauth2.py +96 -0
  632. tools/perplexity_tool.py +280 -0
  633. tools/polymarket/__init__.py +41 -0
  634. tools/polymarket/clob_adapter.py +76 -0
  635. tools/polymarket/models.py +182 -0
  636. tools/polymarket/service.py +2082 -0
  637. tools/self_env/__init__.py +61 -0
  638. tools/self_env/tool.py +274 -0
  639. tools/shell/__init__.py +80 -0
  640. tools/shell/backend_pool.py +118 -0
  641. tools/shell/discipline.py +62 -0
  642. tools/shell/executor.py +157 -0
  643. tools/shell/loopback_allow.py +87 -0
  644. tools/shell/process_registry.py +113 -0
  645. tools/shell/process_tool.py +131 -0
  646. tools/shell/state.py +129 -0
  647. tools/shell/tool.py +151 -0
  648. tools/task_tool.py +637 -0
  649. tools/twitter_tool.py +2086 -0
  650. tools/user_directory.py +179 -0
  651. tools/web_fetch/__init__.py +3 -0
  652. tools/web_fetch/fetcher.py +144 -0
  653. tools/web_fetch/render.py +44 -0
  654. tools/web_fetch/tool.py +60 -0
  655. tools/x402/__init__.py +60 -0
  656. tools/x402/client.py +47 -0
  657. tools/x402/invoice_tool.py +143 -0
  658. tools/x402/real_client.py +250 -0
  659. tools/x402/service.py +131 -0
  660. utils/README.md +498 -0
  661. utils/__init__.py +46 -0
  662. utils/auth_utils.py +114 -0
  663. utils/bounded_collections.py +162 -0
  664. utils/circuit_breaker.py +389 -0
  665. utils/gif_utils.py +435 -0
  666. utils/markdown_utils.py +291 -0
  667. utils/message_utils.py +147 -0
  668. utils/metrics.py +36 -0
  669. utils/path_validator.py +227 -0
  670. utils/rate_limit_manager.py +399 -0
  671. utils/result_size.py +296 -0
  672. utils/time_utils.py +206 -0
  673. utils/user_utils.py +188 -0
  674. webview/README.md +111 -0
  675. webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
  676. webview/__init__.py +8 -0
  677. webview/activity.py +636 -0
  678. webview/owner_auth.py +108 -0
  679. webview/pages.py +448 -0
  680. webview/repair_sessions.py +399 -0
  681. webview/server.py +4323 -0
  682. webview/server_launcher.py +112 -0
  683. webview/static/css/activity.css +89 -0
  684. webview/static/css/chat.css +1772 -0
  685. webview/static/css/components.css +1190 -0
  686. webview/static/css/config-panel.css +651 -0
  687. webview/static/css/pages/admin.css +766 -0
  688. webview/static/css/pages/profile.css +231 -0
  689. webview/static/css/pages/settings.css +297 -0
  690. webview/static/css/pages/signin.css +316 -0
  691. webview/static/css/style.css +4250 -0
  692. webview/static/css/variables.css +147 -0
  693. webview/static/css/workspace-fullwidth-fix.css +109 -0
  694. webview/static/img/favicon.ico +0 -0
  695. webview/static/js/activity.js +394 -0
  696. webview/static/js/admin/activity.js +186 -0
  697. webview/static/js/admin/dashboard.js +163 -0
  698. webview/static/js/admin/user_detail.js +400 -0
  699. webview/static/js/admin/users.js +174 -0
  700. webview/static/js/admin/utils.js +214 -0
  701. webview/static/js/chat.js +3178 -0
  702. webview/static/js/config-panel.js +522 -0
  703. webview/static/js/constants.js +20 -0
  704. webview/static/js/error-handler.js +226 -0
  705. webview/static/js/ethers.min.js +1 -0
  706. webview/static/js/event-filter.js +137 -0
  707. webview/static/js/event-store.js +351 -0
  708. webview/static/js/file-attachments.js +374 -0
  709. webview/static/js/file-loader.js +163 -0
  710. webview/static/js/index.js +103 -0
  711. webview/static/js/performance-utils.js +196 -0
  712. webview/static/js/profile.js +396 -0
  713. webview/static/js/screenshot.js +386 -0
  714. webview/static/js/session.js +1527 -0
  715. webview/static/js/settings.js +1288 -0
  716. webview/static/js/sidebar-data.js +766 -0
  717. webview/static/js/sidebar-toggle.js +296 -0
  718. webview/static/js/socket.io.min.js +7 -0
  719. webview/static/js/stats.js +355 -0
  720. webview/static/js/ui-utils.js +420 -0
  721. webview/static/js/workspace.js +966 -0
  722. webview/stats_service.py +613 -0
  723. webview/templates/__init__.py +1 -0
  724. webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  725. webview/templates/activity.html +36 -0
  726. webview/templates/admin/activity.html +122 -0
  727. webview/templates/admin/dashboard.html +112 -0
  728. webview/templates/admin/user_detail.html +228 -0
  729. webview/templates/admin/users.html +105 -0
  730. webview/templates/autonomy.html +60 -0
  731. webview/templates/error.html +133 -0
  732. webview/templates/finance.html +90 -0
  733. webview/templates/identity.html +73 -0
  734. webview/templates/index.html +42 -0
  735. webview/templates/layout.html +125 -0
  736. webview/templates/memory.html +59 -0
  737. webview/templates/owner_login.html +15 -0
  738. webview/templates/profile.html +121 -0
  739. webview/templates/session.html +312 -0
  740. webview/templates/settings.html +534 -0
  741. webview/templates/sidebar.html +112 -0
  742. webview/templates/signin.html +213 -0
  743. webview/templates/status.html +14 -0
  744. webview/templates/system.html +45 -0
  745. webview/webgate.py +196 -0
@@ -0,0 +1,950 @@
1
+ """KB ingestion engine + `knowledge` agent tool (Task 6).
2
+
3
+ Adds file→KB ingestion and a registerable ``knowledge`` tool on top of the
4
+ Task-5 registry routers. All KB I/O goes through ``modules.memory.registry``
5
+ (no direct provider access here).
6
+
7
+ ``from __future__ import annotations`` is SAFE in this file — the param models are
8
+ explicit (passed to ``BaseTool.action``), so the Registry never needs to introspect
9
+ stringized first-arg annotations here.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import hashlib
15
+ import logging
16
+ import os
17
+ import re
18
+ import subprocess
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Optional, Tuple
22
+
23
+ from pydantic import BaseModel, ConfigDict, Field
24
+
25
+ from tools.base_tool import BaseTool
26
+ from tools.controller.types import ActionResult
27
+ from agents.task.agent.core.secret_guard import (
28
+ is_secret_path,
29
+ is_binary_file,
30
+ estimate_tokens_rough,
31
+ )
32
+ from core.path_safety import is_within_root
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Env-driven knobs (read inline, not in constants.py)
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ def _int_env(name: str, default: int) -> int:
42
+ try:
43
+ return int(os.getenv(name, str(default)))
44
+ except (TypeError, ValueError):
45
+ return default
46
+
47
+
48
+ def _kb_max_files() -> int:
49
+ return _int_env("KB_MAX_FILES", 2000)
50
+
51
+
52
+ def _kb_max_bytes() -> int:
53
+ return _int_env("KB_MAX_BYTES", 25 * 1024 * 1024)
54
+
55
+
56
+ def _kb_chunk_tokens() -> int:
57
+ return _int_env("KB_CHUNK_TOKENS", 800)
58
+
59
+
60
+ def _kb_chunk_overlap() -> int:
61
+ return _int_env("KB_CHUNK_OVERLAP", 100)
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Office / unsupported extension skip list
66
+ # ---------------------------------------------------------------------------
67
+
68
+ _OFFICE_EXTENSIONS: frozenset[str] = frozenset({
69
+ ".doc", ".odt",
70
+ ".xlsx", ".xls", ".ods",
71
+ ".pptx", ".ppt",
72
+ ".epub", ".rtf",
73
+ })
74
+
75
+ # Extensions handled by dedicated extractors (not in _OFFICE_EXTENSIONS skip list)
76
+ _DOCX_EXTENSION = ".docx"
77
+
78
+ _TEXT_EXTENSIONS: frozenset[str] = frozenset({
79
+ ".txt", ".md", ".rst", ".csv",
80
+ ".py", ".js", ".ts", ".tsx", ".jsx",
81
+ ".sh", ".bash", ".zsh",
82
+ ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg",
83
+ ".html", ".xml", ".sql",
84
+ ".go", ".rs", ".c", ".cpp", ".h", ".hpp",
85
+ ".java", ".kt", ".swift", ".rb", ".php",
86
+ ".r", ".scala", ".clj",
87
+ })
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Pure engine helpers
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ def _iter_files(
96
+ root: Path,
97
+ *,
98
+ recursive: bool = True,
99
+ globs: Optional[List[str]] = None,
100
+ max_files: int = 2000,
101
+ max_bytes: int = 25 * 1024 * 1024,
102
+ ) -> Tuple[List[Path], Dict[str, int]]:
103
+ """Bounded, safe walk of *root*.
104
+
105
+ Returns ``(files, skipped)`` where ``skipped`` maps reason→count.
106
+ Prefers ``rg --files`` for gitignore-aware traversal; falls back to
107
+ ``os.walk`` skipping hidden dirs and ``__pycache__``.
108
+
109
+ Hard-skips:
110
+ - ``is_secret_path`` (credentials / env files)
111
+ - ``is_binary_file`` (compiled binaries, images, etc.)
112
+ - Files beyond ``max_files`` / ``max_bytes`` cumulative size limits.
113
+ """
114
+ root = root.resolve()
115
+ skipped: Dict[str, int] = {
116
+ "secret": 0,
117
+ "binary": 0,
118
+ "max_files": 0,
119
+ "max_bytes": 0,
120
+ }
121
+
122
+ # Collect candidate paths ------------------------------------------------
123
+ candidates: List[Path] = []
124
+
125
+ if recursive:
126
+ # Try rg --files for .gitignore awareness
127
+ rg_paths = _rg_files(root)
128
+ if rg_paths is not None:
129
+ candidates = rg_paths
130
+ else:
131
+ # Fallback: os.walk, skip hidden dirs + __pycache__
132
+ for dirpath, dirnames, filenames in os.walk(str(root)):
133
+ # Prune hidden dirs and __pycache__ in-place
134
+ dirnames[:] = [
135
+ d for d in dirnames
136
+ if not d.startswith(".") and d != "__pycache__"
137
+ ]
138
+ for fname in filenames:
139
+ candidates.append(Path(dirpath) / fname)
140
+ else:
141
+ # Non-recursive: direct children only
142
+ try:
143
+ candidates = [p for p in root.iterdir() if p.is_file()]
144
+ except OSError:
145
+ pass
146
+
147
+ # Apply glob filters if requested ----------------------------------------
148
+ if globs:
149
+ import fnmatch as _fnmatch
150
+ filtered = []
151
+ for p in candidates:
152
+ rel = str(p.relative_to(root)) if p.is_absolute() else str(p)
153
+ if any(_fnmatch.fnmatch(rel, g) or _fnmatch.fnmatch(p.name, g) for g in globs):
154
+ filtered.append(p)
155
+ candidates = filtered
156
+
157
+ # Filter: secret / binary; enforce limits --------------------------------
158
+ collected: List[Path] = []
159
+ total_bytes = 0
160
+
161
+ for path in candidates:
162
+ if not path.is_file():
163
+ continue
164
+
165
+ # Secret hard-skip
166
+ if is_secret_path(path, root=root):
167
+ skipped["secret"] += 1
168
+ continue
169
+
170
+ # Binary hard-skip (don't hard-skip .pdf — extractable)
171
+ if is_binary_file(path):
172
+ skipped["binary"] += 1
173
+ continue
174
+
175
+ # File count cap
176
+ if len(collected) >= max_files:
177
+ skipped["max_files"] += 1
178
+ continue
179
+
180
+ # Byte cap (skip oversized files; cumulative cap too)
181
+ try:
182
+ fsize = path.stat().st_size
183
+ except OSError:
184
+ continue
185
+ if total_bytes + fsize > max_bytes:
186
+ skipped["max_bytes"] += 1
187
+ continue
188
+
189
+ collected.append(path)
190
+ total_bytes += fsize
191
+
192
+ return collected, skipped
193
+
194
+
195
+ def _rg_files(root: Path) -> Optional[List[Path]]:
196
+ """Run ``rg --files <root>`` and return the paths, or None if unavailable."""
197
+ try:
198
+ result = subprocess.run(
199
+ ["rg", "--files", str(root)],
200
+ capture_output=True,
201
+ text=True,
202
+ timeout=30,
203
+ )
204
+ if result.returncode != 0:
205
+ return None
206
+ paths = []
207
+ for line in result.stdout.splitlines():
208
+ line = line.strip()
209
+ if line:
210
+ p = Path(line)
211
+ if not p.is_absolute():
212
+ p = root / p
213
+ paths.append(p)
214
+ return paths
215
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
216
+ return None
217
+
218
+
219
+ async def _extract_text(path: Path) -> Tuple[Optional[str], Optional[str]]:
220
+ """Extract text from *path*.
221
+
222
+ Returns ``(text, skip_reason)`` — exactly one of the two is ``None``.
223
+
224
+ - ``.pdf`` → direct ``await PdfExtractionMixin._process_pdf(content)``
225
+ (sync ``_extract_pdf_direct`` fallback only if the mixin raises / imports fail)
226
+ - text / code extensions → ``path.read_text(errors='replace')``
227
+ - office formats → ``(None, "office-skip")`` — no parser yet (Task 19)
228
+ """
229
+ suffix = path.suffix.lower()
230
+
231
+ # .docx — extract via python-docx (fail-open: if lib absent → skip-with-note)
232
+ if suffix == _DOCX_EXTENSION:
233
+ text = _extract_docx(path)
234
+ if text is None:
235
+ return None, f"office-skip:{suffix}"
236
+ return text, None
237
+
238
+ # Other office formats — no parser available
239
+ if suffix in _OFFICE_EXTENSIONS:
240
+ return None, f"office-skip:{suffix}"
241
+
242
+ # PDF extraction (async — directly await the mixin, no sync-from-async bridge)
243
+ if suffix == ".pdf":
244
+ try:
245
+ content_bytes = path.read_bytes()
246
+ text = await _extract_pdf_text(content_bytes)
247
+ if text:
248
+ return text, None
249
+ return None, "pdf-empty"
250
+ except Exception as e:
251
+ logger.debug("PDF extraction failed for %s: %s", path, e)
252
+ return None, f"pdf-error:{type(e).__name__}"
253
+
254
+ # Plain text / code
255
+ try:
256
+ text = path.read_text(errors="replace")
257
+ return text, None
258
+ except OSError as e:
259
+ return None, f"read-error:{type(e).__name__}"
260
+
261
+
262
+ async def _extract_pdf_text(content_bytes: bytes) -> str:
263
+ """Async adapter over ``PdfExtractionMixin._process_pdf``.
264
+
265
+ Directly ``await``s the (async) mixin from within the running loop — no
266
+ ``run_coroutine_sync`` bridge. Falls back to the SYNC ``_extract_pdf_direct``
267
+ only if the mixin raises or its import fails.
268
+ """
269
+ try:
270
+ from tools.filesystem_pdf import PdfExtractionMixin
271
+ mixin = PdfExtractionMixin()
272
+ # _process_pdf reads self.logger / self.container — provide minimal stand-ins.
273
+ mixin.logger = logging.getLogger("knowledge_ingest.pdf") # type: ignore[attr-defined]
274
+ mixin.container = None # type: ignore[attr-defined]
275
+ result = await mixin._process_pdf(content_bytes)
276
+ return result.get("content", "")
277
+ except Exception as e:
278
+ logger.debug("PDF mixin extraction failed, using direct fallback: %s", e)
279
+ return _extract_pdf_direct(content_bytes)
280
+
281
+
282
+ def _extract_pdf_direct(content_bytes: bytes) -> str:
283
+ """SYNC-only fallback PDF extraction using pypdf directly.
284
+
285
+ Used only when ``_process_pdf`` raises or its import fails. No event-loop
286
+ detection, no ``run_coroutine_sync``.
287
+ """
288
+ try:
289
+ import pypdf # type: ignore
290
+ from io import BytesIO
291
+ reader = pypdf.PdfReader(BytesIO(content_bytes), strict=False)
292
+ texts = []
293
+ for page in reader.pages:
294
+ try:
295
+ t = page.extract_text()
296
+ if t:
297
+ texts.append(t)
298
+ except Exception:
299
+ pass
300
+ return "\n\n".join(texts)
301
+ except Exception as e:
302
+ logger.debug("Direct PDF extraction failed: %s", e)
303
+ return ""
304
+
305
+
306
+ def _extract_docx(path: Path) -> Optional[str]:
307
+ """Extract plain text from a ``.docx`` file via ``python-docx``.
308
+
309
+ Lazy import — if ``python-docx`` is not installed, returns ``None`` so
310
+ the caller skips the file with-note (fail-open, never an error).
311
+
312
+ Paragraph text is joined with ``\\n``; table cell text is appended after
313
+ paragraphs so tables aren't silently dropped.
314
+
315
+ Any extraction error (corrupt file, unexpected docx layout) also returns
316
+ ``None`` (fail-open, logged at DEBUG).
317
+ """
318
+ try:
319
+ import docx as _docx # python-docx; import name is 'docx'
320
+ except ImportError:
321
+ logger.debug("python-docx not installed; skipping %s", path)
322
+ return None
323
+
324
+ try:
325
+ doc = _docx.Document(str(path))
326
+ parts: list[str] = []
327
+
328
+ # Body paragraphs
329
+ for para in doc.paragraphs:
330
+ text = para.text
331
+ if text.strip():
332
+ parts.append(text)
333
+
334
+ # Table cells (flattened, row-major)
335
+ for table in doc.tables:
336
+ for row in table.rows:
337
+ for cell in row.cells:
338
+ cell_text = cell.text.strip()
339
+ if cell_text:
340
+ parts.append(cell_text)
341
+
342
+ return "\n".join(parts) if parts else ""
343
+ except Exception as e:
344
+ logger.debug("docx extraction failed for %s: %s", path, e)
345
+ return None
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # Structure-first chunker
350
+ # ---------------------------------------------------------------------------
351
+
352
+ # Markdown heading pattern
353
+ _MD_HEADING_RE = re.compile(r"^#{1,6} .+", re.MULTILINE)
354
+ # Top-level Python def/class
355
+ _PYDEF_RE = re.compile(r"^(?:def |class )\S", re.MULTILINE)
356
+
357
+
358
+ def _chunk(
359
+ text: str,
360
+ *,
361
+ target: Optional[int] = None,
362
+ overlap: Optional[int] = None,
363
+ ) -> List[str]:
364
+ """Structure-first text chunker.
365
+
366
+ Splits on: markdown headings → blank-line paragraphs → top-level
367
+ ``def``/``class`` → raw size. Chunks are sized by ``estimate_tokens_rough``
368
+ to ~``target`` tokens with ``overlap`` token carry.
369
+ """
370
+ target = target if target is not None else _kb_chunk_tokens()
371
+ overlap = overlap if overlap is not None else _kb_chunk_overlap()
372
+
373
+ if not text or not text.strip():
374
+ return []
375
+
376
+ # 1. Try markdown heading split
377
+ parts = _split_on_headings(text)
378
+ if len(parts) <= 1:
379
+ # 2. Try blank-line paragraph split
380
+ parts = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
381
+ if len(parts) <= 1:
382
+ # 3. Try top-level def/class split (Python)
383
+ parts = _split_on_defs(text)
384
+ if len(parts) <= 1:
385
+ # 4. Raw line split
386
+ parts = text.splitlines()
387
+
388
+ # Merge small parts up to target size; carry overlap into next chunk
389
+ return _merge_to_target(parts, target=target, overlap=overlap)
390
+
391
+
392
+ def _split_on_headings(text: str) -> List[str]:
393
+ """Split *text* at markdown ``#`` headings (keep heading in chunk)."""
394
+ positions = [m.start() for m in _MD_HEADING_RE.finditer(text)]
395
+ if not positions:
396
+ return [text]
397
+ parts = []
398
+ positions.append(len(text))
399
+ for i in range(len(positions) - 1):
400
+ chunk = text[positions[i]:positions[i + 1]].strip()
401
+ if chunk:
402
+ parts.append(chunk)
403
+ # Prepend any text before the first heading
404
+ if positions[0] > 0:
405
+ preamble = text[:positions[0]].strip()
406
+ if preamble:
407
+ parts.insert(0, preamble)
408
+ return parts if parts else [text]
409
+
410
+
411
+ def _split_on_defs(text: str) -> List[str]:
412
+ """Split on top-level Python ``def``/``class`` lines."""
413
+ positions = [m.start() for m in _PYDEF_RE.finditer(text)]
414
+ if not positions:
415
+ return [text]
416
+ parts = []
417
+ positions.append(len(text))
418
+ if positions[0] > 0:
419
+ preamble = text[:positions[0]].strip()
420
+ if preamble:
421
+ parts.append(preamble)
422
+ for i in range(len(positions) - 1):
423
+ chunk = text[positions[i]:positions[i + 1]].strip()
424
+ if chunk:
425
+ parts.append(chunk)
426
+ return parts if parts else [text]
427
+
428
+
429
+ def _merge_to_target(parts: List[str], *, target: int, overlap: int) -> List[str]:
430
+ """Merge *parts* into chunks of at most *target* tokens, carrying *overlap*.
431
+
432
+ If a single *part* exceeds *target* tokens, it is further split on word
433
+ boundaries so no output chunk grows unboundedly.
434
+ """
435
+ chunks: List[str] = []
436
+ current_lines: List[str] = []
437
+ current_tokens = 0
438
+ overlap_carry = ""
439
+
440
+ def _flush():
441
+ nonlocal current_lines, current_tokens, overlap_carry
442
+ if not current_lines:
443
+ return
444
+ chunk_text = overlap_carry + ("\n\n" if overlap_carry else "") + "\n\n".join(current_lines)
445
+ chunks.append(chunk_text.strip())
446
+ overlap_carry = _tail_tokens(chunk_text, overlap)
447
+ current_lines = []
448
+ current_tokens = estimate_tokens_rough(overlap_carry)
449
+
450
+ for part in parts:
451
+ part_tokens = estimate_tokens_rough(part)
452
+
453
+ # If a single part is too large, split it on word boundaries first
454
+ if part_tokens > target:
455
+ # Flush anything accumulated so far
456
+ _flush()
457
+ # Split oversized part into word-boundary slices
458
+ for sub in _split_by_tokens(part, target):
459
+ sub_tokens = estimate_tokens_rough(sub)
460
+ if current_tokens + sub_tokens > target and current_lines:
461
+ _flush()
462
+ current_lines.append(sub)
463
+ current_tokens += sub_tokens
464
+ continue
465
+
466
+ if current_tokens + part_tokens > target and current_lines:
467
+ _flush()
468
+
469
+ current_lines.append(part)
470
+ current_tokens += part_tokens
471
+
472
+ # Final flush
473
+ _flush()
474
+
475
+ return [c for c in chunks if c]
476
+
477
+
478
+ def _split_by_tokens(text: str, target: int) -> List[str]:
479
+ """Split *text* into slices of at most *target* tokens (word-boundary split)."""
480
+ words = text.split()
481
+ slices: List[str] = []
482
+ current_words: List[str] = []
483
+ current_chars = 0
484
+ target_chars = target * 4 # estimate_tokens_rough uses // 4
485
+
486
+ for word in words:
487
+ wlen = len(word) + 1 # +1 for space
488
+ if current_chars + wlen > target_chars and current_words:
489
+ slices.append(" ".join(current_words))
490
+ current_words = [word]
491
+ current_chars = wlen
492
+ else:
493
+ current_words.append(word)
494
+ current_chars += wlen
495
+
496
+ if current_words:
497
+ slices.append(" ".join(current_words))
498
+
499
+ return slices if slices else [text]
500
+
501
+
502
+ def _tail_tokens(text: str, n_tokens: int) -> str:
503
+ """Return the last ~*n_tokens* token-equivalents of *text* (char-based approx)."""
504
+ if n_tokens <= 0:
505
+ return ""
506
+ tail_chars = n_tokens * 4 # estimate_tokens_rough uses // 4
507
+ return text[-tail_chars:].lstrip()
508
+
509
+
510
+ # ---------------------------------------------------------------------------
511
+ # Async ingestion entry point
512
+ # ---------------------------------------------------------------------------
513
+
514
+
515
+ async def kb_ingest(
516
+ path: str,
517
+ collection: str = "default",
518
+ recursive: bool = True,
519
+ globs: Optional[List[str]] = None,
520
+ *,
521
+ user_id: str,
522
+ session_id: str,
523
+ source_name: Optional[str] = None,
524
+ ) -> Dict[str, Any]:
525
+ """Ingest a file or directory into the KB.
526
+
527
+ Returns counts: ``{ingested, skipped_secret, skipped_binary, unchanged,
528
+ n_chunks, skipped_office}``.
529
+
530
+ Path confinement:
531
+ - Server sessions: confined to ``pm().get_workspace_dir(session_id, user_id)``
532
+ - CLI (workspace_is_project_root): confined to ``Path.cwd()``
533
+
534
+ ``source_name`` (optional): a STABLE logical identity to store as the chunk's
535
+ ``source_path`` / dedup key instead of the volatile on-disk path. Used by the
536
+ HTTP upload path so a re-upload of the same file dedups by its original filename
537
+ rather than a fresh ``mkstemp`` path. Only honored for single-file ingestion
538
+ (a directory walk has many sources, so ``source_name`` is ignored there). When
539
+ ``None`` the behavior is byte-identical to before (on-disk path is the source).
540
+ """
541
+ from modules.memory.registry import (
542
+ kb_ingest_chunk as _kb_ingest_chunk,
543
+ kb_remove as _kb_remove,
544
+ kb_source_hash as _kb_source_hash,
545
+ )
546
+
547
+ target_path = Path(path)
548
+
549
+ # ------------------------------------------------------------------
550
+ # Path confinement
551
+ # ------------------------------------------------------------------
552
+ allowed_root = _resolve_confinement_root(session_id, user_id)
553
+ resolved = target_path.resolve() if target_path.is_absolute() else (allowed_root / path).resolve()
554
+
555
+ if not is_within_root(str(resolved), str(allowed_root)):
556
+ return {
557
+ "error": f"Path '{path}' is outside the allowed workspace root '{allowed_root}'",
558
+ "ingested": 0,
559
+ "skipped_secret": 0,
560
+ "skipped_binary": 0,
561
+ "unchanged": 0,
562
+ "n_chunks": 0,
563
+ "skipped_office": 0,
564
+ "skipped_too_large": 0,
565
+ "failed": 0,
566
+ }
567
+
568
+ # ------------------------------------------------------------------
569
+ # Walk files
570
+ # ------------------------------------------------------------------
571
+ if resolved.is_file():
572
+ files = [resolved]
573
+ walk_skipped: Dict[str, int] = {
574
+ "secret": 0, "binary": 0, "max_files": 0, "max_bytes": 0, "too_large": 0,
575
+ }
576
+ # Still apply secret/binary guards for single-file ingestion
577
+ if is_secret_path(resolved, root=allowed_root):
578
+ walk_skipped["secret"] += 1
579
+ files = []
580
+ elif is_binary_file(resolved):
581
+ walk_skipped["binary"] += 1
582
+ files = []
583
+ else:
584
+ # Byte cap also applies to a single file — never read an unbounded file
585
+ # into memory (the directory walk caps cumulative size; this caps the
586
+ # one-file path, which would otherwise OOM on a multi-GB file).
587
+ try:
588
+ if resolved.stat().st_size > _kb_max_bytes():
589
+ walk_skipped["too_large"] += 1
590
+ files = []
591
+ except OSError:
592
+ files = []
593
+ elif resolved.is_dir():
594
+ files, walk_skipped = _iter_files(
595
+ resolved,
596
+ recursive=recursive,
597
+ globs=globs,
598
+ max_files=_kb_max_files(),
599
+ max_bytes=_kb_max_bytes(),
600
+ )
601
+ else:
602
+ return {
603
+ "error": f"Path '{path}' does not exist or is not accessible.",
604
+ "ingested": 0,
605
+ "skipped_secret": 0,
606
+ "skipped_binary": 0,
607
+ "unchanged": 0,
608
+ "n_chunks": 0,
609
+ "skipped_office": 0,
610
+ "skipped_too_large": 0,
611
+ "failed": 0,
612
+ }
613
+
614
+ # ------------------------------------------------------------------
615
+ # Ingest each file
616
+ # ------------------------------------------------------------------
617
+ counts: Dict[str, int] = {
618
+ "ingested": 0,
619
+ "skipped_secret": walk_skipped.get("secret", 0),
620
+ "skipped_binary": walk_skipped.get("binary", 0),
621
+ "unchanged": 0,
622
+ "n_chunks": 0,
623
+ "skipped_office": 0,
624
+ # oversized single file ("too_large") + oversized files in a dir walk ("max_bytes")
625
+ "skipped_too_large": walk_skipped.get("too_large", 0) + walk_skipped.get("max_bytes", 0),
626
+ "failed": 0,
627
+ }
628
+
629
+ target_tokens = _kb_chunk_tokens()
630
+ overlap_tokens = _kb_chunk_overlap()
631
+
632
+ # A stable source_name only makes sense for single-file ingestion (a directory
633
+ # walk has many distinct sources). Ignore it for multi-file walks.
634
+ use_source_name = source_name if (source_name and len(files) == 1) else None
635
+
636
+ for fpath in files:
637
+ source_path_str = use_source_name or str(fpath)
638
+
639
+ # Read + hash off the event loop — reading a large file and hashing it is
640
+ # synchronous CPU/IO that would otherwise stall every other session during a
641
+ # bulk ingest.
642
+ try:
643
+ file_hash = await asyncio.to_thread(_read_and_hash, fpath)
644
+ except OSError as e:
645
+ logger.warning("Cannot read %s: %s", fpath, e)
646
+ continue
647
+
648
+ # Check existing hash via registry
649
+ existing_hash = await _kb_source_hash(
650
+ user_id=user_id,
651
+ collection=collection,
652
+ source_path=source_path_str,
653
+ )
654
+
655
+ if existing_hash == file_hash:
656
+ counts["unchanged"] += 1
657
+ continue
658
+
659
+ # If changed (or new): remove old chunks, re-ingest
660
+ if existing_hash is not None:
661
+ await _kb_remove(
662
+ user_id=user_id,
663
+ collection=collection,
664
+ source=source_path_str,
665
+ )
666
+
667
+ # Extract text
668
+ text, skip_reason = await _extract_text(fpath)
669
+ if text is None:
670
+ if skip_reason and skip_reason.startswith("office-skip"):
671
+ counts["skipped_office"] += 1
672
+ else:
673
+ logger.debug("Skipping %s: %s", fpath, skip_reason)
674
+ continue
675
+
676
+ # Chunk (CPU-bound) off the event loop too.
677
+ chunks = await asyncio.to_thread(
678
+ _chunk, text, target=target_tokens, overlap=overlap_tokens
679
+ )
680
+ if not chunks:
681
+ continue
682
+
683
+ # Ingest each chunk
684
+ mime = "application/pdf" if fpath.suffix.lower() == ".pdf" else "text/plain"
685
+ created_at = datetime.now(timezone.utc).isoformat()
686
+
687
+ file_ok = True
688
+ for idx, chunk_text in enumerate(chunks):
689
+ ok = await _kb_ingest_chunk(
690
+ user_id=user_id,
691
+ collection=collection,
692
+ source_path=source_path_str,
693
+ source_hash=file_hash,
694
+ chunk_idx=idx,
695
+ content=chunk_text,
696
+ mime=mime,
697
+ created_at=created_at,
698
+ )
699
+ # A provider that returns False signals a real write failure (None from a
700
+ # legacy/no-op provider is treated as success — unchanged behavior).
701
+ if ok is False:
702
+ file_ok = False
703
+ break
704
+
705
+ if not file_ok:
706
+ # Don't leave the file half-ingested with a current source hash (a re-run
707
+ # would skip it as "unchanged" and never recover the dropped chunks).
708
+ # Remove the partial source so the next ingest retries from scratch.
709
+ try:
710
+ await _kb_remove(
711
+ user_id=user_id,
712
+ collection=collection,
713
+ source=source_path_str,
714
+ )
715
+ except Exception as e:
716
+ logger.debug("partial-ingest cleanup failed for %s: %s", fpath, e)
717
+ counts["failed"] += 1
718
+ continue
719
+
720
+ counts["ingested"] += 1
721
+ counts["n_chunks"] += len(chunks)
722
+
723
+ return counts
724
+
725
+
726
+ def _read_and_hash(path: Path) -> str:
727
+ """Read file bytes and return their sha256 hex digest (runs in a worker thread)."""
728
+ return hashlib.sha256(path.read_bytes()).hexdigest()
729
+
730
+
731
+ def _resolve_confinement_root(session_id: str, user_id: str) -> Path:
732
+ """Return the allowed root for path confinement.
733
+
734
+ Server: ``pm().get_workspace_dir(session_id, user_id)``
735
+ CLI / local mode: ``Path.cwd()``.
736
+
737
+ Fail-CLOSED on the server: if the workspace root can't be resolved (``pm()``
738
+ raises), we must NOT silently widen confinement to the process CWD (e.g.
739
+ ``/opt/rob`` — app code/config). Only local mode (single-user) accepts the
740
+ CWD fallback; on the server the error propagates so the caller refuses.
741
+ """
742
+ try:
743
+ from agents.task.path import pm
744
+ path_manager = pm()
745
+ workspace = path_manager.get_workspace_dir(session_id, user_id)
746
+ return workspace.resolve()
747
+ except Exception:
748
+ from agents.task.constants import local_mode_enabled
749
+ if local_mode_enabled():
750
+ return Path.cwd().resolve()
751
+ raise
752
+
753
+
754
+ # ---------------------------------------------------------------------------
755
+ # Param models
756
+ # ---------------------------------------------------------------------------
757
+
758
+
759
+ class KbIngestParams(BaseModel):
760
+ model_config = ConfigDict(extra="forbid")
761
+ path: str = Field(..., description="File or directory path to ingest into the KB.")
762
+ collection: str = Field("default", description="KB collection name.")
763
+ recursive: bool = Field(True, description="Recurse into subdirectories.")
764
+ globs: Optional[List[str]] = Field(
765
+ None, description="Optional glob patterns to restrict which files are included."
766
+ )
767
+
768
+
769
+ class KbSearchParams(BaseModel):
770
+ model_config = ConfigDict(extra="forbid")
771
+ query: str = Field(..., description="Search query.")
772
+ collection: str = Field("default", description="KB collection to search.")
773
+ limit: int = Field(8, ge=1, le=50, description="Maximum number of results.")
774
+
775
+
776
+ class KbListParams(BaseModel):
777
+ model_config = ConfigDict(extra="forbid")
778
+ collection: Optional[str] = Field(None, description="Filter by collection (omit for all).")
779
+
780
+
781
+ class KbRemoveParams(BaseModel):
782
+ model_config = ConfigDict(extra="forbid")
783
+ collection: str = Field(..., description="KB collection name.")
784
+ source: Optional[str] = Field(None, description="Source path to remove (omit to clear entire collection).")
785
+
786
+
787
+ # ---------------------------------------------------------------------------
788
+ # KnowledgeTool
789
+ # ---------------------------------------------------------------------------
790
+
791
+
792
+ class KnowledgeTool(BaseTool):
793
+ """Agent tool for tenant-scoped knowledge-base ingest and search."""
794
+
795
+ @staticmethod
796
+ def _user(execution_context) -> str:
797
+ return getattr(execution_context, "user_id", None) or "_anonymous_"
798
+
799
+ @staticmethod
800
+ def _session(execution_context) -> str:
801
+ return getattr(execution_context, "session_id", None) or ""
802
+
803
+ @BaseTool.action(
804
+ "Ingest a file or directory into the agent's knowledge base so future kb_search "
805
+ "calls can retrieve it. Skips secrets, binaries, and unchanged files (dedup by hash).",
806
+ param_model=KbIngestParams,
807
+ )
808
+ async def kb_ingest(
809
+ self, params: KbIngestParams, execution_context=None
810
+ ) -> ActionResult:
811
+ user_id = self._user(execution_context)
812
+ session_id = self._session(execution_context)
813
+ try:
814
+ result = await kb_ingest(
815
+ params.path,
816
+ collection=params.collection,
817
+ recursive=params.recursive,
818
+ globs=params.globs,
819
+ user_id=user_id,
820
+ session_id=session_id,
821
+ )
822
+ except Exception as e:
823
+ return ActionResult(error=f"KB ingest failed: {e}", include_in_memory=True)
824
+
825
+ if "error" in result:
826
+ return ActionResult(error=result["error"], include_in_memory=True)
827
+
828
+ summary = (
829
+ f"KB ingest complete — ingested={result['ingested']}, "
830
+ f"unchanged={result['unchanged']}, chunks={result['n_chunks']}, "
831
+ f"failed={result.get('failed', 0)}, "
832
+ f"skipped(secret={result['skipped_secret']}, "
833
+ f"binary={result['skipped_binary']}, "
834
+ f"office={result.get('skipped_office', 0)}, "
835
+ f"too_large={result.get('skipped_too_large', 0)})"
836
+ )
837
+ return ActionResult(extracted_content=summary, include_in_memory=True)
838
+
839
+ @BaseTool.action(
840
+ "Search the agent's knowledge base for relevant content.",
841
+ param_model=KbSearchParams,
842
+ )
843
+ async def kb_search(
844
+ self, params: KbSearchParams, execution_context=None
845
+ ) -> ActionResult:
846
+ from modules.memory.registry import kb_search as _kb_search
847
+ user_id = self._user(execution_context)
848
+ try:
849
+ result = await _kb_search(
850
+ params.query,
851
+ user_id=user_id,
852
+ collection=params.collection,
853
+ limit=params.limit,
854
+ )
855
+ except Exception as e:
856
+ return ActionResult(error=f"KB search failed: {e}", include_in_memory=True)
857
+ if not result:
858
+ return ActionResult(
859
+ extracted_content="No KB results found.", include_in_memory=True
860
+ )
861
+ return ActionResult(extracted_content=result, include_in_memory=True)
862
+
863
+ @BaseTool.action(
864
+ "List ingested sources in the knowledge base.",
865
+ param_model=KbListParams,
866
+ )
867
+ async def kb_list(
868
+ self, params: KbListParams, execution_context=None
869
+ ) -> ActionResult:
870
+ from modules.memory.registry import kb_list_sources as _kb_list_sources
871
+ user_id = self._user(execution_context)
872
+ try:
873
+ sources = await _kb_list_sources(
874
+ user_id=user_id, collection=params.collection
875
+ )
876
+ except Exception as e:
877
+ return ActionResult(error=f"KB list failed: {e}", include_in_memory=True)
878
+ if not sources:
879
+ return ActionResult(
880
+ extracted_content="No sources ingested.", include_in_memory=True
881
+ )
882
+ lines = [f"- {s}" for s in sources]
883
+ return ActionResult(
884
+ extracted_content="KB sources:\n" + "\n".join(lines),
885
+ include_in_memory=True,
886
+ )
887
+
888
+ @BaseTool.action(
889
+ "Remove a source (or entire collection) from the knowledge base.",
890
+ param_model=KbRemoveParams,
891
+ )
892
+ async def kb_remove(
893
+ self, params: KbRemoveParams, execution_context=None
894
+ ) -> ActionResult:
895
+ from modules.memory.registry import kb_remove as _kb_remove
896
+ user_id = self._user(execution_context)
897
+ try:
898
+ removed = await _kb_remove(
899
+ user_id=user_id,
900
+ collection=params.collection,
901
+ source=params.source,
902
+ )
903
+ except Exception as e:
904
+ return ActionResult(error=f"KB remove failed: {e}", include_in_memory=True)
905
+ return ActionResult(
906
+ extracted_content=f"Removed {removed} chunk(s) from KB.",
907
+ include_in_memory=True,
908
+ )
909
+
910
+
911
+ # ---------------------------------------------------------------------------
912
+ # Registration
913
+ # ---------------------------------------------------------------------------
914
+
915
+
916
+ def kb_enabled() -> bool:
917
+ from agents.task.constants import AutonomyConfig
918
+ return AutonomyConfig.kb_enabled()
919
+
920
+
921
+ def register_knowledge_tool(force: bool = False) -> bool:
922
+ """Register the 'knowledge' descriptor + class IFF KB_ENABLED (or forced).
923
+
924
+ Delegates to ``register_optional_tool``. No-op when KB is off, so default
925
+ deploys are unaffected. ``knowledge`` is never in the default ``tool_ids``
926
+ — agents opt in via ``tool_ids=['knowledge']``.
927
+ """
928
+ from tools.descriptors import (
929
+ ToolDescriptor,
930
+ ToolCategory,
931
+ register_optional_tool,
932
+ )
933
+
934
+ return register_optional_tool(
935
+ "knowledge",
936
+ KnowledgeTool,
937
+ ToolDescriptor(
938
+ name="knowledge",
939
+ description=(
940
+ "Ingest/search a tenant-scoped knowledge base "
941
+ "(kb_ingest/kb_search/kb_list/kb_remove)"
942
+ ),
943
+ category=ToolCategory.INTEGRATION,
944
+ required_config=[],
945
+ init_priority=80,
946
+ is_optional=True,
947
+ ),
948
+ kb_enabled,
949
+ force=force,
950
+ )