@turmind/halo 0.1.9

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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/admin-out/404.html +1 -0
  4. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
  5. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
  6. package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
  7. package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
  8. package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
  9. package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
  10. package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
  11. package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
  12. package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
  13. package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
  14. package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
  15. package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
  16. package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
  17. package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
  18. package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
  19. package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
  20. package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
  21. package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
  22. package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
  23. package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
  24. package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
  25. package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
  26. package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
  27. package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
  28. package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
  29. package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
  30. package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
  31. package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
  32. package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  33. package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
  34. package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
  35. package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
  36. package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  37. package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  38. package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  39. package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  40. package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  41. package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  42. package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  43. package/admin-out/favicon.ico +0 -0
  44. package/admin-out/icon.svg +18 -0
  45. package/admin-out/index.html +1 -0
  46. package/admin-out/index.txt +23 -0
  47. package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
  48. package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
  49. package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
  50. package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
  51. package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
  52. package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
  53. package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
  54. package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
  55. package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
  56. package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  57. package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
  58. package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
  59. package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
  60. package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
  61. package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
  62. package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
  63. package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
  64. package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
  65. package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
  66. package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
  67. package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
  68. package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
  69. package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  70. package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
  71. package/admin-out/monaco/vs/editor/editor.main.css +1 -0
  72. package/admin-out/monaco/vs/editor/editor.main.js +5 -0
  73. package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
  74. package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
  75. package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
  76. package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
  77. package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  78. package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
  79. package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
  80. package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
  81. package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
  82. package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
  83. package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
  84. package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
  85. package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
  86. package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
  87. package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
  88. package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
  89. package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  90. package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
  91. package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
  92. package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
  93. package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  94. package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
  95. package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
  96. package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
  97. package/admin-out/monaco/vs/loader.js +1368 -0
  98. package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
  99. package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
  100. package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
  101. package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
  102. package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
  103. package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
  104. package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
  105. package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
  106. package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
  107. package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
  108. package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
  109. package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
  110. package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
  111. package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
  112. package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
  113. package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
  114. package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
  115. package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
  116. package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
  117. package/admin-out/monaco/vs/nls.messages.js.js +10 -0
  118. package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
  119. package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
  120. package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
  121. package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
  122. package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
  123. package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
  124. package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
  125. package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
  126. package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
  127. package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  128. package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
  129. package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
  130. package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
  131. package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
  132. package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  133. package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  134. package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
  135. package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  136. package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
  137. package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
  138. package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  139. package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
  140. package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
  141. package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
  142. package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
  143. package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  144. package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
  145. package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
  146. package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
  147. package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
  148. package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
  149. package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
  150. package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
  151. package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  152. package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  153. package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
  154. package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
  155. package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
  156. package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
  157. package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  158. package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
  159. package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
  160. package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
  161. package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
  162. package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
  163. package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
  164. package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  165. package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
  166. package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
  167. package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
  168. package/bin/halo.js +9 -0
  169. package/bundled-docs/design/agent.md +145 -0
  170. package/bundled-docs/design/architecture.md +270 -0
  171. package/bundled-docs/design/background-dispatch.md +151 -0
  172. package/bundled-docs/design/command.md +58 -0
  173. package/bundled-docs/design/cron.md +88 -0
  174. package/bundled-docs/design/evolution.md +383 -0
  175. package/bundled-docs/design/express-self.md +145 -0
  176. package/bundled-docs/design/feishu.md +224 -0
  177. package/bundled-docs/design/halo-city.md +112 -0
  178. package/bundled-docs/design/prompt-system.md +411 -0
  179. package/bundled-docs/design/session.md +321 -0
  180. package/bundled-docs/design/slack.md +212 -0
  181. package/bundled-docs/design/storage.md +578 -0
  182. package/bundled-docs/design/telegram.md +161 -0
  183. package/bundled-docs/design/web.md +222 -0
  184. package/bundled-docs/design/wechat.md +146 -0
  185. package/bundled-docs/design/ws.md +144 -0
  186. package/bundled-docs/dev/acp-adapter.md +337 -0
  187. package/bundled-docs/dev/add-channel.md +376 -0
  188. package/bundled-docs/dev/add-model-provider.md +386 -0
  189. package/bundled-docs/dev/api.md +686 -0
  190. package/bundled-docs/dev/deploy.md +198 -0
  191. package/bundled-docs/dev/desktop-packaging.md +327 -0
  192. package/bundled-docs/dev/env.md +158 -0
  193. package/bundled-docs/dev/logging.md +85 -0
  194. package/bundled-docs/dev/previews.md +225 -0
  195. package/bundled-docs/dev/tools.md +331 -0
  196. package/bundled-docs/guide/README.md +29 -0
  197. package/bundled-docs/guide/agents.md +264 -0
  198. package/bundled-docs/guide/channels/README.md +62 -0
  199. package/bundled-docs/guide/channels/acp.md +150 -0
  200. package/bundled-docs/guide/channels/feishu.md +154 -0
  201. package/bundled-docs/guide/channels/slack.md +156 -0
  202. package/bundled-docs/guide/channels/telegram.md +112 -0
  203. package/bundled-docs/guide/channels/web.md +149 -0
  204. package/bundled-docs/guide/channels/wechat.md +110 -0
  205. package/bundled-docs/guide/chat.md +112 -0
  206. package/bundled-docs/guide/cli.md +197 -0
  207. package/bundled-docs/guide/getting-started.md +93 -0
  208. package/bundled-docs/guide/secrets-and-credentials.md +161 -0
  209. package/bundled-docs/guide/sessions.md +104 -0
  210. package/bundled-docs/guide/skills.md +302 -0
  211. package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
  212. package/bundled-docs/guide/workspace.md +131 -0
  213. package/bundled-docs/plans/cron-tasks.md +362 -0
  214. package/bundled-docs/plans/design-suggestions.md +203 -0
  215. package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
  216. package/bundled-docs/plans/launch-plan.md +111 -0
  217. package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
  218. package/bundled-docs/plans/remote-mac-worker.md +196 -0
  219. package/bundled-docs/plans/roadmap.md +40 -0
  220. package/bundled-docs/plans/self-evolution.md +1379 -0
  221. package/bundled-docs/plans/todo.md +32 -0
  222. package/bundled-docs/plans/weixin-channel.md +255 -0
  223. package/bundled-docs/requirements/agent.md +92 -0
  224. package/bundled-docs/requirements/chat.md +81 -0
  225. package/bundled-docs/requirements/command.md +130 -0
  226. package/bundled-docs/requirements/editor.md +82 -0
  227. package/bundled-docs/requirements/explorer.md +89 -0
  228. package/bundled-docs/requirements/overview.md +62 -0
  229. package/bundled-docs/requirements/session.md +73 -0
  230. package/bundled-docs/requirements/settings.md +224 -0
  231. package/bundled-docs/requirements/skill.md +125 -0
  232. package/bundled-docs/requirements/source-control.md +82 -0
  233. package/bundled-docs/requirements/terminal.md +118 -0
  234. package/bundled-docs/requirements/workspace.md +80 -0
  235. package/bundled-docs/test/docs-seeding.md +101 -0
  236. package/bundled-docs/test/session.md +374 -0
  237. package/dist/index.js +26750 -0
  238. package/package.json +67 -0
  239. package/templates/INSTRUCTIONS.md +25 -0
  240. package/templates/agents/__apply_agent__/AGENT.md +229 -0
  241. package/templates/agents/__apply_agent__/agent.yaml +30 -0
  242. package/templates/agents/__evo_agent__/AGENT.md +323 -0
  243. package/templates/agents/__evo_agent__/agent.yaml +29 -0
  244. package/templates/agents/__score__/AGENT.md +177 -0
  245. package/templates/agents/__score__/agent.yaml +29 -0
  246. package/templates/agents/deep-executor/AGENT.md +60 -0
  247. package/templates/agents/deep-executor/agent.yaml +25 -0
  248. package/templates/agents/default/AGENT.md +38 -0
  249. package/templates/agents/default/agent.yaml +51 -0
  250. package/templates/agents/executor/AGENT.md +38 -0
  251. package/templates/agents/executor/agent.yaml +25 -0
  252. package/templates/aliases.yaml +26 -0
  253. package/templates/canvas/self.html +660 -0
  254. package/templates/config.yaml +69 -0
  255. package/templates/models/anthropic.yaml +47 -0
  256. package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
  257. package/templates/models/aws-bedrock-mantle.yaml +89 -0
  258. package/templates/models/deepseek.yaml +62 -0
  259. package/templates/models/doubao.yaml +137 -0
  260. package/templates/models/hunyuan.yaml +56 -0
  261. package/templates/models/kimi.yaml +41 -0
  262. package/templates/models/mimo-token-plan-china.yaml +61 -0
  263. package/templates/models/minimax.yaml +80 -0
  264. package/templates/models/openai.yaml +51 -0
  265. package/templates/models/qwen.yaml +78 -0
  266. package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
  267. package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
  268. package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
  269. package/templates/prompts/all/TOOL_SHELL.md +4 -0
  270. package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
  271. package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
  272. package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
  273. package/templates/schema.sql +30 -0
  274. package/templates/skills/acp/SKILL.md +236 -0
  275. package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
  276. package/templates/skills/acp/templates/ask.py +176 -0
  277. package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
  278. package/templates/skills/agent/SKILL.md +128 -0
  279. package/templates/skills/aws-knowledge/SKILL.md +82 -0
  280. package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
  281. package/templates/skills/cron/SKILL.md +217 -0
  282. package/templates/skills/cron/manage-cron.py +434 -0
  283. package/templates/skills/halo/SKILL.md +98 -0
  284. package/templates/skills/nova-web-search/SKILL.md +65 -0
  285. package/templates/skills/self/SKILL.md +139 -0
  286. package/templates/skills/send-file/SKILL.md +28 -0
  287. package/templates/skills/send-file/feishu.md +16 -0
  288. package/templates/skills/send-file/slack.md +7 -0
  289. package/templates/skills/send-file/telegram.md +9 -0
  290. package/templates/skills/send-file/web.md +6 -0
  291. package/templates/skills/send-file/wechat.md +8 -0
  292. package/templates/skills/skill/SKILL.md +131 -0
  293. package/templates/skills/workspace/SKILL.md +28 -0
  294. package/templates/skills/workspace/setup-tidy.md +265 -0
  295. package/templates/skills/workspace/share.md +187 -0
  296. package/templates/skills/workspace/stage.py +487 -0
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ manage-cron — CRUD + listing for the global cron-jobs db.
4
+
5
+ Skill helper: agents call this rather than handcrafting sqlite3 queries.
6
+ The script handles id generation, JSON encoding for `targets`, schedule
7
+ validation hint (we leave the actual cron parse to the runner), and
8
+ keeps `updated_at` fresh.
9
+
10
+ Server's cron daemon polls cron_jobs every 10 seconds, so any change
11
+ made here goes live within ~10s without a server restart.
12
+
13
+ Output is always JSON on stdout for easy parsing. Errors go to stderr
14
+ with non-zero exit.
15
+
16
+ Subcommands:
17
+ list [--chat-id <id>] → list all jobs (sorted by createdAt desc).
18
+ --chat-id filters to jobs that push to that
19
+ chat — useful for "delete my cron" inside a
20
+ channel where the agent only knows the
21
+ current chatId.
22
+ get <id> → one job + last 5 runs
23
+ create <args> → insert a new job, prints the generated id
24
+ update <id> <args> → patch an existing job
25
+ enable <id> / disable <id> → toggle the `enabled` flag
26
+ delete <id> → remove the job and its run history
27
+ channels → list bound channel accounts that can be
28
+ used as cron targets (for `--targets`)
29
+ runs <jobId> [--limit N] [--before <runId>]
30
+ → run history, cursor-paginated by runId
31
+ (which is an ISO timestamp = sort order)
32
+
33
+ `--targets` arg: comma-separated `channelType:accountId` pairs.
34
+ example: `--targets telegram:halo_agent_bot,wechat:alice`
35
+ """
36
+ import argparse
37
+ import json
38
+ import os
39
+ import sqlite3
40
+ import sys
41
+ import time
42
+ from pathlib import Path
43
+
44
+ DB = Path.home() / '.halo' / 'global' / 'cron.db'
45
+ CHANNELS_DB = Path.home() / '.halo' / 'secrets' / 'channels' / 'channels.db'
46
+
47
+
48
+ def open_db() -> sqlite3.Connection:
49
+ if not DB.exists():
50
+ die(f'cron db not found at {DB}; is the halo server running and initialized?')
51
+ conn = sqlite3.connect(str(DB))
52
+ conn.row_factory = sqlite3.Row
53
+ return conn
54
+
55
+
56
+ def die(msg: str, code: int = 1):
57
+ sys.stderr.write(f'error: {msg}\n')
58
+ sys.exit(code)
59
+
60
+
61
+ def now_ms() -> int:
62
+ return int(time.time() * 1000)
63
+
64
+
65
+ def gen_job_id() -> str:
66
+ """Match the admin route's id format: cron-<timestamp36>-<rand6>."""
67
+ import random
68
+ import string
69
+ ts = format(int(time.time() * 1000), 'x')[-8:]
70
+ rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
71
+ return f'cron-{ts}-{rand}'
72
+
73
+
74
+ def parse_targets(raw: str) -> list[dict]:
75
+ """`telegram:bot1,wechat:user2` → [{channelType, accountId}, ...].
76
+
77
+ Optional explicit chat id: `telegram:bot1:12345` — useful when the
78
+ cron was created from inside a chat and should keep replying there
79
+ instead of fanning out to every whitelisted user. Accepted forms:
80
+
81
+ telegram:bot1 → fan-out (telegram allowedUsers)
82
+ telegram:bot1:12345 → only chat 12345
83
+ wechat:owner_id → bot owner (QR-bind userId)
84
+ wechat:owner_id:o123 → only that openId
85
+
86
+ Also accepts a JSON array for callers that want to set complex shapes.
87
+ """
88
+ raw = raw.strip()
89
+ if not raw:
90
+ return []
91
+ if raw.startswith('['):
92
+ return json.loads(raw)
93
+ out = []
94
+ for token in raw.split(','):
95
+ token = token.strip()
96
+ if not token:
97
+ continue
98
+ if ':' not in token:
99
+ die(f'invalid target "{token}": expected "channelType:accountId[:chatId]"')
100
+ parts = token.split(':', 2)
101
+ ct, aid = parts[0].strip(), parts[1].strip()
102
+ item: dict = {'channelType': ct, 'accountId': aid}
103
+ if len(parts) == 3 and parts[2].strip():
104
+ item['chatId'] = parts[2].strip()
105
+ out.append(item)
106
+ return out
107
+
108
+
109
+ def parse_run_at(raw: str | None) -> int | None:
110
+ """Accepts ISO-8601 (`2026-05-24T09:00:00`) or a unix-ms integer; returns
111
+ epoch ms or None. Local time without offset is parsed as host TZ —
112
+ same convention as `datetime.fromisoformat`."""
113
+ if not raw:
114
+ return None
115
+ raw = raw.strip()
116
+ if not raw:
117
+ return None
118
+ if raw.isdigit():
119
+ return int(raw)
120
+ from datetime import datetime
121
+ try:
122
+ dt = datetime.fromisoformat(raw)
123
+ except ValueError:
124
+ die(f'invalid --run-at "{raw}": expected ISO-8601 (e.g. 2026-05-24T09:00) or unix ms')
125
+ if dt.tzinfo is None:
126
+ dt = dt.astimezone() # treat as host-local
127
+ return int(dt.timestamp() * 1000)
128
+
129
+
130
+ def row_to_job(row: sqlite3.Row) -> dict:
131
+ j = dict(row)
132
+ try:
133
+ j['targets'] = json.loads(j.get('targets') or '[]')
134
+ except Exception:
135
+ j['targets'] = []
136
+ j['enabled'] = bool(j['enabled'])
137
+ return j
138
+
139
+
140
+ # ── subcommand impls ──────────────────────────────────────────────────
141
+
142
+
143
+ def cmd_list(args):
144
+ conn = open_db()
145
+ rows = conn.execute(
146
+ 'SELECT * FROM cron_jobs ORDER BY created_at DESC'
147
+ ).fetchall()
148
+ jobs = [row_to_job(r) for r in rows]
149
+ # Filter to jobs whose targets push to this chat — used by the
150
+ # in-channel "delete my cron" / "list my crons" flow, where the
151
+ # agent only knows the current chatId and needs to find which
152
+ # cron_jobs reference it. chatId formats don't collide across
153
+ # channels (telegram numeric, slack D…/C…, feishu oc_…, wechat o…)
154
+ # so a single chatId match is unambiguous.
155
+ if args.chat_id:
156
+ jobs = [
157
+ j for j in jobs
158
+ if any((t.get('chatId') or '') == args.chat_id for t in j.get('targets') or [])
159
+ ]
160
+ print(json.dumps(jobs, ensure_ascii=False, indent=2))
161
+
162
+
163
+ def cmd_get(args):
164
+ conn = open_db()
165
+ row = conn.execute(
166
+ 'SELECT * FROM cron_jobs WHERE id = ?', (args.id,)
167
+ ).fetchone()
168
+ if not row:
169
+ die(f'job {args.id} not found', 2)
170
+ job = row_to_job(row)
171
+ runs = conn.execute(
172
+ 'SELECT id, status, started_at, completed_at, exit_code, failure_reason '
173
+ 'FROM cron_runs WHERE job_id = ? ORDER BY id DESC LIMIT 5',
174
+ (args.id,),
175
+ ).fetchall()
176
+ job['recentRuns'] = [dict(r) for r in runs]
177
+ print(json.dumps(job, ensure_ascii=False, indent=2))
178
+
179
+
180
+ def cmd_create(args):
181
+ if not args.workspace:
182
+ die('--workspace required')
183
+ if not args.prompt:
184
+ die('--prompt required')
185
+ has_schedule = bool(args.schedule and args.schedule.strip())
186
+ run_at_ms = parse_run_at(args.run_at)
187
+ if not has_schedule and run_at_ms is None:
188
+ die('--schedule or --run-at required')
189
+ if has_schedule and run_at_ms is not None:
190
+ die('--schedule and --run-at are mutually exclusive')
191
+ if run_at_ms is not None and run_at_ms <= now_ms():
192
+ die('--run-at must be in the future')
193
+
194
+ job_id = args.id or gen_job_id()
195
+ now = now_ms()
196
+ targets = parse_targets(args.targets) if args.targets else []
197
+
198
+ conn = open_db()
199
+ existing = conn.execute('SELECT 1 FROM cron_jobs WHERE id = ?', (job_id,)).fetchone()
200
+ if existing:
201
+ die(f'job id {job_id} already exists; pick another with --id', 2)
202
+
203
+ conn.execute(
204
+ 'INSERT INTO cron_jobs(id, label, workspace_path, agent_id, user_prompt, schedule, '
205
+ 'run_at, timezone, targets, enabled, created_at, updated_at) '
206
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
207
+ (
208
+ job_id,
209
+ args.label,
210
+ args.workspace,
211
+ args.agent or 'default',
212
+ args.prompt,
213
+ args.schedule or '',
214
+ run_at_ms,
215
+ args.timezone,
216
+ json.dumps(targets, ensure_ascii=False),
217
+ 0 if args.disabled else 1,
218
+ now, now,
219
+ ),
220
+ )
221
+ conn.commit()
222
+ print(json.dumps({'id': job_id, 'created': True}, ensure_ascii=False))
223
+
224
+
225
+ def cmd_update(args):
226
+ conn = open_db()
227
+ row = conn.execute('SELECT * FROM cron_jobs WHERE id = ?', (args.id,)).fetchone()
228
+ if not row:
229
+ die(f'job {args.id} not found', 2)
230
+
231
+ sets: list[tuple[str, object]] = []
232
+ if args.label is not None: sets.append(('label', args.label))
233
+ if args.workspace is not None: sets.append(('workspace_path', args.workspace))
234
+ if args.agent is not None: sets.append(('agent_id', args.agent))
235
+ if args.prompt is not None: sets.append(('user_prompt', args.prompt))
236
+ if args.schedule is not None: sets.append(('schedule', args.schedule))
237
+ if args.run_at is not None:
238
+ run_at_ms = parse_run_at(args.run_at)
239
+ if run_at_ms is not None and run_at_ms <= now_ms():
240
+ die('--run-at must be in the future')
241
+ sets.append(('run_at', run_at_ms))
242
+ if args.timezone is not None: sets.append(('timezone', args.timezone))
243
+ if args.targets is not None:
244
+ sets.append(('targets', json.dumps(parse_targets(args.targets), ensure_ascii=False)))
245
+ if not sets:
246
+ die('nothing to update — pass at least one --label/--workspace/--agent/--prompt/--schedule/--run-at/--timezone/--targets')
247
+ sets.append(('updated_at', now_ms()))
248
+
249
+ placeholders = ', '.join(f'{k} = ?' for k, _ in sets)
250
+ values = [v for _, v in sets] + [args.id]
251
+ conn.execute(f'UPDATE cron_jobs SET {placeholders} WHERE id = ?', values)
252
+ conn.commit()
253
+ print(json.dumps({'id': args.id, 'updated': True}, ensure_ascii=False))
254
+
255
+
256
+ def cmd_enable(args):
257
+ _toggle_enabled(args.id, True)
258
+
259
+
260
+ def cmd_disable(args):
261
+ _toggle_enabled(args.id, False)
262
+
263
+
264
+ def _toggle_enabled(job_id: str, on: bool):
265
+ conn = open_db()
266
+ cur = conn.execute(
267
+ 'UPDATE cron_jobs SET enabled = ?, updated_at = ? WHERE id = ?',
268
+ (1 if on else 0, now_ms(), job_id),
269
+ )
270
+ if cur.rowcount == 0:
271
+ die(f'job {job_id} not found', 2)
272
+ conn.commit()
273
+ print(json.dumps({'id': job_id, 'enabled': on}, ensure_ascii=False))
274
+
275
+
276
+ def cmd_delete(args):
277
+ conn = open_db()
278
+ cur = conn.execute('DELETE FROM cron_jobs WHERE id = ?', (args.id,))
279
+ if cur.rowcount == 0:
280
+ die(f'job {args.id} not found', 2)
281
+ conn.execute('DELETE FROM cron_runs WHERE job_id = ?', (args.id,))
282
+ conn.commit()
283
+ print(json.dumps({'id': args.id, 'deleted': True}, ensure_ascii=False))
284
+
285
+
286
+ def cmd_channels(_):
287
+ """List channel accounts that can be referenced as cron targets.
288
+
289
+ Mirrors the admin /api/cron/channel-targets endpoint:
290
+ telegram = ok if numeric id in allowedUsers OR a cached
291
+ lastActiveChatId; wechat = ok if account.userId (QR-bind owner)
292
+ OR cached lastActiveChatId.
293
+ """
294
+ if not CHANNELS_DB.exists():
295
+ print(json.dumps([], ensure_ascii=False))
296
+ return
297
+ conn = sqlite3.connect(str(CHANNELS_DB))
298
+ conn.row_factory = sqlite3.Row
299
+ rows = conn.execute(
300
+ "SELECT account_id, channel_type, label, enabled, workspace_path, config "
301
+ "FROM channel_accounts WHERE enabled = 1"
302
+ ).fetchall()
303
+ out = []
304
+ import re
305
+ for r in rows:
306
+ try:
307
+ cfg = json.loads(r['config'] or '{}')
308
+ except Exception:
309
+ cfg = {}
310
+ last = cfg.get('lastActiveChatId')
311
+ ct = r['channel_type']
312
+ if ct == 'telegram':
313
+ allowed = cfg.get('allowedUsers') or ''
314
+ has_whitelist_id = bool(re.search(r'\b-?\d+\b', allowed))
315
+ ready = has_whitelist_id or bool(last)
316
+ elif ct == 'wechat':
317
+ owner = cfg.get('userId')
318
+ ready = bool(owner) or bool(last)
319
+ else:
320
+ ready = bool(last)
321
+ out.append({
322
+ 'channelType': ct,
323
+ 'accountId': r['account_id'],
324
+ 'label': r['label'] or r['account_id'],
325
+ 'workspacePath': r['workspace_path'],
326
+ 'ready': ready,
327
+ })
328
+ print(json.dumps(out, ensure_ascii=False, indent=2))
329
+
330
+
331
+ def cmd_runs(args):
332
+ conn = open_db()
333
+ sql = (
334
+ 'SELECT id, job_id, trigger_kind, status, started_at, completed_at, '
335
+ 'exit_code, failure_reason, output, dispatch_results '
336
+ 'FROM cron_runs WHERE job_id = ?'
337
+ )
338
+ params: list[object] = [args.job_id]
339
+ if args.before:
340
+ # runId is `<isoTimestamp>-<rand>`, sortable as a string. id < cursor
341
+ # is the "older than" check for descending pagination.
342
+ sql += ' AND id < ?'
343
+ params.append(args.before)
344
+ sql += ' ORDER BY id DESC LIMIT ?'
345
+ params.append(args.limit + 1) # fetch one extra to detect hasMore
346
+ rows = conn.execute(sql, params).fetchall()
347
+ has_more = len(rows) > args.limit
348
+ rows = rows[:args.limit]
349
+ runs = []
350
+ for r in rows:
351
+ d = dict(r)
352
+ try:
353
+ d['dispatch_results'] = json.loads(d['dispatch_results']) if d['dispatch_results'] else None
354
+ except Exception:
355
+ pass
356
+ runs.append(d)
357
+ out = {
358
+ 'runs': runs,
359
+ 'hasMore': has_more,
360
+ 'nextCursor': runs[-1]['id'] if has_more and runs else None,
361
+ }
362
+ print(json.dumps(out, ensure_ascii=False, indent=2))
363
+
364
+
365
+ # ── argparse ──────────────────────────────────────────────────────────
366
+
367
+
368
+ def main():
369
+ p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
370
+ sub = p.add_subparsers(dest='cmd', required=True)
371
+
372
+ ls = sub.add_parser('list')
373
+ ls.add_argument('--chat-id', dest='chat_id',
374
+ help='filter to jobs that push to this chat id (use {{channel.chat_id}} from inside a chat)')
375
+ ls.set_defaults(func=cmd_list)
376
+
377
+ g = sub.add_parser('get')
378
+ g.add_argument('id')
379
+ g.set_defaults(func=cmd_get)
380
+
381
+ c = sub.add_parser('create')
382
+ c.add_argument('--id', help='custom job id (default: auto-generated)')
383
+ c.add_argument('--label')
384
+ c.add_argument('--workspace', required=True, help='absolute workspace path')
385
+ c.add_argument('--agent', help="agent id (default: 'default')")
386
+ c.add_argument('--prompt', required=True)
387
+ c.add_argument('--schedule', help='5-field cron expression (recurring); mutually exclusive with --run-at')
388
+ c.add_argument('--run-at', dest='run_at',
389
+ help='one-shot fire time, ISO-8601 (e.g. 2026-05-24T09:00) or unix ms — auto-disables after fire')
390
+ c.add_argument('--timezone', help='IANA tz, e.g. Asia/Shanghai')
391
+ c.add_argument('--targets',
392
+ help='comma-separated channelType:accountId[:chatId] list, or JSON array. '
393
+ 'chatId pins delivery to a specific chat (e.g. when scheduling from inside a chat).')
394
+ c.add_argument('--disabled', action='store_true', help='create paused (default: enabled)')
395
+ c.set_defaults(func=cmd_create)
396
+
397
+ u = sub.add_parser('update')
398
+ u.add_argument('id')
399
+ u.add_argument('--label')
400
+ u.add_argument('--workspace')
401
+ u.add_argument('--agent')
402
+ u.add_argument('--prompt')
403
+ u.add_argument('--schedule')
404
+ u.add_argument('--run-at', dest='run_at')
405
+ u.add_argument('--timezone')
406
+ u.add_argument('--targets')
407
+ u.set_defaults(func=cmd_update)
408
+
409
+ e = sub.add_parser('enable')
410
+ e.add_argument('id')
411
+ e.set_defaults(func=cmd_enable)
412
+
413
+ d = sub.add_parser('disable')
414
+ d.add_argument('id')
415
+ d.set_defaults(func=cmd_disable)
416
+
417
+ rm = sub.add_parser('delete')
418
+ rm.add_argument('id')
419
+ rm.set_defaults(func=cmd_delete)
420
+
421
+ sub.add_parser('channels').set_defaults(func=cmd_channels)
422
+
423
+ rs = sub.add_parser('runs')
424
+ rs.add_argument('job_id')
425
+ rs.add_argument('--limit', type=int, default=20)
426
+ rs.add_argument('--before', help='runId cursor; pages older runs')
427
+ rs.set_defaults(func=cmd_runs)
428
+
429
+ args = p.parse_args()
430
+ args.func(args)
431
+
432
+
433
+ if __name__ == '__main__':
434
+ main()
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: halo
3
+ description: Platform self-knowledge for Halo — configuration directories, MD file scopes, USER.md schema, and the bundled-doc lookup table. Activate when the user asks anything about Halo itself (how to configure it, where files live, how agents/skills/INSTRUCTIONS work, what doc to read for a given topic) or when you need to edit USER.md / INSTRUCTIONS.md / AGENT.md and want the scope rule before writing.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Halo Platform Knowledge
8
+
9
+ You are running inside **Halo**, a multi-agent collaboration workspace.
10
+
11
+ ## Configuration Directories
12
+
13
+ | Location | Scope | Purpose |
14
+ |----------|-------|---------|
15
+ | `~/.halo/global/` | Global (all projects) | User profile, global instructions, global agents & skills, system prompts, bundled docs |
16
+ | `<workspace>/.halo/` | Per-project | Project instructions, knowledge index, project agents & skills |
17
+
18
+ `.halo/` is excluded from `grep` and `glob`. Use `file_read` with the exact path to access files inside.
19
+
20
+ ## MD Files
21
+
22
+ | File | Purpose | Paths |
23
+ |------|---------|-------|
24
+ | **USER.md** | User profile (names, language, communication style) | `~/.halo/global/USER.md`; `<workspace>/.halo/USER.md` (override) |
25
+ | **AGENT.md** | Agent personality and behavior | `~/.halo/global/agents/<id>/AGENT.md`; `<workspace>/.halo/agents/<id>/AGENT.md` (override) |
26
+ | **INSTRUCTIONS.md** | User preferences injected into every agent | `~/.halo/global/INSTRUCTIONS.md`; `<workspace>/.halo/INSTRUCTIONS.md`; `<workspace>/<subdir>/.halo/INSTRUCTIONS.md` (cumulative per depth) |
27
+ | **INDEX.md** | Project documentation index | `<workspace>/.halo/INDEX.md` |
28
+ | **System prompts** | Platform-level instructions | `~/.halo/global/prompts/{bootstrap,all,root}/*.md` |
29
+
30
+ Changes take effect on the next conversation or session reset.
31
+
32
+ ## Organizing the workspace
33
+
34
+ The `workspace` skill handles both initial setup and ongoing cleanup of `.halo/INDEX.md`, `INSTRUCTIONS.md`, and `memory/`.
35
+
36
+ - **No INDEX.md yet** and the user starts engaging with the project's structure or goals (not casual browsing) → suggest `/workspace setup`. Drafts INDEX.md + INSTRUCTIONS.md from the README and a few questions.
37
+ - **INDEX.md already exists** and the user wants to "clean up / reorganize" the workspace → `/workspace tidy`. Reviews what's there, prunes stale entries, fixes broken links, and reshapes sections without rewriting from scratch.
38
+
39
+ ## Editing Scope Rule
40
+
41
+ USER.md / INSTRUCTIONS.md / AGENT.md exist at multiple scopes (global vs workspace). When editing:
42
+
43
+ 1. If a workspace file exists, edit that one
44
+ 2. Otherwise edit the global file
45
+ 3. If ambiguous, ask the user
46
+
47
+ Creating a new override in the wrong scope (e.g. writing to global when a workspace file already shadows it) hides the user's edits at runtime.
48
+
49
+ ## Managing User Profile
50
+
51
+ USER.md schema:
52
+
53
+ ```markdown
54
+ ---
55
+ user_name: [their name]
56
+ ai_name: [your name]
57
+ lang: [e.g. zh-CN, en]
58
+ ---
59
+
60
+ ## Communication Style
61
+ [Preferences]
62
+ ```
63
+
64
+ To update: apply the scope rule above, `file_read` the target, update fields, `file_write` back. Effective next conversation.
65
+
66
+ ## Platform Documentation
67
+
68
+ Bundled docs: `~/.halo/global/docs/`. These reflect the installed Halo version; training data may be outdated. For platform questions, pick the row below and `file_read` that file.
69
+
70
+ What's bundled (and what's not):
71
+
72
+ - `guide/` — end-user guides (8 files, see table below)
73
+ - `dev/` — developer-facing topics (3 files: tools, add-model-provider, add-channel)
74
+ - `requirements/` — product requirements (2 files: settings, command)
75
+ - `design/`, `plans/`, `test/` — **not bundled** (live in the source repo only). Cross-links from bundled docs into these paths will 404; answer from the doc body, not the link.
76
+
77
+ | User asks … | Read |
78
+ |---|---|
79
+ | How do I log in / open a workspace? | `guide/getting-started.md` |
80
+ | UI / keyboard shortcuts? | `guide/workspace.md` |
81
+ | Chat panel, @ mentions, slash commands, context injection? | `guide/chat.md` |
82
+ | Where do I see past conversations? | `guide/sessions.md` |
83
+ | Create / edit / delete an agent? AGENT.md vs agent.yaml? Global vs workspace? Thinking / prompt caching? | `guide/agents.md` |
84
+ | What tools can an agent use? How do I add one? | `dev/tools.md` |
85
+ | What is a skill? How do I create / test one? | `guide/skills.md` + `guide/testing-agents-and-skills.md` |
86
+ | How do I test the agent I just made? | `guide/testing-agents-and-skills.md` |
87
+ | Slash commands (how they work, registering) | `guide/skills.md` (skill-as-command) + `requirements/command.md` |
88
+ | `{{params.x.y}}` placeholders? | `guide/skills.md` (Placeholders section) |
89
+ | Where do I put API keys / credentials? | `guide/secrets-and-credentials.md` |
90
+ | What goes in settings.yaml? | `requirements/settings.md` |
91
+ | Add a new model provider (OpenAI / Gemini / …)? | `dev/add-model-provider.md` |
92
+ | Add a new IM / chat channel (Slack / Discord / …)? | `dev/add-channel.md` |
93
+
94
+ Questions outside these rows aren't in the bundled docs (backend internals, REST API reference, design specs live in the Halo source repo). Read the code or tell the user you don't know.
95
+
96
+ ## Skills
97
+
98
+ Skills are referenced by id from an agent's `agent.yaml`. The system prompt shows each skill's name + description; the full body is loaded on demand via `activate_skill`.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: nova-web-search
3
+ description: Real-time web search using Amazon Nova 2 Lite's built-in nova_grounding system tool. Use this skill when the user asks about current events, news, latest updates, prices, or any real-time information. This replaces the web_search tool.
4
+ user-invocable: false
5
+ ---
6
+ # Nova Web Search
7
+
8
+ Perform real-time web search using Amazon Nova 2 Lite's built-in `nova_grounding` system tool. Returns a synthesized answer with source URLs.
9
+
10
+ ## How to Use
11
+
12
+ Run the following Python script via `shell_exec`:
13
+
14
+ ```python
15
+ import boto3
16
+ from botocore.config import Config
17
+
18
+ bedrock = boto3.client("bedrock-runtime", region_name="us-east-1", config=Config(read_timeout=60))
19
+
20
+ query = "YOUR_QUERY_HERE" # Replace with the actual search query
21
+
22
+ response = bedrock.converse(
23
+ modelId="us.amazon.nova-2-lite-v1:0",
24
+ messages=[{"role": "user", "content": [{"text": query}]}],
25
+ toolConfig={"tools": [{"systemTool": {"name": "nova_grounding"}}]}
26
+ )
27
+
28
+ answer = ""
29
+ sources = []
30
+ for content in response["output"]["message"]["content"]:
31
+ if "text" in content:
32
+ answer += content["text"]
33
+ elif "citationsContent" in content:
34
+ for c in content["citationsContent"].get("citations", []):
35
+ url = c.get("location", {}).get("web", {}).get("url", "")
36
+ if url and url not in sources:
37
+ sources.append(url)
38
+
39
+ print(answer)
40
+ if sources:
41
+ print("\nSources:", sources)
42
+ ```
43
+
44
+ ## Query Formulation
45
+
46
+ Nova grounds better against full natural-language questions than against
47
+ keyword stubs — raw keywords return more loosely-related sources, while
48
+ a question gives Nova enough context to filter.
49
+
50
+ | User Intent | Query Template |
51
+ |---|---|
52
+ | News / updates / latest developments | `"What are the latest news about X?"` |
53
+ | Product / specs / concept lookup | `"Tell me about X, including specs, price, and latest updates."` |
54
+ | General search | `"Search for information about X."` |
55
+
56
+ **Examples:**
57
+ - ✅ `"What are the latest news about the Middle East conflict?"`
58
+ - ✅ `"Tell me about the NVIDIA RTX 5080, including specs and price."`
59
+ - ✗ `"RTX 5080"` — terse keywords leave Nova guessing at intent
60
+
61
+ ## Requirements
62
+
63
+ - Region **must** be `us-east-1`
64
+ - English queries yield better results than other languages
65
+ - Model ID: `us.amazon.nova-2-lite-v1:0`