@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.
- package/README.md +212 -42
- package/dist/src/agent.d.ts +60 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +92 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +14 -12
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/engine.d.ts +177 -0
- package/dist/src/code-execution/engine.d.ts.map +1 -0
- package/dist/src/code-execution/engine.js +494 -0
- package/dist/src/code-execution/engine.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +5 -10
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js +5 -9
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/isolate/calls.d.ts +114 -0
- package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/calls.js +145 -0
- package/dist/src/code-execution/isolate/calls.js.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.js +5 -0
- package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
- package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
- package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-main.js +132 -0
- package/dist/src/code-execution/isolate/child-main.js.map +1 -0
- package/dist/src/code-execution/isolate/child.d.ts +44 -0
- package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child.js +315 -0
- package/dist/src/code-execution/isolate/child.js.map +1 -0
- package/dist/src/code-execution/isolate/frames.d.ts +80 -0
- package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/frames.js +210 -0
- package/dist/src/code-execution/isolate/frames.js.map +1 -0
- package/dist/src/code-execution/isolate/output.d.ts +68 -0
- package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/output.js +143 -0
- package/dist/src/code-execution/isolate/output.js.map +1 -0
- package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
- package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/prelude.js +401 -0
- package/dist/src/code-execution/isolate/prelude.js.map +1 -0
- package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
- package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/protocol.js +150 -0
- package/dist/src/code-execution/isolate/protocol.js.map +1 -0
- package/dist/src/code-execution/isolate/render.d.ts +57 -0
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/render.js +214 -0
- package/dist/src/code-execution/isolate/render.js.map +1 -0
- package/dist/src/code-execution/isolate/runner.d.ts +250 -0
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/runner.js +854 -0
- package/dist/src/code-execution/isolate/runner.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/scripts.js +357 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -0
- package/dist/src/code-execution/quickjs.wasm +0 -0
- package/dist/src/code-execution/tool-definition.d.ts +93 -43
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +176 -142
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/deadline-timer.d.ts +3 -0
- package/dist/src/deadline-timer.d.ts.map +1 -1
- package/dist/src/deadline-timer.js +1 -1
- package/dist/src/deadline-timer.js.map +1 -1
- package/dist/src/framework.d.ts +1027 -440
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4093 -2238
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +28 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +88 -10
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -3
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +16 -3
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +471 -75
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1673 -337
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +12 -7
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +12 -7
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +9 -5
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +50 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +130 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -2
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +6 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +8 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +15 -6
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +168 -53
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -5
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +4 -5
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +97 -10
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +173 -16
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -14
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +227 -50
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +6 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +4 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +4 -4
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +50 -21
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +158 -74
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +33 -0
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +55 -7
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +163 -34
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +22 -6
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +4 -2
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +100 -13
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +8 -13
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +27 -4
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +15 -3
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +27 -5
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +4 -1
- package/dist/src/modules/unknown-tool.d.ts.map +1 -1
- package/dist/src/modules/unknown-tool.js +4 -1
- package/dist/src/modules/unknown-tool.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +3 -1
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
- package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-plan.js +794 -0
- package/dist/src/modules/workspace/edit-plan.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +117 -0
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-text.js +584 -0
- package/dist/src/modules/workspace/edit-text.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +5 -4
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +78 -25
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +484 -61
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2173 -645
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +213 -38
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +416 -36
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +15 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/text.d.ts +32 -0
- package/dist/src/modules/workspace/text.d.ts.map +1 -0
- package/dist/src/modules/workspace/text.js +67 -0
- package/dist/src/modules/workspace/text.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +210 -6
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +3 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/operator-log.d.ts +52 -24
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +26 -1
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +164 -39
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +23 -4
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +11 -0
- package/dist/src/send-tools.d.ts.map +1 -0
- package/dist/src/send-tools.js +11 -0
- package/dist/src/send-tools.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +4 -3
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +5 -4
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +1 -1
- package/dist/src/tool-result-history.js +1 -1
- package/dist/src/tune-out/coordinator.d.ts +21 -0
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +139 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +4 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +62 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +62 -58
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +40 -8
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +111 -10
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/types.d.ts +16 -0
- package/dist/src/usage/types.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +14 -1
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +81 -41
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +6 -5
- package/src/agent.ts +93 -12
- package/src/api/server.ts +14 -12
- package/src/code-execution/engine.ts +595 -0
- package/src/code-execution/index.ts +5 -17
- package/src/code-execution/isolate/calls.ts +199 -0
- package/src/code-execution/isolate/child-entry.ts +5 -0
- package/src/code-execution/isolate/child-main.ts +132 -0
- package/src/code-execution/isolate/child.ts +327 -0
- package/src/code-execution/isolate/frames.ts +209 -0
- package/src/code-execution/isolate/output.ts +161 -0
- package/src/code-execution/isolate/prelude.ts +409 -0
- package/src/code-execution/isolate/protocol.ts +240 -0
- package/src/code-execution/isolate/render.ts +230 -0
- package/src/code-execution/isolate/runner.ts +911 -0
- package/src/code-execution/isolate/scripts.ts +479 -0
- package/src/code-execution/quickjs.wasm +0 -0
- package/src/code-execution/quickjs.wasm.LICENSES +149 -0
- package/src/code-execution/tool-definition.ts +236 -167
- package/src/deadline-timer.ts +1 -1
- package/src/framework.ts +4468 -2418
- package/src/gate/event-gate.ts +78 -11
- package/src/held-writes.ts +17 -3
- package/src/index.ts +30 -22
- package/src/mcpl/channel-registry.ts +1825 -368
- package/src/mcpl/conversation-router.ts +12 -7
- package/src/mcpl/convert-block.ts +9 -5
- package/src/mcpl/inbound-source.ts +131 -0
- package/src/mcpl/index.ts +4 -2
- package/src/mcpl/log-value.ts +9 -0
- package/src/mcpl/modern-connection.ts +162 -53
- package/src/mcpl/prose-grammar.ts +4 -5
- package/src/mcpl/protocol-family.ts +199 -21
- package/src/mcpl/push-coalescer.ts +306 -62
- package/src/mcpl/push-handler.ts +10 -2
- package/src/mcpl/reference-fetcher.ts +4 -4
- package/src/mcpl/server-connection.ts +180 -79
- package/src/mcpl/tags.ts +36 -0
- package/src/mcpl/tool-result-normalize.ts +2 -1
- package/src/mcpl/transport.ts +162 -33
- package/src/mcpl/types.ts +23 -6
- package/src/module-registry.ts +92 -13
- package/src/modules/discord/index.ts +8 -13
- package/src/modules/history/index.ts +25 -3
- package/src/modules/history/semantic.ts +29 -6
- package/src/modules/index.ts +1 -0
- package/src/modules/unknown-tool.ts +4 -1
- package/src/modules/workspace/branch-intent.ts +3 -1
- package/src/modules/workspace/edit-plan.ts +954 -0
- package/src/modules/workspace/edit-text.ts +618 -0
- package/src/modules/workspace/effects.ts +70 -19
- package/src/modules/workspace/index.ts +2247 -642
- package/src/modules/workspace/observe.ts +465 -56
- package/src/modules/workspace/reconcile.ts +14 -7
- package/src/modules/workspace/text.ts +68 -0
- package/src/modules/workspace/types.ts +211 -6
- package/src/modules/workspace/watcher.ts +3 -2
- package/src/operator-log.ts +47 -19
- package/src/recovery/discord-awareness-outbox.ts +1500 -356
- package/src/recovery/offline-branch.ts +189 -55
- package/src/recovery/recover-cli.ts +25 -4
- package/src/send-tools.ts +11 -0
- package/src/tool-result-guard.ts +6 -5
- package/src/tool-result-history.ts +1 -1
- package/src/tune-out/coordinator.ts +163 -42
- package/src/types/agent.ts +4 -0
- package/src/types/events.ts +62 -9
- package/src/types/framework.ts +62 -58
- package/src/types/module.ts +40 -8
- package/src/types/trace.ts +121 -15
- package/src/usage/types.ts +16 -0
- package/src/usage/usage-tracker.ts +86 -43
- package/dist/src/code-execution/py-runner.d.ts +0 -191
- package/dist/src/code-execution/py-runner.d.ts.map +0 -1
- package/dist/src/code-execution/py-runner.js +0 -655
- package/dist/src/code-execution/py-runner.js.map +0 -1
- package/dist/src/code-execution/runtime-py.d.ts +0 -50
- package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
- package/dist/src/code-execution/runtime-py.js +0 -677
- package/dist/src/code-execution/runtime-py.js.map +0 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
- package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
- package/dist/src/mcpl/prose-stream-router.js +0 -291
- package/dist/src/mcpl/prose-stream-router.js.map +0 -1
- package/src/code-execution/py-runner.ts +0 -791
- package/src/code-execution/runtime-py.ts +0 -677
- 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
|
-
`;
|