@animalabs/agent-framework 0.21.0 → 0.22.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.
- package/README.md +43 -0
- package/dist/src/agent.d.ts +17 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +102 -42
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +22 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +64 -9
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +4 -0
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +215 -38
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/framework.d.ts +250 -39
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1602 -371
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +21 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +40 -7
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +26 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +28 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +17 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +31 -7
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +1 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +85 -22
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +32 -4
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +46 -13
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +5 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +41 -11
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +4 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +45 -8
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +5 -2
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +25 -7
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +47 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
- package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
- package/dist/src/mcpl/tool-pattern-check.js +79 -0
- package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +15 -5
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +39 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -0
- package/dist/src/mcpl/visible-content.js +54 -0
- package/dist/src/mcpl/visible-content.js.map +1 -0
- package/dist/src/module-registry.d.ts +81 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +257 -16
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
- package/dist/src/modules/workspace/branch-intent.js +83 -0
- package/dist/src/modules/workspace/branch-intent.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.js +259 -0
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +84 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -0
- package/dist/src/modules/workspace/effects.js +368 -0
- package/dist/src/modules/workspace/effects.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +155 -17
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1059 -424
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +278 -0
- package/dist/src/modules/workspace/observe.d.ts.map +1 -0
- package/dist/src/modules/workspace/observe.js +805 -0
- package/dist/src/modules/workspace/observe.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts +224 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
- package/dist/src/modules/workspace/reconcile.js +901 -0
- package/dist/src/modules/workspace/reconcile.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +6 -59
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +6 -257
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +77 -10
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/record-journal.d.ts +127 -0
- package/dist/src/record-journal.d.ts.map +1 -0
- package/dist/src/record-journal.js +202 -0
- package/dist/src/record-journal.js.map +1 -0
- package/dist/src/shutdown.d.ts +13 -0
- package/dist/src/shutdown.d.ts.map +1 -0
- package/dist/src/shutdown.js +46 -0
- package/dist/src/shutdown.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +74 -0
- package/dist/src/silent-heartbeat.d.ts.map +1 -0
- package/dist/src/silent-heartbeat.js +77 -0
- package/dist/src/silent-heartbeat.js.map +1 -0
- package/dist/src/tool-presentation.d.ts +50 -0
- package/dist/src/tool-presentation.d.ts.map +1 -0
- package/dist/src/tool-presentation.js +235 -0
- package/dist/src/tool-presentation.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +125 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +310 -35
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +13 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +21 -1
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +18 -2
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +4 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +12 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +14 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +73 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/batched-wake-locus.test.d.ts +2 -0
- package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
- package/dist/test/batched-wake-locus.test.js +166 -0
- package/dist/test/batched-wake-locus.test.js.map +1 -0
- package/dist/test/code-execution.test.js +386 -2
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/component-defaults.test.d.ts +2 -0
- package/dist/test/component-defaults.test.d.ts.map +1 -0
- package/dist/test/component-defaults.test.js +54 -0
- package/dist/test/component-defaults.test.js.map +1 -0
- package/dist/test/conversation-routing.test.js +57 -1
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/ephemeral-settle.test.js +3 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate.test.js +118 -0
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +977 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
- package/dist/test/helpers/pytc-stop-rows.js +143 -0
- package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +19 -6
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
- package/dist/test/mcpl-config-diagnostics.test.js +449 -0
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
- package/dist/test/mcpl-empty-content.test.d.ts +2 -0
- package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
- package/dist/test/mcpl-empty-content.test.js +368 -0
- package/dist/test/mcpl-empty-content.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
- package/dist/test/prose-stream-router.test.js +41 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/record-journal.test.d.ts +6 -0
- package/dist/test/record-journal.test.d.ts.map +1 -0
- package/dist/test/record-journal.test.js +269 -0
- package/dist/test/record-journal.test.js.map +1 -0
- package/dist/test/save-recent-image.test.js +102 -0
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.d.ts +2 -0
- package/dist/test/shutdown-errors.test.d.ts.map +1 -0
- package/dist/test/shutdown-errors.test.js +662 -0
- package/dist/test/shutdown-errors.test.js.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.js +451 -0
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.js +49 -38
- package/dist/test/silent-heartbeat.test.js.map +1 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +672 -0
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.d.ts +8 -0
- package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
- package/dist/test/tool-batch-hook.test.js +275 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -0
- package/dist/test/tool-presentation.test.d.ts +2 -0
- package/dist/test/tool-presentation.test.d.ts.map +1 -0
- package/dist/test/tool-presentation.test.js +268 -0
- package/dist/test/tool-presentation.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.js +2330 -18
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-history.test.js +2 -1
- package/dist/test/tool-result-history.test.js.map +1 -1
- package/dist/test/turn-trigger-provenance.test.js +35 -0
- package/dist/test/turn-trigger-provenance.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
- package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
- package/dist/test/unconvertible-tool-result.test.js +358 -0
- package/dist/test/unconvertible-tool-result.test.js.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.js +252 -0
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
- package/dist/test/workspace-three-way.test.d.ts +12 -0
- package/dist/test/workspace-three-way.test.d.ts.map +1 -0
- package/dist/test/workspace-three-way.test.js +3079 -0
- package/dist/test/workspace-three-way.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/agent.ts +120 -44
- package/src/code-execution/py-runner.ts +66 -9
- package/src/code-execution/runtime-py.ts +215 -38
- package/src/framework.ts +1661 -363
- package/src/gate/event-gate.ts +59 -7
- package/src/index.ts +11 -0
- package/src/mcpl/channel-registry.ts +24 -3
- package/src/mcpl/conversation-router.ts +34 -1
- package/src/mcpl/feature-set-manager.ts +36 -11
- package/src/mcpl/index.ts +2 -0
- package/src/mcpl/prose-stream-router.ts +79 -23
- package/src/mcpl/push-coalescer.ts +62 -13
- package/src/mcpl/push-handler.ts +41 -12
- package/src/mcpl/server-connection.ts +41 -10
- package/src/mcpl/server-registry.ts +24 -7
- package/src/mcpl/tool-lifecycle.ts +77 -2
- package/src/mcpl/tool-pattern-check.ts +106 -0
- package/src/mcpl/types.ts +15 -5
- package/src/mcpl/visible-content.ts +57 -0
- package/src/module-registry.ts +252 -16
- package/src/modules/workspace/branch-intent.ts +123 -0
- package/src/modules/workspace/disk-agreement.ts +325 -0
- package/src/modules/workspace/effects.ts +362 -0
- package/src/modules/workspace/index.ts +1058 -446
- package/src/modules/workspace/observe.ts +839 -0
- package/src/modules/workspace/reconcile.ts +1136 -0
- package/src/modules/workspace/sync.ts +6 -336
- package/src/modules/workspace/types.ts +75 -10
- package/src/record-journal.ts +246 -0
- package/src/shutdown.ts +49 -0
- package/src/silent-heartbeat.ts +141 -0
- package/src/tool-presentation.ts +188 -0
- package/src/tool-result-guard.ts +335 -33
- package/src/tool-result-history.ts +25 -1
- package/src/types/agent.ts +18 -2
- package/src/types/events.ts +4 -0
- package/src/types/framework.ts +12 -0
- package/src/types/module.ts +15 -0
- package/src/types/trace.ts +80 -0
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
* {op:"wake", id, exec_id, line, payload}
|
|
38
38
|
* {op:"exec_result", id, stdout, stderr, return_code, tail?}
|
|
39
39
|
*
|
|
40
|
+
* At a deadline the host sends cancel and then SIGINT, repeated until the
|
|
41
|
+
* script reports (not on Windows): the cancel stops a script waiting on an
|
|
42
|
+
* await, the signal also one stuck in blocking code (see _on_sigint).
|
|
43
|
+
*
|
|
40
44
|
* IMPORTANT: the python source below must not contain backticks or the
|
|
41
45
|
* sequence dollar+brace (TS template literal syntax). String.raw preserves
|
|
42
46
|
* backslashes, so \n inside python string literals is fine.
|
|
@@ -49,6 +53,7 @@ import inspect
|
|
|
49
53
|
import io
|
|
50
54
|
import json
|
|
51
55
|
import os
|
|
56
|
+
import signal
|
|
52
57
|
import sys
|
|
53
58
|
import threading
|
|
54
59
|
import traceback
|
|
@@ -169,6 +174,90 @@ _next_wake_id = 0
|
|
|
169
174
|
_current_exec_id = None
|
|
170
175
|
_current_exec_task = None
|
|
171
176
|
|
|
177
|
+
# How the host stops a script (its time limit, or an abort): ONE per-script
|
|
178
|
+
# state, so the script is stopped exactly once, by whichever path gets there
|
|
179
|
+
# first. Every other path then does nothing to that script.
|
|
180
|
+
# PENDING -> nothing asked yet.
|
|
181
|
+
# REQUESTED -> asked while the script was outside its own code (waiting on
|
|
182
|
+
# an await, writing a protocol line, or not started yet). A
|
|
183
|
+
# cancellation is queued on the loop, or delivered as the
|
|
184
|
+
# script starts.
|
|
185
|
+
# DELIVERED -> KeyboardInterrupt raised in its code, or its task cancelled.
|
|
186
|
+
# Final: whatever the script does next (except/finally, awaits
|
|
187
|
+
# in its cleanup) runs to completion or to the host's kill.
|
|
188
|
+
# Transitions happen only in the main thread: in the SIGINT handler (between
|
|
189
|
+
# bytecodes, so it never cancels anything itself -- see _on_sigint) and in
|
|
190
|
+
# loop callbacks. Neither can interleave a transition of the other halfway
|
|
191
|
+
# in a way that matters: the handler only moves PENDING to REQUESTED or, with
|
|
192
|
+
# the script's own frame on the stack, to DELIVERED; a loop callback never
|
|
193
|
+
# runs while the script's frame is on the stack.
|
|
194
|
+
#
|
|
195
|
+
# Where the signal (or the cancel op) can land, and what happens:
|
|
196
|
+
# 1. script's own code, running or blocking -> KeyboardInterrupt there
|
|
197
|
+
# 2. script waiting on an await (loop idle) -> cancellation queued; it wakes
|
|
198
|
+
# the loop and lands at that await
|
|
199
|
+
# 3. main() mid-dispatch, between the done() check of a future the script
|
|
200
|
+
# awaits and its set_result (reply behind the cancellation) -> queued;
|
|
201
|
+
# set_result completes; the script gets CancelledError instead of it
|
|
202
|
+
# 4. the script writing a protocol line (send) -> queued; the line stays
|
|
203
|
+
# whole; the cancellation lands at the script's next await
|
|
204
|
+
# 5. reply ahead of the queued cancellation, script then awaits -> the
|
|
205
|
+
# cancellation lands at that await
|
|
206
|
+
# 6. reply ahead, script then blocks -> the next signal (the host repeats
|
|
207
|
+
# it) raises KeyboardInterrupt there; the queued cancellation then does
|
|
208
|
+
# nothing, so cleanup that awaits is not cut short
|
|
209
|
+
# 7. reply ahead, script finishes before the queued cancellation runs ->
|
|
210
|
+
# it does nothing, and never reaches the next script
|
|
211
|
+
# 8. script not started yet (signal, or the cancel op) -> delivered as
|
|
212
|
+
# CancelledError when it starts, so it still reports
|
|
213
|
+
# 9. cancel op before the signal -> delivered; the signal and its repeats
|
|
214
|
+
# do nothing, so cleanup after a caught CancelledError runs on
|
|
215
|
+
# 10. cancel op or repeated signal after delivery -> nothing
|
|
216
|
+
# 11. script finished, reporting its result -> nothing
|
|
217
|
+
# 12. between scripts -> nothing
|
|
218
|
+
# 13. a previous script installed an eager task factory -> no effect: the
|
|
219
|
+
# runtime builds its script task directly, and the state is in place
|
|
220
|
+
# before the script's first step
|
|
221
|
+
_PENDING = "pending"
|
|
222
|
+
_REQUESTED = "requested"
|
|
223
|
+
_DELIVERED = "delivered"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _ScriptStop:
|
|
227
|
+
__slots__ = ("task", "running", "state", "interrupt")
|
|
228
|
+
|
|
229
|
+
def __init__(self):
|
|
230
|
+
self.task = None
|
|
231
|
+
self.running = False # the script's own code may be on the stack
|
|
232
|
+
self.state = _PENDING
|
|
233
|
+
self.interrupt = None # the KeyboardInterrupt raised in its code
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_stop = None # the running script's _ScriptStop; None between scripts
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _deliver_cancel(st):
|
|
240
|
+
# Loop context only: the cancel op in main()'s dispatch, or queued by the
|
|
241
|
+
# SIGINT handler. Never from inside the handler, where a cancellation
|
|
242
|
+
# could land between main()'s done() check of a future the script awaits
|
|
243
|
+
# and its set_result.
|
|
244
|
+
if st is None or st is not _stop or st.state == _DELIVERED:
|
|
245
|
+
return
|
|
246
|
+
if not st.running:
|
|
247
|
+
st.state = _REQUESTED # not started yet: delivered as it starts
|
|
248
|
+
return
|
|
249
|
+
st.state = _DELIVERED
|
|
250
|
+
st.task.cancel()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _resolve(fut, value):
|
|
254
|
+
# Defense in depth: the script may have been cancelled since the lookup.
|
|
255
|
+
if fut is not None and not fut.done():
|
|
256
|
+
try:
|
|
257
|
+
fut.set_result(value)
|
|
258
|
+
except asyncio.InvalidStateError:
|
|
259
|
+
pass
|
|
260
|
+
|
|
172
261
|
|
|
173
262
|
def _make_tool_fn(tool_name, py_name):
|
|
174
263
|
async def tool_fn(args=None):
|
|
@@ -279,8 +368,8 @@ def handle_init(msg):
|
|
|
279
368
|
SCRIPT_GLOBALS.pop("wake_agent", None)
|
|
280
369
|
|
|
281
370
|
|
|
282
|
-
async def _run_script(exec_id, code):
|
|
283
|
-
global _current_exec_id, _current_exec_task
|
|
371
|
+
async def _run_script(exec_id, code, st):
|
|
372
|
+
global _current_exec_id, _current_exec_task, _stop
|
|
284
373
|
if BACKGROUND:
|
|
285
374
|
out = LogTee(LOG_PATH, TAIL_CHARS)
|
|
286
375
|
err = out # interleave, terminal-style; tail is shared
|
|
@@ -293,17 +382,38 @@ async def _run_script(exec_id, code):
|
|
|
293
382
|
return_code = 0
|
|
294
383
|
try:
|
|
295
384
|
compiled = compile(code, "<script>", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
385
|
+
st.running = True
|
|
386
|
+
try:
|
|
387
|
+
if st.state == _REQUESTED:
|
|
388
|
+
# Stopped before it started (row 8).
|
|
389
|
+
st.state = _DELIVERED
|
|
390
|
+
raise asyncio.CancelledError()
|
|
391
|
+
result = eval(compiled, SCRIPT_GLOBALS)
|
|
392
|
+
if inspect.iscoroutine(result):
|
|
393
|
+
await result
|
|
394
|
+
finally:
|
|
395
|
+
st.running = False
|
|
299
396
|
except asyncio.CancelledError:
|
|
300
397
|
err.write("\nKeyboardInterrupt: script cancelled by host\n")
|
|
301
398
|
return_code = 1
|
|
302
|
-
except BaseException:
|
|
303
|
-
|
|
399
|
+
except BaseException as exc:
|
|
400
|
+
if exc is st.interrupt:
|
|
401
|
+
# Where the script was when it was stopped, without the runtime's frames.
|
|
402
|
+
frames = [
|
|
403
|
+
f for f in traceback.extract_tb(exc.__traceback__)
|
|
404
|
+
if f.filename != _RUN_SCRIPT_CODE.co_filename
|
|
405
|
+
]
|
|
406
|
+
err.write("Traceback (most recent call last):\n")
|
|
407
|
+
err.write("".join(traceback.format_list(frames)))
|
|
408
|
+
err.write("KeyboardInterrupt: script interrupted by host\n")
|
|
409
|
+
else:
|
|
410
|
+
traceback.print_exc(file=err)
|
|
304
411
|
return_code = 1
|
|
305
412
|
finally:
|
|
306
413
|
sys.stdout, sys.stderr, sys.stdin = old_out, old_err, old_in
|
|
414
|
+
st.interrupt = None
|
|
415
|
+
if _stop is st:
|
|
416
|
+
_stop = None
|
|
307
417
|
_current_exec_id = None
|
|
308
418
|
_current_exec_task = None
|
|
309
419
|
for fut in list(_pending_tool_futures.values()):
|
|
@@ -343,8 +453,47 @@ async def _run_script(exec_id, code):
|
|
|
343
453
|
})
|
|
344
454
|
|
|
345
455
|
|
|
456
|
+
def _dispatch(msg):
|
|
457
|
+
"""Handle one protocol message from the host; False means stop reading."""
|
|
458
|
+
global _current_exec_id, _current_exec_task, _stop
|
|
459
|
+
op = msg.get("op")
|
|
460
|
+
if op == "init":
|
|
461
|
+
handle_init(msg)
|
|
462
|
+
elif op == "exec":
|
|
463
|
+
if _current_exec_task is not None and not _current_exec_task.done():
|
|
464
|
+
send({
|
|
465
|
+
"op": "exec_result",
|
|
466
|
+
"id": msg.get("id"),
|
|
467
|
+
"stdout": "",
|
|
468
|
+
"stderr": "RuntimeError: another script is already running in this interpreter",
|
|
469
|
+
"return_code": 1,
|
|
470
|
+
})
|
|
471
|
+
return True
|
|
472
|
+
# Everything _on_sigint reads is in place before the script can run
|
|
473
|
+
# (row 13). The task is built directly, not through the loop's task
|
|
474
|
+
# factory: a script may have installed one (interpreter state
|
|
475
|
+
# persists), and an eager one would run the next script inside this
|
|
476
|
+
# call, before the assignments below.
|
|
477
|
+
st = _ScriptStop()
|
|
478
|
+
_stop = st
|
|
479
|
+
_current_exec_id = msg.get("id")
|
|
480
|
+
_current_exec_task = asyncio.Task(
|
|
481
|
+
_run_script(msg.get("id"), msg.get("code") or "", st),
|
|
482
|
+
loop=asyncio.get_running_loop(),
|
|
483
|
+
)
|
|
484
|
+
st.task = _current_exec_task
|
|
485
|
+
elif op == "tool_result":
|
|
486
|
+
_resolve(_pending_tool_futures.get(msg.get("id")), str(msg.get("result", "")))
|
|
487
|
+
elif op == "wake_ack":
|
|
488
|
+
_resolve(_pending_wake_futures.get(msg.get("id")), msg.get("error") or None)
|
|
489
|
+
elif op == "cancel":
|
|
490
|
+
_deliver_cancel(_stop)
|
|
491
|
+
elif op == "exit":
|
|
492
|
+
return False
|
|
493
|
+
return True
|
|
494
|
+
|
|
495
|
+
|
|
346
496
|
async def main():
|
|
347
|
-
global _current_exec_id, _current_exec_task
|
|
348
497
|
loop = asyncio.get_running_loop()
|
|
349
498
|
queue = asyncio.Queue()
|
|
350
499
|
|
|
@@ -353,7 +502,17 @@ async def main():
|
|
|
353
502
|
loop.call_soon_threadsafe(queue.put_nowait, raw_line)
|
|
354
503
|
loop.call_soon_threadsafe(queue.put_nowait, None)
|
|
355
504
|
|
|
356
|
-
|
|
505
|
+
# The reader thread blocks SIGINT (it inherits the mask while it starts),
|
|
506
|
+
# so the signal reaches the main thread, where a blocking call in the
|
|
507
|
+
# script is waiting to be interrupted.
|
|
508
|
+
masked = hasattr(signal, "pthread_sigmask") and hasattr(signal, "SIGINT")
|
|
509
|
+
if masked:
|
|
510
|
+
previous_mask = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGINT})
|
|
511
|
+
try:
|
|
512
|
+
threading.Thread(target=reader, daemon=True).start()
|
|
513
|
+
finally:
|
|
514
|
+
if masked:
|
|
515
|
+
signal.pthread_sigmask(signal.SIG_SETMASK, previous_mask)
|
|
357
516
|
send({"op": "ready"})
|
|
358
517
|
|
|
359
518
|
while True:
|
|
@@ -369,35 +528,7 @@ async def main():
|
|
|
369
528
|
REAL_STDERR.write("[pytc-runtime] bad protocol line\n")
|
|
370
529
|
REAL_STDERR.flush()
|
|
371
530
|
continue
|
|
372
|
-
|
|
373
|
-
if op == "init":
|
|
374
|
-
handle_init(msg)
|
|
375
|
-
elif op == "exec":
|
|
376
|
-
if _current_exec_task is not None and not _current_exec_task.done():
|
|
377
|
-
send({
|
|
378
|
-
"op": "exec_result",
|
|
379
|
-
"id": msg.get("id"),
|
|
380
|
-
"stdout": "",
|
|
381
|
-
"stderr": "RuntimeError: another script is already running in this interpreter",
|
|
382
|
-
"return_code": 1,
|
|
383
|
-
})
|
|
384
|
-
continue
|
|
385
|
-
_current_exec_id = msg.get("id")
|
|
386
|
-
_current_exec_task = asyncio.ensure_future(
|
|
387
|
-
_run_script(msg.get("id"), msg.get("code") or "")
|
|
388
|
-
)
|
|
389
|
-
elif op == "tool_result":
|
|
390
|
-
fut = _pending_tool_futures.get(msg.get("id"))
|
|
391
|
-
if fut is not None and not fut.done():
|
|
392
|
-
fut.set_result(str(msg.get("result", "")))
|
|
393
|
-
elif op == "wake_ack":
|
|
394
|
-
fut = _pending_wake_futures.get(msg.get("id"))
|
|
395
|
-
if fut is not None and not fut.done():
|
|
396
|
-
fut.set_result(msg.get("error") or None)
|
|
397
|
-
elif op == "cancel":
|
|
398
|
-
if _current_exec_task is not None and not _current_exec_task.done():
|
|
399
|
-
_current_exec_task.cancel()
|
|
400
|
-
elif op == "exit":
|
|
531
|
+
if not _dispatch(msg):
|
|
401
532
|
break
|
|
402
533
|
|
|
403
534
|
# Drain: give a cancelled exec a moment to emit its exec_result.
|
|
@@ -409,6 +540,52 @@ async def main():
|
|
|
409
540
|
pass
|
|
410
541
|
|
|
411
542
|
|
|
543
|
+
# At the deadline the host sends a cancel op, which lands only when the script
|
|
544
|
+
# awaits: a script blocked in time.sleep(), a busy loop or a blocking read
|
|
545
|
+
# never gives the event loop control back. So the host also sends SIGINT,
|
|
546
|
+
# repeated until the script reports, and this stops the script through its
|
|
547
|
+
# _ScriptStop (see the table there):
|
|
548
|
+
# - With the script's own code on the stack (_run_script's frame, reached
|
|
549
|
+
# before any send frame), it raises KeyboardInterrupt there; _run_script
|
|
550
|
+
# catches it, so the output so far and the interpreter's globals survive.
|
|
551
|
+
# Checking the current task instead is not enough: asyncio runs its own
|
|
552
|
+
# code for the task between steps, and an exception raised there escapes
|
|
553
|
+
# the event loop and ends the interpreter.
|
|
554
|
+
# - Anywhere else it only queues the cancellation on the loop (which also
|
|
555
|
+
# wakes select(), otherwise resuming its wait after a signal): cancelling
|
|
556
|
+
# here could cancel a future between main()'s done() check and its
|
|
557
|
+
# set_result, and an exception mid-send would tear a protocol line.
|
|
558
|
+
_RUN_SCRIPT_CODE = _run_script.__code__
|
|
559
|
+
_SEND_CODE = send.__code__
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _on_sigint(signum, frame):
|
|
563
|
+
st = _stop
|
|
564
|
+
if st is None or st.state == _DELIVERED:
|
|
565
|
+
return
|
|
566
|
+
if st.running:
|
|
567
|
+
while frame is not None:
|
|
568
|
+
code = frame.f_code
|
|
569
|
+
if code is _SEND_CODE:
|
|
570
|
+
break
|
|
571
|
+
if code is _RUN_SCRIPT_CODE:
|
|
572
|
+
st.state = _DELIVERED
|
|
573
|
+
st.interrupt = KeyboardInterrupt("script interrupted by host")
|
|
574
|
+
raise st.interrupt
|
|
575
|
+
frame = frame.f_back
|
|
576
|
+
if st.state == _PENDING:
|
|
577
|
+
st.state = _REQUESTED
|
|
578
|
+
if st.running:
|
|
579
|
+
st.task.get_loop().call_soon_threadsafe(_deliver_cancel, st)
|
|
580
|
+
|
|
581
|
+
|
|
412
582
|
if __name__ == "__main__":
|
|
583
|
+
# A handler of our own also keeps asyncio.run from installing one that
|
|
584
|
+
# would cancel main() and end the interpreter.
|
|
585
|
+
if hasattr(signal, "SIGINT"):
|
|
586
|
+
try:
|
|
587
|
+
signal.signal(signal.SIGINT, _on_sigint)
|
|
588
|
+
except (ValueError, OSError):
|
|
589
|
+
pass
|
|
413
590
|
asyncio.run(main())
|
|
414
591
|
`;
|