@animalabs/agent-framework 0.23.2 → 0.24.0

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 (338) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +60 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +92 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/code-execution/engine.d.ts +177 -0
  10. package/dist/src/code-execution/engine.d.ts.map +1 -0
  11. package/dist/src/code-execution/engine.js +494 -0
  12. package/dist/src/code-execution/engine.js.map +1 -0
  13. package/dist/src/code-execution/index.d.ts +5 -10
  14. package/dist/src/code-execution/index.d.ts.map +1 -1
  15. package/dist/src/code-execution/index.js +5 -9
  16. package/dist/src/code-execution/index.js.map +1 -1
  17. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  18. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  19. package/dist/src/code-execution/isolate/calls.js +145 -0
  20. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  21. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  22. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  24. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  26. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-main.js +132 -0
  28. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  30. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child.js +315 -0
  32. package/dist/src/code-execution/isolate/child.js.map +1 -0
  33. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  34. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/frames.js +210 -0
  36. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  37. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  38. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/output.js +143 -0
  40. package/dist/src/code-execution/isolate/output.js.map +1 -0
  41. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  42. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/prelude.js +401 -0
  44. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  45. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  46. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/protocol.js +150 -0
  48. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  49. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  50. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/render.js +214 -0
  52. package/dist/src/code-execution/isolate/render.js.map +1 -0
  53. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  54. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/runner.js +854 -0
  56. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  57. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  58. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/scripts.js +357 -0
  60. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  61. package/dist/src/code-execution/quickjs.wasm +0 -0
  62. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  63. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  64. package/dist/src/code-execution/tool-definition.js +176 -142
  65. package/dist/src/code-execution/tool-definition.js.map +1 -1
  66. package/dist/src/deadline-timer.d.ts +3 -0
  67. package/dist/src/deadline-timer.d.ts.map +1 -1
  68. package/dist/src/deadline-timer.js +1 -1
  69. package/dist/src/deadline-timer.js.map +1 -1
  70. package/dist/src/framework.d.ts +1027 -440
  71. package/dist/src/framework.d.ts.map +1 -1
  72. package/dist/src/framework.js +4093 -2238
  73. package/dist/src/framework.js.map +1 -1
  74. package/dist/src/gate/event-gate.d.ts +28 -1
  75. package/dist/src/gate/event-gate.d.ts.map +1 -1
  76. package/dist/src/gate/event-gate.js +88 -10
  77. package/dist/src/gate/event-gate.js.map +1 -1
  78. package/dist/src/held-writes.d.ts +16 -3
  79. package/dist/src/held-writes.d.ts.map +1 -1
  80. package/dist/src/held-writes.js +16 -3
  81. package/dist/src/held-writes.js.map +1 -1
  82. package/dist/src/index.d.ts +6 -6
  83. package/dist/src/index.d.ts.map +1 -1
  84. package/dist/src/index.js +9 -8
  85. package/dist/src/index.js.map +1 -1
  86. package/dist/src/mcpl/channel-registry.d.ts +471 -75
  87. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  88. package/dist/src/mcpl/channel-registry.js +1673 -337
  89. package/dist/src/mcpl/channel-registry.js.map +1 -1
  90. package/dist/src/mcpl/conversation-router.d.ts +12 -7
  91. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  92. package/dist/src/mcpl/conversation-router.js +12 -7
  93. package/dist/src/mcpl/conversation-router.js.map +1 -1
  94. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  95. package/dist/src/mcpl/convert-block.js +9 -5
  96. package/dist/src/mcpl/convert-block.js.map +1 -1
  97. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  98. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  99. package/dist/src/mcpl/inbound-source.js +130 -0
  100. package/dist/src/mcpl/inbound-source.js.map +1 -1
  101. package/dist/src/mcpl/index.d.ts +2 -1
  102. package/dist/src/mcpl/index.d.ts.map +1 -1
  103. package/dist/src/mcpl/index.js +3 -2
  104. package/dist/src/mcpl/index.js.map +1 -1
  105. package/dist/src/mcpl/log-value.d.ts +6 -0
  106. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  107. package/dist/src/mcpl/log-value.js +8 -0
  108. package/dist/src/mcpl/log-value.js.map +1 -1
  109. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  110. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  111. package/dist/src/mcpl/modern-connection.js +168 -53
  112. package/dist/src/mcpl/modern-connection.js.map +1 -1
  113. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  114. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  115. package/dist/src/mcpl/prose-grammar.js +4 -5
  116. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  117. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  118. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  119. package/dist/src/mcpl/protocol-family.js +173 -16
  120. package/dist/src/mcpl/protocol-family.js.map +1 -1
  121. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  122. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  123. package/dist/src/mcpl/push-coalescer.js +227 -50
  124. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  125. package/dist/src/mcpl/push-handler.d.ts +6 -1
  126. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-handler.js +4 -1
  128. package/dist/src/mcpl/push-handler.js.map +1 -1
  129. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  130. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  131. package/dist/src/mcpl/reference-fetcher.js +4 -4
  132. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  133. package/dist/src/mcpl/server-connection.d.ts +50 -21
  134. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  135. package/dist/src/mcpl/server-connection.js +158 -74
  136. package/dist/src/mcpl/server-connection.js.map +1 -1
  137. package/dist/src/mcpl/tags.d.ts +24 -0
  138. package/dist/src/mcpl/tags.d.ts.map +1 -1
  139. package/dist/src/mcpl/tags.js +33 -0
  140. package/dist/src/mcpl/tags.js.map +1 -1
  141. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  142. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  143. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  144. package/dist/src/mcpl/transport.d.ts +55 -7
  145. package/dist/src/mcpl/transport.d.ts.map +1 -1
  146. package/dist/src/mcpl/transport.js +163 -34
  147. package/dist/src/mcpl/transport.js.map +1 -1
  148. package/dist/src/mcpl/types.d.ts +22 -6
  149. package/dist/src/mcpl/types.d.ts.map +1 -1
  150. package/dist/src/mcpl/types.js.map +1 -1
  151. package/dist/src/module-registry.d.ts +4 -2
  152. package/dist/src/module-registry.d.ts.map +1 -1
  153. package/dist/src/module-registry.js +100 -13
  154. package/dist/src/module-registry.js.map +1 -1
  155. package/dist/src/modules/discord/index.d.ts.map +1 -1
  156. package/dist/src/modules/discord/index.js +8 -13
  157. package/dist/src/modules/discord/index.js.map +1 -1
  158. package/dist/src/modules/history/index.d.ts.map +1 -1
  159. package/dist/src/modules/history/index.js +27 -4
  160. package/dist/src/modules/history/index.js.map +1 -1
  161. package/dist/src/modules/history/semantic.d.ts +15 -3
  162. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  163. package/dist/src/modules/history/semantic.js +27 -5
  164. package/dist/src/modules/history/semantic.js.map +1 -1
  165. package/dist/src/modules/index.d.ts +1 -1
  166. package/dist/src/modules/index.d.ts.map +1 -1
  167. package/dist/src/modules/index.js.map +1 -1
  168. package/dist/src/modules/unknown-tool.d.ts +4 -1
  169. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  170. package/dist/src/modules/unknown-tool.js +4 -1
  171. package/dist/src/modules/unknown-tool.js.map +1 -1
  172. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  173. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  174. package/dist/src/modules/workspace/branch-intent.js +3 -1
  175. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  176. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  177. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  178. package/dist/src/modules/workspace/edit-plan.js +794 -0
  179. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  180. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  181. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-text.js +584 -0
  183. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  184. package/dist/src/modules/workspace/effects.d.ts +5 -4
  185. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/effects.js +78 -25
  187. package/dist/src/modules/workspace/effects.js.map +1 -1
  188. package/dist/src/modules/workspace/index.d.ts +484 -61
  189. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/index.js +2173 -645
  191. package/dist/src/modules/workspace/index.js.map +1 -1
  192. package/dist/src/modules/workspace/observe.d.ts +213 -38
  193. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/observe.js +416 -36
  195. package/dist/src/modules/workspace/observe.js.map +1 -1
  196. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/reconcile.js +15 -8
  198. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  199. package/dist/src/modules/workspace/text.d.ts +32 -0
  200. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  201. package/dist/src/modules/workspace/text.js +67 -0
  202. package/dist/src/modules/workspace/text.js.map +1 -0
  203. package/dist/src/modules/workspace/types.d.ts +210 -6
  204. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/types.js.map +1 -1
  206. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  207. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  208. package/dist/src/modules/workspace/watcher.js.map +1 -1
  209. package/dist/src/operator-log.d.ts +52 -24
  210. package/dist/src/operator-log.d.ts.map +1 -1
  211. package/dist/src/operator-log.js.map +1 -1
  212. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  213. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  214. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  215. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  216. package/dist/src/recovery/offline-branch.d.ts +26 -1
  217. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  218. package/dist/src/recovery/offline-branch.js +164 -39
  219. package/dist/src/recovery/offline-branch.js.map +1 -1
  220. package/dist/src/recovery/recover-cli.js +23 -4
  221. package/dist/src/recovery/recover-cli.js.map +1 -1
  222. package/dist/src/send-tools.d.ts +11 -0
  223. package/dist/src/send-tools.d.ts.map +1 -0
  224. package/dist/src/send-tools.js +11 -0
  225. package/dist/src/send-tools.js.map +1 -0
  226. package/dist/src/tool-result-guard.d.ts +4 -3
  227. package/dist/src/tool-result-guard.d.ts.map +1 -1
  228. package/dist/src/tool-result-guard.js +5 -4
  229. package/dist/src/tool-result-guard.js.map +1 -1
  230. package/dist/src/tool-result-history.d.ts +1 -1
  231. package/dist/src/tool-result-history.js +1 -1
  232. package/dist/src/tune-out/coordinator.d.ts +21 -0
  233. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  234. package/dist/src/tune-out/coordinator.js +139 -37
  235. package/dist/src/tune-out/coordinator.js.map +1 -1
  236. package/dist/src/types/agent.d.ts +4 -0
  237. package/dist/src/types/agent.d.ts.map +1 -1
  238. package/dist/src/types/events.d.ts +62 -9
  239. package/dist/src/types/events.d.ts.map +1 -1
  240. package/dist/src/types/framework.d.ts +62 -58
  241. package/dist/src/types/framework.d.ts.map +1 -1
  242. package/dist/src/types/module.d.ts +40 -8
  243. package/dist/src/types/module.d.ts.map +1 -1
  244. package/dist/src/types/trace.d.ts +111 -10
  245. package/dist/src/types/trace.d.ts.map +1 -1
  246. package/dist/src/usage/types.d.ts +16 -0
  247. package/dist/src/usage/types.d.ts.map +1 -1
  248. package/dist/src/usage/usage-tracker.d.ts +14 -1
  249. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  250. package/dist/src/usage/usage-tracker.js +81 -41
  251. package/dist/src/usage/usage-tracker.js.map +1 -1
  252. package/package.json +6 -5
  253. package/src/agent.ts +93 -12
  254. package/src/api/server.ts +14 -12
  255. package/src/code-execution/engine.ts +595 -0
  256. package/src/code-execution/index.ts +5 -17
  257. package/src/code-execution/isolate/calls.ts +199 -0
  258. package/src/code-execution/isolate/child-entry.ts +5 -0
  259. package/src/code-execution/isolate/child-main.ts +132 -0
  260. package/src/code-execution/isolate/child.ts +327 -0
  261. package/src/code-execution/isolate/frames.ts +209 -0
  262. package/src/code-execution/isolate/output.ts +161 -0
  263. package/src/code-execution/isolate/prelude.ts +409 -0
  264. package/src/code-execution/isolate/protocol.ts +240 -0
  265. package/src/code-execution/isolate/render.ts +230 -0
  266. package/src/code-execution/isolate/runner.ts +911 -0
  267. package/src/code-execution/isolate/scripts.ts +479 -0
  268. package/src/code-execution/quickjs.wasm +0 -0
  269. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  270. package/src/code-execution/tool-definition.ts +236 -167
  271. package/src/deadline-timer.ts +1 -1
  272. package/src/framework.ts +4468 -2418
  273. package/src/gate/event-gate.ts +78 -11
  274. package/src/held-writes.ts +17 -3
  275. package/src/index.ts +30 -22
  276. package/src/mcpl/channel-registry.ts +1825 -368
  277. package/src/mcpl/conversation-router.ts +12 -7
  278. package/src/mcpl/convert-block.ts +9 -5
  279. package/src/mcpl/inbound-source.ts +131 -0
  280. package/src/mcpl/index.ts +4 -2
  281. package/src/mcpl/log-value.ts +9 -0
  282. package/src/mcpl/modern-connection.ts +162 -53
  283. package/src/mcpl/prose-grammar.ts +4 -5
  284. package/src/mcpl/protocol-family.ts +199 -21
  285. package/src/mcpl/push-coalescer.ts +306 -62
  286. package/src/mcpl/push-handler.ts +10 -2
  287. package/src/mcpl/reference-fetcher.ts +4 -4
  288. package/src/mcpl/server-connection.ts +180 -79
  289. package/src/mcpl/tags.ts +36 -0
  290. package/src/mcpl/tool-result-normalize.ts +2 -1
  291. package/src/mcpl/transport.ts +162 -33
  292. package/src/mcpl/types.ts +23 -6
  293. package/src/module-registry.ts +92 -13
  294. package/src/modules/discord/index.ts +8 -13
  295. package/src/modules/history/index.ts +25 -3
  296. package/src/modules/history/semantic.ts +29 -6
  297. package/src/modules/index.ts +1 -0
  298. package/src/modules/unknown-tool.ts +4 -1
  299. package/src/modules/workspace/branch-intent.ts +3 -1
  300. package/src/modules/workspace/edit-plan.ts +954 -0
  301. package/src/modules/workspace/edit-text.ts +618 -0
  302. package/src/modules/workspace/effects.ts +70 -19
  303. package/src/modules/workspace/index.ts +2247 -642
  304. package/src/modules/workspace/observe.ts +465 -56
  305. package/src/modules/workspace/reconcile.ts +14 -7
  306. package/src/modules/workspace/text.ts +68 -0
  307. package/src/modules/workspace/types.ts +211 -6
  308. package/src/modules/workspace/watcher.ts +3 -2
  309. package/src/operator-log.ts +47 -19
  310. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  311. package/src/recovery/offline-branch.ts +189 -55
  312. package/src/recovery/recover-cli.ts +25 -4
  313. package/src/send-tools.ts +11 -0
  314. package/src/tool-result-guard.ts +6 -5
  315. package/src/tool-result-history.ts +1 -1
  316. package/src/tune-out/coordinator.ts +163 -42
  317. package/src/types/agent.ts +4 -0
  318. package/src/types/events.ts +62 -9
  319. package/src/types/framework.ts +62 -58
  320. package/src/types/module.ts +40 -8
  321. package/src/types/trace.ts +121 -15
  322. package/src/usage/types.ts +16 -0
  323. package/src/usage/usage-tracker.ts +86 -43
  324. package/dist/src/code-execution/py-runner.d.ts +0 -191
  325. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  326. package/dist/src/code-execution/py-runner.js +0 -655
  327. package/dist/src/code-execution/py-runner.js.map +0 -1
  328. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  329. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  330. package/dist/src/code-execution/runtime-py.js +0 -677
  331. package/dist/src/code-execution/runtime-py.js.map +0 -1
  332. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  333. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  334. package/dist/src/mcpl/prose-stream-router.js +0 -291
  335. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  336. package/src/code-execution/py-runner.ts +0 -791
  337. package/src/code-execution/runtime-py.ts +0 -677
  338. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -1,677 +0,0 @@
