calfkit-tools 0.1.0__py3-none-any.whl
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.
- calfkit_tools/__init__.py +20 -0
- calfkit_tools/hermes/__init__.py +0 -0
- calfkit_tools/hermes/_shims/__init__.py +0 -0
- calfkit_tools/hermes/_shims/agent/__init__.py +0 -0
- calfkit_tools/hermes/_shims/agent/auxiliary_client.py +11 -0
- calfkit_tools/hermes/_shims/agent/lsp/__init__.py +10 -0
- calfkit_tools/hermes/_shims/agent/lsp/range_shift.py +6 -0
- calfkit_tools/hermes/_shims/agent/lsp/reporter.py +11 -0
- calfkit_tools/hermes/_shims/agent/lsp/servers.py +4 -0
- calfkit_tools/hermes/_shims/gateway/__init__.py +0 -0
- calfkit_tools/hermes/_shims/gateway/session_context.py +24 -0
- calfkit_tools/hermes/_shims/gateway/status.py +28 -0
- calfkit_tools/hermes/_shims/hermes_cli/__init__.py +0 -0
- calfkit_tools/hermes/_shims/hermes_cli/_subprocess_compat.py +17 -0
- calfkit_tools/hermes/_shims/hermes_cli/auth.py +21 -0
- calfkit_tools/hermes/_shims/hermes_cli/config.py +53 -0
- calfkit_tools/hermes/_shims/hermes_cli/nous_account.py +13 -0
- calfkit_tools/hermes/_shims/hermes_cli/plugins.py +10 -0
- calfkit_tools/hermes/_shims/hermes_cli/profiles.py +8 -0
- calfkit_tools/hermes/_shims/model_tools.py +64 -0
- calfkit_tools/hermes/_vendor/__init__.py +0 -0
- calfkit_tools/hermes/_vendor/agent/__init__.py +0 -0
- calfkit_tools/hermes/_vendor/agent/async_utils.py +68 -0
- calfkit_tools/hermes/_vendor/agent/file_safety.py +640 -0
- calfkit_tools/hermes/_vendor/agent/i18n.py +302 -0
- calfkit_tools/hermes/_vendor/agent/redact.py +496 -0
- calfkit_tools/hermes/_vendor/agent/skill_utils.py +666 -0
- calfkit_tools/hermes/_vendor/agent/web_search_provider.py +185 -0
- calfkit_tools/hermes/_vendor/agent/web_search_registry.py +245 -0
- calfkit_tools/hermes/_vendor/hermes_constants.py +471 -0
- calfkit_tools/hermes/_vendor/locales/af.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/de.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/en.yaml +372 -0
- calfkit_tools/hermes/_vendor/locales/es.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/fr.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/ga.yaml +361 -0
- calfkit_tools/hermes/_vendor/locales/hu.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/it.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/ja.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/ko.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/pt.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/ru.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/tr.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/uk.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/zh-hant.yaml +357 -0
- calfkit_tools/hermes/_vendor/locales/zh.yaml +357 -0
- calfkit_tools/hermes/_vendor/tools/__init__.py +25 -0
- calfkit_tools/hermes/_vendor/tools/ansi_strip.py +44 -0
- calfkit_tools/hermes/_vendor/tools/approval.py +1697 -0
- calfkit_tools/hermes/_vendor/tools/binary_extensions.py +42 -0
- calfkit_tools/hermes/_vendor/tools/budget_config.py +51 -0
- calfkit_tools/hermes/_vendor/tools/code_execution_tool.py +1831 -0
- calfkit_tools/hermes/_vendor/tools/credential_files.py +454 -0
- calfkit_tools/hermes/_vendor/tools/env_passthrough.py +163 -0
- calfkit_tools/hermes/_vendor/tools/environments/__init__.py +14 -0
- calfkit_tools/hermes/_vendor/tools/environments/base.py +895 -0
- calfkit_tools/hermes/_vendor/tools/environments/daytona.py +270 -0
- calfkit_tools/hermes/_vendor/tools/environments/docker.py +1296 -0
- calfkit_tools/hermes/_vendor/tools/environments/file_sync.py +403 -0
- calfkit_tools/hermes/_vendor/tools/environments/local.py +697 -0
- calfkit_tools/hermes/_vendor/tools/environments/managed_modal.py +282 -0
- calfkit_tools/hermes/_vendor/tools/environments/modal.py +478 -0
- calfkit_tools/hermes/_vendor/tools/environments/modal_utils.py +204 -0
- calfkit_tools/hermes/_vendor/tools/environments/singularity.py +262 -0
- calfkit_tools/hermes/_vendor/tools/environments/ssh.py +319 -0
- calfkit_tools/hermes/_vendor/tools/file_operations.py +2292 -0
- calfkit_tools/hermes/_vendor/tools/file_state.py +332 -0
- calfkit_tools/hermes/_vendor/tools/file_tools.py +1533 -0
- calfkit_tools/hermes/_vendor/tools/fuzzy_match.py +860 -0
- calfkit_tools/hermes/_vendor/tools/interrupt.py +98 -0
- calfkit_tools/hermes/_vendor/tools/lazy_deps.py +623 -0
- calfkit_tools/hermes/_vendor/tools/managed_tool_gateway.py +192 -0
- calfkit_tools/hermes/_vendor/tools/patch_parser.py +622 -0
- calfkit_tools/hermes/_vendor/tools/path_security.py +43 -0
- calfkit_tools/hermes/_vendor/tools/process_registry.py +1616 -0
- calfkit_tools/hermes/_vendor/tools/registry.py +594 -0
- calfkit_tools/hermes/_vendor/tools/terminal_tool.py +2611 -0
- calfkit_tools/hermes/_vendor/tools/thread_context.py +120 -0
- calfkit_tools/hermes/_vendor/tools/tirith_security.py +820 -0
- calfkit_tools/hermes/_vendor/tools/todo_tool.py +279 -0
- calfkit_tools/hermes/_vendor/tools/tool_backend_helpers.py +182 -0
- calfkit_tools/hermes/_vendor/tools/tool_output_limits.py +110 -0
- calfkit_tools/hermes/_vendor/tools/url_safety.py +361 -0
- calfkit_tools/hermes/_vendor/tools/web_providers/__init__.py +0 -0
- calfkit_tools/hermes/_vendor/tools/web_providers/brave_free.py +137 -0
- calfkit_tools/hermes/_vendor/tools/web_providers/ddgs.py +104 -0
- calfkit_tools/hermes/_vendor/tools/web_providers/searxng.py +140 -0
- calfkit_tools/hermes/_vendor/tools/web_providers/tavily.py +220 -0
- calfkit_tools/hermes/_vendor/utils.py +376 -0
- calfkit_tools/hermes/node/__init__.py +44 -0
- calfkit_tools/hermes/node/_runtime.py +109 -0
- calfkit_tools/hermes/node/code.py +110 -0
- calfkit_tools/hermes/node/files.py +193 -0
- calfkit_tools/hermes/node/shell.py +162 -0
- calfkit_tools/hermes/node/todo.py +116 -0
- calfkit_tools/hermes/node/web.py +156 -0
- calfkit_tools/web_fetch/__init__.py +0 -0
- calfkit_tools/web_fetch/_vendor/__init__.py +0 -0
- calfkit_tools/web_fetch/_vendor/_ssrf.py +622 -0
- calfkit_tools/web_fetch/_vendor/common_tools/__init__.py +0 -0
- calfkit_tools/web_fetch/_vendor/common_tools/web_fetch.py +166 -0
- calfkit_tools/web_fetch/node/__init__.py +58 -0
- calfkit_tools/web_fetch/results.py +37 -0
- calfkit_tools-0.1.0.dist-info/METADATA +112 -0
- calfkit_tools-0.1.0.dist-info/RECORD +110 -0
- calfkit_tools-0.1.0.dist-info/WHEEL +4 -0
- calfkit_tools-0.1.0.dist-info/licenses/LICENSE +201 -0
- calfkit_tools-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +43 -0
- calfkit_tools-0.1.0.dist-info/licenses/vendor/hermes/LICENSE +21 -0
- calfkit_tools-0.1.0.dist-info/licenses/vendor/web_fetch/LICENSE +21 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""calfkit-tools — vendored agent tools for the calfkit SDK.
|
|
2
|
+
|
|
3
|
+
Tools are organised by the upstream source they were vendored from, each as a
|
|
4
|
+
subpackage of ``calfkit_tools``. Import the deployable tool nodes directly from a
|
|
5
|
+
source's ``node`` module::
|
|
6
|
+
|
|
7
|
+
from calfkit_tools.hermes.node import HERMES_NODES, terminal, todo, web_search
|
|
8
|
+
from calfkit_tools.web_fetch.node import web_fetch
|
|
9
|
+
|
|
10
|
+
Each subpackage keeps its vendored upstream code under ``_vendor`` (and, for
|
|
11
|
+
hermes, app-runtime stand-ins under ``_shims``); provenance for every source
|
|
12
|
+
lives outside the package in ``vendor/<source>/`` (upstream ``LICENSE`` +
|
|
13
|
+
``METADATA.yaml``). See ``THIRD_PARTY_NOTICES.md``.
|
|
14
|
+
|
|
15
|
+
This module is intentionally side-effect free: importing ``calfkit_tools`` pulls
|
|
16
|
+
in no tool code or optional dependencies. Import the specific ``.node`` module
|
|
17
|
+
for the tools you need.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__all__: list[str] = []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``agent.auxiliary_client``.
|
|
2
|
+
|
|
3
|
+
``call_llm`` makes a secondary LLM call (used only by approval's "smart approve" path,
|
|
4
|
+
which is non-default). calfkit wires its own LLM elsewhere; this raises so the caller's
|
|
5
|
+
try/except falls back to escalation (the secure direction). Stage D may wire this to
|
|
6
|
+
calfkit's LLM if smart-approve is wanted.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def call_llm(*args, **kwargs):
|
|
11
|
+
raise RuntimeError("call_llm is not available in the calfkit-tools hermes shim")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``agent.lsp`` — LSP-diagnostics-on-edit enrichment.
|
|
2
|
+
|
|
3
|
+
LSP is a pure enrichment layer for file edits; every call site is gated and try/except'd.
|
|
4
|
+
The shim fails closed: ``get_service`` returns None so file edits proceed without
|
|
5
|
+
post-edit diagnostics. (Port the real ``agent/lsp`` later if diagnostics are wanted.)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_service():
|
|
10
|
+
return None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Shim for ``agent.lsp.reporter``. No diagnostics to report."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def report_for_file(*args, **kwargs) -> str:
|
|
5
|
+
return ""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def truncate(s: str, *, limit: int = 10000) -> str:
|
|
9
|
+
if s is None:
|
|
10
|
+
return s
|
|
11
|
+
return s if len(s) <= limit else s[:limit]
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``gateway.session_context``.
|
|
2
|
+
|
|
3
|
+
Provides the symbols the vendored tree imports: ``_UNSET``, ``_VAR_MAP``,
|
|
4
|
+
``get_session_env``. Upstream backs these with per-request ContextVars to inject
|
|
5
|
+
session-scoped env vars into subprocesses.
|
|
6
|
+
|
|
7
|
+
NOTE (Stage D): the real per-tenant isolation lives here. For the adapt stage this is a
|
|
8
|
+
minimal env-backed version so the tree imports and single-tenant behaviour is correct;
|
|
9
|
+
``_VAR_MAP`` is empty so no session vars are injected. Stage D must replace this with the
|
|
10
|
+
real ContextVar mechanism (re-keyed on the calfkit session_key) — see design doc §6, §12.
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
_UNSET: Any = object()
|
|
16
|
+
|
|
17
|
+
# Maps session-var name -> ContextVar upstream; empty here (no injection until Stage D).
|
|
18
|
+
_VAR_MAP: dict = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_session_env(name: str, default: str = "") -> str:
|
|
22
|
+
"""Return a session-scoped env value, falling back to the process environment."""
|
|
23
|
+
value = os.environ.get(name)
|
|
24
|
+
return value if value is not None else default
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``gateway.status`` (faithful, tiny).
|
|
2
|
+
|
|
3
|
+
``_pid_exists`` is a cross-platform "is this PID alive" check that does NOT signal the
|
|
4
|
+
target (critical on Windows, where ``os.kill(pid, 0)`` is not a no-op). Used by the
|
|
5
|
+
background-process registry's detached crash-recovery path.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _pid_exists(pid: Optional[int]) -> bool:
|
|
12
|
+
if not pid:
|
|
13
|
+
return False
|
|
14
|
+
try:
|
|
15
|
+
import psutil
|
|
16
|
+
|
|
17
|
+
return psutil.pid_exists(pid)
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
try:
|
|
21
|
+
os.kill(pid, 0)
|
|
22
|
+
except ProcessLookupError:
|
|
23
|
+
return False
|
|
24
|
+
except PermissionError:
|
|
25
|
+
return True
|
|
26
|
+
except OSError:
|
|
27
|
+
return False
|
|
28
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``hermes_cli._subprocess_compat`` (faithful, tiny).
|
|
2
|
+
|
|
3
|
+
``windows_hide_flags`` returns Win32 creationflags that hide a child console window
|
|
4
|
+
(0 on non-Windows). Must return an int (it is passed unconditionally as Popen
|
|
5
|
+
``creationflags=`` in process_registry.py), never None.
|
|
6
|
+
"""
|
|
7
|
+
import platform
|
|
8
|
+
|
|
9
|
+
_IS_WINDOWS = platform.system() == "Windows"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def windows_hide_flags() -> int:
|
|
13
|
+
if not _IS_WINDOWS:
|
|
14
|
+
return 0
|
|
15
|
+
import subprocess
|
|
16
|
+
|
|
17
|
+
return getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``hermes_cli.auth``.
|
|
2
|
+
|
|
3
|
+
``PROVIDER_REGISTRY`` feeds local.py's subprocess secret *blocklist*. Empty here is
|
|
4
|
+
deliberate: Stage D replaces the blocklist with an allowlist env model (design doc
|
|
5
|
+
§6.1), so the blocklist data is moot for the calfkit node.
|
|
6
|
+
|
|
7
|
+
SECURITY: do NOT read "empty" as "no secrets to block". local.py keeps a static
|
|
8
|
+
blocklist, but it does NOT cover calfkit's own infra secrets (Kafka SASL, etc.). The
|
|
9
|
+
subprocess env is only truly contained by Stage D's allowlist — until then this
|
|
10
|
+
component must not run untrusted commands in a multi-tenant worker (design §6.1, §12).
|
|
11
|
+
|
|
12
|
+
``resolve_nous_access_token`` is a Nous-portal token resolver, irrelevant to calfkit —
|
|
13
|
+
returns None (callers wrap it in try/except and fail closed).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Import-safe stub; real subprocess env hygiene = Stage-D allowlist.
|
|
17
|
+
PROVIDER_REGISTRY: dict = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_nous_access_token(*args, **kwargs):
|
|
21
|
+
return None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``hermes_cli.config`` — only the symbols the vendored tree uses.
|
|
2
|
+
|
|
3
|
+
``get_hermes_home`` re-exports the vendored real implementation. ``cfg_get`` is the pure
|
|
4
|
+
nested-dict helper (faithful). The config-reading functions return empty defaults: the
|
|
5
|
+
vendored tools all consume them inside try/except with sane fallbacks, and real
|
|
6
|
+
subprocess env hygiene is handled by the Stage-D allowlist (not this blocklist-feeding
|
|
7
|
+
config). ``OPTIONAL_ENV_VARS`` is intentionally empty here for the same reason.
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Re-export the real implementation (lives in the vendored constants module).
|
|
13
|
+
from calfkit_tools.hermes._vendor.hermes_constants import get_hermes_home # noqa: F401
|
|
14
|
+
|
|
15
|
+
# Import-safe stub; real env hygiene = Stage-D allowlist (see design doc §6.1, §3.2).
|
|
16
|
+
OPTIONAL_ENV_VARS: dict = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cfg_get(cfg, *keys, default=None):
|
|
20
|
+
"""Traverse nested dict keys safely, returning ``default`` on any miss (faithful)."""
|
|
21
|
+
if not isinstance(cfg, dict):
|
|
22
|
+
return default
|
|
23
|
+
node = cfg
|
|
24
|
+
for key in keys:
|
|
25
|
+
if not isinstance(node, dict) or key not in node:
|
|
26
|
+
return default
|
|
27
|
+
node = node[key]
|
|
28
|
+
return node
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_raw_config() -> dict:
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config() -> dict:
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_config(config: dict) -> None:
|
|
40
|
+
# No-op: persisting host config is not a calfkit-node responsibility.
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_env() -> dict:
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_env_value(key: str):
|
|
49
|
+
return os.environ.get(key)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_config_path() -> Path:
|
|
53
|
+
return get_hermes_home() / "config.yaml"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``hermes_cli.nous_account`` (Nous-portal account info).
|
|
2
|
+
|
|
3
|
+
Irrelevant to calfkit; only referenced by the managed-tool-gateway path inside
|
|
4
|
+
try/except. Safe no-ops.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_nous_portal_account_info(*args, **kwargs):
|
|
9
|
+
return None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_nous_portal_entitlement_message(*args, **kwargs) -> str:
|
|
13
|
+
return ""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``hermes_cli.plugins``.
|
|
2
|
+
|
|
3
|
+
``invoke_hook`` dispatches user/project plugin hooks; calfkit has no such plugin
|
|
4
|
+
system here. Returns an empty list of hook results (callers iterate the result).
|
|
5
|
+
"""
|
|
6
|
+
from typing import Any, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def invoke_hook(hook_name: str, **kwargs: Any) -> List[Any]:
|
|
10
|
+
return []
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shim for hermes-agent ``model_tools`` — only the symbols the vendored tree uses.
|
|
2
|
+
|
|
3
|
+
- ``_run_async`` / ``_sanitize_tool_error``: faithful pure helpers used by the vendored
|
|
4
|
+
``registry.dispatch`` (the shell/file tools register no async handlers, so ``_run_async``
|
|
5
|
+
is off the hot path here, but it stays behaviour-correct).
|
|
6
|
+
- ``handle_function_call``: the Programmatic-Tool-Calling (PTC) dispatcher used by
|
|
7
|
+
``code_execution_tool``'s RPC bridge. The full upstream version layers a tool-search
|
|
8
|
+
bridge + hooks + guardrails; here it is a thin delegate to the vendored registry's own
|
|
9
|
+
``dispatch`` (lookup + async-bridge + error-sanitize), scoped to this node's tools.
|
|
10
|
+
"""
|
|
11
|
+
import asyncio
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
# --- _sanitize_tool_error (constants + body copied verbatim from upstream) ---------
|
|
16
|
+
_TOOL_ERROR_ROLE_TAG_RE = re.compile(
|
|
17
|
+
r'</?(?:tool_call|function_call|result|response|output|input|system|assistant|user)>',
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
_TOOL_ERROR_FENCE_OPEN_RE = re.compile(r'^\s*```(?:json|xml|html|markdown)?\s*', re.MULTILINE)
|
|
21
|
+
_TOOL_ERROR_FENCE_CLOSE_RE = re.compile(r'\s*```\s*$', re.MULTILINE)
|
|
22
|
+
_TOOL_ERROR_CDATA_RE = re.compile(r'<!\[CDATA\[.*?\]\]>', re.DOTALL)
|
|
23
|
+
_TOOL_ERROR_MAX_LEN = 2000
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sanitize_tool_error(error_msg: str) -> str:
|
|
27
|
+
"""Strip structural framing tokens from a tool error before showing it to the model."""
|
|
28
|
+
if not error_msg:
|
|
29
|
+
return "[TOOL_ERROR] "
|
|
30
|
+
sanitized = _TOOL_ERROR_ROLE_TAG_RE.sub("", error_msg)
|
|
31
|
+
sanitized = _TOOL_ERROR_FENCE_OPEN_RE.sub("", sanitized)
|
|
32
|
+
sanitized = _TOOL_ERROR_FENCE_CLOSE_RE.sub("", sanitized)
|
|
33
|
+
sanitized = _TOOL_ERROR_CDATA_RE.sub("", sanitized)
|
|
34
|
+
if len(sanitized) > _TOOL_ERROR_MAX_LEN:
|
|
35
|
+
sanitized = sanitized[: _TOOL_ERROR_MAX_LEN - 3] + "..."
|
|
36
|
+
return f"[TOOL_ERROR] {sanitized}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_async(coro):
|
|
40
|
+
"""Run a coroutine from sync code (sync->async bridge for async tool handlers).
|
|
41
|
+
|
|
42
|
+
Simplified vs. upstream's persistent-loop optimization (which keeps cached async
|
|
43
|
+
clients alive). The shell/file toolset registers no async handlers, so this is not
|
|
44
|
+
exercised on the hot path; it stays correct for any future async handler.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
running = asyncio.get_running_loop()
|
|
48
|
+
except RuntimeError:
|
|
49
|
+
running = None
|
|
50
|
+
if running and running.is_running():
|
|
51
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
52
|
+
return pool.submit(lambda: asyncio.run(coro)).result()
|
|
53
|
+
return asyncio.run(coro)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def handle_function_call(function_name, function_args, task_id=None, **kwargs):
|
|
57
|
+
"""PTC dispatcher: route an in-sandbox tool call into the vendored registry.
|
|
58
|
+
|
|
59
|
+
Thin delegate to ``registry.dispatch`` (this node's own tools). Imported lazily to
|
|
60
|
+
avoid an import cycle (registry imports this module for ``_run_async``).
|
|
61
|
+
"""
|
|
62
|
+
from calfkit_tools.hermes._vendor.tools.registry import registry
|
|
63
|
+
|
|
64
|
+
return registry.dispatch(function_name, function_args, task_id=task_id)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Async/sync bridging helpers.
|
|
2
|
+
|
|
3
|
+
The codebase has ~30 sites that schedule a coroutine onto an event loop from a
|
|
4
|
+
worker thread via :func:`asyncio.run_coroutine_threadsafe`. That function can
|
|
5
|
+
raise :class:`RuntimeError` (e.g. the loop was closed during a shutdown race),
|
|
6
|
+
and when it does the coroutine object is never awaited and never closed —
|
|
7
|
+
which triggers a ``"coroutine '<name>' was never awaited"`` RuntimeWarning and
|
|
8
|
+
leaks the coroutine's frame until GC.
|
|
9
|
+
|
|
10
|
+
:func:`safe_schedule_threadsafe` wraps the call, closes the coroutine on
|
|
11
|
+
scheduling failure, and returns ``None`` (instead of a half-formed future) so
|
|
12
|
+
callers can branch cleanly:
|
|
13
|
+
|
|
14
|
+
fut = safe_schedule_threadsafe(coro, loop)
|
|
15
|
+
if fut is None:
|
|
16
|
+
return # or fallback behavior
|
|
17
|
+
fut.result(timeout=5)
|
|
18
|
+
|
|
19
|
+
The helper deliberately does NOT also handle ``future.result()`` failures —
|
|
20
|
+
that is a separate concern. Once the loop has accepted the coroutine, its
|
|
21
|
+
lifecycle belongs to the loop, not the scheduling thread.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import logging
|
|
27
|
+
from concurrent.futures import Future
|
|
28
|
+
from typing import Any, Coroutine, Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_DEFAULT_LOGGER = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def safe_schedule_threadsafe(
|
|
35
|
+
coro: Coroutine[Any, Any, Any],
|
|
36
|
+
loop: Optional[asyncio.AbstractEventLoop],
|
|
37
|
+
*,
|
|
38
|
+
logger: Optional[logging.Logger] = None,
|
|
39
|
+
log_message: str = "Failed to schedule coroutine on loop",
|
|
40
|
+
log_level: int = logging.DEBUG,
|
|
41
|
+
) -> Optional[Future]:
|
|
42
|
+
"""Schedule ``coro`` on ``loop`` from a sync context, leak-safe.
|
|
43
|
+
|
|
44
|
+
Returns the :class:`concurrent.futures.Future` on success, or ``None`` if
|
|
45
|
+
the loop is missing or :func:`asyncio.run_coroutine_threadsafe` raised
|
|
46
|
+
(e.g. the loop was closed during a shutdown race). In all failure paths
|
|
47
|
+
the coroutine is :meth:`close`-d so it does not trigger
|
|
48
|
+
``"coroutine was never awaited"`` warnings or leak its frame.
|
|
49
|
+
|
|
50
|
+
Callers retain full control over what to do with the returned future
|
|
51
|
+
(call ``.result(timeout=...)``, attach ``add_done_callback``, ignore it
|
|
52
|
+
fire-and-forget, etc.).
|
|
53
|
+
"""
|
|
54
|
+
log = logger if logger is not None else _DEFAULT_LOGGER
|
|
55
|
+
|
|
56
|
+
if loop is None:
|
|
57
|
+
if asyncio.iscoroutine(coro):
|
|
58
|
+
coro.close()
|
|
59
|
+
log.log(log_level, "%s: loop is None", log_message)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
return asyncio.run_coroutine_threadsafe(coro, loop)
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
if asyncio.iscoroutine(coro):
|
|
66
|
+
coro.close()
|
|
67
|
+
log.log(log_level, "%s: %s", log_message, exc)
|
|
68
|
+
return None
|