@code-yeongyu/senpi 2026.7.11 → 2026.7.13
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/CHANGELOG.md +17 -0
- package/dist/core/agent-session.d.ts +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +56 -15
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +76 -25
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js.map +1 -1
- package/dist/core/resource-loader.d.ts +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +20 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/modes/interactive/compaction-queue-transfer.d.ts +22 -0
- package/dist/modes/interactive/compaction-queue-transfer.d.ts.map +1 -0
- package/dist/modes/interactive/compaction-queue-transfer.js +82 -0
- package/dist/modes/interactive/compaction-queue-transfer.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +83 -69
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@babel/helper-string-parser/LICENSE +22 -0
- package/node_modules/@babel/helper-string-parser/README.md +19 -0
- package/node_modules/@babel/helper-string-parser/lib/index.js +295 -0
- package/node_modules/@babel/helper-string-parser/lib/index.js.map +1 -0
- package/node_modules/@babel/helper-string-parser/package.json +31 -0
- package/node_modules/@babel/helper-validator-identifier/LICENSE +22 -0
- package/node_modules/@babel/helper-validator-identifier/README.md +19 -0
- package/node_modules/@babel/helper-validator-identifier/lib/identifier.js +70 -0
- package/node_modules/@babel/helper-validator-identifier/lib/identifier.js.map +1 -0
- package/node_modules/@babel/helper-validator-identifier/lib/index.js +57 -0
- package/node_modules/@babel/helper-validator-identifier/lib/index.js.map +1 -0
- package/node_modules/@babel/helper-validator-identifier/lib/keyword.js +35 -0
- package/node_modules/@babel/helper-validator-identifier/lib/keyword.js.map +1 -0
- package/node_modules/@babel/helper-validator-identifier/package.json +31 -0
- package/node_modules/@babel/parser/LICENSE +19 -0
- package/node_modules/@babel/parser/README.md +19 -0
- package/node_modules/@babel/parser/bin/babel-parser.js +15 -0
- package/node_modules/@babel/parser/lib/index.js +14599 -0
- package/node_modules/@babel/parser/lib/index.js.map +1 -0
- package/node_modules/@babel/parser/package.json +50 -0
- package/node_modules/@babel/parser/typings/babel-parser.d.ts +262 -0
- package/node_modules/@babel/types/LICENSE +22 -0
- package/node_modules/@babel/types/README.md +19 -0
- package/node_modules/@babel/types/lib/asserts/assertNode.js +16 -0
- package/node_modules/@babel/types/lib/asserts/assertNode.js.map +1 -0
- package/node_modules/@babel/types/lib/asserts/generated/index.js +1251 -0
- package/node_modules/@babel/types/lib/asserts/generated/index.js.map +1 -0
- package/node_modules/@babel/types/lib/ast-types/generated/index.js +3 -0
- package/node_modules/@babel/types/lib/ast-types/generated/index.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js +18 -0
- package/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js +31 -0
- package/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/generated/index.js +29 -0
- package/node_modules/@babel/types/lib/builders/generated/index.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/generated/lowercase.js +2902 -0
- package/node_modules/@babel/types/lib/builders/generated/lowercase.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/generated/uppercase.js +272 -0
- package/node_modules/@babel/types/lib/builders/generated/uppercase.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/productions.js +12 -0
- package/node_modules/@babel/types/lib/builders/productions.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/react/buildChildren.js +24 -0
- package/node_modules/@babel/types/lib/builders/react/buildChildren.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js +22 -0
- package/node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js.map +1 -0
- package/node_modules/@babel/types/lib/builders/validateNode.js +21 -0
- package/node_modules/@babel/types/lib/builders/validateNode.js.map +1 -0
- package/node_modules/@babel/types/lib/clone/clone.js +12 -0
- package/node_modules/@babel/types/lib/clone/clone.js.map +1 -0
- package/node_modules/@babel/types/lib/clone/cloneDeep.js +12 -0
- package/node_modules/@babel/types/lib/clone/cloneDeep.js.map +1 -0
- package/node_modules/@babel/types/lib/clone/cloneDeepWithoutLoc.js +12 -0
- package/node_modules/@babel/types/lib/clone/cloneDeepWithoutLoc.js.map +1 -0
- package/node_modules/@babel/types/lib/clone/cloneNode.js +107 -0
- package/node_modules/@babel/types/lib/clone/cloneNode.js.map +1 -0
- package/node_modules/@babel/types/lib/clone/cloneWithoutLoc.js +12 -0
- package/node_modules/@babel/types/lib/clone/cloneWithoutLoc.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/addComment.js +15 -0
- package/node_modules/@babel/types/lib/comments/addComment.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/addComments.js +22 -0
- package/node_modules/@babel/types/lib/comments/addComments.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/inheritInnerComments.js +12 -0
- package/node_modules/@babel/types/lib/comments/inheritInnerComments.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/inheritLeadingComments.js +12 -0
- package/node_modules/@babel/types/lib/comments/inheritLeadingComments.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/inheritTrailingComments.js +12 -0
- package/node_modules/@babel/types/lib/comments/inheritTrailingComments.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/inheritsComments.js +17 -0
- package/node_modules/@babel/types/lib/comments/inheritsComments.js.map +1 -0
- package/node_modules/@babel/types/lib/comments/removeComments.js +15 -0
- package/node_modules/@babel/types/lib/comments/removeComments.js.map +1 -0
- package/node_modules/@babel/types/lib/constants/generated/index.js +60 -0
- package/node_modules/@babel/types/lib/constants/generated/index.js.map +1 -0
- package/node_modules/@babel/types/lib/constants/index.js +31 -0
- package/node_modules/@babel/types/lib/constants/index.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/ensureBlock.js +14 -0
- package/node_modules/@babel/types/lib/converters/ensureBlock.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js +65 -0
- package/node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toBindingIdentifierName.js +14 -0
- package/node_modules/@babel/types/lib/converters/toBindingIdentifierName.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toBlock.js +29 -0
- package/node_modules/@babel/types/lib/converters/toBlock.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toComputedKey.js +14 -0
- package/node_modules/@babel/types/lib/converters/toComputedKey.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toExpression.js +28 -0
- package/node_modules/@babel/types/lib/converters/toExpression.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toIdentifier.js +25 -0
- package/node_modules/@babel/types/lib/converters/toIdentifier.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toKeyAlias.js +38 -0
- package/node_modules/@babel/types/lib/converters/toKeyAlias.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toSequenceExpression.js +19 -0
- package/node_modules/@babel/types/lib/converters/toSequenceExpression.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/toStatement.js +39 -0
- package/node_modules/@babel/types/lib/converters/toStatement.js.map +1 -0
- package/node_modules/@babel/types/lib/converters/valueToNode.js +89 -0
- package/node_modules/@babel/types/lib/converters/valueToNode.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/core.js +1725 -0
- package/node_modules/@babel/types/lib/definitions/core.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/deprecated-aliases.js +11 -0
- package/node_modules/@babel/types/lib/definitions/deprecated-aliases.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/experimental.js +124 -0
- package/node_modules/@babel/types/lib/definitions/experimental.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/flow.js +495 -0
- package/node_modules/@babel/types/lib/definitions/flow.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/index.js +106 -0
- package/node_modules/@babel/types/lib/definitions/index.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/jsx.js +157 -0
- package/node_modules/@babel/types/lib/definitions/jsx.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/misc.js +31 -0
- package/node_modules/@babel/types/lib/definitions/misc.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/placeholders.js +27 -0
- package/node_modules/@babel/types/lib/definitions/placeholders.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/typescript.js +528 -0
- package/node_modules/@babel/types/lib/definitions/typescript.js.map +1 -0
- package/node_modules/@babel/types/lib/definitions/utils.js +296 -0
- package/node_modules/@babel/types/lib/definitions/utils.js.map +1 -0
- package/node_modules/@babel/types/lib/index-legacy.d.ts +2797 -0
- package/node_modules/@babel/types/lib/index.d.ts +3617 -0
- package/node_modules/@babel/types/lib/index.js +582 -0
- package/node_modules/@babel/types/lib/index.js.flow +2650 -0
- package/node_modules/@babel/types/lib/index.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/appendToMemberExpression.js +15 -0
- package/node_modules/@babel/types/lib/modifications/appendToMemberExpression.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/flow/removeTypeDuplicates.js +65 -0
- package/node_modules/@babel/types/lib/modifications/flow/removeTypeDuplicates.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/inherits.js +28 -0
- package/node_modules/@babel/types/lib/modifications/inherits.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/prependToMemberExpression.js +17 -0
- package/node_modules/@babel/types/lib/modifications/prependToMemberExpression.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/removeProperties.js +24 -0
- package/node_modules/@babel/types/lib/modifications/removeProperties.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/removePropertiesDeep.js +14 -0
- package/node_modules/@babel/types/lib/modifications/removePropertiesDeep.js.map +1 -0
- package/node_modules/@babel/types/lib/modifications/typescript/removeTypeDuplicates.js +66 -0
- package/node_modules/@babel/types/lib/modifications/typescript/removeTypeDuplicates.js.map +1 -0
- package/node_modules/@babel/types/lib/retrievers/getAssignmentIdentifiers.js +48 -0
- package/node_modules/@babel/types/lib/retrievers/getAssignmentIdentifiers.js.map +1 -0
- package/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js +102 -0
- package/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js.map +1 -0
- package/node_modules/@babel/types/lib/retrievers/getFunctionName.js +63 -0
- package/node_modules/@babel/types/lib/retrievers/getFunctionName.js.map +1 -0
- package/node_modules/@babel/types/lib/retrievers/getOuterBindingIdentifiers.js +13 -0
- package/node_modules/@babel/types/lib/retrievers/getOuterBindingIdentifiers.js.map +1 -0
- package/node_modules/@babel/types/lib/traverse/traverse.js +50 -0
- package/node_modules/@babel/types/lib/traverse/traverse.js.map +1 -0
- package/node_modules/@babel/types/lib/traverse/traverseFast.js +40 -0
- package/node_modules/@babel/types/lib/traverse/traverseFast.js.map +1 -0
- package/node_modules/@babel/types/lib/utils/deprecationWarning.js +44 -0
- package/node_modules/@babel/types/lib/utils/deprecationWarning.js.map +1 -0
- package/node_modules/@babel/types/lib/utils/inherit.js +13 -0
- package/node_modules/@babel/types/lib/utils/inherit.js.map +1 -0
- package/node_modules/@babel/types/lib/utils/react/cleanJSXElementLiteralChild.js +40 -0
- package/node_modules/@babel/types/lib/utils/react/cleanJSXElementLiteralChild.js.map +1 -0
- package/node_modules/@babel/types/lib/utils/shallowEqual.js +17 -0
- package/node_modules/@babel/types/lib/utils/shallowEqual.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/buildMatchMemberExpression.js +13 -0
- package/node_modules/@babel/types/lib/validators/buildMatchMemberExpression.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/generated/index.js +2797 -0
- package/node_modules/@babel/types/lib/validators/generated/index.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/is.js +27 -0
- package/node_modules/@babel/types/lib/validators/is.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isBinding.js +27 -0
- package/node_modules/@babel/types/lib/validators/isBinding.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isBlockScoped.js +13 -0
- package/node_modules/@babel/types/lib/validators/isBlockScoped.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isImmutable.js +21 -0
- package/node_modules/@babel/types/lib/validators/isImmutable.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isLet.js +13 -0
- package/node_modules/@babel/types/lib/validators/isLet.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isNode.js +12 -0
- package/node_modules/@babel/types/lib/validators/isNode.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isNodesEquivalent.js +57 -0
- package/node_modules/@babel/types/lib/validators/isNodesEquivalent.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isPlaceholderType.js +15 -0
- package/node_modules/@babel/types/lib/validators/isPlaceholderType.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isReferenced.js +96 -0
- package/node_modules/@babel/types/lib/validators/isReferenced.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isScope.js +18 -0
- package/node_modules/@babel/types/lib/validators/isScope.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isSpecifierDefault.js +14 -0
- package/node_modules/@babel/types/lib/validators/isSpecifierDefault.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isType.js +17 -0
- package/node_modules/@babel/types/lib/validators/isType.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isValidES3Identifier.js +13 -0
- package/node_modules/@babel/types/lib/validators/isValidES3Identifier.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isValidIdentifier.js +18 -0
- package/node_modules/@babel/types/lib/validators/isValidIdentifier.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/isVar.js +15 -0
- package/node_modules/@babel/types/lib/validators/isVar.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/matchesPattern.js +44 -0
- package/node_modules/@babel/types/lib/validators/matchesPattern.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/react/isCompatTag.js +11 -0
- package/node_modules/@babel/types/lib/validators/react/isCompatTag.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/react/isReactComponent.js +11 -0
- package/node_modules/@babel/types/lib/validators/react/isReactComponent.js.map +1 -0
- package/node_modules/@babel/types/lib/validators/validate.js +42 -0
- package/node_modules/@babel/types/lib/validators/validate.js.map +1 -0
- package/node_modules/@babel/types/package.json +39 -0
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +10 -2
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +128 -78
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +15 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/reserved.ts +9 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/agent-bridge.ts +197 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/output-bridge.ts +96 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/schema-injection.ts +3 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/completion/handler.ts +95 -26
- package/node_modules/@code-yeongyu/senpi-codemode/src/completion/tool-bridge.ts +11 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/config/settings.ts +96 -8
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +24 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +155 -21
- package/node_modules/@code-yeongyu/senpi-codemode/src/interpreters/detect.ts +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/prelude.jl +252 -48
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/runner.jl +263 -83
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +11 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/local-module-loader.ts +108 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/prelude.ts +8 -6
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/rewrite-imports.ts +164 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +6 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +307 -41
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +3 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +792 -79
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/prelude.rb +234 -21
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/runner.rb +154 -51
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/output/output-meta.ts +89 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/output/streaming-output.ts +296 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +107 -30
- package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/bridge-timeout.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/idle-timeout.ts +8 -19
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-handler.ts +175 -113
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +130 -120
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/image.ts +274 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/json-tree.ts +247 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +641 -38
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/status-events.ts +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +29 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +16 -16
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +6 -6
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +6 -6
|
@@ -1,33 +1,75 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
# noqa: SIZE_OK — this dependency-free subprocess prelude must ship as one file.
|
|
3
4
|
import ast
|
|
4
|
-
import asyncio
|
|
5
|
+
import asyncio # noqa: ANYIO_OK — stdlib-only embedded kernel runner.
|
|
5
6
|
import base64
|
|
7
|
+
import codecs
|
|
6
8
|
import contextlib
|
|
7
|
-
import io
|
|
8
9
|
import inspect
|
|
10
|
+
import io
|
|
9
11
|
import json
|
|
12
|
+
import locale
|
|
10
13
|
import os
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
11
16
|
import sys
|
|
12
17
|
import time
|
|
13
18
|
import traceback
|
|
14
19
|
import urllib.error
|
|
15
20
|
import urllib.request
|
|
16
21
|
import uuid
|
|
17
|
-
from
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
18
24
|
from pathlib import Path
|
|
19
|
-
from
|
|
25
|
+
from threading import Lock
|
|
26
|
+
from typing import Any, Callable
|
|
27
|
+
from urllib.parse import unquote
|
|
20
28
|
|
|
21
29
|
SESSION_ID = ""
|
|
22
30
|
CONNECTION: dict[str, Any] = {}
|
|
23
31
|
USER_NS: dict[str, Any] = {"__name__": "__main__", "__doc__": None, "__builtins__": __builtins__}
|
|
24
32
|
LOOP = asyncio.new_event_loop()
|
|
25
33
|
asyncio.set_event_loop(LOOP)
|
|
34
|
+
EMIT_LOCK = Lock()
|
|
35
|
+
|
|
36
|
+
# Mirrors src/bridge/reserved.ts; this standalone subprocess asset cannot import TypeScript.
|
|
37
|
+
RESERVED_AGENT_TOOL = "__agent__"
|
|
38
|
+
RESERVED_OUTPUT_TOOL = "__output__"
|
|
39
|
+
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
40
|
+
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PreludeRuntimeError(RuntimeError):
|
|
44
|
+
"""Host bridge or magic execution failed."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PreludeValueError(ValueError):
|
|
48
|
+
"""A helper received an invalid value."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PreludeTypeError(TypeError):
|
|
52
|
+
"""A helper received an invalid value type."""
|
|
53
|
+
|
|
54
|
+
_INTERNAL_URL_RE = re.compile(r"^([a-z][a-z0-9+.-]*)://(.*)$", re.IGNORECASE)
|
|
55
|
+
_ASSIGN_LINE_RE = re.compile(
|
|
56
|
+
r"^(?P<indent>[ \t]*)(?P<lhs>[A-Za-z_][A-Za-z_0-9.\[\], ]*?)\s*=\s*(?P<rhs>.+)$"
|
|
57
|
+
)
|
|
58
|
+
_SHELL_READ_CHUNK_BYTES = 8192
|
|
59
|
+
_SHELL_CAPTURE_MAX_BYTES = 1024 * 1024
|
|
60
|
+
_SHELL_CAPTURE_MAX_LINES = 3000
|
|
61
|
+
_SHELL_TRUNCATION_NOTICE = (
|
|
62
|
+
f"[output truncated: shell helper exceeded {_SHELL_CAPTURE_MAX_BYTES} bytes "
|
|
63
|
+
f"or {_SHELL_CAPTURE_MAX_LINES} lines; remaining output discarded]\n"
|
|
64
|
+
)
|
|
65
|
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
|
26
66
|
|
|
27
67
|
|
|
28
68
|
def emit(frame: dict[str, Any]) -> None:
|
|
29
|
-
|
|
30
|
-
|
|
69
|
+
encoded = json.dumps(frame, ensure_ascii=False, default=repr) + "\n"
|
|
70
|
+
with EMIT_LOCK:
|
|
71
|
+
sys.__stdout__.write(encoded)
|
|
72
|
+
sys.__stdout__.flush()
|
|
31
73
|
|
|
32
74
|
|
|
33
75
|
def bridge_error(exc: BaseException) -> dict[str, str]:
|
|
@@ -43,19 +85,126 @@ def text(stream: str, data: str) -> None:
|
|
|
43
85
|
emit({"type": "text", "stream": stream, "data": data})
|
|
44
86
|
|
|
45
87
|
|
|
88
|
+
def b64_text(value: str) -> str:
|
|
89
|
+
return base64.b64encode(value.encode("utf-8")).decode("ascii")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _emit_display(mime_type: str, data: Any) -> None:
|
|
93
|
+
if isinstance(data, (bytes, bytearray)):
|
|
94
|
+
encoded = base64.b64encode(bytes(data)).decode("ascii")
|
|
95
|
+
elif mime_type.startswith("image/"):
|
|
96
|
+
if isinstance(data, str):
|
|
97
|
+
encoded = data
|
|
98
|
+
else:
|
|
99
|
+
encoded = base64.b64encode(repr(data).encode("utf-8")).decode("ascii")
|
|
100
|
+
elif mime_type == "application/json":
|
|
101
|
+
encoded = b64_text(json.dumps(data, ensure_ascii=False, default=repr))
|
|
102
|
+
else:
|
|
103
|
+
encoded = b64_text(str(data))
|
|
104
|
+
emit({"type": "display", "mimeType": mime_type, "dataBase64": encoded})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _display_bundle(bundle: dict[str, Any]) -> bool:
|
|
108
|
+
for mime_type in (
|
|
109
|
+
"image/png",
|
|
110
|
+
"image/jpeg",
|
|
111
|
+
"application/json",
|
|
112
|
+
"text/markdown",
|
|
113
|
+
"text/html",
|
|
114
|
+
"image/svg+xml",
|
|
115
|
+
"text/latex",
|
|
116
|
+
"text/plain",
|
|
117
|
+
):
|
|
118
|
+
if mime_type in bundle:
|
|
119
|
+
_emit_display(mime_type, bundle[mime_type])
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_matplotlib_figure(value: Any) -> bool:
|
|
125
|
+
figure_module = sys.modules.get("matplotlib.figure")
|
|
126
|
+
figure_class = getattr(figure_module, "Figure", None)
|
|
127
|
+
if isinstance(figure_class, type) and isinstance(value, figure_class):
|
|
128
|
+
return True
|
|
129
|
+
value_type = type(value)
|
|
130
|
+
return value_type.__module__ == "matplotlib.figure" and value_type.__name__ == "Figure"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _matplotlib_png(value: Any) -> bytes | None:
|
|
134
|
+
if not _is_matplotlib_figure(value):
|
|
135
|
+
return None
|
|
136
|
+
savefig = getattr(value, "savefig", None)
|
|
137
|
+
if not callable(savefig):
|
|
138
|
+
return None
|
|
139
|
+
try:
|
|
140
|
+
buffer = io.BytesIO()
|
|
141
|
+
savefig(buffer, format="png", bbox_inches="tight")
|
|
142
|
+
return buffer.getvalue()
|
|
143
|
+
except Exception: # noqa: BROAD_EXCEPT_OK — user-defined rendering hooks are isolated fallbacks.
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _rich_bundle(value: Any) -> dict[str, Any]:
|
|
148
|
+
bundle: dict[str, Any] = {}
|
|
149
|
+
mime_bundle = getattr(value, "_repr_mimebundle_", None)
|
|
150
|
+
if callable(mime_bundle):
|
|
151
|
+
try:
|
|
152
|
+
data = mime_bundle()
|
|
153
|
+
if isinstance(data, tuple):
|
|
154
|
+
data = data[0]
|
|
155
|
+
if isinstance(data, dict):
|
|
156
|
+
bundle.update({str(key): item for key, item in data.items()})
|
|
157
|
+
except Exception: # noqa: BROAD_EXCEPT_OK — a broken repr must fall through to the next representation.
|
|
158
|
+
bundle.clear()
|
|
159
|
+
|
|
160
|
+
for attribute, mime_type in (
|
|
161
|
+
("_repr_markdown_", "text/markdown"),
|
|
162
|
+
("_repr_png_", "image/png"),
|
|
163
|
+
("_repr_jpeg_", "image/jpeg"),
|
|
164
|
+
("_repr_html_", "text/html"),
|
|
165
|
+
("_repr_json_", "application/json"),
|
|
166
|
+
("_repr_svg_", "image/svg+xml"),
|
|
167
|
+
("_repr_latex_", "text/latex"),
|
|
168
|
+
):
|
|
169
|
+
if mime_type in bundle:
|
|
170
|
+
continue
|
|
171
|
+
representation = getattr(value, attribute, None)
|
|
172
|
+
if not callable(representation):
|
|
173
|
+
continue
|
|
174
|
+
try:
|
|
175
|
+
data = representation()
|
|
176
|
+
except Exception: # noqa: BROAD_EXCEPT_OK — a broken repr must fall through to the next representation.
|
|
177
|
+
continue
|
|
178
|
+
if data is not None:
|
|
179
|
+
bundle[mime_type] = data
|
|
180
|
+
|
|
181
|
+
if "image/png" not in bundle:
|
|
182
|
+
figure_png = _matplotlib_png(value)
|
|
183
|
+
if figure_png is not None:
|
|
184
|
+
bundle["image/png"] = figure_png
|
|
185
|
+
return bundle
|
|
186
|
+
|
|
187
|
+
|
|
46
188
|
def display(value: Any) -> None:
|
|
47
189
|
if isinstance(value, (dict, list, tuple)):
|
|
48
|
-
|
|
49
|
-
emit({"type": "display", "mimeType": "application/json", "dataBase64": b64(payload)})
|
|
190
|
+
_emit_display("application/json", value)
|
|
50
191
|
return
|
|
51
192
|
if isinstance(value, (bytes, bytearray)):
|
|
52
|
-
|
|
193
|
+
_emit_display("application/octet-stream", bytes(value))
|
|
53
194
|
return
|
|
54
|
-
|
|
195
|
+
bundle = _rich_bundle(value)
|
|
196
|
+
if bundle and _display_bundle(bundle):
|
|
197
|
+
return
|
|
198
|
+
_emit_display("text/plain", str(value))
|
|
55
199
|
|
|
56
200
|
|
|
57
|
-
def
|
|
58
|
-
return
|
|
201
|
+
def _status_events_enabled() -> bool:
|
|
202
|
+
return CONNECTION.get("statusEvents", True) is not False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def emit_status(op: str, *, force: bool = False, **data: Any) -> None:
|
|
206
|
+
if force or _status_events_enabled():
|
|
207
|
+
emit({"type": "status", "event": {"op": op, **data}})
|
|
59
208
|
|
|
60
209
|
|
|
61
210
|
def log(message: Any) -> None:
|
|
@@ -68,106 +217,657 @@ def phase(title: Any) -> None:
|
|
|
68
217
|
|
|
69
218
|
def env(key: str | None = None, value: str | None = None) -> Any:
|
|
70
219
|
if key is None:
|
|
71
|
-
|
|
220
|
+
items = dict(sorted(os.environ.items()))
|
|
221
|
+
emit_status("env", count=len(items), keys=list(items.keys())[:20])
|
|
222
|
+
return items
|
|
72
223
|
if value is not None:
|
|
73
224
|
os.environ[key] = value
|
|
225
|
+
emit_status("env", key=key, value=value, action="set")
|
|
74
226
|
return value
|
|
75
|
-
|
|
227
|
+
resolved = os.environ.get(key)
|
|
228
|
+
emit_status("env", key=key, value=resolved, action="get")
|
|
229
|
+
return resolved
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _resolve_helper_path(path: str | Path) -> Path:
|
|
233
|
+
if not isinstance(path, str):
|
|
234
|
+
return Path(path)
|
|
235
|
+
match = _INTERNAL_URL_RE.match(path)
|
|
236
|
+
if not match:
|
|
237
|
+
return Path(path)
|
|
238
|
+
scheme = match.group(1).lower()
|
|
239
|
+
roots = CONNECTION.get("localRoots")
|
|
240
|
+
root = roots.get(scheme) if isinstance(roots, dict) else None
|
|
241
|
+
if not isinstance(root, str) or not root:
|
|
242
|
+
raise PreludeValueError(f"Protocol paths are not supported by this helper: {path}")
|
|
243
|
+
relative = unquote(match.group(2).replace("\\", "/"))
|
|
244
|
+
root_path = os.path.abspath(root)
|
|
245
|
+
if relative == "":
|
|
246
|
+
return Path(root_path)
|
|
247
|
+
relative_path = Path(relative)
|
|
248
|
+
if relative_path.is_absolute() or ".." in relative_path.parts:
|
|
249
|
+
raise PreludeValueError(f"Unsafe {scheme}:// path (absolute or traversal): {path}")
|
|
250
|
+
resolved = os.path.abspath(os.path.join(root_path, relative))
|
|
251
|
+
if resolved != root_path and not resolved.startswith(root_path + os.sep):
|
|
252
|
+
raise PreludeValueError(f"{scheme}:// path escapes its root: {path}")
|
|
253
|
+
return Path(resolved)
|
|
76
254
|
|
|
77
255
|
|
|
78
256
|
def read(path: str | Path, offset: int = 1, limit: int | None = None) -> str:
|
|
79
|
-
|
|
257
|
+
target = _resolve_helper_path(path)
|
|
258
|
+
data = target.read_text(encoding="utf-8")
|
|
80
259
|
if offset > 1 or limit is not None:
|
|
81
260
|
lines = data.splitlines(keepends=True)
|
|
82
261
|
start = max(0, offset - 1)
|
|
83
262
|
end = start + limit if limit is not None else len(lines)
|
|
84
|
-
|
|
263
|
+
data = "".join(lines[start:end])
|
|
264
|
+
emit_status("read", path=str(target), chars=len(data), preview=data[:500])
|
|
85
265
|
return data
|
|
86
266
|
|
|
87
267
|
|
|
88
|
-
def write(path: str | Path, content: str) ->
|
|
89
|
-
target =
|
|
268
|
+
def write(path: str | Path, content: str) -> Path:
|
|
269
|
+
target = _resolve_helper_path(path)
|
|
90
270
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
91
271
|
target.write_text(content, encoding="utf-8")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def parallel(callables: list[Any], width: int = 4) -> list[Any]:
|
|
96
|
-
with ThreadPoolExecutor(max_workers=max(1, width)) as pool:
|
|
97
|
-
return list(pool.map(lambda fn: fn(), callables))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def pipeline(items: list[Any], *stages: Any) -> list[Any]:
|
|
101
|
-
values = items
|
|
102
|
-
for stage in stages:
|
|
103
|
-
values = parallel([lambda item=item: stage(item) for item in values])
|
|
104
|
-
return values
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class ToolProxy:
|
|
108
|
-
def __getattr__(self, name: str) -> Any:
|
|
109
|
-
def call(args: Any = None) -> Any:
|
|
110
|
-
return bridge_post("/call", {"callId": f"py-{uuid.uuid4()}", "toolName": name, "args": {} if args is None else args})
|
|
111
|
-
return call
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def completion(prompt: str, **options: Any) -> Any:
|
|
115
|
-
return bridge_post("/completion", {"prompt": prompt, "opts": options})
|
|
272
|
+
emit_status("write", path=str(target), chars=len(content))
|
|
273
|
+
return target
|
|
116
274
|
|
|
117
275
|
|
|
118
276
|
def bridge_post(path: str, payload: dict[str, Any]) -> Any:
|
|
119
277
|
port = CONNECTION.get("port")
|
|
120
278
|
token = CONNECTION.get("token")
|
|
121
279
|
if not isinstance(port, int) or not isinstance(token, str):
|
|
122
|
-
raise
|
|
123
|
-
|
|
124
|
-
|
|
280
|
+
raise PreludeRuntimeError("Python tool bridge is not initialized")
|
|
281
|
+
request_data = json.dumps(payload, ensure_ascii=False, default=repr).encode("utf-8")
|
|
282
|
+
request = urllib.request.Request(
|
|
125
283
|
f"http://127.0.0.1:{port}{path}",
|
|
126
|
-
data=
|
|
284
|
+
data=request_data,
|
|
127
285
|
headers={"authorization": f"Bearer {token}", "content-type": "application/json"},
|
|
128
286
|
method="POST",
|
|
129
287
|
)
|
|
288
|
+
emit_status(TIMEOUT_PAUSE_OP, force=True)
|
|
130
289
|
try:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
290
|
+
try:
|
|
291
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
292
|
+
response_data = response.read()
|
|
293
|
+
except urllib.error.HTTPError as exc:
|
|
294
|
+
response_data = exc.read()
|
|
295
|
+
finally:
|
|
296
|
+
emit_status(TIMEOUT_RESUME_OP, force=True)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
body = json.loads(response_data.decode("utf-8"))
|
|
300
|
+
except json.JSONDecodeError as exc:
|
|
301
|
+
raise PreludeRuntimeError(f"Bridge returned invalid JSON: {response_data[:200]!r}") from exc
|
|
135
302
|
if isinstance(body, dict) and body.get("ok") is True:
|
|
136
303
|
return body.get("value")
|
|
137
304
|
error = body.get("error") if isinstance(body, dict) else body
|
|
138
305
|
if isinstance(error, dict):
|
|
139
|
-
raise
|
|
140
|
-
raise
|
|
306
|
+
raise PreludeRuntimeError(str(error.get("message", error)))
|
|
307
|
+
raise PreludeRuntimeError(str(error))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ToolCallable:
|
|
311
|
+
__slots__ = ("_name",)
|
|
312
|
+
|
|
313
|
+
def __init__(self, name: str) -> None:
|
|
314
|
+
self._name = name
|
|
315
|
+
|
|
316
|
+
def __repr__(self) -> str:
|
|
317
|
+
return f"<tool.{self._name}>"
|
|
318
|
+
|
|
319
|
+
def __call__(self, args: Any = None, /, **kwargs: Any) -> Any:
|
|
320
|
+
if args is None:
|
|
321
|
+
merged: dict[str, Any] = {}
|
|
322
|
+
elif isinstance(args, dict):
|
|
323
|
+
merged = dict(args)
|
|
324
|
+
else:
|
|
325
|
+
raise PreludeTypeError(
|
|
326
|
+
f"tool.{self._name}(...) expects a dict of arguments (got {type(args).__name__})"
|
|
327
|
+
)
|
|
328
|
+
merged.update(kwargs)
|
|
329
|
+
merged.setdefault("i", "py prelude")
|
|
330
|
+
return bridge_post(
|
|
331
|
+
"/call",
|
|
332
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": self._name, "args": merged},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ToolProxy:
|
|
337
|
+
__slots__ = ()
|
|
338
|
+
|
|
339
|
+
def __getattr__(self, name: str) -> ToolCallable:
|
|
340
|
+
if name.startswith("_"):
|
|
341
|
+
raise AttributeError(name)
|
|
342
|
+
return ToolCallable(name)
|
|
343
|
+
|
|
344
|
+
def __getitem__(self, name: str) -> ToolCallable:
|
|
345
|
+
return ToolCallable(name)
|
|
346
|
+
|
|
347
|
+
def __repr__(self) -> str:
|
|
348
|
+
return "<tool proxy>"
|
|
141
349
|
|
|
142
350
|
|
|
143
351
|
tool = ToolProxy()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"
|
|
155
|
-
|
|
156
|
-
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def completion(
|
|
355
|
+
prompt: str,
|
|
356
|
+
model: str = "default",
|
|
357
|
+
system: str | None = None,
|
|
358
|
+
schema: dict[str, Any] | None = None,
|
|
359
|
+
**kwargs: Any,
|
|
360
|
+
) -> Any:
|
|
361
|
+
options: dict[str, Any] = {}
|
|
362
|
+
if model != "default":
|
|
363
|
+
options["model"] = model
|
|
364
|
+
options.update(kwargs)
|
|
365
|
+
if system is not None:
|
|
366
|
+
options["system"] = system
|
|
367
|
+
if schema is not None:
|
|
368
|
+
options["schema"] = schema
|
|
369
|
+
response = bridge_post("/completion", {"prompt": prompt, "opts": options})
|
|
370
|
+
if not isinstance(response, dict):
|
|
371
|
+
return response
|
|
372
|
+
if "value" in response:
|
|
373
|
+
return response["value"]
|
|
374
|
+
return response.get("text", response)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def output(
|
|
378
|
+
*ids: str,
|
|
379
|
+
format: str = "raw",
|
|
380
|
+
offset: int | None = None,
|
|
381
|
+
limit: int | None = None,
|
|
382
|
+
) -> Any:
|
|
383
|
+
if not ids:
|
|
384
|
+
raise PreludeValueError("At least one output ID is required")
|
|
385
|
+
if format not in ("raw", "tail"):
|
|
386
|
+
raise PreludeValueError("output() format must be 'raw' or 'tail'")
|
|
387
|
+
args: dict[str, Any] = {"ids": list(ids), "format": format}
|
|
388
|
+
if offset is not None:
|
|
389
|
+
args["offset"] = offset
|
|
390
|
+
if limit is not None:
|
|
391
|
+
args["limit"] = limit
|
|
392
|
+
return bridge_post(
|
|
393
|
+
"/call",
|
|
394
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_OUTPUT_TOOL, "args": args},
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def agent(
|
|
399
|
+
prompt: str,
|
|
400
|
+
*,
|
|
401
|
+
agent: str | None = "task",
|
|
402
|
+
model: str | None = None,
|
|
403
|
+
label: str | None = None,
|
|
404
|
+
schema: dict[str, Any] | None = None,
|
|
405
|
+
isolated: bool | None = None,
|
|
406
|
+
apply: bool | None = None,
|
|
407
|
+
merge: bool | None = None,
|
|
408
|
+
handle: bool = False,
|
|
409
|
+
) -> Any:
|
|
410
|
+
args: dict[str, Any] = {"prompt": prompt}
|
|
411
|
+
if agent is not None:
|
|
412
|
+
args["agent"] = agent
|
|
413
|
+
if model is not None:
|
|
414
|
+
args["model"] = model
|
|
415
|
+
if label is not None:
|
|
416
|
+
args["label"] = label
|
|
417
|
+
if schema is not None:
|
|
418
|
+
args["schema"] = schema
|
|
419
|
+
if isolated is not None:
|
|
420
|
+
args["isolated"] = bool(isolated)
|
|
421
|
+
if apply is not None:
|
|
422
|
+
args["apply"] = bool(apply)
|
|
423
|
+
if merge is not None:
|
|
424
|
+
args["merge"] = bool(merge)
|
|
425
|
+
if handle:
|
|
426
|
+
args["handle"] = True
|
|
427
|
+
|
|
428
|
+
response = bridge_post(
|
|
429
|
+
"/call",
|
|
430
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_AGENT_TOOL, "args": args},
|
|
431
|
+
)
|
|
432
|
+
response_record = response if isinstance(response, dict) else {}
|
|
433
|
+
text_value = response_record.get("text", response)
|
|
434
|
+
parsed = response_record.get("data")
|
|
435
|
+
if schema is not None and "data" not in response_record:
|
|
436
|
+
parsed = json.loads(str(text_value))
|
|
437
|
+
elif schema is None:
|
|
438
|
+
parsed = text_value
|
|
439
|
+
if not handle:
|
|
440
|
+
return parsed
|
|
441
|
+
|
|
442
|
+
agent_id = response_record.get("id")
|
|
443
|
+
handle_value = response_record.get("handle")
|
|
444
|
+
if handle_value is None and agent_id is not None:
|
|
445
|
+
handle_value = f"agent://{agent_id}"
|
|
446
|
+
node: dict[str, Any] = {
|
|
447
|
+
"text": text_value,
|
|
448
|
+
"output": text_value,
|
|
449
|
+
"handle": handle_value,
|
|
450
|
+
"id": agent_id,
|
|
451
|
+
"agent": response_record.get("agent", agent),
|
|
452
|
+
}
|
|
453
|
+
if schema is not None:
|
|
454
|
+
node["data"] = parsed
|
|
455
|
+
for key in (
|
|
456
|
+
"isolated",
|
|
457
|
+
"patch_path",
|
|
458
|
+
"branch_name",
|
|
459
|
+
"nested_patches",
|
|
460
|
+
"changes_applied",
|
|
461
|
+
"isolation_summary",
|
|
462
|
+
):
|
|
463
|
+
if key in response_record:
|
|
464
|
+
node[key] = response_record[key]
|
|
465
|
+
return node
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _pool_map(items: Iterable[Any], function: Callable[[Any], Any]) -> list[Any]:
|
|
469
|
+
values = list(items)
|
|
470
|
+
if not values:
|
|
471
|
+
return []
|
|
472
|
+
configured_width = CONNECTION.get("parallelPoolWidth", 4)
|
|
473
|
+
width = (
|
|
474
|
+
int(configured_width)
|
|
475
|
+
if isinstance(configured_width, (int, float)) and not isinstance(configured_width, bool)
|
|
476
|
+
else 4
|
|
477
|
+
)
|
|
478
|
+
workers = min(max(1, width), len(values))
|
|
479
|
+
results: list[Any] = [None] * len(values)
|
|
480
|
+
errors: dict[int, BaseException] = {}
|
|
481
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
482
|
+
futures = {pool.submit(function, value): index for index, value in enumerate(values)}
|
|
483
|
+
for future in as_completed(futures):
|
|
484
|
+
index = futures[future]
|
|
485
|
+
try:
|
|
486
|
+
results[index] = future.result()
|
|
487
|
+
except BaseException as exc: # noqa: BROAD_EXCEPT_OK — preserve user thunk failures for deterministic re-raise.
|
|
488
|
+
errors[index] = exc
|
|
489
|
+
if errors:
|
|
490
|
+
raise errors[min(errors)]
|
|
491
|
+
return results
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def parallel(callables: Iterable[Callable[[], Any]]) -> list[Any]:
|
|
495
|
+
thunks = list(callables)
|
|
496
|
+
for thunk in thunks:
|
|
497
|
+
if not callable(thunk):
|
|
498
|
+
raise PreludeTypeError("parallel() expects an iterable of zero-arg callables")
|
|
499
|
+
return _pool_map(thunks, lambda thunk: thunk())
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def pipeline(items: Iterable[Any], *stages: Callable[[Any], Any]) -> list[Any]:
|
|
503
|
+
values = list(items)
|
|
504
|
+
for stage in stages:
|
|
505
|
+
if not callable(stage):
|
|
506
|
+
raise PreludeTypeError("pipeline() stages must be callables")
|
|
507
|
+
values = _pool_map(values, stage)
|
|
508
|
+
return values
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _fold_continuations(lines: list[str], start: int) -> tuple[str, int]:
|
|
512
|
+
parts: list[str] = []
|
|
513
|
+
index = start
|
|
514
|
+
while index < len(lines):
|
|
515
|
+
line = lines[index]
|
|
516
|
+
if line.endswith("\\"):
|
|
517
|
+
parts.append(line[:-1])
|
|
518
|
+
index += 1
|
|
519
|
+
continue
|
|
520
|
+
parts.append(line)
|
|
521
|
+
index += 1
|
|
522
|
+
break
|
|
523
|
+
return "".join(parts), index - start
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _quote_arg(text_value: str) -> str:
|
|
527
|
+
return json.dumps(text_value, ensure_ascii=False)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _split_magic_head(text_value: str) -> tuple[str, str]:
|
|
531
|
+
stripped = text_value.lstrip()
|
|
532
|
+
if not stripped:
|
|
533
|
+
return "", ""
|
|
534
|
+
match = re.match(r"([A-Za-z_][A-Za-z_0-9]*)(?:\s+(.*))?$", stripped)
|
|
535
|
+
if not match:
|
|
536
|
+
return "", stripped
|
|
537
|
+
return match.group(1), (match.group(2) or "").rstrip()
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _is_escaped(text_value: str, index: int) -> bool:
|
|
541
|
+
backslashes = 0
|
|
542
|
+
cursor = index - 1
|
|
543
|
+
while cursor >= 0 and text_value[cursor] == "\\":
|
|
544
|
+
backslashes += 1
|
|
545
|
+
cursor -= 1
|
|
546
|
+
return backslashes % 2 == 1
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _advance_triple_quote_state(line: str, active_quote: str | None) -> str | None:
|
|
550
|
+
index = 0
|
|
551
|
+
quote = active_quote
|
|
552
|
+
while index < len(line):
|
|
553
|
+
if quote is not None:
|
|
554
|
+
closing = line.find(quote, index)
|
|
555
|
+
if closing < 0:
|
|
556
|
+
return quote
|
|
557
|
+
if _is_escaped(line, closing):
|
|
558
|
+
index = closing + 1
|
|
559
|
+
continue
|
|
560
|
+
quote = None
|
|
561
|
+
index = closing + 3
|
|
562
|
+
continue
|
|
563
|
+
|
|
564
|
+
character = line[index]
|
|
565
|
+
if character == "#":
|
|
566
|
+
return None
|
|
567
|
+
if character not in ("'", '"'):
|
|
568
|
+
index += 1
|
|
569
|
+
continue
|
|
570
|
+
triple = character * 3
|
|
571
|
+
if line.startswith(triple, index):
|
|
572
|
+
quote = triple
|
|
573
|
+
index += 3
|
|
574
|
+
continue
|
|
575
|
+
index += 1
|
|
576
|
+
while index < len(line):
|
|
577
|
+
if line[index] == character and not _is_escaped(line, index):
|
|
578
|
+
index += 1
|
|
579
|
+
break
|
|
580
|
+
index += 1
|
|
581
|
+
return quote
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def transform_cell(source: str) -> str:
|
|
585
|
+
if "%" not in source and "!" not in source:
|
|
586
|
+
return source
|
|
587
|
+
|
|
588
|
+
lines = source.splitlines()
|
|
589
|
+
transformed: list[str] = []
|
|
590
|
+
index = 0
|
|
591
|
+
triple_quote: str | None = None
|
|
592
|
+
while index < len(lines):
|
|
593
|
+
line = lines[index]
|
|
594
|
+
protected = triple_quote is not None
|
|
595
|
+
stripped = line.lstrip()
|
|
596
|
+
indent = line[: len(line) - len(stripped)]
|
|
597
|
+
|
|
598
|
+
if not protected and stripped.startswith("%%"):
|
|
599
|
+
name, args = _split_magic_head(stripped[2:])
|
|
600
|
+
body = "\n".join(lines[index + 1 :])
|
|
601
|
+
transformed.append(
|
|
602
|
+
f"{indent}__senpi_magic_cell({_quote_arg(name)}, {_quote_arg(args)}, {_quote_arg(body)})"
|
|
603
|
+
)
|
|
604
|
+
return "\n".join(transformed)
|
|
605
|
+
|
|
606
|
+
if not protected and stripped.startswith("%"):
|
|
607
|
+
folded, consumed = _fold_continuations(lines, index)
|
|
608
|
+
folded_stripped = folded.lstrip()
|
|
609
|
+
folded_indent = folded[: len(folded) - len(folded_stripped)]
|
|
610
|
+
name, args = _split_magic_head(folded_stripped[1:])
|
|
611
|
+
transformed.append(f"{folded_indent}__senpi_magic({_quote_arg(name)}, {_quote_arg(args)})")
|
|
612
|
+
index += consumed
|
|
613
|
+
continue
|
|
614
|
+
|
|
615
|
+
if not protected and stripped.startswith("!"):
|
|
616
|
+
folded, consumed = _fold_continuations(lines, index)
|
|
617
|
+
folded_stripped = folded.lstrip()
|
|
618
|
+
folded_indent = folded[: len(folded) - len(folded_stripped)]
|
|
619
|
+
command = folded_stripped[1:].strip()
|
|
620
|
+
transformed.append(f"{folded_indent}__senpi_shell({_quote_arg(command)})")
|
|
621
|
+
index += consumed
|
|
622
|
+
continue
|
|
623
|
+
|
|
624
|
+
if not protected:
|
|
625
|
+
assignment = _ASSIGN_LINE_RE.match(line)
|
|
626
|
+
if assignment:
|
|
627
|
+
right_hand_side = assignment.group("rhs").strip()
|
|
628
|
+
if right_hand_side.startswith("!"):
|
|
629
|
+
command = right_hand_side[1:].strip()
|
|
630
|
+
transformed.append(
|
|
631
|
+
f"{assignment.group('indent')}{assignment.group('lhs').rstrip()} = "
|
|
632
|
+
f"__senpi_shell({_quote_arg(command)})"
|
|
633
|
+
)
|
|
634
|
+
index += 1
|
|
635
|
+
continue
|
|
636
|
+
if right_hand_side.startswith("%") and not right_hand_side.startswith("%%"):
|
|
637
|
+
name, args = _split_magic_head(right_hand_side[1:])
|
|
638
|
+
transformed.append(
|
|
639
|
+
f"{assignment.group('indent')}{assignment.group('lhs').rstrip()} = "
|
|
640
|
+
f"__senpi_magic({_quote_arg(name)}, {_quote_arg(args)})"
|
|
641
|
+
)
|
|
642
|
+
index += 1
|
|
643
|
+
continue
|
|
644
|
+
|
|
645
|
+
transformed.append(line)
|
|
646
|
+
triple_quote = _advance_triple_quote_state(line, triple_quote)
|
|
647
|
+
index += 1
|
|
648
|
+
return "\n".join(transformed)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _magic_cd(args: str) -> str:
|
|
652
|
+
path = os.path.expanduser(args.strip()) or os.path.expanduser("~")
|
|
653
|
+
os.chdir(path)
|
|
654
|
+
cwd = os.getcwd()
|
|
655
|
+
emit_status("cd", path=cwd)
|
|
656
|
+
return cwd
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _magic_env(args: str) -> Any:
|
|
660
|
+
stripped = args.strip()
|
|
661
|
+
if not stripped:
|
|
662
|
+
return env()
|
|
663
|
+
if "=" in stripped:
|
|
664
|
+
key, value = stripped.split("=", 1)
|
|
665
|
+
return env(key.strip(), value.strip())
|
|
666
|
+
return env(stripped)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
_LINE_MAGICS: dict[str, Callable[[str], Any]] = {
|
|
670
|
+
"cd": _magic_cd,
|
|
671
|
+
"env": _magic_env,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _magic(name: str, args: str) -> Any:
|
|
676
|
+
handler = _LINE_MAGICS.get(name)
|
|
677
|
+
if handler is None:
|
|
678
|
+
raise PreludeRuntimeError(f"Unsupported line magic: %{name}")
|
|
679
|
+
return handler(args)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _magic_cell(name: str, args: str, body: str) -> Any:
|
|
683
|
+
if name in ("bash", "sh"):
|
|
684
|
+
command = "\n".join(part for part in (args, body) if part)
|
|
685
|
+
return _shell(command)
|
|
686
|
+
raise PreludeRuntimeError(f"Unsupported cell magic: %%{name}")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _take_prefix_by_lines(value: str, max_lines: int) -> str:
|
|
690
|
+
if max_lines <= 0:
|
|
691
|
+
return ""
|
|
692
|
+
cursor = 0
|
|
693
|
+
for _ in range(max_lines):
|
|
694
|
+
newline = value.find("\n", cursor)
|
|
695
|
+
if newline < 0:
|
|
696
|
+
return value
|
|
697
|
+
cursor = newline + 1
|
|
698
|
+
return value[:cursor]
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _take_prefix_by_encoded_bytes(value: str, max_bytes: int, encoding: str) -> str:
|
|
702
|
+
if max_bytes <= 0:
|
|
703
|
+
return ""
|
|
704
|
+
if len(value.encode(encoding, errors="replace")) <= max_bytes:
|
|
705
|
+
return value
|
|
706
|
+
low = 0
|
|
707
|
+
high = len(value)
|
|
708
|
+
while low < high:
|
|
709
|
+
middle = (low + high + 1) // 2
|
|
710
|
+
if len(value[:middle].encode(encoding, errors="replace")) <= max_bytes:
|
|
711
|
+
low = middle
|
|
712
|
+
else:
|
|
713
|
+
high = middle - 1
|
|
714
|
+
return value[:low]
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class _ShellOutputLimiter:
|
|
718
|
+
def __init__(self, *, max_bytes: int, max_lines: int, encoding: str) -> None:
|
|
719
|
+
self._remaining_bytes = max_bytes
|
|
720
|
+
self._remaining_lines = max_lines
|
|
721
|
+
self._encoding = encoding
|
|
722
|
+
self._truncated = False
|
|
723
|
+
self._at_line_start = True
|
|
724
|
+
|
|
725
|
+
def write(self, value: str) -> None:
|
|
726
|
+
if not value or self._truncated:
|
|
727
|
+
return
|
|
728
|
+
line_limited = _take_prefix_by_lines(value, self._remaining_lines)
|
|
729
|
+
truncated = line_limited != value
|
|
730
|
+
byte_limited = _take_prefix_by_encoded_bytes(
|
|
731
|
+
line_limited,
|
|
732
|
+
self._remaining_bytes,
|
|
733
|
+
self._encoding,
|
|
734
|
+
)
|
|
735
|
+
truncated = truncated or byte_limited != line_limited
|
|
736
|
+
if byte_limited:
|
|
737
|
+
text("stdout", byte_limited)
|
|
738
|
+
self._remaining_bytes -= len(
|
|
739
|
+
byte_limited.encode(self._encoding, errors="replace")
|
|
740
|
+
)
|
|
741
|
+
self._remaining_lines -= byte_limited.count("\n")
|
|
742
|
+
self._at_line_start = byte_limited.endswith("\n")
|
|
743
|
+
if truncated:
|
|
744
|
+
self._emit_truncation_notice()
|
|
745
|
+
|
|
746
|
+
def _emit_truncation_notice(self) -> None:
|
|
747
|
+
if self._truncated:
|
|
748
|
+
return
|
|
749
|
+
prefix = "" if self._at_line_start else "\n"
|
|
750
|
+
text("stdout", prefix + _SHELL_TRUNCATION_NOTICE)
|
|
751
|
+
self._truncated = True
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
class _BoundedTextCapture:
|
|
755
|
+
def __init__(self, max_bytes: int, max_lines: int, encoding: str) -> None:
|
|
756
|
+
self._remaining_bytes = max_bytes
|
|
757
|
+
self._remaining_lines = max_lines
|
|
758
|
+
self._encoding = encoding
|
|
759
|
+
self._parts: list[str] = []
|
|
760
|
+
|
|
761
|
+
def add(self, value: str) -> None:
|
|
762
|
+
if self._remaining_bytes <= 0 or self._remaining_lines <= 0:
|
|
763
|
+
return
|
|
764
|
+
line_limited = _take_prefix_by_lines(value, self._remaining_lines)
|
|
765
|
+
part = _take_prefix_by_encoded_bytes(
|
|
766
|
+
line_limited,
|
|
767
|
+
self._remaining_bytes,
|
|
768
|
+
self._encoding,
|
|
769
|
+
)
|
|
770
|
+
if not part:
|
|
771
|
+
return
|
|
772
|
+
self._parts.append(part)
|
|
773
|
+
self._remaining_bytes -= len(part.encode(self._encoding, errors="replace"))
|
|
774
|
+
self._remaining_lines -= part.count("\n")
|
|
775
|
+
|
|
776
|
+
def value(self) -> str:
|
|
777
|
+
return "".join(self._parts)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
class ShellResult(list[str]):
|
|
781
|
+
def __init__(self, lines: list[str], returncode: int) -> None:
|
|
782
|
+
super().__init__(lines)
|
|
783
|
+
self.returncode = returncode
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def n(self) -> str:
|
|
787
|
+
return "\n".join(self)
|
|
788
|
+
|
|
789
|
+
@property
|
|
790
|
+
def s(self) -> str:
|
|
791
|
+
return " ".join(self)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _shell(command: str) -> ShellResult:
|
|
795
|
+
process = subprocess.Popen(
|
|
796
|
+
command,
|
|
797
|
+
shell=True,
|
|
798
|
+
stdout=subprocess.PIPE,
|
|
799
|
+
stderr=subprocess.STDOUT,
|
|
800
|
+
)
|
|
801
|
+
if process.stdout is None:
|
|
802
|
+
return ShellResult([], process.wait())
|
|
803
|
+
|
|
804
|
+
encoding = locale.getpreferredencoding(False) or "utf-8"
|
|
805
|
+
decoder = codecs.getincrementaldecoder(encoding)(errors="replace")
|
|
806
|
+
limiter = _ShellOutputLimiter(
|
|
807
|
+
max_bytes=_SHELL_CAPTURE_MAX_BYTES,
|
|
808
|
+
max_lines=_SHELL_CAPTURE_MAX_LINES,
|
|
809
|
+
encoding=encoding,
|
|
810
|
+
)
|
|
811
|
+
capture = _BoundedTextCapture(
|
|
812
|
+
_SHELL_CAPTURE_MAX_BYTES,
|
|
813
|
+
_SHELL_CAPTURE_MAX_LINES,
|
|
814
|
+
encoding,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
def consume(chunk_text: str) -> None:
|
|
818
|
+
if not chunk_text:
|
|
819
|
+
return
|
|
820
|
+
limiter.write(chunk_text)
|
|
821
|
+
capture.add(chunk_text)
|
|
822
|
+
|
|
823
|
+
while True:
|
|
824
|
+
raw_chunk = os.read(process.stdout.fileno(), _SHELL_READ_CHUNK_BYTES)
|
|
825
|
+
if not raw_chunk:
|
|
826
|
+
break
|
|
827
|
+
consume(decoder.decode(raw_chunk))
|
|
828
|
+
consume(decoder.decode(b"", final=True))
|
|
829
|
+
return ShellResult(capture.value().splitlines(), process.wait())
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
USER_NS.update(
|
|
833
|
+
{
|
|
834
|
+
"display": display,
|
|
835
|
+
"print": print,
|
|
836
|
+
"log": log,
|
|
837
|
+
"phase": phase,
|
|
838
|
+
"env": env,
|
|
839
|
+
"read": read,
|
|
840
|
+
"write": write,
|
|
841
|
+
"parallel": parallel,
|
|
842
|
+
"pipeline": pipeline,
|
|
843
|
+
"tool": tool,
|
|
844
|
+
"completion": completion,
|
|
845
|
+
"agent": agent,
|
|
846
|
+
"output": output,
|
|
847
|
+
"__senpi_magic": _magic,
|
|
848
|
+
"__senpi_magic_cell": _magic_cell,
|
|
849
|
+
"__senpi_shell": _shell,
|
|
850
|
+
}
|
|
851
|
+
)
|
|
157
852
|
|
|
158
853
|
TLA_FLAG = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x2000)
|
|
159
854
|
|
|
160
855
|
|
|
161
856
|
def compile_cell(source: str) -> tuple[Any | None, Any | None]:
|
|
162
|
-
module = ast.parse(source, mode="exec")
|
|
857
|
+
module = ast.parse(transform_cell(source), mode="exec")
|
|
163
858
|
if not module.body:
|
|
164
859
|
return None, None
|
|
165
860
|
last = module.body[-1]
|
|
166
861
|
if isinstance(last, ast.Expr):
|
|
167
862
|
body = ast.Module(body=module.body[:-1], type_ignores=[])
|
|
168
|
-
|
|
169
|
-
ast.copy_location(
|
|
170
|
-
return compile(body, "<cell>", "exec", flags=TLA_FLAG), compile(
|
|
863
|
+
expression = ast.Expression(body=last.value)
|
|
864
|
+
ast.copy_location(expression, last)
|
|
865
|
+
return compile(body, "<cell>", "exec", flags=TLA_FLAG), compile(
|
|
866
|
+
expression,
|
|
867
|
+
"<cell>",
|
|
868
|
+
"eval",
|
|
869
|
+
flags=TLA_FLAG,
|
|
870
|
+
)
|
|
171
871
|
return compile(module, "<cell>", "exec", flags=TLA_FLAG), None
|
|
172
872
|
|
|
173
873
|
|
|
@@ -189,19 +889,32 @@ def run_cell(cell_id: str, code: str) -> None:
|
|
|
189
889
|
stderr = io.StringIO()
|
|
190
890
|
try:
|
|
191
891
|
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
192
|
-
body,
|
|
892
|
+
body, expression = compile_cell(code)
|
|
193
893
|
LOOP.run_until_complete(run_code(body, False))
|
|
194
|
-
value = LOOP.run_until_complete(run_code(
|
|
894
|
+
value = LOOP.run_until_complete(run_code(expression, True))
|
|
195
895
|
text("stdout", stdout.getvalue())
|
|
196
896
|
text("stderr", stderr.getvalue())
|
|
197
|
-
result: dict[str, Any] = {
|
|
897
|
+
result: dict[str, Any] = {
|
|
898
|
+
"type": "result",
|
|
899
|
+
"cellId": cell_id,
|
|
900
|
+
"ok": True,
|
|
901
|
+
"durationMs": elapsed(start),
|
|
902
|
+
}
|
|
198
903
|
if value is not None:
|
|
199
904
|
result["valueRepr"] = repr(value)
|
|
200
905
|
emit(result)
|
|
201
|
-
except BaseException as exc:
|
|
906
|
+
except BaseException as exc: # noqa: BROAD_EXCEPT_OK — cell boundary serializes user errors and interrupts.
|
|
202
907
|
text("stdout", stdout.getvalue())
|
|
203
908
|
text("stderr", stderr.getvalue())
|
|
204
|
-
emit(
|
|
909
|
+
emit(
|
|
910
|
+
{
|
|
911
|
+
"type": "result",
|
|
912
|
+
"cellId": cell_id,
|
|
913
|
+
"ok": False,
|
|
914
|
+
"error": bridge_error(exc),
|
|
915
|
+
"durationMs": elapsed(start),
|
|
916
|
+
}
|
|
917
|
+
)
|
|
205
918
|
|
|
206
919
|
|
|
207
920
|
def elapsed(start: float) -> int:
|
|
@@ -210,8 +923,8 @@ def elapsed(start: float) -> int:
|
|
|
210
923
|
|
|
211
924
|
def handle(message: dict[str, Any]) -> bool:
|
|
212
925
|
global SESSION_ID, CONNECTION
|
|
213
|
-
|
|
214
|
-
if
|
|
926
|
+
message_type = message.get("type")
|
|
927
|
+
if message_type == "init":
|
|
215
928
|
SESSION_ID = str(message.get("sessionId", ""))
|
|
216
929
|
connection = message.get("connection")
|
|
217
930
|
if not isinstance(connection, dict):
|
|
@@ -220,10 +933,10 @@ def handle(message: dict[str, Any]) -> bool:
|
|
|
220
933
|
CONNECTION = connection
|
|
221
934
|
emit({"type": "ready"})
|
|
222
935
|
return True
|
|
223
|
-
if
|
|
936
|
+
if message_type == "run":
|
|
224
937
|
run_cell(str(message.get("cellId", "")), str(message.get("code", "")))
|
|
225
938
|
return True
|
|
226
|
-
if
|
|
939
|
+
if message_type == "close":
|
|
227
940
|
emit({"type": "closed"})
|
|
228
941
|
return False
|
|
229
942
|
return True
|
|
@@ -234,7 +947,7 @@ def main() -> None:
|
|
|
234
947
|
try:
|
|
235
948
|
if not handle(json.loads(raw)):
|
|
236
949
|
break
|
|
237
|
-
except BaseException as exc:
|
|
950
|
+
except BaseException as exc: # noqa: BROAD_EXCEPT_OK — process boundary serializes malformed input and interrupts.
|
|
238
951
|
emit({"type": "init-failed", "error": bridge_error(exc)})
|
|
239
952
|
|
|
240
953
|
|