1
- /**
2
- * Python runtime for client-side programmatic tool calling (PTC).
3
- *
4
- * This is the interpreter-side half of the `code_execution` tool: a small
5
- * asyncio REPL that mimics the environment Anthropic's server-side PTC was
6
- * trained on (see docs/en/agents-and-tools/tool-use/programmatic-tool-calling):
7
- *
8
- * - scripts run with top-level `await` (PyCF_ALLOW_TOP_LEVEL_AWAIT)
9
- * - host tools are injected as async functions taking a single dict and
10
- * returning the tool_result text as a str (ToolResult), which also
11
- * carries .is_error and .structured
12
- * - a pending tool call that gets no response raises TimeoutError with the
13
- * same message shape the managed runtime produces
14
- * - interpreter globals persist across execs (container-reuse semantics)
15
- *
16
- * BACKGROUND MODE (model-authored daemons): when init carries
17
- * `background: true`, the exec additionally gets:
18
- * - `wake_agent(payload)` — async; reports the caller's line number (which
19
- * indexes into the agent-authored script — self-correlation) and delivers
20
- * the payload to the host, which injects it into the agent's context and
21
- * triggers inference. Awaits a host ack so rate limiting backpressures
22
- * inside the script instead of dropping wakes.
23
- * - stdout/stderr tee'd line-buffered to `log_path` (agent-readable via
24
- * their file tools) with an in-memory tail kept for the crash envelope.
25
- *
26
- * Embedded as a TS string because tsc does not copy non-TS assets to dist/;
27
- * PyRunner materializes it to a temp file at spawn.
28
- *
29
- * Protocol (JSON lines):
30
- * host -> py : {op:"init", tools:[{py_name,tool_name}], call_timeout_s,
31
- * background?, log_path?, tail_chars?}
32
- * {op:"exec", id, code}
33
- * {op:"tool_result", id, result, is_error?, structured_json?}
34
- * {op:"wake_ack", id, error?}
35
- * {op:"cancel", id, reason?}
36
- * py -> host : {op:"ready"}
37
- * {op:"tool_call", id, exec_id, name, args}
38
- * {op:"wake", id, exec_id, line, payload}
39
- * {op:"exec_result", id, stdout, stderr, return_code, tail?}
40
- *
41
- * At a deadline the host sends cancel and then SIGINT, repeated until the
42
- * script reports (not on Windows): the cancel stops a script waiting on an
43
- * await, the signal also one stuck in blocking code (see _on_sigint).
44
- *
45
- * IMPORTANT: the python source below must not contain backticks or the
46
- * sequence dollar+brace (TS template literal syntax). String.raw preserves
47
- * backslashes, so \n inside python string literals is fine.
48
- */
49
-
50
- export const PYTHON_RUNTIME_SOURCE: string = String.raw`
51
- import ast
52
- import asyncio
53
- import functools
54
- import inspect
55
- import io
56
- import json
57
- import os
58
- import re
59
- import signal
60
- import sys
61
- import threading
62
- import traceback
63
-
64
- PROTO_OUT = sys.stdout # protocol channel (real stdout)
65
- REAL_STDERR = sys.stderr # runtime diagnostics outside execs
66
- CALL_TIMEOUT_S = 270.0
67
- OUTPUT_CAP_CHARS = 2_000_000
68
- LOG_FILE_CAP_BYTES = 50_000_000
69
-
70
- BACKGROUND = False
71
- LOG_PATH = None
72
- TAIL_CHARS = 4000
73
-
74
- _send_lock = threading.Lock()
75
-
76
- # A str can hold a lone surrogate, which UTF-8 can't encode: on POSIX,
77
- # os.fsdecode makes one of each byte in a filename that doesn't decode as
78
- # UTF-8, and json.loads makes one of a "\ud800" escape. PYTHONIOENCODING makes
79
- # the protocol channel strict UTF-8, so writing one would fail the whole
80
- # message, and a script's result would never arrive. Each goes as U+FFFD.
81
- _LONE_SURROGATE = re.compile(r"[\ud800-\udfff]")
82
-
83
-
84
- def send(obj):
85
- line = _LONE_SURROGATE.sub("\ufffd", json.dumps(obj, ensure_ascii=False, default=str))
86
- with _send_lock:
87
- PROTO_OUT.write(line + "\n")
88
- PROTO_OUT.flush()
89
-
90
-
91
- class CappedIO(io.StringIO):
92
- """StringIO that stops recording past a cap (keeps a truncation flag)."""
93
-
94
- def __init__(self, cap):
95
- super().__init__()
96
- self._cap = cap
97
- self.truncated = False
98
-
99
- def write(self, s):
100
- if not isinstance(s, str):
101
- s = str(s)
102
- if self.tell() >= self._cap:
103
- self.truncated = True
104
- return len(s)
105
- remaining = self._cap - self.tell()
106
- if len(s) > remaining:
107
- self.truncated = True
108
- return super().write(s[:remaining])
109
- return super().write(s)
110
-
111
-
112
- class LogTee:
113
- """Background-mode writer: line-buffered file append + rolling tail.
114
-
115
- The file is the agent-readable journal (their read/grep/shell tools work
116
- on it); the tail rides the exec_result so the host can build a crash
117
- envelope without re-reading the file.
118
- """
119
-
120
- def __init__(self, path, tail_chars):
121
- self._fh = None
122
- self._written = 0
123
- self._tail = ""
124
- self._tail_chars = tail_chars
125
- self.truncated = False
126
- if path:
127
- try:
128
- os.makedirs(os.path.dirname(path), exist_ok=True)
129
- self._fh = open(path, "a", encoding="utf-8", errors="replace")
130
- except Exception as exc:
131
- REAL_STDERR.write("[pytc-runtime] cannot open log file: " + str(exc) + "\n")
132
- REAL_STDERR.flush()
133
-
134
- def write(self, s):
135
- if not isinstance(s, str):
136
- s = str(s)
137
- self._tail = (self._tail + s)[-self._tail_chars:]
138
- if self._fh is not None and not self.truncated:
139
- if self._written + len(s) > LOG_FILE_CAP_BYTES:
140
- self.truncated = True
141
- try:
142
- self._fh.write("\n[log capped at " + str(LOG_FILE_CAP_BYTES) + " bytes]\n")
143
- self._fh.flush()
144
- except Exception:
145
- pass
146
- else:
147
- try:
148
- self._fh.write(s)
149
- if "\n" in s:
150
- self._fh.flush()
151
- self._written += len(s)
152
- except Exception:
153
- pass
154
- return len(s)
155
-
156
- def flush(self):
157
- if self._fh is not None:
158
- try:
159
- self._fh.flush()
160
- except Exception:
161
- pass
162
-
163
- def close(self):
164
- if self._fh is not None:
165
- try:
166
- self._fh.close()
167
- except Exception:
168
- pass
169
- self._fh = None
170
-
171
- def tail(self):
172
- return self._tail
173
-
174
-
175
- # Persistent interpreter state (mirrors container reuse: variables survive
176
- # between exec calls until the host reclaims the interpreter).
177
- SCRIPT_GLOBALS = {"__name__": "__main__", "__builtins__": __builtins__}
178
- _injected_py_names = set()
179
-
180
- _pending_tool_futures = {}
181
- _pending_wake_futures = {}
182
- _next_tool_call_id = 0
183
- _next_wake_id = 0
184
- _current_exec_id = None
185
- _current_exec_task = None
186
-
187
- # How the host stops a script (its time limit, or an abort): ONE per-script
188
- # state, so the script is stopped exactly once, by whichever path gets there
189
- # first. Every other path then does nothing to that script.
190
- # PENDING -> nothing asked yet.
191
- # REQUESTED -> asked while the script was outside its own code (waiting on
192
- # an await, writing a protocol line, or not started yet). A
193
- # cancellation is queued on the loop, or delivered as the
194
- # script starts.
195
- # DELIVERED -> KeyboardInterrupt raised in its code, or its task cancelled.
196
- # Final: whatever the script does next (except/finally, awaits
197
- # in its cleanup) runs to completion or to the host's kill.
198
- # Transitions happen only in the main thread: in the SIGINT handler (between
199
- # bytecodes, so it never cancels anything itself -- see _on_sigint) and in
200
- # loop callbacks. Neither can interleave a transition of the other halfway
201
- # in a way that matters: the handler only moves PENDING to REQUESTED or, with
202
- # the script's own frame on the stack, to DELIVERED; a loop callback never
203
- # runs while the script's frame is on the stack.
204
- #
205
- # Where the signal (or the cancel op) can land, and what happens:
206
- # 1. script's own code, running or blocking -> KeyboardInterrupt there
207
- # 2. script waiting on an await (loop idle) -> cancellation queued; it wakes
208
- # the loop and lands at that await
209
- # 3. main() mid-dispatch, between the done() check of a future the script
210
- # awaits and its set_result (reply behind the cancellation) -> queued;
211
- # set_result completes; the script gets CancelledError instead of it
212
- # 4. the script writing a protocol line (send) -> queued; the line stays
213
- # whole; the cancellation lands at the script's next await
214
- # 5. reply ahead of the queued cancellation, script then awaits -> the
215
- # cancellation lands at that await
216
- # 6. reply ahead, script then blocks -> the next signal (the host repeats
217
- # it) raises KeyboardInterrupt there; the queued cancellation then does
218
- # nothing, so cleanup that awaits is not cut short
219
- # 7. reply ahead, script finishes before the queued cancellation runs ->
220
- # it does nothing, and never reaches the next script
221
- # 8. script not started yet (signal, or the cancel op) -> delivered as
222
- # CancelledError when it starts, so it still reports
223
- # 9. cancel op before the signal -> delivered; the signal and its repeats
224
- # do nothing, so cleanup after a caught CancelledError runs on
225
- # 10. cancel op or repeated signal after delivery -> nothing
226
- # 11. script finished, reporting its result -> nothing
227
- # 12. between scripts -> nothing
228
- # 13. a previous script installed an eager task factory -> no effect: the
229
- # runtime builds its script task directly, and the state is in place
230
- # before the script's first step
231
- _PENDING = "pending"
232
- _REQUESTED = "requested"
233
- _DELIVERED = "delivered"
234
-
235
-
236
- class _ScriptStop:
237
- __slots__ = ("task", "running", "state", "interrupt")
238
-
239
- def __init__(self):
240
- self.task = None
241
- self.running = False # the script's own code may be on the stack
242
- self.state = _PENDING
243
- self.interrupt = None # the KeyboardInterrupt raised in its code
244
-
245
-
246
- _stop = None # the running script's _ScriptStop; None between scripts
247
-
248
-
249
- def _deliver_cancel(st):
250
- # Loop context only: the cancel op in main()'s dispatch, or queued by the
251
- # SIGINT handler. Never from inside the handler, where a cancellation
252
- # could land between main()'s done() check of a future the script awaits
253
- # and its set_result.
254
- if st is None or st is not _stop or st.state == _DELIVERED:
255
- return
256
- if not st.running:
257
- st.state = _REQUESTED # not started yet: delivered as it starts
258
- return
259
- st.state = _DELIVERED
260
- st.task.cancel()
261
-
262
-
263
- class ToolResult(str):
264
- """A tool's result: the string the tool gave, with two attributes.
265
-
266
- is_error: True if the result is an error, False if not, None if the
267
- host didn't say.
268
- structured: the parsed structuredContent of a structured result's JSON
269
- object, or None.
270
- """
271
-
272
- # Defaults, since pickle and copy rebuild a str subclass from its text
273
- # alone and then restore the attributes.
274
- def __new__(cls, text="", is_error=None, structured_json=False):
275
- self = str.__new__(cls, text)
276
- self.is_error = is_error
277
- self._structured_json = structured_json
278
- return self
279
-
280
- # Read from the string itself, the one copy sent, on first use only.
281
- @functools.cached_property
282
- def structured(self):
283
- if not self._structured_json:
284
- return None
285
- return json.loads(self).get("structuredContent")
286
-
287
-
288
- def _resolve(fut, value):
289
- # Defense in depth: the script may have been cancelled since the lookup.
290
- if fut is not None and not fut.done():
291
- try:
292
- fut.set_result(value)
293
- except asyncio.InvalidStateError:
294
- pass
295
-
296
-
297
- def _brief(value):
298
- # A value as a refusal shows it: by its type, such as <str>, which fails
299
- # loudly if copied rather than sending something else. The agent has its
300
- # values in the call it just wrote. No value's own code runs here: its
301
- # __repr__ isn't called, and its type is named through type's own
302
- # descriptor, so a metaclass's __name__ doesn't run either.
303
- return "<" + type.__dict__["__name__"].__get__(type(value)) + ">"
304
-
305
-
306
- def _make_tool_fn(tool_name, py_name):
307
- async def tool_fn(*positional, **kwargs):
308
- global _next_tool_call_id
309
- if len(positional) > 1:
310
- raise TypeError(
311
- py_name + " wasn't called: a tool function takes one dict of "
312
- "arguments, such as await " + py_name + "({...}), and this call "
313
- "gave it " + str(len(positional)) + " positional arguments"
314
- )
315
- # args= on its own is the dict, as it has always been. Beside other
316
- # keywords it's one of them, since a tool such as utils has a field
317
- # of that name, so the suggested dict keeps it.
318
- if positional:
319
- args = positional[0]
320
- elif list(kwargs) == ["args"]:
321
- args = kwargs.pop("args")
322
- else:
323
- args = None
324
- if kwargs:
325
- if args is not None and not isinstance(args, dict):
326
- raise TypeError(
327
- py_name + " wasn't called: a tool function takes one dict of "
328
- "arguments, not keyword arguments, and this call also gave it "
329
- + _brief(args) + ", so put every argument in one dict: await "
330
- + py_name + "({...})"
331
- )
332
- # The likely intent is plain: the same arguments in one dict.
333
- meant = dict(args) if isinstance(args, dict) else {}
334
- meant.update(kwargs)
335
- # A key is what the agent has to write, so a str key is shown
336
- # whole up to 60 characters, as names are elsewhere; a longer one,
337
- # or any other key, is shown as a value is. A tool's result counts
338
- # as a str: ToolResult keeps str's repr, so no script code runs.
339
- shown = ", ".join((repr(k) if (type(k) is str or type(k) is ToolResult) and len(k) <= 60 else _brief(k)) + ": " + _brief(v)
340
- for k, v in meant.items())
341
- raise TypeError(
342
- py_name + " wasn't called: a tool function takes one dict of "
343
- "arguments, not keyword arguments, so write await "
344
- + py_name + "({" + shown + "})"
345
- )
346
- if args is None:
347
- args = {}
348
- if not isinstance(args, dict):
349
- raise TypeError(
350
- py_name + " wasn't called: a tool function takes one dict of "
351
- "arguments, such as await " + py_name + "({...}), and this call "
352
- "gave it " + _brief(args)
353
- )
354
- _next_tool_call_id += 1
355
- call_id = "t" + str(_next_tool_call_id)
356
- fut = asyncio.get_running_loop().create_future()
357
- _pending_tool_futures[call_id] = fut
358
- send({
359
- "op": "tool_call",
360
- "id": call_id,
361
- "exec_id": _current_exec_id,
362
- "name": tool_name,
363
- "args": args,
364
- })
365
- try:
366
- return await asyncio.wait_for(fut, timeout=CALL_TIMEOUT_S)
367
- except asyncio.TimeoutError:
368
- raise TimeoutError(
369
- "Calling tool ['" + tool_name + "'] timed out (no response after "
370
- + str(int(CALL_TIMEOUT_S)) + "s)."
371
- )
372
- finally:
373
- _pending_tool_futures.pop(call_id, None)
374
- tool_fn.__name__ = py_name
375
- tool_fn.__qualname__ = py_name
376
- tool_fn.__doc__ = (
377
- "Host tool '" + tool_name + "'. Takes a single dict of arguments and "
378
- "returns the tool result as a string, with .is_error and .structured "
379
- "beside it."
380
- )
381
- return tool_fn
382
-
383
-
384
- async def wake_agent(payload=None):
385
- """Wake the agent: deliver payload into their context and trigger a turn.
386
-
387
- Reports the caller's line number (indexes into the script the agent
388
- wrote). Awaits the host ack — rate limiting backpressures here rather
389
- than dropping wakes. Raises RuntimeError when the host refuses (wake cap
390
- exceeded, script cancelled).
391
- """
392
- global _next_wake_id
393
- frame = inspect.currentframe()
394
- line = frame.f_back.f_lineno if frame is not None and frame.f_back is not None else -1
395
- _next_wake_id += 1
396
- wake_id = "w" + str(_next_wake_id)
397
- fut = asyncio.get_running_loop().create_future()
398
- _pending_wake_futures[wake_id] = fut
399
- send({
400
- "op": "wake",
401
- "id": wake_id,
402
- "exec_id": _current_exec_id,
403
- "line": line,
404
- "payload": payload,
405
- })
406
- try:
407
- error = await fut
408
- if error:
409
- raise RuntimeError("wake_agent refused by host: " + str(error))
410
- return None
411
- finally:
412
- _pending_wake_futures.pop(wake_id, None)
413
-
414
-
415
- def handle_init(msg):
416
- global CALL_TIMEOUT_S, BACKGROUND, LOG_PATH, TAIL_CHARS
417
- timeout = msg.get("call_timeout_s")
418
- if isinstance(timeout, (int, float)) and timeout > 0:
419
- CALL_TIMEOUT_S = float(timeout)
420
- BACKGROUND = bool(msg.get("background"))
421
- LOG_PATH = msg.get("log_path") or None
422
- tail = msg.get("tail_chars")
423
- if isinstance(tail, int) and tail > 0:
424
- TAIL_CHARS = tail
425
-
426
- # Remove functions injected by a previous init that are no longer present
427
- # (tool list changed) without touching user-defined globals.
428
- new_tools = msg.get("tools") or []
429
- new_py_names = set()
430
- tools_dict = {}
431
- for entry in new_tools:
432
- py_name = entry.get("py_name")
433
- tool_name = entry.get("tool_name")
434
- if not py_name or not tool_name:
435
- continue
436
- fn = _make_tool_fn(tool_name, py_name)
437
- SCRIPT_GLOBALS[py_name] = fn
438
- tools_dict[tool_name] = fn
439
- new_py_names.add(py_name)
440
- for stale in _injected_py_names - new_py_names:
441
- SCRIPT_GLOBALS.pop(stale, None)
442
- _injected_py_names.clear()
443
- _injected_py_names.update(new_py_names)
444
- # Exact-name escape hatch: tools["mcpl--server--tool"]({...})
445
- SCRIPT_GLOBALS["tools"] = tools_dict
446
- # The doorbell exists only in background mode — a foreground script's
447
- # agent is already awake (mid-turn).
448
- if BACKGROUND:
449
- SCRIPT_GLOBALS["wake_agent"] = wake_agent
450
- else:
451
- SCRIPT_GLOBALS.pop("wake_agent", None)
452
-
453
-
454
- async def _run_script(exec_id, code, st):
455
- global _current_exec_id, _current_exec_task, _stop
456
- if BACKGROUND:
457
- out = LogTee(LOG_PATH, TAIL_CHARS)
458
- err = out # interleave, terminal-style; tail is shared
459
- else:
460
- out = CappedIO(OUTPUT_CAP_CHARS)
461
- err = CappedIO(OUTPUT_CAP_CHARS)
462
- old_out, old_err, old_in = sys.stdout, sys.stderr, sys.stdin
463
- sys.stdout, sys.stderr = out, err
464
- sys.stdin = io.StringIO("") # input() must not steal the protocol channel
465
- return_code = 0
466
- try:
467
- compiled = compile(code, "<script>", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
468
- st.running = True
469
- try:
470
- if st.state == _REQUESTED:
471
- # Stopped before it started (row 8).
472
- st.state = _DELIVERED
473
- raise asyncio.CancelledError()
474
- result = eval(compiled, SCRIPT_GLOBALS)
475
- if inspect.iscoroutine(result):
476
- await result
477
- finally:
478
- st.running = False
479
- except asyncio.CancelledError:
480
- err.write("\nKeyboardInterrupt: script cancelled by host\n")
481
- return_code = 1
482
- except BaseException as exc:
483
- if exc is st.interrupt:
484
- # Where the script was when it was stopped, without the runtime's frames.
485
- frames = [
486
- f for f in traceback.extract_tb(exc.__traceback__)
487
- if f.filename != _RUN_SCRIPT_CODE.co_filename
488
- ]
489
- err.write("Traceback (most recent call last):\n")
490
- err.write("".join(traceback.format_list(frames)))
491
- err.write("KeyboardInterrupt: script interrupted by host\n")
492
- else:
493
- traceback.print_exc(file=err)
494
- return_code = 1
495
- finally:
496
- sys.stdout, sys.stderr, sys.stdin = old_out, old_err, old_in
497
- st.interrupt = None
498
- if _stop is st:
499
- _stop = None
500
- _current_exec_id = None
501
- _current_exec_task = None
502
- for fut in list(_pending_tool_futures.values()):
503
- if not fut.done():
504
- fut.cancel()
505
- _pending_tool_futures.clear()
506
- for fut in list(_pending_wake_futures.values()):
507
- if not fut.done():
508
- fut.cancel()
509
- _pending_wake_futures.clear()
510
-
511
- if BACKGROUND:
512
- out.flush()
513
- out.close()
514
- send({
515
- "op": "exec_result",
516
- "id": exec_id,
517
- "stdout": "",
518
- "stderr": "",
519
- "tail": out.tail(),
520
- "return_code": return_code,
521
- })
522
- return
523
-
524
- stdout_text = out.getvalue()
525
- if out.truncated:
526
- stdout_text += "\n[stdout truncated at " + str(OUTPUT_CAP_CHARS) + " chars]"
527
- stderr_text = err.getvalue()
528
- if err.truncated:
529
- stderr_text += "\n[stderr truncated at " + str(OUTPUT_CAP_CHARS) + " chars]"
530
- send({
531
- "op": "exec_result",
532
- "id": exec_id,
533
- "stdout": stdout_text,
534
- "stderr": stderr_text,
535
- "return_code": return_code,
536
- })
537
-
538
-
539
- def _dispatch(msg):
540
- """Handle one protocol message from the host; False means stop reading."""
541
- global _current_exec_id, _current_exec_task, _stop
542
- op = msg.get("op")
543
- if op == "init":
544
- handle_init(msg)
545
- elif op == "exec":
546
- if _current_exec_task is not None and not _current_exec_task.done():
547
- send({
548
- "op": "exec_result",
549
- "id": msg.get("id"),
550
- "stdout": "",
551
- "stderr": "RuntimeError: another script is already running in this interpreter",
552
- "return_code": 1,
553
- })
554
- return True
555
- # Everything _on_sigint reads is in place before the script can run
556
- # (row 13). The task is built directly, not through the loop's task
557
- # factory: a script may have installed one (interpreter state
558
- # persists), and an eager one would run the next script inside this
559
- # call, before the assignments below.
560
- st = _ScriptStop()
561
- _stop = st
562
- _current_exec_id = msg.get("id")
563
- _current_exec_task = asyncio.Task(
564
- _run_script(msg.get("id"), msg.get("code") or "", st),
565
- loop=asyncio.get_running_loop(),
566
- )
567
- st.task = _current_exec_task
568
- elif op == "tool_result":
569
- _resolve(
570
- _pending_tool_futures.get(msg.get("id")),
571
- ToolResult(str(msg.get("result", "")), msg.get("is_error"), msg.get("structured_json") is True),
572
- )
573
- elif op == "wake_ack":
574
- _resolve(_pending_wake_futures.get(msg.get("id")), msg.get("error") or None)
575
- elif op == "cancel":
576
- _deliver_cancel(_stop)
577
- elif op == "exit":
578
- return False
579
- return True
580
-
581
-
582
- async def main():
583
- loop = asyncio.get_running_loop()
584
- queue = asyncio.Queue()
585
-
586
- def reader():
587
- for raw_line in sys.stdin:
588
- loop.call_soon_threadsafe(queue.put_nowait, raw_line)
589
- loop.call_soon_threadsafe(queue.put_nowait, None)
590
-
591
- # The reader thread blocks SIGINT (it inherits the mask while it starts),
592
- # so the signal reaches the main thread, where a blocking call in the
593
- # script is waiting to be interrupted.
594
- masked = hasattr(signal, "pthread_sigmask") and hasattr(signal, "SIGINT")
595
- if masked:
596
- previous_mask = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGINT})
597
- try:
598
- threading.Thread(target=reader, daemon=True).start()
599
- finally:
600
- if masked:
601
- signal.pthread_sigmask(signal.SIG_SETMASK, previous_mask)
602
- send({"op": "ready"})
603
-
604
- while True:
605
- line = await queue.get()
606
- if line is None:
607
- break
608
- line = line.strip()
609
- if not line:
610
- continue
611
- try:
612
- msg = json.loads(line)
613
- except Exception:
614
- REAL_STDERR.write("[pytc-runtime] bad protocol line\n")
615
- REAL_STDERR.flush()
616
- continue
617
- if not _dispatch(msg):
618
- break
619
-
620
- # Drain: give a cancelled exec a moment to emit its exec_result.
621
- if _current_exec_task is not None and not _current_exec_task.done():
622
- _current_exec_task.cancel()
623
- try:
624
- await asyncio.wait_for(_current_exec_task, timeout=2)
625
- except BaseException:
626
- pass
627
-
628
-
629
- # At the deadline the host sends a cancel op, which lands only when the script
630
- # awaits: a script blocked in time.sleep(), a busy loop or a blocking read
631
- # never gives the event loop control back. So the host also sends SIGINT,
632
- # repeated until the script reports, and this stops the script through its
633
- # _ScriptStop (see the table there):
634
- # - With the script's own code on the stack (_run_script's frame, reached
635
- # before any send frame), it raises KeyboardInterrupt there; _run_script
636
- # catches it, so the output so far and the interpreter's globals survive.
637
- # Checking the current task instead is not enough: asyncio runs its own
638
- # code for the task between steps, and an exception raised there escapes
639
- # the event loop and ends the interpreter.
640
- # - Anywhere else it only queues the cancellation on the loop (which also
641
- # wakes select(), otherwise resuming its wait after a signal): cancelling
642
- # here could cancel a future between main()'s done() check and its
643
- # set_result, and an exception mid-send would tear a protocol line.
644
- _RUN_SCRIPT_CODE = _run_script.__code__
645
- _SEND_CODE = send.__code__
646
-
647
-
648
- def _on_sigint(signum, frame):
649
- st = _stop
650
- if st is None or st.state == _DELIVERED:
651
- return
652
- if st.running:
653
- while frame is not None:
654
- code = frame.f_code
655
- if code is _SEND_CODE:
656
- break
657
- if code is _RUN_SCRIPT_CODE:
658
- st.state = _DELIVERED
659
- st.interrupt = KeyboardInterrupt("script interrupted by host")
660
- raise st.interrupt
661
- frame = frame.f_back
662
- if st.state == _PENDING:
663
- st.state = _REQUESTED
664
- if st.running:
665
- st.task.get_loop().call_soon_threadsafe(_deliver_cancel, st)
666
-
667
-
668
- if __name__ == "__main__":
669
- # A handler of our own also keeps asyncio.run from installing one that
670
- # would cancel main() and end the interpreter.
671
- if hasattr(signal, "SIGINT"):
672
- try:
673
- signal.signal(signal.SIGINT, _on_sigint)
674
- except (ValueError, OSError):
675
- pass
676
- asyncio.run(main())
677
- `;