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
cron/schedule.py ADDED
@@ -0,0 +1,194 @@
1
+ """Schedule parsing + next-run computation for the cron subsystem (roadmap P5).
2
+
3
+ Pure and tz-naive (local wall-clock) so it is deterministic and unit-testable —
4
+ every entry point takes the reference time explicitly. Supported spec formats:
5
+
6
+ - duration interval ............ ``30m``, ``2h``, ``1d``, ``45s`` (also ``every 15m``)
7
+ - one-shot ISO timestamp ....... ``2026-06-07T09:30:00``
8
+ - 5-field cron ................. ``*/15 * * * *`` (min hour dom mon dow; dow 0/7=Sun)
9
+ - every-phrase ................. ``every day 14:30``, ``every monday 09:00``
10
+
11
+ Anything else raises :class:`ScheduleError`.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass
17
+ from datetime import datetime, timedelta
18
+ from typing import List, Optional
19
+
20
+ _UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
21
+ _WEEKDAYS = {
22
+ "monday": 1, "tuesday": 2, "wednesday": 3, "thursday": 4,
23
+ "friday": 5, "saturday": 6, "sunday": 0,
24
+ }
25
+ _DURATION_RE = re.compile(r"^(\d+)\s*([smhd])$")
26
+ _HHMM_RE = re.compile(r"^(\d{1,2}):(\d{2})$")
27
+ _MAX_LOOKAHEAD_MINUTES = 366 * 24 * 60 # ~1 year safety bound
28
+
29
+
30
+ class ScheduleError(ValueError):
31
+ """Raised when a schedule spec cannot be parsed."""
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Schedule:
36
+ kind: str # "interval" | "cron" | "once"
37
+ one_shot: bool
38
+ interval_seconds: Optional[int] = None
39
+ once_at: Optional[datetime] = None
40
+ cron_fields: Optional[tuple] = None # (minutes, hours, doms, months, dows) sets
41
+ # POSIX day-of-month / day-of-week are OR'd when BOTH are restricted (not "*").
42
+ dom_restricted: bool = False
43
+ dow_restricted: bool = False
44
+
45
+ def next_run_after(self, after: datetime) -> Optional[datetime]:
46
+ """Return the next fire time strictly after ``after`` (None if none)."""
47
+ if self.kind == "interval":
48
+ return after + timedelta(seconds=self.interval_seconds)
49
+ if self.kind == "once":
50
+ return self.once_at if self.once_at > after else None
51
+ if self.kind == "cron":
52
+ return self._next_cron(after)
53
+ raise ScheduleError(f"unknown schedule kind: {self.kind}")
54
+
55
+ def _next_cron(self, after: datetime) -> Optional[datetime]:
56
+ minutes, hours, doms, months, dows = self.cron_fields
57
+ dt = after.replace(second=0, microsecond=0) + timedelta(minutes=1)
58
+ for _ in range(_MAX_LOOKAHEAD_MINUTES):
59
+ cron_dow = dt.isoweekday() % 7 # Mon..Sun(7)->1..0 ; Sunday == 0
60
+ # POSIX rule: when BOTH day-of-month and day-of-week are restricted, the
61
+ # job runs when EITHER matches (union); otherwise AND the restricted one.
62
+ if self.dom_restricted and self.dow_restricted:
63
+ day_match = (dt.day in doms) or (cron_dow in dows)
64
+ elif self.dom_restricted:
65
+ day_match = dt.day in doms
66
+ elif self.dow_restricted:
67
+ day_match = cron_dow in dows
68
+ else:
69
+ day_match = True
70
+ if (dt.minute in minutes and dt.hour in hours
71
+ and dt.month in months and day_match):
72
+ return dt
73
+ dt += timedelta(minutes=1)
74
+ return None
75
+
76
+
77
+ def parse_schedule(spec: str) -> Schedule:
78
+ if not spec or not spec.strip():
79
+ raise ScheduleError("empty schedule spec")
80
+ raw = spec.strip()
81
+ low = raw.lower()
82
+
83
+ # every-phrase
84
+ if low.startswith("every "):
85
+ return _parse_every(low[len("every "):].strip())
86
+
87
+ # duration interval
88
+ m = _DURATION_RE.match(low)
89
+ if m:
90
+ seconds = int(m.group(1)) * _UNIT_SECONDS[m.group(2)]
91
+ if seconds <= 0:
92
+ raise ScheduleError(f"non-positive interval: {spec}")
93
+ return Schedule(kind="interval", one_shot=False, interval_seconds=seconds)
94
+
95
+ # 5-field cron
96
+ if len(raw.split()) == 5:
97
+ return _parse_cron(raw)
98
+
99
+ # ISO timestamp (one-shot)
100
+ try:
101
+ once_at = datetime.fromisoformat(raw)
102
+ except ValueError:
103
+ raise ScheduleError(f"unrecognized schedule spec: {spec!r}")
104
+ # Normalize a tz-aware input to naive LOCAL time so it compares against the
105
+ # scheduler's naive datetime.now() — a tz-aware once_at otherwise raised
106
+ # "can't compare offset-naive and offset-aware datetimes" out of next_run_after.
107
+ if once_at.tzinfo is not None:
108
+ once_at = once_at.astimezone().replace(tzinfo=None)
109
+ return Schedule(kind="once", one_shot=True, once_at=once_at)
110
+
111
+
112
+ def _parse_every(rest: str) -> Schedule:
113
+ # "every 15m" -> interval
114
+ m = _DURATION_RE.match(rest)
115
+ if m:
116
+ seconds = int(m.group(1)) * _UNIT_SECONDS[m.group(2)]
117
+ if seconds <= 0:
118
+ # A zero/negative interval makes every scheduler tick see the job as due,
119
+ # re-running it forever (runaway cost). Reject like the bare-duration path.
120
+ raise ScheduleError(f"interval must be positive: every {rest!r}")
121
+ return Schedule(kind="interval", one_shot=False, interval_seconds=seconds)
122
+
123
+ parts = rest.split()
124
+ if len(parts) != 2:
125
+ raise ScheduleError(f"unrecognized 'every' phrase: every {rest!r}")
126
+ when, time_str = parts
127
+ hm = _HHMM_RE.match(time_str)
128
+ if not hm:
129
+ raise ScheduleError(f"bad time in 'every' phrase: {time_str!r}")
130
+ hour, minute = int(hm.group(1)), int(hm.group(2))
131
+ if not (0 <= hour < 24 and 0 <= minute < 60):
132
+ raise ScheduleError(f"time out of range: {time_str!r}")
133
+
134
+ if when == "day":
135
+ dows = set(range(7))
136
+ dow_restricted = False # every day -> DOW not restricted
137
+ elif when in _WEEKDAYS:
138
+ dows = {_WEEKDAYS[when]}
139
+ dow_restricted = True # every <weekday> -> DOW restricted, honor the set
140
+ else:
141
+ raise ScheduleError(f"unknown day in 'every' phrase: {when!r}")
142
+ fields = ({minute}, {hour}, set(range(1, 32)), set(range(1, 13)), dows)
143
+ return Schedule(kind="cron", one_shot=False, cron_fields=fields,
144
+ dom_restricted=False, dow_restricted=dow_restricted)
145
+
146
+
147
+ def _parse_cron(raw: str) -> Schedule:
148
+ fields = raw.split()
149
+ ranges = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 7)]
150
+ try:
151
+ minutes = _expand(fields[0], *ranges[0])
152
+ hours = _expand(fields[1], *ranges[1])
153
+ doms = _expand(fields[2], *ranges[2])
154
+ months = _expand(fields[3], *ranges[3])
155
+ dows = _expand(fields[4], *ranges[4])
156
+ except ScheduleError:
157
+ raise
158
+ except Exception as e:
159
+ raise ScheduleError(f"bad cron expression {raw!r}: {e}")
160
+ if 7 in dows: # cron allows 7 for Sunday; normalize to 0
161
+ dows = (dows - {7}) | {0}
162
+ # A field is "restricted" when it is not a bare/stepped wildcard (Vixie rule:
163
+ # first char != '*'), which selects POSIX OR semantics when both are restricted.
164
+ dom_restricted = not fields[2].strip().startswith("*")
165
+ dow_restricted = not fields[4].strip().startswith("*")
166
+ return Schedule(kind="cron", one_shot=False,
167
+ cron_fields=(minutes, hours, doms, months, dows),
168
+ dom_restricted=dom_restricted, dow_restricted=dow_restricted)
169
+
170
+
171
+ def _expand(field: str, lo: int, hi: int) -> set:
172
+ """Expand one cron field into a set of ints. Supports * , - and */n."""
173
+ result: set = set()
174
+ for part in field.split(","):
175
+ step = 1
176
+ body = part
177
+ if "/" in part:
178
+ body, step_s = part.split("/", 1)
179
+ step = int(step_s)
180
+ if step <= 0:
181
+ raise ScheduleError(f"bad step in {field!r}")
182
+ if body == "*":
183
+ start, end = lo, hi
184
+ elif "-" in body:
185
+ a, b = body.split("-", 1)
186
+ start, end = int(a), int(b)
187
+ else:
188
+ start = end = int(body)
189
+ if start < lo or end > hi or start > end:
190
+ raise ScheduleError(f"cron field out of range: {part!r} (allowed {lo}-{hi})")
191
+ result.update(range(start, end + 1, step))
192
+ if not result:
193
+ raise ScheduleError(f"empty cron field: {field!r}")
194
+ return result
cron/scheduler.py ADDED
@@ -0,0 +1,201 @@
1
+ """Cron tick scheduler (roadmap P5).
2
+
3
+ Finds due jobs, runs each through an injected async ``runner`` under a hard
4
+ duration cap, then reschedules (recurring) or completes (one-shot). A file-based
5
+ tick lock makes a tick safe to fire from multiple processes (``UVICORN_WORKERS>1``)
6
+ without double-running jobs. The runner is injected so the scheduler is fully
7
+ unit-testable without the agent stack; the production runner (see
8
+ ``cron.runner``) reuses the agent core with the per-run cap.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import logging
14
+ import os
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from typing import Awaitable, Callable, List, Optional
19
+
20
+ from cron.jobs import CronJob, CronJobStore
21
+ from cron.schedule import parse_schedule, ScheduleError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # A held lock older than this (seconds) is considered stale and stolen — guards
26
+ # against a crashed worker leaving the tick permanently locked.
27
+ _LOCK_STALE_SECONDS = 600
28
+
29
+ Runner = Callable[[CronJob], Awaitable[bool]]
30
+
31
+
32
+ class TickLock:
33
+ """Exclusive cross-process lock via O_CREAT|O_EXCL, with stale-steal."""
34
+
35
+ def __init__(self, lock_path: str):
36
+ self.lock_path = lock_path
37
+ self._fd: Optional[int] = None
38
+
39
+ def acquire(self) -> bool:
40
+ parent = os.path.dirname(self.lock_path)
41
+ if parent:
42
+ os.makedirs(parent, exist_ok=True)
43
+ try:
44
+ self._fd = os.open(self.lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
45
+ except FileExistsError:
46
+ if self._is_stale():
47
+ try:
48
+ os.unlink(self.lock_path)
49
+ except OSError:
50
+ return False
51
+ return self.acquire()
52
+ return False
53
+ os.write(self._fd, str(os.getpid()).encode())
54
+ return True
55
+
56
+ def _is_stale(self) -> bool:
57
+ try:
58
+ age = time.time() - os.path.getmtime(self.lock_path)
59
+ except OSError:
60
+ return False
61
+ return age > _LOCK_STALE_SECONDS
62
+
63
+ def refresh(self) -> None:
64
+ """Bump the lock-file mtime so a long-but-LIVE tick is not seen as stale.
65
+
66
+ Without this the mtime is fixed at O_CREAT time; a tick that legitimately
67
+ runs longer than _LOCK_STALE_SECONDS (several due jobs each capped at
68
+ ~180s) would be judged stale and stolen by another worker, whose
69
+ reclaim_stale_running() then resets the live worker's rows and double-runs.
70
+ """
71
+ try:
72
+ os.utime(self.lock_path, None)
73
+ except OSError:
74
+ pass
75
+
76
+ def _owner_pid(self) -> Optional[int]:
77
+ try:
78
+ with open(self.lock_path, "rb") as f:
79
+ return int((f.read().strip() or b"0").decode() or "0")
80
+ except (OSError, ValueError):
81
+ return None
82
+
83
+ def release(self) -> None:
84
+ if self._fd is not None:
85
+ try:
86
+ os.close(self._fd)
87
+ finally:
88
+ self._fd = None
89
+ # Ownership-checked unlink: if a heartbeat gap let our lock be stolen and
90
+ # another worker recreated it, do NOT delete THEIR lock.
91
+ try:
92
+ if self._owner_pid() == os.getpid():
93
+ os.unlink(self.lock_path)
94
+ except OSError:
95
+ pass
96
+
97
+
98
+ @dataclass
99
+ class TickResult:
100
+ ran: List[str] = field(default_factory=list)
101
+ failed: List[str] = field(default_factory=list)
102
+ skipped_locked: bool = False
103
+ skipped_busy: bool = False
104
+
105
+
106
+ class CronScheduler:
107
+ def __init__(self, store: CronJobStore, runner: Runner, *, lock_path: str):
108
+ self.store = store
109
+ self.runner = runner
110
+ self.lock_path = lock_path
111
+
112
+ async def tick(self, now: Optional[datetime] = None) -> TickResult:
113
+ from core.interactive_gate import is_interactive_busy
114
+ if is_interactive_busy():
115
+ # human mid-turn in the REPL; defer cron execution (jobs stay due for
116
+ # the next idle tick, sharing the CWD workspace safely).
117
+ return TickResult(skipped_busy=True)
118
+ now = now or datetime.now()
119
+ lock = TickLock(self.lock_path)
120
+ if not lock.acquire():
121
+ logger.debug("cron tick skipped: lock held")
122
+ return TickResult(skipped_locked=True)
123
+ try:
124
+ # C2: also hold the cross-process workspace lock so a cron run in this
125
+ # process doesn't mutate the shared CWD workspace while a `rob` REPL in
126
+ # another process is mid-turn. Non-blocking (timeout=0). Acquire is
127
+ # fail-open (any lock error -> defer the tick); errors from the run
128
+ # itself propagate normally (not masked by the lock handling).
129
+ from core.interactive_gate import workspace_turn_lock
130
+ ws = workspace_turn_lock(timeout=0)
131
+ try:
132
+ ws.__enter__()
133
+ except Exception:
134
+ logger.debug("cron tick skipped: workspace lock unavailable/held")
135
+ return TickResult(skipped_busy=True)
136
+ # Heartbeat the tick lock so a legitimately long tick (>_LOCK_STALE_SECONDS)
137
+ # is never seen as stale + stolen by another worker (which would then
138
+ # reclaim + double-run this tick's in-flight jobs).
139
+ hb = asyncio.create_task(self._lock_heartbeat(lock))
140
+ try:
141
+ return await self._run_due(now)
142
+ finally:
143
+ hb.cancel()
144
+ try:
145
+ ws.__exit__(None, None, None)
146
+ except Exception:
147
+ pass
148
+ finally:
149
+ lock.release()
150
+
151
+ async def _lock_heartbeat(self, lock: "TickLock") -> None:
152
+ interval = max(30.0, _LOCK_STALE_SECONDS / 3.0)
153
+ try:
154
+ while True:
155
+ await asyncio.sleep(interval)
156
+ lock.refresh()
157
+ except asyncio.CancelledError:
158
+ pass
159
+
160
+ async def _run_due(self, now: datetime) -> TickResult:
161
+ result = TickResult()
162
+ # Reclaim crash-orphaned 'running' jobs here — we hold the TickLock, so any
163
+ # 'running' row is genuinely stale (a live run always writes a terminal status
164
+ # after itself). This replaces the unsafe reclaim-in-__init__.
165
+ self.store.reclaim_stale_running()
166
+ for job in self.store.due(now):
167
+ # Atomic claim: only run if WE flipped it scheduled->running. Guards against
168
+ # ever double-running a job (defense-in-depth alongside the TickLock).
169
+ if not self.store.claim_for_run(job.id):
170
+ continue
171
+ success = await self._run_one(job)
172
+ self._record(job, now, success)
173
+ (result.ran if success else result.failed).append(job.id)
174
+ return result
175
+
176
+ async def _run_one(self, job: CronJob) -> bool:
177
+ try:
178
+ return bool(await asyncio.wait_for(
179
+ self.runner(job), timeout=max(job.max_duration_seconds, 0.001),
180
+ ))
181
+ except asyncio.TimeoutError:
182
+ logger.warning("cron job %s timed out after %ss", job.id, job.max_duration_seconds)
183
+ return False
184
+ except Exception as e: # runner blew up — never crash the tick
185
+ logger.error("cron job %s raised: %s", job.id, e, exc_info=True)
186
+ return False
187
+
188
+ def _record(self, job: CronJob, now: datetime, success: bool) -> None:
189
+ if job.one_shot:
190
+ self.store.update_after_run(
191
+ job.id, last_run_at=now, next_run_at=None,
192
+ status="done" if success else "failed",
193
+ )
194
+ return
195
+ # recurring: reschedule from the schedule spec
196
+ try:
197
+ nxt = parse_schedule(job.schedule_spec).next_run_after(now)
198
+ except ScheduleError:
199
+ nxt = None
200
+ status = "scheduled" if nxt else "done"
201
+ self.store.update_after_run(job.id, last_run_at=now, next_run_at=nxt, status=status)
cron/service.py ADDED
@@ -0,0 +1,57 @@
1
+ """CronService — the schedule/list/cancel API over the job store (roadmap P5).
2
+
3
+ Thin, fully testable orchestration of :mod:`cron.schedule` + :mod:`cron.jobs`.
4
+ The agent-facing ``cronjob`` tool (``tools/cronjob_tools.py``) and any HTTP/CLI
5
+ surface delegate here so scheduling logic lives in exactly one place.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from datetime import datetime
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+ from cron.jobs import CronJob, CronJobStore
14
+ from cron.schedule import ScheduleError, parse_schedule
15
+
16
+ _DEFAULT_MAX_DURATION_S = 180 # 3-minute hard cap for cron sessions
17
+
18
+
19
+ class CronService:
20
+ def __init__(self, store: CronJobStore, *, now: Optional[Callable[[], datetime]] = None,
21
+ id_factory: Optional[Callable[[], str]] = None):
22
+ self.store = store
23
+ self._now = now or datetime.now
24
+ self._id = id_factory or (lambda: uuid.uuid4().hex[:12])
25
+
26
+ def schedule(self, *, task: str, schedule_spec: str, user_id: str,
27
+ payload: Optional[Dict[str, Any]] = None,
28
+ max_duration_seconds: int = _DEFAULT_MAX_DURATION_S,
29
+ job_id: Optional[str] = None) -> CronJob:
30
+ """Validate the spec, compute the first run, and persist the job.
31
+
32
+ Raises ScheduleError for an unparseable spec or one with no future run
33
+ (e.g. a one-shot timestamp already in the past).
34
+
35
+ NOTE (ME-D2): ``skip_memory`` was removed from this signature — it was
36
+ never consumed by the runner or the goal dispatcher (dead, inert knob).
37
+ A cron run gets cross-session memory recall like any other session; see
38
+ ``CronJob.skip_memory`` in ``cron/jobs.py`` for the dormant DB column.
39
+ """
40
+ schedule = parse_schedule(schedule_spec) # raises ScheduleError
41
+ now = self._now()
42
+ next_run = schedule.next_run_after(now)
43
+ if next_run is None:
44
+ raise ScheduleError(f"schedule {schedule_spec!r} has no future run time")
45
+ job = CronJob(
46
+ id=job_id or self._id(), task=task, schedule_spec=schedule_spec,
47
+ user_id=user_id, next_run_at=next_run, one_shot=schedule.one_shot,
48
+ max_duration_seconds=max_duration_seconds,
49
+ payload=payload or {}, created_at=now,
50
+ )
51
+ return self.store.add(job)
52
+
53
+ def list_jobs(self, user_id: Optional[str] = None) -> List[CronJob]:
54
+ return self.store.list(user_id=user_id)
55
+
56
+ def cancel(self, job_id: str, *, user_id: Optional[str] = None) -> bool:
57
+ return self.store.cancel(job_id, user_id=user_id)
cron/wake_gate.py ADDED
@@ -0,0 +1,223 @@
1
+ """Wake change-gate: skip a change-gated cron
2
+ review tick when nothing observable changed since the last tick.
3
+
4
+ The observed autonomy economy was ~23/25 no-op review wakes — every tick paid
5
+ for a full model run to conclude "nothing new". The gate computes a cheap
6
+ fingerprint over the tenant's observable autonomy state (goal board + goal
7
+ events + cron activity + newest episode) and compares it to the baseline stored
8
+ at the previous tick; equality means the paid model call is skipped ($0 tick,
9
+ same shape as ``wake_agent=False``).
10
+
11
+ Scope guards (both must hold before a tick can be skipped):
12
+ - global: ``AutonomyConfig.wake_change_gate()`` (``WAKE_CHANGE_GATE``, posture
13
+ ``full`` turns it on by default — it pairs with ``CRON_ENABLED``);
14
+ - per-job: ``payload.change_gated`` truthy AND no ``payload.deliver`` — a
15
+ scheduled delivery (daily digest) must fire even with zero change.
16
+
17
+ Fail-open everywhere: a missing DB, a schema surprise, or any exception in a
18
+ fingerprint source contributes a neutral value or disables the skip — the gate
19
+ can only ever suppress a tick when every read succeeded AND matched the
20
+ baseline. The baseline is recorded AFTER a run, tagged with the run's outcome
21
+ (``record_wake_outcome``): a skip requires the last gated run to have SUCCEEDED,
22
+ so a persistently-failing job (which mutates no observable state and would
23
+ otherwise fingerprint as "unchanged") always retries instead of being silently
24
+ swallowed.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import logging
30
+ import os
31
+ import sqlite3
32
+ import time
33
+ from typing import Any, Callable, Optional
34
+
35
+ from core.sqlite_util import execute_retry
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def _neutral_on_missing_table(fn: Callable[[], str], neutral: str) -> str:
41
+ """A missing table/DB is a known-empty source (nothing to observe) → a stable
42
+ neutral token. Any OTHER error re-raises so the caller's fail-open varying
43
+ token forces the tick to run."""
44
+ try:
45
+ return fn()
46
+ except sqlite3.OperationalError as e:
47
+ if "no such table" in str(e).lower():
48
+ return neutral
49
+ raise
50
+
51
+
52
+ class WakeGateStore:
53
+ """Per-job fingerprint baselines, colocated in cron.db (the gate is a cron
54
+ concern; a side table keeps the baseline server-owned and out of the
55
+ agent-mutable job payload)."""
56
+
57
+ def __init__(self, db_path: str):
58
+ self.db_path = db_path
59
+ os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
60
+ execute_retry(
61
+ db_path,
62
+ """CREATE TABLE IF NOT EXISTS wake_gate (
63
+ job_id TEXT PRIMARY KEY,
64
+ user_id TEXT,
65
+ fingerprint TEXT NOT NULL,
66
+ ok INTEGER NOT NULL DEFAULT 1,
67
+ updated_at REAL NOT NULL
68
+ )""",
69
+ )
70
+ try: # idempotent migration for pre-`ok` tables
71
+ execute_retry(db_path, "ALTER TABLE wake_gate ADD COLUMN ok INTEGER NOT NULL DEFAULT 1")
72
+ except sqlite3.OperationalError:
73
+ pass
74
+
75
+ def get(self, job_id: str) -> Optional[dict]:
76
+ row = execute_retry(
77
+ self.db_path, "SELECT fingerprint, ok FROM wake_gate WHERE job_id=?",
78
+ (job_id,), fetch="one",
79
+ )
80
+ return dict(row) if row else None
81
+
82
+ def put(self, job_id: str, user_id: str, fingerprint: str, *, ok: bool = True) -> None:
83
+ execute_retry(
84
+ self.db_path,
85
+ """INSERT INTO wake_gate (job_id, user_id, fingerprint, ok, updated_at)
86
+ VALUES (?,?,?,?,?)
87
+ ON CONFLICT(job_id) DO UPDATE SET
88
+ fingerprint=excluded.fingerprint, ok=excluded.ok,
89
+ updated_at=excluded.updated_at""",
90
+ (job_id, user_id, fingerprint, 1 if ok else 0, time.time()),
91
+ )
92
+
93
+
94
+ def _goal_board_cursor(user_id: str, data_dir: str) -> str:
95
+ """MAX(goal_events.id) for the tenant + goal counts by status — bumps on every
96
+ goal/ask/objective mutation (goal_events is AUTOINCREMENT)."""
97
+ db = os.path.join(data_dir, "goals.db")
98
+ if not os.path.exists(db):
99
+ return "goals:none"
100
+ return _neutral_on_missing_table(lambda: _goal_board_read(user_id, db), "goals:none")
101
+
102
+
103
+ def _goal_board_read(user_id: str, db: str) -> str:
104
+ max_ev = execute_retry(
105
+ db,
106
+ "SELECT MAX(e.id) AS m FROM goal_events e JOIN goals g ON g.id=e.goal_id "
107
+ "WHERE g.user_id=?",
108
+ (user_id,), fetch="one",
109
+ )
110
+ counts = execute_retry(
111
+ db,
112
+ "SELECT status, COUNT(*) AS n FROM goals WHERE user_id=? GROUP BY status "
113
+ "ORDER BY status",
114
+ (user_id,), fetch="all",
115
+ )
116
+ count_sig = ",".join(f"{r['status']}={r['n']}" for r in (counts or []))
117
+ return f"goals:{(max_ev or {})['m'] if max_ev else None}:{count_sig}"
118
+
119
+
120
+ def _cron_cursor(user_id: str, data_dir: str, exclude_job_id: Optional[str]) -> str:
121
+ """Newest last_run_at across the tenant's OTHER cron jobs (another job having
122
+ run is observable change for a review tick)."""
123
+ db = os.path.join(data_dir, "cron.db")
124
+ if not os.path.exists(db):
125
+ return "cron:none"
126
+
127
+ def read() -> str:
128
+ row = execute_retry(
129
+ db,
130
+ "SELECT MAX(last_run_at) AS m FROM cron_jobs WHERE user_id=? AND id != ?",
131
+ (user_id, exclude_job_id or ""), fetch="one",
132
+ )
133
+ return f"cron:{row['m'] if row else None}"
134
+
135
+ return _neutral_on_missing_table(read, "cron:none")
136
+
137
+
138
+ def _episode_cursor(user_id: str, data_dir: str) -> str:
139
+ """Newest episode row for the tenant (a completed session/goal/cron run since
140
+ the last tick is observable change). Reads the memory DB directly and
141
+ fail-opens to neutral on any surprise — the provider owns the schema."""
142
+ for name in ("memory.db",):
143
+ db = os.path.join(data_dir, name)
144
+ if not os.path.exists(db):
145
+ continue
146
+ def read() -> str:
147
+ row = execute_retry(
148
+ db, "SELECT MAX(id) AS m FROM episodes WHERE user_id=?",
149
+ (user_id,), fetch="one",
150
+ )
151
+ return f"episodes:{row['m'] if row else None}"
152
+
153
+ return _neutral_on_missing_table(read, "episodes:none")
154
+ return "episodes:none"
155
+
156
+
157
+ def compute_wake_fingerprint(
158
+ user_id: str, *, data_dir: str, exclude_job_id: Optional[str] = None
159
+ ) -> str:
160
+ """Stable hash of the tenant's observable autonomy state (cheap, no LLM)."""
161
+ parts = []
162
+ for source in (
163
+ lambda: _goal_board_cursor(user_id, data_dir),
164
+ lambda: _cron_cursor(user_id, data_dir, exclude_job_id),
165
+ lambda: _episode_cursor(user_id, data_dir),
166
+ ):
167
+ try:
168
+ parts.append(source())
169
+ except Exception:
170
+ # a failing source contributes a per-call-varying token so the
171
+ # fingerprint reads as "changed" — fail-open, never suppress a wake
172
+ parts.append(f"err:{time.time()}")
173
+ return hashlib.sha256("|".join(parts).encode()).hexdigest()
174
+
175
+
176
+ def gate_applies(job: Any) -> bool:
177
+ """Both guards: the global flag AND the per-job opt-in (never delivery jobs)."""
178
+ try:
179
+ from agents.task.constants import AutonomyConfig
180
+ if not AutonomyConfig.wake_change_gate():
181
+ return False
182
+ payload = dict(getattr(job, "payload", None) or {})
183
+ return bool(payload.get("change_gated")) and not payload.get("deliver")
184
+ except Exception:
185
+ return False
186
+
187
+
188
+ def should_skip_wake(job: Any, *, data_dir: str) -> bool:
189
+ """Whether this change-gated tick can be skipped ($0). READ-ONLY: a skip
190
+ requires an existing baseline that (a) matches the current fingerprint and
191
+ (b) was recorded after a SUCCESSFUL run — a failed run never establishes a
192
+ skippable baseline, so failing jobs always retry. Callers record the
193
+ baseline after the run via :func:`record_wake_outcome`."""
194
+ try:
195
+ if not gate_applies(job):
196
+ return False
197
+ store = WakeGateStore(os.path.join(data_dir, "cron.db"))
198
+ baseline = store.get(job.id)
199
+ if not baseline or not baseline.get("ok"):
200
+ return False
201
+ fp = compute_wake_fingerprint(
202
+ job.user_id, data_dir=data_dir, exclude_job_id=getattr(job, "id", None)
203
+ )
204
+ return fp == baseline.get("fingerprint")
205
+ except Exception:
206
+ logger.warning("wake gate error — failing open (running the tick)", exc_info=True)
207
+ return False
208
+
209
+
210
+ def record_wake_outcome(job: Any, *, data_dir: str, ok: bool) -> None:
211
+ """Record the post-run baseline (fingerprint computed AFTER the run, so the
212
+ run's own writes don't read as 'change' next tick) tagged with the outcome.
213
+ Fail-open — a store error just means the next tick runs."""
214
+ try:
215
+ if not gate_applies(job):
216
+ return
217
+ store = WakeGateStore(os.path.join(data_dir, "cron.db"))
218
+ fp = compute_wake_fingerprint(
219
+ job.user_id, data_dir=data_dir, exclude_job_id=getattr(job, "id", None)
220
+ )
221
+ store.put(job.id, job.user_id, fp, ok=ok)
222
+ except Exception:
223
+ logger.warning("wake gate outcome record failed (next tick will run)", exc_info=True)
data/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """POLYROB seed data (prompts, etc.) shipped with the package."""