@animalabs/agent-framework 0.23.3 → 0.24.1
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 +65 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +98 -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/clock-rows.d.ts +115 -0
- package/dist/src/clock-rows.d.ts.map +1 -0
- package/dist/src/clock-rows.js +222 -0
- package/dist/src/clock-rows.js.map +1 -0
- 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 +1120 -457
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4635 -2505
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +28 -4
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +89 -13
- 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 +8 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +483 -115
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1735 -425
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -39
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +27 -59
- 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 +174 -17
- 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 +9 -20
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +7 -49
- 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 +493 -62
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2216 -652
- 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/timezone.d.ts +4 -2
- package/dist/src/timezone.d.ts.map +1 -1
- package/dist/src/timezone.js +4 -2
- package/dist/src/timezone.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts +46 -7
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +60 -7
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +14 -11
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +17 -14
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +3 -2
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +3 -2
- package/dist/src/tool-result-history.js.map +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 +142 -40
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +31 -6
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +66 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +69 -62
- 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 -8
- package/src/agent.ts +99 -12
- package/src/api/server.ts +14 -12
- package/src/clock-rows.ts +302 -0
- 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 +4978 -2671
- package/src/gate/event-gate.ts +79 -17
- package/src/held-writes.ts +17 -3
- package/src/index.ts +32 -22
- package/src/mcpl/channel-registry.ts +1892 -471
- package/src/mcpl/conversation-router.ts +26 -72
- 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 +200 -22
- package/src/mcpl/push-coalescer.ts +306 -62
- package/src/mcpl/push-handler.ts +12 -54
- 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 +2293 -650
- 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/timezone.ts +4 -2
- package/src/tool-image-ledger.ts +62 -8
- package/src/tool-result-guard.ts +18 -15
- package/src/tool-result-history.ts +3 -2
- package/src/tune-out/coordinator.ts +166 -45
- package/src/types/agent.ts +32 -6
- package/src/types/events.ts +66 -9
- package/src/types/framework.ts +69 -62
- 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,655 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PyRunner — subprocess lifecycle + protocol driver for the client-side
|
|
3
|
-
* programmatic tool calling (`code_execution`) runtime.
|
|
4
|
-
*
|
|
5
|
-
* One runner per agent. The interpreter is spawned lazily on first exec,
|
|
6
|
-
* persists between execs (script globals survive — container-reuse
|
|
7
|
-
* semantics), and is reclaimed after an idle period, mirroring the managed
|
|
8
|
-
* runtime's ~5-minute container reclaim.
|
|
9
|
-
*
|
|
10
|
-
* This is a ROBUSTNESS boundary, not a security sandbox — same doctrine as
|
|
11
|
-
* GateScript: the agent already has broader host access through its tools.
|
|
12
|
-
* What this class guarantees is liveness: a wedged or runaway script cannot
|
|
13
|
-
* hang the agent turn (cancel + SIGINT -> grace -> SIGKILL -> respawn) and a crashed
|
|
14
|
-
* interpreter surfaces as a tool result, never as an unhandled rejection.
|
|
15
|
-
*/
|
|
16
|
-
import { spawn } from 'node:child_process';
|
|
17
|
-
import { buildChildEnv } from '../mcpl/transport.js';
|
|
18
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
|
-
import { tmpdir } from 'node:os';
|
|
20
|
-
import { join, resolve } from 'node:path';
|
|
21
|
-
import { createInterface } from 'node:readline';
|
|
22
|
-
import { boundedErrorMessage } from '../error-message.js';
|
|
23
|
-
import { PYTHON_RUNTIME_SOURCE } from './runtime-py.js';
|
|
24
|
-
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 270_000;
|
|
25
|
-
const DEFAULT_SCRIPT_TIMEOUT_MS = 600_000;
|
|
26
|
-
const DEFAULT_IDLE_RECLAIM_MS = 300_000;
|
|
27
|
-
const CANCEL_GRACE_MS = 10_000;
|
|
28
|
-
/** After the deadline, SIGINT is repeated at this interval until the script reports or is killed. */
|
|
29
|
-
const INTERRUPT_REPEAT_MS = 200;
|
|
30
|
-
/** The longest delay Node's timers honour (~24.8 days); a longer one fires after ~1 ms. */
|
|
31
|
-
export const MAX_TIMER_MS = 2_147_483_647;
|
|
32
|
-
/** A time limit for a message: "45s", "10 min", "2.5 h". */
|
|
33
|
-
export function formatLimit(ms) {
|
|
34
|
-
if (ms < 120_000)
|
|
35
|
-
return `${Math.round(ms / 1000)}s`;
|
|
36
|
-
if (ms < 7_200_000)
|
|
37
|
-
return `${Math.round(ms / 6_000) / 10} min`;
|
|
38
|
-
return `${Math.round(ms / 360_000) / 10} h`;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* What a script stopped at its time limit is told after why: that the tool
|
|
42
|
-
* calls it made keep their effects, and whether a longer limit can help,
|
|
43
|
-
* which only the deployment's ceiling for its kind of script can say. A
|
|
44
|
-
* foreground script at its ceiling is offered background mode too, when its
|
|
45
|
-
* caller passes `backgroundCeilingMs` (the framework does for an agent that
|
|
46
|
-
* could start a background script as the script starts) and a background
|
|
47
|
-
* script may run longer.
|
|
48
|
-
*/
|
|
49
|
-
function limitRemedy(limitMs, ceilingMs, background, backgroundCeilingMs) {
|
|
50
|
-
const effects = 'Tool calls it had already made keep their effects, so check them before running it again.';
|
|
51
|
-
if (ceilingMs === undefined)
|
|
52
|
-
return effects;
|
|
53
|
-
if (limitMs < ceilingMs)
|
|
54
|
-
return `${effects} To give it longer, ${background ? 'start it with' : 'pass'} time_limit_ms up to ${ceilingMs} (${formatLimit(ceilingMs)}).`;
|
|
55
|
-
if (background)
|
|
56
|
-
return `${effects} ${formatLimit(limitMs)} is the longest a background script runs here; starting it again runs it from the top.`;
|
|
57
|
-
const inBackground = backgroundCeilingMs !== undefined && backgroundCeilingMs > limitMs
|
|
58
|
-
? `, or run it with background: true, where a script can run up to ${formatLimit(backgroundCeilingMs)}` : '';
|
|
59
|
-
return `${effects} ${formatLimit(limitMs)} is the longest a script can run here, so split the work across shorter scripts${inBackground}.`;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Why a script didn't run when its interpreter didn't start. A spawn that
|
|
63
|
-
* found nothing it could run (ENOENT, EACCES) fails the same way until the
|
|
64
|
-
* host changes, so that one says code_execution can't run here rather than
|
|
65
|
-
* to try again. A failure before the spawn, while creating or writing the
|
|
66
|
-
* runtime's files, is about those files rather than the interpreter, and
|
|
67
|
-
* keeps Node's own text, which names the path for an open or a mkdtemp.
|
|
68
|
-
*/
|
|
69
|
-
function startFailure(pythonPath, err) {
|
|
70
|
-
const { code, syscall } = (err ?? {});
|
|
71
|
-
const spawning = typeof syscall === 'string' && syscall.startsWith('spawn');
|
|
72
|
-
if (spawning && (code === 'ENOENT' || code === 'EACCES')) {
|
|
73
|
-
const until = code === 'ENOENT' ? 'installs it or points this host at one' : 'makes it executable or points this host at another';
|
|
74
|
-
return `script didn't run: this host can't start its Python interpreter, ${pythonPath} (${code}), so code_execution can't run scripts here until whoever runs this host ${until}. Calling it again before then fails the same way.`;
|
|
75
|
-
}
|
|
76
|
-
if (typeof syscall === 'string' && !spawning) {
|
|
77
|
-
return `script didn't run: this host couldn't create or write the files its Python runtime starts from (${boundedErrorMessage(err)}), so none of the script ran. Run it again; if it fails the same way, ask whoever runs this host.`;
|
|
78
|
-
}
|
|
79
|
-
const why = typeof code === 'string' ? `couldn't be started (${code})` : boundedErrorMessage(err);
|
|
80
|
-
return `script didn't run: the Python interpreter, ${pythonPath}, ${why}, so none of the script ran. Run it again; if it fails the same way, ask whoever runs this host.`;
|
|
81
|
-
}
|
|
82
|
-
/** Why a script stopped when its interpreter exited under it, and what running it again does. */
|
|
83
|
-
function interpreterExit(exitCode, signal, background) {
|
|
84
|
-
const how = signal ? `was ended by ${signal}` : `exited with code ${exitCode}`;
|
|
85
|
-
return `script stopped: the Python interpreter ${how} while it ran, so the rest of the script didn't run`
|
|
86
|
-
+ `${background ? '' : ', and variables from earlier scripts are gone'}. Tool calls it had already made keep their effects, `
|
|
87
|
-
+ 'so check them before you run it again in a new interpreter. If it stops the same way, check whether the script itself '
|
|
88
|
-
+ `ends the process, for example ${signal ? 'by using too much memory' : 'with os._exit()'}; if not, ask whoever runs this host.`;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* The interpreter to spawn. A bare name is looked up on PATH. A path with a
|
|
92
|
-
* separator is resolved against the host's working directory, where it
|
|
93
|
-
* resolved before the interpreter got a working directory of its own: spawn
|
|
94
|
-
* would resolve it against the runtime directory instead.
|
|
95
|
-
*/
|
|
96
|
-
function spawnablePath(pythonPath) {
|
|
97
|
-
const hasSeparator = pythonPath.includes('/') || (process.platform === 'win32' && pythonPath.includes('\\'));
|
|
98
|
-
return hasSeparator ? resolve(pythonPath) : pythonPath;
|
|
99
|
-
}
|
|
100
|
-
export class PyRunner {
|
|
101
|
-
pythonPath;
|
|
102
|
-
toolCallTimeoutMs;
|
|
103
|
-
scriptTimeoutMs;
|
|
104
|
-
cancelGraceMs;
|
|
105
|
-
idleReclaimMs;
|
|
106
|
-
onToolCall;
|
|
107
|
-
label;
|
|
108
|
-
envOptions;
|
|
109
|
-
onWake = null;
|
|
110
|
-
child = null;
|
|
111
|
-
childReady = null;
|
|
112
|
-
reader = null;
|
|
113
|
-
runtimeDir = null;
|
|
114
|
-
pending = null;
|
|
115
|
-
idleTimer = null;
|
|
116
|
-
execCounter = 0;
|
|
117
|
-
disposed = false;
|
|
118
|
-
constructor(options) {
|
|
119
|
-
this.pythonPath = options.pythonPath ?? 'python3';
|
|
120
|
-
this.toolCallTimeoutMs = options.toolCallTimeoutMs ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
|
|
121
|
-
this.scriptTimeoutMs = options.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS;
|
|
122
|
-
this.cancelGraceMs = options.cancelGraceMs ?? CANCEL_GRACE_MS;
|
|
123
|
-
this.idleReclaimMs = options.idleReclaimMs ?? DEFAULT_IDLE_RECLAIM_MS;
|
|
124
|
-
this.onToolCall = options.onToolCall;
|
|
125
|
-
this.label = options.label ?? 'pytc';
|
|
126
|
-
// The interpreter runs model-authored code: the same env discipline as an
|
|
127
|
-
// MCPL connector child (#175), for a child that deserves it more. Stored
|
|
128
|
-
// as options, not a snapshot — the env is rebuilt at every spawn so a
|
|
129
|
-
// respawn after idle reclaim sees current host values, as the old
|
|
130
|
-
// per-spawn `{ ...process.env }` did.
|
|
131
|
-
this.envOptions = { env: options.env, inheritEnv: options.inheritEnv };
|
|
132
|
-
}
|
|
133
|
-
get busy() {
|
|
134
|
-
return this.pending !== null;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Run one script. Tools are (re-)injected before every exec so the
|
|
138
|
-
* interpreter always reflects the current tool surface (list_changed etc.).
|
|
139
|
-
* Never rejects: every failure mode resolves to an ExecResult.
|
|
140
|
-
*
|
|
141
|
-
* With `background` options the exec is a daemon: output journals to the
|
|
142
|
-
* log file, wake_agent() is available in-script, and the deadline is the
|
|
143
|
-
* background lifetime. A background runner should be DEDICATED to that one
|
|
144
|
-
* script (the framework creates one per background script).
|
|
145
|
-
*
|
|
146
|
-
* `opts.deadlineMs` replaces the runner's scriptTimeoutMs for this exec (a
|
|
147
|
-
* per-call time limit); a background exec uses its `lifetimeMs` instead.
|
|
148
|
-
* `opts.ceilingMs` is the longest limit the deployment allows this kind of
|
|
149
|
-
* script: a script stopped at its limit is offered a longer one only below
|
|
150
|
-
* it. Without it, the stop doesn't say whether a longer limit would help.
|
|
151
|
-
* `opts.backgroundCeilingMs`, for a foreground exec, is the longest a
|
|
152
|
-
* background script may run: one stopped at its ceiling is offered
|
|
153
|
-
* background mode when that is longer.
|
|
154
|
-
*/
|
|
155
|
-
async exec(code, tools, background, opts) {
|
|
156
|
-
if (this.disposed) {
|
|
157
|
-
return { stdout: '', stderr: 'code_execution runner disposed', returnCode: 1, aborted: true };
|
|
158
|
-
}
|
|
159
|
-
if (this.busy) {
|
|
160
|
-
return {
|
|
161
|
-
stdout: '',
|
|
162
|
-
stderr: "script didn't run: another code_execution script of yours is already running, and they run one at a time. "
|
|
163
|
-
+ 'Run this one again once that one has finished, or combine the two into one script.',
|
|
164
|
-
returnCode: 1,
|
|
165
|
-
aborted: true,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
this.clearIdleTimer();
|
|
169
|
-
// The exec claims the runner before the await: a same-tick second exec
|
|
170
|
-
// sees it busy, and abort() or dispose() while the interpreter starts
|
|
171
|
-
// settles this exec, whose script then never runs.
|
|
172
|
-
let resolveResult;
|
|
173
|
-
const settled = new Promise((resolve) => {
|
|
174
|
-
resolveResult = resolve;
|
|
175
|
-
});
|
|
176
|
-
const pending = {
|
|
177
|
-
id: `e${++this.execCounter}`,
|
|
178
|
-
resolve: resolveResult,
|
|
179
|
-
deadlineTimer: null,
|
|
180
|
-
killTimer: null,
|
|
181
|
-
settled: false,
|
|
182
|
-
deadlineMs: null,
|
|
183
|
-
ceilingMs: opts?.ceilingMs,
|
|
184
|
-
backgroundCeilingMs: opts?.backgroundCeilingMs,
|
|
185
|
-
background: background !== undefined,
|
|
186
|
-
sent: false,
|
|
187
|
-
};
|
|
188
|
-
this.pending = pending;
|
|
189
|
-
try {
|
|
190
|
-
await this.ensureChild();
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
// Settled already when an abort or dispose reclaimed the interpreter
|
|
194
|
-
// as it started; another exec may have claimed the runner since.
|
|
195
|
-
if (!pending.settled) {
|
|
196
|
-
this.settlePending({
|
|
197
|
-
stdout: '',
|
|
198
|
-
stderr: startFailure(this.pythonPath, err),
|
|
199
|
-
returnCode: 1,
|
|
200
|
-
aborted: true,
|
|
201
|
-
});
|
|
202
|
-
this.reclaim('spawn-failed');
|
|
203
|
-
}
|
|
204
|
-
return settled;
|
|
205
|
-
}
|
|
206
|
-
if (pending.settled)
|
|
207
|
-
return settled;
|
|
208
|
-
const execId = pending.id;
|
|
209
|
-
// Clamped: a deadline past Node's timer range would fire at once instead of never.
|
|
210
|
-
const deadlineMs = Math.min(MAX_TIMER_MS, background?.lifetimeMs ?? opts?.deadlineMs ?? this.scriptTimeoutMs);
|
|
211
|
-
this.onWake = background?.onWake ?? null;
|
|
212
|
-
pending.deadlineTimer = setTimeout(() => {
|
|
213
|
-
pending.deadlineMs = deadlineMs;
|
|
214
|
-
// Deadline: ask politely first (script sees CancelledError and its
|
|
215
|
-
// exec_result still flows back), then kill on unresponsiveness.
|
|
216
|
-
this.send({ op: 'cancel', id: execId, reason: 'deadline' });
|
|
217
|
-
// The cancel lands only when the script awaits. Blocking code (time.sleep, a
|
|
218
|
-
// busy loop, a blocking read) never does, so interrupt it as well: the runtime
|
|
219
|
-
// raises KeyboardInterrupt in the script's own code, or schedules the
|
|
220
|
-
// cancellation of a script that is waiting. Repeated, because a waiting script
|
|
221
|
-
// can resume into blocking code before that cancellation runs; the runtime
|
|
222
|
-
// ignores the repeats once the script was interrupted or cancelled.
|
|
223
|
-
this.interruptChild();
|
|
224
|
-
pending.interruptTimer = setInterval(() => this.interruptChild(), INTERRUPT_REPEAT_MS);
|
|
225
|
-
pending.killTimer = setTimeout(() => {
|
|
226
|
-
pending.deadlineMs = null; // this message already says why
|
|
227
|
-
this.settlePending({
|
|
228
|
-
stdout: '',
|
|
229
|
-
stderr: `script stopped: it reached its ${formatLimit(deadlineMs)} time limit and did not respond, so it was killed` +
|
|
230
|
-
(pending.background ? '. ' : ' and the interpreter restarted (variables from earlier scripts are gone). ') +
|
|
231
|
-
limitRemedy(deadlineMs, pending.ceilingMs, pending.background, pending.backgroundCeilingMs),
|
|
232
|
-
returnCode: 1,
|
|
233
|
-
aborted: true,
|
|
234
|
-
});
|
|
235
|
-
this.reclaim('deadline-kill');
|
|
236
|
-
}, this.cancelGraceMs);
|
|
237
|
-
}, deadlineMs);
|
|
238
|
-
// A day-scale background deadline must not hold the process open.
|
|
239
|
-
if (background)
|
|
240
|
-
pending.deadlineTimer.unref?.();
|
|
241
|
-
pending.sent = true;
|
|
242
|
-
this.send({
|
|
243
|
-
op: 'init',
|
|
244
|
-
tools: tools.map((t) => ({ py_name: t.pyName, tool_name: t.toolName })),
|
|
245
|
-
call_timeout_s: Math.round(this.toolCallTimeoutMs / 1000),
|
|
246
|
-
...(background
|
|
247
|
-
? { background: true, log_path: background.logPath ?? null }
|
|
248
|
-
: {}),
|
|
249
|
-
});
|
|
250
|
-
this.send({ op: 'exec', id: execId, code });
|
|
251
|
-
const result = await settled;
|
|
252
|
-
this.onWake = null;
|
|
253
|
-
if (!background)
|
|
254
|
-
this.armIdleTimer();
|
|
255
|
-
return result;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Abort the runner's script (its turn ended or is reset to run again, or the
|
|
259
|
-
* agent was reset or stopped), whether it's running or still waiting for its
|
|
260
|
-
* interpreter to start; a waiting script then never runs. The exec settles
|
|
261
|
-
* at once, and the interpreter is reclaimed, a starting one too: after an
|
|
262
|
-
* abort its state is suspect (a script died midway), and the next exec gets
|
|
263
|
-
* a fresh one.
|
|
264
|
-
*/
|
|
265
|
-
abort(reason) {
|
|
266
|
-
if (!this.pending)
|
|
267
|
-
return;
|
|
268
|
-
this.send({ op: 'cancel', id: this.pending.id, reason });
|
|
269
|
-
this.settlePending({
|
|
270
|
-
stdout: '',
|
|
271
|
-
stderr: `script aborted by host: ${reason}`,
|
|
272
|
-
returnCode: 1,
|
|
273
|
-
aborted: true,
|
|
274
|
-
});
|
|
275
|
-
this.reclaim(`abort: ${reason}`);
|
|
276
|
-
}
|
|
277
|
-
dispose() {
|
|
278
|
-
if (this.disposed)
|
|
279
|
-
return;
|
|
280
|
-
this.disposed = true;
|
|
281
|
-
if (this.pending) {
|
|
282
|
-
this.settlePending({
|
|
283
|
-
stdout: '',
|
|
284
|
-
stderr: 'code_execution runner disposed',
|
|
285
|
-
returnCode: 1,
|
|
286
|
-
aborted: true,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
this.reclaim('dispose');
|
|
290
|
-
}
|
|
291
|
-
// -------------------------------------------------------------------------
|
|
292
|
-
async ensureChild() {
|
|
293
|
-
if (this.child && this.child.exitCode === null && !this.child.killed) {
|
|
294
|
-
return this.childReady ?? Promise.resolve();
|
|
295
|
-
}
|
|
296
|
-
this.teardownChild();
|
|
297
|
-
// Absolute, since the interpreter runs in it: from a relative TMPDIR,
|
|
298
|
-
// runtime.py's path would otherwise resolve against the directory itself.
|
|
299
|
-
this.runtimeDir = resolve(mkdtempSync(join(tmpdir(), 'af-pytc-')));
|
|
300
|
-
const runtimePath = join(this.runtimeDir, 'runtime.py');
|
|
301
|
-
writeFileSync(runtimePath, PYTHON_RUNTIME_SOURCE, 'utf8');
|
|
302
|
-
const child = spawn(spawnablePath(this.pythonPath), [runtimePath], {
|
|
303
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
304
|
-
// The runtime directory, not the host's working directory: a script's
|
|
305
|
-
// relative paths land here, and go at teardown, rather than beside the
|
|
306
|
-
// host's .env and data. An absolute path still reaches the host's files.
|
|
307
|
-
cwd: this.runtimeDir,
|
|
308
|
-
// Set last, over a declared env: the protocol needs both. It writes
|
|
309
|
-
// non-ASCII JSON on piped stdio, which Python before 3.15 on Windows
|
|
310
|
-
// encodes in the ANSI code page unless told otherwise, and a host's
|
|
311
|
-
// own PYTHONUTF8 no longer reaches the interpreter.
|
|
312
|
-
env: { ...buildChildEnv(this.envOptions), PYTHONUNBUFFERED: '1', PYTHONIOENCODING: 'utf-8' },
|
|
313
|
-
});
|
|
314
|
-
this.child = child;
|
|
315
|
-
child.stderr.setEncoding('utf8');
|
|
316
|
-
child.stderr.on('data', (chunk) => {
|
|
317
|
-
// Outside an exec, python stderr is runtime diagnostics (crash
|
|
318
|
-
// tracebacks etc.) — surface them in host logs.
|
|
319
|
-
console.error(`[pytc:${this.label}] ${chunk.trimEnd()}`);
|
|
320
|
-
});
|
|
321
|
-
// A write to an interpreter that no longer reads its input fails on the
|
|
322
|
-
// stream after write() returns; with no listener, that error is thrown as
|
|
323
|
-
// an uncaught exception. Such an interpreter can go on running, as after a
|
|
324
|
-
// script closes file descriptor 0, and every later message to it is lost:
|
|
325
|
-
// its exec would wait out its time limit. So end it, and its exit settles
|
|
326
|
-
// the exec now. One already dead or killed is left to its own exit.
|
|
327
|
-
let inputFailed = false;
|
|
328
|
-
child.stdin.on('error', (err) => {
|
|
329
|
-
console.error(`[pytc:${this.label}] write to the python runtime failed: ${err.message}`);
|
|
330
|
-
if (child.exitCode === null && !child.killed && child.pid !== undefined) {
|
|
331
|
-
inputFailed = true;
|
|
332
|
-
child.kill('SIGKILL');
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
child.on('error', (err) => {
|
|
336
|
-
// Spawn failure lands here (e.g. python3 missing); readiness rejects.
|
|
337
|
-
console.error(`[pytc:${this.label}] python process error: ${err.message}`);
|
|
338
|
-
});
|
|
339
|
-
child.on('exit', (exitCode, signal) => {
|
|
340
|
-
// An exec still waiting for the interpreter is settled by exec, as a
|
|
341
|
-
// runtime that failed to start.
|
|
342
|
-
if (this.pending?.sent) {
|
|
343
|
-
this.settlePending({
|
|
344
|
-
stdout: '',
|
|
345
|
-
stderr: inputFailed && signal === 'SIGKILL'
|
|
346
|
-
? 'python runtime stopped reading its input, so it was killed and the interpreter restarted ' +
|
|
347
|
-
'(variables from earlier scripts are gone). A script that closes its standard input ' +
|
|
348
|
-
'(file descriptor 0) does this.'
|
|
349
|
-
: interpreterExit(exitCode, signal, this.pending.background),
|
|
350
|
-
returnCode: 1,
|
|
351
|
-
aborted: true,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
if (this.child === child) {
|
|
355
|
-
this.teardownChild();
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
this.reader = createInterface({ input: child.stdout });
|
|
359
|
-
this.reader.on('line', (line) => this.handleLine(line, child));
|
|
360
|
-
this.childReady = new Promise((resolve, reject) => {
|
|
361
|
-
const onReady = () => {
|
|
362
|
-
cleanup();
|
|
363
|
-
resolve();
|
|
364
|
-
};
|
|
365
|
-
const onExit = () => {
|
|
366
|
-
cleanup();
|
|
367
|
-
reject(new Error('exited as it started'));
|
|
368
|
-
};
|
|
369
|
-
const onError = (err) => {
|
|
370
|
-
cleanup();
|
|
371
|
-
reject(err);
|
|
372
|
-
};
|
|
373
|
-
const timeout = setTimeout(() => {
|
|
374
|
-
cleanup();
|
|
375
|
-
reject(new Error("didn't start within 15 seconds"));
|
|
376
|
-
}, 15_000);
|
|
377
|
-
const cleanup = () => {
|
|
378
|
-
clearTimeout(timeout);
|
|
379
|
-
this.readyResolver = null;
|
|
380
|
-
this.readyFailer = null;
|
|
381
|
-
child.off('exit', onExit);
|
|
382
|
-
child.off('error', onError);
|
|
383
|
-
};
|
|
384
|
-
this.readyResolver = onReady;
|
|
385
|
-
this.readyFailer = onError;
|
|
386
|
-
child.on('exit', onExit);
|
|
387
|
-
child.on('error', onError);
|
|
388
|
-
});
|
|
389
|
-
return this.childReady;
|
|
390
|
-
}
|
|
391
|
-
readyResolver = null;
|
|
392
|
-
/** Fails the readiness of an interpreter that is still starting (see reclaim). */
|
|
393
|
-
readyFailer = null;
|
|
394
|
-
handleLine(line, child) {
|
|
395
|
-
let msg;
|
|
396
|
-
try {
|
|
397
|
-
msg = JSON.parse(line);
|
|
398
|
-
}
|
|
399
|
-
catch {
|
|
400
|
-
console.error(`[pytc:${this.label}] non-protocol stdout line: ${line.slice(0, 200)}`);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
switch (msg.op) {
|
|
404
|
-
case 'ready':
|
|
405
|
-
this.readyResolver?.();
|
|
406
|
-
return;
|
|
407
|
-
case 'tool_call': {
|
|
408
|
-
const callId = msg.id;
|
|
409
|
-
const toolName = msg.name;
|
|
410
|
-
if (!callId || !toolName)
|
|
411
|
-
return;
|
|
412
|
-
const args = msg.args && typeof msg.args === 'object' && !Array.isArray(msg.args)
|
|
413
|
-
? msg.args
|
|
414
|
-
: {};
|
|
415
|
-
this.onToolCall(toolName, args)
|
|
416
|
-
.catch((err) => ({
|
|
417
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
418
|
-
isError: true,
|
|
419
|
-
}))
|
|
420
|
-
.then((result) => {
|
|
421
|
-
// Only an object is a ScriptToolResult. Anything else a handler
|
|
422
|
-
// resolves with, a string or a value its type rules out, is sent
|
|
423
|
-
// as it is, so the call is still answered.
|
|
424
|
-
const reply = typeof result === 'object' && result !== null
|
|
425
|
-
? {
|
|
426
|
-
op: 'tool_result',
|
|
427
|
-
id: callId,
|
|
428
|
-
result: result.text,
|
|
429
|
-
is_error: result.isError,
|
|
430
|
-
...(result.structuredJson ? { structured_json: true } : {}),
|
|
431
|
-
}
|
|
432
|
-
: { op: 'tool_result', id: callId, result };
|
|
433
|
-
this.reply(child, reply, `result of ${toolName} call ${callId}`);
|
|
434
|
-
});
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
case 'wake': {
|
|
438
|
-
const wakeId = msg.id;
|
|
439
|
-
if (!wakeId)
|
|
440
|
-
return;
|
|
441
|
-
const line = typeof msg.line === 'number'
|
|
442
|
-
? msg.line
|
|
443
|
-
: -1;
|
|
444
|
-
const payload = msg.payload;
|
|
445
|
-
const handler = this.onWake;
|
|
446
|
-
const refuse = handler
|
|
447
|
-
? handler(line, payload).catch((err) => `wake handler failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
448
|
-
: Promise.resolve('this script is not allowed to wake the agent');
|
|
449
|
-
void refuse.then((error) => {
|
|
450
|
-
this.reply(child, { op: 'wake_ack', id: wakeId, ...(error ? { error } : {}) }, `ack of wake ${wakeId}`);
|
|
451
|
-
});
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
case 'exec_result': {
|
|
455
|
-
if (this.pending && msg.id === this.pending.id) {
|
|
456
|
-
this.settlePending({
|
|
457
|
-
stdout: msg.stdout ?? '',
|
|
458
|
-
stderr: msg.stderr ?? '',
|
|
459
|
-
returnCode: typeof msg.return_code === 'number' ? msg.return_code : 1,
|
|
460
|
-
...(typeof msg.tail === 'string'
|
|
461
|
-
? { tail: msg.tail }
|
|
462
|
-
: {}),
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
default:
|
|
468
|
-
console.error(`[pytc:${this.label}] unknown protocol op: ${String(msg.op)}`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
settlePending(result) {
|
|
472
|
-
const pending = this.pending;
|
|
473
|
-
if (!pending || pending.settled)
|
|
474
|
-
return;
|
|
475
|
-
pending.settled = true;
|
|
476
|
-
// Say why the script stopped: the in-script cancellation only reads "cancelled by host".
|
|
477
|
-
// Only a script that ended unsuccessfully was stopped: one that caught the cancellation
|
|
478
|
-
// and finished its work returns 0, and must not read as stopped.
|
|
479
|
-
if (pending.deadlineMs !== null && result.returnCode !== 0) {
|
|
480
|
-
const note = `script stopped: it reached its ${formatLimit(pending.deadlineMs)} time limit. `
|
|
481
|
-
+ `${limitRemedy(pending.deadlineMs, pending.ceilingMs, pending.background, pending.backgroundCeilingMs)}\n`;
|
|
482
|
-
// On a line of its own: the runner's own texts, such as an interpreter's exit, end without a newline.
|
|
483
|
-
const withNote = (text) => (text === '' || text.endsWith('\n') ? text : `${text}\n`) + note;
|
|
484
|
-
// A background script reports its output tail, so the note goes there too.
|
|
485
|
-
result = { ...result, stderr: withNote(result.stderr), ...(result.tail !== undefined ? { tail: withNote(result.tail) } : {}) };
|
|
486
|
-
}
|
|
487
|
-
if (pending.deadlineTimer)
|
|
488
|
-
clearTimeout(pending.deadlineTimer);
|
|
489
|
-
if (pending.killTimer)
|
|
490
|
-
clearTimeout(pending.killTimer);
|
|
491
|
-
if (pending.interruptTimer)
|
|
492
|
-
clearInterval(pending.interruptTimer);
|
|
493
|
-
this.pending = null;
|
|
494
|
-
pending.resolve(result);
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Answer the interpreter that asked. Call and wake ids restart in every
|
|
498
|
-
* interpreter, so an answer that outlives its interpreter (the script was
|
|
499
|
-
* aborted, killed or crashed, or the interpreter reclaimed) would resolve
|
|
500
|
-
* the same id in the one that replaced it, inside another script: drop it.
|
|
501
|
-
*/
|
|
502
|
-
reply(child, msg, what) {
|
|
503
|
-
if (child !== this.child) {
|
|
504
|
-
console.error(`[pytc:${this.label}] dropped late ${what}: the interpreter that asked for it is gone`);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
this.send(msg);
|
|
508
|
-
}
|
|
509
|
-
send(obj) {
|
|
510
|
-
const child = this.child;
|
|
511
|
-
if (!child || child.exitCode !== null || child.killed)
|
|
512
|
-
return;
|
|
513
|
-
try {
|
|
514
|
-
child.stdin.write(JSON.stringify(obj) + '\n');
|
|
515
|
-
}
|
|
516
|
-
catch (err) {
|
|
517
|
-
console.error(`[pytc:${this.label}] protocol write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* SIGINT the interpreter: blocking code in the running script raises
|
|
522
|
-
* KeyboardInterrupt. Not on Windows, where Node's kill() ends the process
|
|
523
|
-
* outright; there the cancel op and the kill grace remain.
|
|
524
|
-
* process.kill rather than child.kill, which would mark the child `killed`:
|
|
525
|
-
* a killed child gets no more protocol messages and is respawned on the
|
|
526
|
-
* next exec, losing the interpreter state this keeps.
|
|
527
|
-
*/
|
|
528
|
-
interruptChild() {
|
|
529
|
-
const child = this.child;
|
|
530
|
-
if (process.platform === 'win32' || !child || child.exitCode !== null || child.killed)
|
|
531
|
-
return;
|
|
532
|
-
if (child.pid === undefined)
|
|
533
|
-
return;
|
|
534
|
-
try {
|
|
535
|
-
process.kill(child.pid, 'SIGINT');
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
// already gone: the exit handler settles the exec
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
/** Kill interpreter + clean temp dir. State is lost (by design). */
|
|
542
|
-
reclaim(reason) {
|
|
543
|
-
if (this.child) {
|
|
544
|
-
console.error(`[pytc:${this.label}] reclaiming python interpreter (${reason})`);
|
|
545
|
-
}
|
|
546
|
-
// An interpreter killed as it starts never says it's ready: fail its
|
|
547
|
-
// readiness now, so an exec waiting on it returns at once rather than at
|
|
548
|
-
// the 15 s timeout. Here rather than in teardownChild, which an
|
|
549
|
-
// interpreter's own exit also runs, so that one still reads "exited as
|
|
550
|
-
// it started".
|
|
551
|
-
this.readyFailer?.(new Error(`python runtime reclaimed (${reason}) before it was ready`));
|
|
552
|
-
this.teardownChild();
|
|
553
|
-
this.clearIdleTimer();
|
|
554
|
-
}
|
|
555
|
-
teardownChild() {
|
|
556
|
-
if (this.reader) {
|
|
557
|
-
this.reader.close();
|
|
558
|
-
this.reader = null;
|
|
559
|
-
}
|
|
560
|
-
const child = this.child;
|
|
561
|
-
if (child) {
|
|
562
|
-
this.child = null;
|
|
563
|
-
this.childReady = null;
|
|
564
|
-
this.readyResolver = null;
|
|
565
|
-
this.readyFailer = null;
|
|
566
|
-
child.removeAllListeners('exit');
|
|
567
|
-
try {
|
|
568
|
-
child.stdin.end();
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
// stream may already be destroyed
|
|
572
|
-
}
|
|
573
|
-
// A child whose spawn failed has no pid, and its exitCode stays null
|
|
574
|
-
// until Node's 'error' for it, a tick later. A kill() in that tick, as
|
|
575
|
-
// from an abort or dispose on the tick exec was called, signals no
|
|
576
|
-
// child of ours but whatever pid Node's handle held: the host's own
|
|
577
|
-
// process group on Node 20.20.2, another process's pid on 24.19.
|
|
578
|
-
if (child.exitCode === null && !child.killed && child.pid !== undefined) {
|
|
579
|
-
child.kill('SIGKILL');
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
const dir = this.runtimeDir;
|
|
583
|
-
this.runtimeDir = null;
|
|
584
|
-
if (dir) {
|
|
585
|
-
const remove = () => {
|
|
586
|
-
try {
|
|
587
|
-
rmSync(dir, { recursive: true, force: true });
|
|
588
|
-
}
|
|
589
|
-
catch {
|
|
590
|
-
// temp cleanup is best-effort
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
// The directory is the interpreter's working directory, which Windows
|
|
594
|
-
// won't remove while the process lives, so there it goes once the
|
|
595
|
-
// process has. Elsewhere it goes now, so a host that exits right after
|
|
596
|
-
// this doesn't leave it behind. A child that never started has no exit
|
|
597
|
-
// to wait for.
|
|
598
|
-
const running = child !== null && child.pid !== undefined && child.exitCode === null && child.signalCode === null;
|
|
599
|
-
if (running && process.platform === 'win32')
|
|
600
|
-
child.once('exit', remove);
|
|
601
|
-
else
|
|
602
|
-
remove();
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
armIdleTimer() {
|
|
606
|
-
this.clearIdleTimer();
|
|
607
|
-
if (this.idleReclaimMs <= 0 || !this.child)
|
|
608
|
-
return;
|
|
609
|
-
this.idleTimer = setTimeout(() => {
|
|
610
|
-
if (!this.pending)
|
|
611
|
-
this.reclaim('idle');
|
|
612
|
-
}, this.idleReclaimMs);
|
|
613
|
-
// Do not hold the process open just to reclaim an idle interpreter.
|
|
614
|
-
this.idleTimer.unref?.();
|
|
615
|
-
}
|
|
616
|
-
clearIdleTimer() {
|
|
617
|
-
if (this.idleTimer) {
|
|
618
|
-
clearTimeout(this.idleTimer);
|
|
619
|
-
this.idleTimer = null;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Map framework tool names to python identifiers: `--` (the framework's
|
|
625
|
-
* prefix separator) becomes `__`, and every remaining character that is not
|
|
626
|
-
* valid in a python identifier becomes `_` (fleet reality: module and server
|
|
627
|
-
* ids contain single hyphens — `mcpl-admin`, `dog-events` — found live on
|
|
628
|
-
* the first canary run, 2026-07-26). A leading digit gets a `_` prefix.
|
|
629
|
-
*
|
|
630
|
-
* Names that sanitize to nothing, or that collide after sanitization, are
|
|
631
|
-
* skipped loudly — a skipped tool is unreachable from scripts (the
|
|
632
|
-
* exact-name tools[...] dict is built from this same list).
|
|
633
|
-
*/
|
|
634
|
-
export function buildInjectedTools(toolNames, log = (m) => console.error(m)) {
|
|
635
|
-
const seen = new Map();
|
|
636
|
-
const injected = [];
|
|
637
|
-
for (const toolName of toolNames) {
|
|
638
|
-
let pyName = toolName.replace(/--/g, '__').replace(/[^A-Za-z0-9_]/g, '_');
|
|
639
|
-
if (/^[0-9]/.test(pyName))
|
|
640
|
-
pyName = '_' + pyName;
|
|
641
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(pyName)) {
|
|
642
|
-
log(`[pytc] tool '${toolName}' skipped: cannot derive a python identifier`);
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
const existing = seen.get(pyName);
|
|
646
|
-
if (existing) {
|
|
647
|
-
log(`[pytc] tool '${toolName}' skipped: python name '${pyName}' collides with '${existing}'`);
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
seen.set(pyName, toolName);
|
|
651
|
-
injected.push({ pyName, toolName });
|
|
652
|
-
}
|
|
653
|
-
return injected;
|
|
654
|
-
}
|
|
655
|
-
//# sourceMappingURL=py-runner.js.map
|