py-kodo 0.1.5__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.
- kodo/__init__.py +5 -0
- kodo/__main__.py +12 -0
- kodo/binutils/__init__.py +25 -0
- kodo/binutils/_utils.py +377 -0
- kodo/common/__init__.py +15 -0
- kodo/common/_envelope.py +173 -0
- kodo/common/_protocols.py +72 -0
- kodo/common/_tempdir.py +44 -0
- kodo/common/py.typed +0 -0
- kodo/guided_state/__init__.py +44 -0
- kodo/guided_state/_paths.py +42 -0
- kodo/guided_state/_records.py +108 -0
- kodo/guided_state/_scan.py +40 -0
- kodo/guided_state/_store.py +143 -0
- kodo/guided_state/py.typed +0 -0
- kodo/llms/__init__.py +114 -0
- kodo/llms/_cloud_registry.py +153 -0
- kodo/llms/_context.py +54 -0
- kodo/llms/_gateway.py +295 -0
- kodo/llms/_hardware.py +150 -0
- kodo/llms/_interface.py +277 -0
- kodo/llms/_local_registry.py +2492 -0
- kodo/llms/_logger.py +185 -0
- kodo/llms/_sanitize.py +24 -0
- kodo/llms/_tool_logger.py +73 -0
- kodo/llms/anthropic/__init__.py +12 -0
- kodo/llms/anthropic/_cache.py +111 -0
- kodo/llms/anthropic/_claude.py +260 -0
- kodo/llms/anthropic/_retry.py +185 -0
- kodo/llms/anthropic/_usage.py +49 -0
- kodo/llms/anthropic/py.typed +0 -0
- kodo/llms/llamacpp/__init__.py +71 -0
- kodo/llms/llamacpp/_installer.py +519 -0
- kodo/llms/llamacpp/_llama.py +891 -0
- kodo/llms/llamacpp/_llama_server.py +474 -0
- kodo/llms/llamacpp/_manager.py +180 -0
- kodo/llms/llamacpp/py.typed +0 -0
- kodo/llms/local/__init__.py +41 -0
- kodo/llms/local/_hf.py +185 -0
- kodo/llms/local/_http.py +371 -0
- kodo/llms/local/_manager.py +772 -0
- kodo/llms/local/_state.py +129 -0
- kodo/llms/local/_types.py +166 -0
- kodo/llms/local/py.typed +0 -0
- kodo/llms/py.typed +0 -0
- kodo/mirror/__init__.py +24 -0
- kodo/mirror/_mirror.py +346 -0
- kodo/mirror/py.typed +0 -0
- kodo/project/__init__.py +25 -0
- kodo/project/_layout.py +363 -0
- kodo/project/_manifest.py +96 -0
- kodo/project/py.typed +0 -0
- kodo/py.typed +0 -0
- kodo/runtime/__init__.py +32 -0
- kodo/runtime/_attachments.py +202 -0
- kodo/runtime/_checkpoints.py +639 -0
- kodo/runtime/_cyclic_thinking.py +157 -0
- kodo/runtime/_engine/__init__.py +35 -0
- kodo/runtime/_engine/_checkpointing.py +308 -0
- kodo/runtime/_engine/_compaction.py +387 -0
- kodo/runtime/_engine/_core.py +1218 -0
- kodo/runtime/_engine/_events.py +377 -0
- kodo/runtime/_engine/_history.py +661 -0
- kodo/runtime/_engine/_llm.py +528 -0
- kodo/runtime/_engine/_proto.py +324 -0
- kodo/runtime/_engine/_resume.py +389 -0
- kodo/runtime/_engine/_services.py +127 -0
- kodo/runtime/_engine/_shared.py +148 -0
- kodo/runtime/_engine/_subagents.py +576 -0
- kodo/runtime/_engine/_titling.py +214 -0
- kodo/runtime/_engine/_turns.py +939 -0
- kodo/runtime/_engine/_watchdog.py +650 -0
- kodo/runtime/_engine/_worker.py +167 -0
- kodo/runtime/_gates.py +639 -0
- kodo/runtime/_security_rules.py +54 -0
- kodo/runtime/_session.py +157 -0
- kodo/runtime/_session_log.py +84 -0
- kodo/runtime/py.typed +0 -0
- kodo/security/__init__.py +68 -0
- kodo/security/_analysis.py +378 -0
- kodo/security/_classify.py +327 -0
- kodo/security/_defaults.py +617 -0
- kodo/security/_layer.py +353 -0
- kodo/security/_rules.py +835 -0
- kodo/security/_store.py +171 -0
- kodo/security/py.typed +0 -0
- kodo/server/__init__.py +21 -0
- kodo/server/__main__.py +79 -0
- kodo/server/_app.py +2041 -0
- kodo/server/_config.py +164 -0
- kodo/server/_connection_registry.py +194 -0
- kodo/server/_key_broker.py +92 -0
- kodo/server/_lifecycle.py +203 -0
- kodo/server/_session.py +37 -0
- kodo/server/_session_manager.py +588 -0
- kodo/server/py.typed +0 -0
- kodo/shellparser/__init__.py +23 -0
- kodo/shellparser/_parser.py +284 -0
- kodo/shellparser/_powershell.py +204 -0
- kodo/shellparser/py.typed +0 -0
- kodo/state/__init__.py +20 -0
- kodo/state/_toolcall_store.py +130 -0
- kodo/state/_transient.py +1254 -0
- kodo/state/py.typed +0 -0
- kodo/subagents/__init__.py +16 -0
- kodo/subagents/_loader.py +259 -0
- kodo/subagents/_registry.py +395 -0
- kodo/subagents/_subagentspec.py +51 -0
- kodo/subagents/agent_guide.md +313 -0
- kodo/subagents/agent_judge.md +85 -0
- kodo/subagents/agent_problem_solver.md +208 -0
- kodo/subagents/base_dependencies.md +102 -0
- kodo/subagents/base_toolchain.md +138 -0
- kodo/subagents/preamble_performance.md +85 -0
- kodo/subagents/preamble_security.md +43 -0
- kodo/subagents/py.typed +0 -0
- kodo/subagents/specs/__init__.py +105 -0
- kodo/subagents/specs/_architect.py +33 -0
- kodo/subagents/specs/_architect_critic.py +27 -0
- kodo/subagents/specs/_code_critic.py +37 -0
- kodo/subagents/specs/_coder.py +23 -0
- kodo/subagents/specs/_compactor.py +33 -0
- kodo/subagents/specs/_developer.py +92 -0
- kodo/subagents/specs/_e2e_test_code_critic.py +46 -0
- kodo/subagents/specs/_e2e_test_coder.py +37 -0
- kodo/subagents/specs/_e2e_test_design_critic.py +32 -0
- kodo/subagents/specs/_e2e_test_designer.py +31 -0
- kodo/subagents/specs/_functional_design_critic.py +31 -0
- kodo/subagents/specs/_functional_designer.py +29 -0
- kodo/subagents/specs/_investigator.py +117 -0
- kodo/subagents/specs/_narrative_author.py +44 -0
- kodo/subagents/specs/_planner.py +98 -0
- kodo/subagents/specs/_requirements_author.py +27 -0
- kodo/subagents/specs/_requirements_critic.py +32 -0
- kodo/subagents/specs/_shapes.py +188 -0
- kodo/subagents/specs/_test_coder.py +30 -0
- kodo/subagents/specs/_test_design_critic.py +33 -0
- kodo/subagents/specs/_test_designer.py +21 -0
- kodo/subagents/specs/_toolchain_cpp.py +76 -0
- kodo/subagents/specs/_toolchain_depsmgr.py +103 -0
- kodo/subagents/specs/_toolchain_python.py +68 -0
- kodo/subagents/specs/_toolchain_rust.py +68 -0
- kodo/subagents/specs/_web_search_agent.py +96 -0
- kodo/subagents/subagent_architect.md +128 -0
- kodo/subagents/subagent_architect_critic.md +87 -0
- kodo/subagents/subagent_code_critic.md +101 -0
- kodo/subagents/subagent_coder.md +126 -0
- kodo/subagents/subagent_compactor.md +37 -0
- kodo/subagents/subagent_developer.md +97 -0
- kodo/subagents/subagent_e2e_test_code_critic.md +95 -0
- kodo/subagents/subagent_e2e_test_coder.md +94 -0
- kodo/subagents/subagent_e2e_test_design_critic.md +82 -0
- kodo/subagents/subagent_e2e_test_designer.md +123 -0
- kodo/subagents/subagent_functional_design_critic.md +97 -0
- kodo/subagents/subagent_functional_designer.md +173 -0
- kodo/subagents/subagent_investigator.md +73 -0
- kodo/subagents/subagent_narrative_author.md +181 -0
- kodo/subagents/subagent_planner.md +59 -0
- kodo/subagents/subagent_requirements_author.md +107 -0
- kodo/subagents/subagent_requirements_critic.md +82 -0
- kodo/subagents/subagent_test_coder.md +83 -0
- kodo/subagents/subagent_test_design_critic.md +91 -0
- kodo/subagents/subagent_test_designer.md +96 -0
- kodo/subagents/subagent_toolchain_cpp.md +84 -0
- kodo/subagents/subagent_toolchain_depsmgr.md +103 -0
- kodo/subagents/subagent_toolchain_python.md +73 -0
- kodo/subagents/subagent_toolchain_rust.md +73 -0
- kodo/subagents/subagent_web_search.md +94 -0
- kodo/titling/__init__.py +35 -0
- kodo/titling/_server.py +574 -0
- kodo/tools/__init__.py +131 -0
- kodo/tools/_ask_user.py +77 -0
- kodo/tools/_context.py +697 -0
- kodo/tools/_create_directory.py +39 -0
- kodo/tools/_create_file.py +59 -0
- kodo/tools/_create_new_project.py +62 -0
- kodo/tools/_disable_autonomous_mode.py +22 -0
- kodo/tools/_dispatch.py +572 -0
- kodo/tools/_document_feedback.py +59 -0
- kodo/tools/_edit_file.py +114 -0
- kodo/tools/_edit_review.py +40 -0
- kodo/tools/_escalate_blocker.py +40 -0
- kodo/tools/_filesystem.py +145 -0
- kodo/tools/_finalize_project.py +21 -0
- kodo/tools/_find_files.py +107 -0
- kodo/tools/_find_text_in_files.py +136 -0
- kodo/tools/_get_root_paths.py +43 -0
- kodo/tools/_get_web_search_state.py +25 -0
- kodo/tools/_guided_dev_status.py +29 -0
- kodo/tools/_init_project.py +53 -0
- kodo/tools/_paths.py +211 -0
- kodo/tools/_query_search_engine.py +121 -0
- kodo/tools/_read_attachment.py +47 -0
- kodo/tools/_read_file.py +164 -0
- kodo/tools/_read_webpage.py +151 -0
- kodo/tools/_remaining_time.py +26 -0
- kodo/tools/_return_result.py +50 -0
- kodo/tools/_rollback.py +35 -0
- kodo/tools/_run_author_critic_iteration.py +56 -0
- kodo/tools/_run_command.py +144 -0
- kodo/tools/_run_subagent.py +30 -0
- kodo/tools/_search.py +87 -0
- kodo/tools/_submit_evaluation.py +56 -0
- kodo/tools/_tool.py +69 -0
- kodo/tools/_toolchain_build.py +121 -0
- kodo/tools/_toolchain_deps.py +135 -0
- kodo/tools/_update_web_search_state.py +33 -0
- kodo/tools/_wait.py +33 -0
- kodo/tools/_web_search.py +75 -0
- kodo/toolspecs/__init__.py +155 -0
- kodo/toolspecs/_ask_user.py +127 -0
- kodo/toolspecs/_compliance.py +212 -0
- kodo/toolspecs/_create_directory.py +102 -0
- kodo/toolspecs/_create_file.py +182 -0
- kodo/toolspecs/_create_new_project.py +99 -0
- kodo/toolspecs/_disable_autonomous_mode.py +47 -0
- kodo/toolspecs/_document_feedback.py +96 -0
- kodo/toolspecs/_edit_file.py +198 -0
- kodo/toolspecs/_escalate_blocker.py +109 -0
- kodo/toolspecs/_filesystem.py +201 -0
- kodo/toolspecs/_finalize_project.py +37 -0
- kodo/toolspecs/_find_files.py +137 -0
- kodo/toolspecs/_find_text_in_files.py +160 -0
- kodo/toolspecs/_get_root_paths.py +113 -0
- kodo/toolspecs/_get_web_search_state.py +56 -0
- kodo/toolspecs/_guided_dev_status.py +72 -0
- kodo/toolspecs/_init_project.py +96 -0
- kodo/toolspecs/_intent.py +59 -0
- kodo/toolspecs/_query_search_engine.py +103 -0
- kodo/toolspecs/_read_attachment.py +60 -0
- kodo/toolspecs/_read_file.py +154 -0
- kodo/toolspecs/_read_webpage.py +124 -0
- kodo/toolspecs/_remaining_time.py +49 -0
- kodo/toolspecs/_return_result.py +59 -0
- kodo/toolspecs/_rollback.py +57 -0
- kodo/toolspecs/_run_author_critic_iteration.py +97 -0
- kodo/toolspecs/_run_command.py +112 -0
- kodo/toolspecs/_run_subagent.py +62 -0
- kodo/toolspecs/_spec.py +117 -0
- kodo/toolspecs/_submit_evaluation.py +78 -0
- kodo/toolspecs/_toolchain_build.py +162 -0
- kodo/toolspecs/_toolchain_deps.py +141 -0
- kodo/toolspecs/_update_web_search_state.py +69 -0
- kodo/toolspecs/_visibility.py +77 -0
- kodo/toolspecs/_wait.py +63 -0
- kodo/toolspecs/_web_search.py +135 -0
- kodo/toolspecs/_workspace.py +18 -0
- kodo/toolspecs/py.typed +0 -0
- kodo/transport/__init__.py +213 -0
- kodo/transport/_connection.py +226 -0
- kodo/transport/_messages.py +894 -0
- kodo/transport/_outbox.py +102 -0
- kodo/transport/_ws.py +195 -0
- kodo/transport/py.typed +0 -0
- kodo/validator/__init__.py +70 -0
- kodo/validator/__main__.py +329 -0
- kodo/validator/_client.py +458 -0
- kodo/validator/_evaluate.py +378 -0
- kodo/validator/_harness.py +471 -0
- kodo/validator/_home.py +136 -0
- kodo/validator/_models.py +229 -0
- kodo/validator/_scenario.py +242 -0
- kodo/validator/_server.py +202 -0
- kodo/validator/_transcript.py +247 -0
- kodo/validator/_user.py +204 -0
- kodo/validator/_vllm.py +353 -0
- kodo/validator/_workspace.py +170 -0
- kodo/validator/prompts/__init__.py +18 -0
- kodo/validator/prompts/_registry.py +108 -0
- kodo/validator/prompts/tictactoe/detailed_task.md +28 -0
- kodo/validator/prompts/tictactoe/rvp.md +62 -0
- kodo/validator/prompts/tictactoe/sparse_task.md +15 -0
- kodo/validator/prompts/tictactoe/upp.md +37 -0
- kodo/validator/scenarios/__init__.py +175 -0
- kodo/validator/scenarios/__main__.py +225 -0
- kodo/validator/scenarios/qwen35-9b/tictactoe_console.py +40 -0
- kodo/validator/scenarios/qwen36-27b/tictactoe_upp.py +40 -0
- kodo/websearch/__init__.py +63 -0
- kodo/websearch/_browser.py +306 -0
- kodo/websearch/_curlfetch.py +77 -0
- kodo/websearch/_enginequery.py +107 -0
- kodo/websearch/_engines.py +230 -0
- kodo/websearch/_engines_static.py +240 -0
- kodo/websearch/_htmlextract.py +307 -0
- kodo/websearch/_readpage.py +418 -0
- kodo/websearch/_state.py +129 -0
- kodo/websearch/_validate.py +71 -0
- py_kodo-0.1.5.dist-info/METADATA +363 -0
- py_kodo-0.1.5.dist-info/RECORD +292 -0
- py_kodo-0.1.5.dist-info/WHEEL +4 -0
- py_kodo-0.1.5.dist-info/entry_points.txt +4 -0
- py_kodo-0.1.5.dist-info/licenses/LICENSE +201 -0
kodo/__init__.py
ADDED
kodo/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Portable third-party util manager (uv, ripgrep, fd) under ``~/.kodo/bin/``.
|
|
2
|
+
|
|
3
|
+
See :mod:`kodo.binutils._utils` for the manifest convention shared with the VS
|
|
4
|
+
Code extension and the install/lookup API. These are called **utils** (not
|
|
5
|
+
"tools") to avoid colliding with the agent-facing tool catalog in
|
|
6
|
+
:mod:`kodo.toolspecs`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._utils import (
|
|
10
|
+
UTIL_SPECS,
|
|
11
|
+
UtilInstall,
|
|
12
|
+
UtilSpec,
|
|
13
|
+
ensure_all_utils,
|
|
14
|
+
ensure_util,
|
|
15
|
+
find_util,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"UTIL_SPECS",
|
|
20
|
+
"UtilInstall",
|
|
21
|
+
"UtilSpec",
|
|
22
|
+
"ensure_all_utils",
|
|
23
|
+
"ensure_util",
|
|
24
|
+
"find_util",
|
|
25
|
+
]
|
kodo/binutils/_utils.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Portable third-party util manager.
|
|
2
|
+
|
|
3
|
+
Kōdo bundles three external CLI utils — **uv**, **ripgrep**, and **fd** — under
|
|
4
|
+
``~/.kodo/bin/``. Each util lives in its own directory with the binary placed
|
|
5
|
+
directly inside it, alongside a sibling JSON manifest recording the pinned
|
|
6
|
+
version, the absolute binary path, and the URL it was downloaded from::
|
|
7
|
+
|
|
8
|
+
~/.kodo/bin/
|
|
9
|
+
uv.json uv/uv (uv\\uv.exe on Windows)
|
|
10
|
+
ripgrep.json ripgrep/rg
|
|
11
|
+
fd.json fd/fd
|
|
12
|
+
|
|
13
|
+
These are called **utils** (not "tools") to avoid colliding with the agent-facing
|
|
14
|
+
tool catalog (``kodo.toolspecs.ToolSpec`` and friends), which is an unrelated
|
|
15
|
+
concept.
|
|
16
|
+
|
|
17
|
+
The manifest schema (``{name, version, path, download_url}``) is shared with the
|
|
18
|
+
VS Code extension (``kodo-vsix`` ``src/uv-setup.ts``), which installs **uv** the
|
|
19
|
+
same way before the Python backend exists. Both sides read the manifest and
|
|
20
|
+
only download when the pinned version is missing, so whichever runs first wins
|
|
21
|
+
and the other is a no-op.
|
|
22
|
+
|
|
23
|
+
The extension installs only uv (it needs uv to build the venv before any Python
|
|
24
|
+
runs); this module installs **all three**, so a future console-only build of
|
|
25
|
+
Kōdo works without the extension. Versions are pinned — bumping one is a code
|
|
26
|
+
change here (and in the extension, for uv).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import platform
|
|
34
|
+
import shutil
|
|
35
|
+
import stat
|
|
36
|
+
import tarfile
|
|
37
|
+
import tempfile
|
|
38
|
+
import urllib.request
|
|
39
|
+
import zipfile
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import cast
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"UtilInstall",
|
|
46
|
+
"UtilSpec",
|
|
47
|
+
"UTIL_SPECS",
|
|
48
|
+
"ensure_all_utils",
|
|
49
|
+
"ensure_util",
|
|
50
|
+
"find_util",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
_log = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
_USER_AGENT = "kodo-util-manager/0.1 (github.com/thehiddenone/kodo)"
|
|
56
|
+
|
|
57
|
+
_IS_WINDOWS = platform.system() == "Windows"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Util specs (pinned)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class UtilSpec:
|
|
67
|
+
"""A pinned third-party util installable into ``~/.kodo/bin/<name>/``.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
name: Util / manifest name (``"uv"``, ``"ripgrep"``, ``"fd"``).
|
|
71
|
+
version: Pinned release version.
|
|
72
|
+
binary: Unix binary name (``.exe`` is appended on Windows).
|
|
73
|
+
targets: ``"<os>-<arch>"`` platform key → release-target token embedded
|
|
74
|
+
in the asset filename. ``os`` ∈ ``darwin``/``linux``/``windows``;
|
|
75
|
+
``arch`` ∈ ``x86_64``/``aarch64``.
|
|
76
|
+
archive_template: Asset filename template using ``{version}``,
|
|
77
|
+
``{target}``, ``{ext}``.
|
|
78
|
+
url_template: Release download URL template using ``{version}``,
|
|
79
|
+
``{archive}`` (``{version}`` is the git tag, so embed any ``v``
|
|
80
|
+
prefix here).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
version: str
|
|
85
|
+
binary: str
|
|
86
|
+
targets: dict[str, str]
|
|
87
|
+
archive_template: str
|
|
88
|
+
url_template: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# x64-linux ripgrep ships only a musl build; arm64-linux ships gnu. All three
|
|
92
|
+
# utils now publish a native Windows-arm64 (aarch64-pc-windows-msvc) build.
|
|
93
|
+
UTIL_SPECS: dict[str, UtilSpec] = {
|
|
94
|
+
"uv": UtilSpec(
|
|
95
|
+
name="uv",
|
|
96
|
+
version="0.11.24",
|
|
97
|
+
binary="uv",
|
|
98
|
+
targets={
|
|
99
|
+
"darwin-x86_64": "x86_64-apple-darwin",
|
|
100
|
+
"darwin-aarch64": "aarch64-apple-darwin",
|
|
101
|
+
"linux-x86_64": "x86_64-unknown-linux-gnu",
|
|
102
|
+
"linux-aarch64": "aarch64-unknown-linux-gnu",
|
|
103
|
+
"windows-x86_64": "x86_64-pc-windows-msvc",
|
|
104
|
+
"windows-aarch64": "aarch64-pc-windows-msvc",
|
|
105
|
+
},
|
|
106
|
+
archive_template="uv-{target}.{ext}",
|
|
107
|
+
url_template="https://github.com/astral-sh/uv/releases/download/{version}/{archive}",
|
|
108
|
+
),
|
|
109
|
+
"ripgrep": UtilSpec(
|
|
110
|
+
name="ripgrep",
|
|
111
|
+
version="15.1.0",
|
|
112
|
+
binary="rg",
|
|
113
|
+
targets={
|
|
114
|
+
"darwin-x86_64": "x86_64-apple-darwin",
|
|
115
|
+
"darwin-aarch64": "aarch64-apple-darwin",
|
|
116
|
+
"linux-x86_64": "x86_64-unknown-linux-musl",
|
|
117
|
+
"linux-aarch64": "aarch64-unknown-linux-gnu",
|
|
118
|
+
"windows-x86_64": "x86_64-pc-windows-msvc",
|
|
119
|
+
"windows-aarch64": "aarch64-pc-windows-msvc",
|
|
120
|
+
},
|
|
121
|
+
archive_template="ripgrep-{version}-{target}.{ext}",
|
|
122
|
+
url_template="https://github.com/BurntSushi/ripgrep/releases/download/{version}/{archive}",
|
|
123
|
+
),
|
|
124
|
+
"fd": UtilSpec(
|
|
125
|
+
name="fd",
|
|
126
|
+
version="10.4.2",
|
|
127
|
+
binary="fd",
|
|
128
|
+
targets={
|
|
129
|
+
"darwin-x86_64": "x86_64-apple-darwin",
|
|
130
|
+
"darwin-aarch64": "aarch64-apple-darwin",
|
|
131
|
+
"linux-x86_64": "x86_64-unknown-linux-gnu",
|
|
132
|
+
"linux-aarch64": "aarch64-unknown-linux-gnu",
|
|
133
|
+
"windows-x86_64": "x86_64-pc-windows-msvc",
|
|
134
|
+
"windows-aarch64": "aarch64-pc-windows-msvc",
|
|
135
|
+
},
|
|
136
|
+
archive_template="fd-v{version}-{target}.{ext}",
|
|
137
|
+
url_template="https://github.com/sharkdp/fd/releases/download/v{version}/{archive}",
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class UtilInstall:
|
|
144
|
+
"""Metadata for an installed util (mirrors the on-disk manifest).
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
name: Util name.
|
|
148
|
+
version: Installed version.
|
|
149
|
+
path: Absolute path to the binary.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
name: str
|
|
153
|
+
version: str
|
|
154
|
+
path: Path
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Paths
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _bin_root(kodo_dir: Path) -> Path:
|
|
163
|
+
return kodo_dir / "bin"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _util_dir(kodo_dir: Path, name: str) -> Path:
|
|
167
|
+
return _bin_root(kodo_dir) / name
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _manifest_path(kodo_dir: Path, name: str) -> Path:
|
|
171
|
+
return _bin_root(kodo_dir) / f"{name}.json"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _binary_name(spec: UtilSpec) -> str:
|
|
175
|
+
return f"{spec.binary}.exe" if _IS_WINDOWS else spec.binary
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _binary_path(kodo_dir: Path, spec: UtilSpec) -> Path:
|
|
179
|
+
return _util_dir(kodo_dir, spec.name) / _binary_name(spec)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# Platform → release target
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _platform_key() -> str:
|
|
188
|
+
machine = platform.machine().lower()
|
|
189
|
+
arch = "aarch64" if machine in ("arm64", "aarch64") else "x86_64"
|
|
190
|
+
system = platform.system()
|
|
191
|
+
if system == "Windows":
|
|
192
|
+
os_key = "windows"
|
|
193
|
+
elif system == "Darwin":
|
|
194
|
+
os_key = "darwin"
|
|
195
|
+
elif system == "Linux":
|
|
196
|
+
os_key = "linux"
|
|
197
|
+
else:
|
|
198
|
+
raise RuntimeError(f"Unsupported platform: {system!r}")
|
|
199
|
+
return f"{os_key}-{arch}"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _resolve_asset(spec: UtilSpec) -> tuple[str, str]:
|
|
203
|
+
"""Return ``(archive_name, download_url)`` for the current platform."""
|
|
204
|
+
key = _platform_key()
|
|
205
|
+
target = spec.targets.get(key)
|
|
206
|
+
if target is None:
|
|
207
|
+
raise RuntimeError(f"{spec.name}: no release target for platform {key}")
|
|
208
|
+
ext = "zip" if _IS_WINDOWS else "tar.gz"
|
|
209
|
+
archive = spec.archive_template.format(version=spec.version, target=target, ext=ext)
|
|
210
|
+
url = spec.url_template.format(version=spec.version, archive=archive)
|
|
211
|
+
return archive, url
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Manifest I/O (schema shared with kodo-vsix src/uv-setup.ts)
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _read_manifest(kodo_dir: Path, name: str) -> dict[str, object] | None:
|
|
220
|
+
p = _manifest_path(kodo_dir, name)
|
|
221
|
+
if not p.is_file():
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
225
|
+
if isinstance(data, dict):
|
|
226
|
+
return cast(dict[str, object], data)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _write_manifest(kodo_dir: Path, name: str, version: str, path: Path, url: str) -> None:
|
|
233
|
+
data: dict[str, object] = {
|
|
234
|
+
"name": name,
|
|
235
|
+
"version": version,
|
|
236
|
+
"path": str(path),
|
|
237
|
+
"download_url": url,
|
|
238
|
+
}
|
|
239
|
+
p = _manifest_path(kodo_dir, name)
|
|
240
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
p.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Download + extract
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _download(url: str, dest: Path) -> None:
|
|
250
|
+
_log.info("Downloading %s", url)
|
|
251
|
+
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
|
|
252
|
+
with urllib.request.urlopen(req, timeout=600) as resp, dest.open("wb") as f:
|
|
253
|
+
shutil.copyfileobj(resp, f)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _extract_binary(archive: Path, ext: str, binary_name: str, dest_bin: Path) -> None:
|
|
257
|
+
"""Extract *archive* and copy the binary named *binary_name* to *dest_bin*.
|
|
258
|
+
|
|
259
|
+
The binary is nested in a versioned subdirectory inside the archive, so the
|
|
260
|
+
whole tree is searched for it.
|
|
261
|
+
"""
|
|
262
|
+
with tempfile.TemporaryDirectory(dir=str(dest_bin.parent)) as tmp:
|
|
263
|
+
tmp_dir = Path(tmp)
|
|
264
|
+
if ext == "zip":
|
|
265
|
+
with zipfile.ZipFile(archive, "r") as zf:
|
|
266
|
+
zf.extractall(tmp_dir)
|
|
267
|
+
else:
|
|
268
|
+
with tarfile.open(archive, "r:gz") as tf:
|
|
269
|
+
tf.extractall(tmp_dir, filter="data")
|
|
270
|
+
|
|
271
|
+
src = next((p for p in tmp_dir.rglob(binary_name) if p.is_file()), None)
|
|
272
|
+
if src is None:
|
|
273
|
+
raise RuntimeError(f"{binary_name} not found in downloaded archive {archive.name}")
|
|
274
|
+
|
|
275
|
+
dest_bin.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
shutil.copy2(src, dest_bin)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Public API
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def find_util(kodo_dir: Path, name: str) -> UtilInstall | None:
|
|
285
|
+
"""Return the installed util recorded in ``~/.kodo/bin/<name>.json``.
|
|
286
|
+
|
|
287
|
+
Reads the manifest only — no filesystem scanning. Returns ``None`` if the
|
|
288
|
+
manifest is absent/unreadable or the recorded binary no longer exists.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
kodo_dir (Path): User-level ``~/.kodo`` directory.
|
|
292
|
+
name (str): Util name (``"uv"``, ``"ripgrep"``, ``"fd"``).
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
UtilInstall | None: Install metadata, or ``None`` if not installed.
|
|
296
|
+
"""
|
|
297
|
+
meta = _read_manifest(kodo_dir, name)
|
|
298
|
+
if meta is None:
|
|
299
|
+
return None
|
|
300
|
+
try:
|
|
301
|
+
version = str(meta["version"])
|
|
302
|
+
path = Path(str(meta["path"]))
|
|
303
|
+
except KeyError:
|
|
304
|
+
return None
|
|
305
|
+
if not path.exists():
|
|
306
|
+
return None
|
|
307
|
+
return UtilInstall(name=name, version=version, path=path)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def ensure_util(kodo_dir: Path, name: str) -> UtilInstall:
|
|
311
|
+
"""Ensure *name* is installed at the pinned version and return its metadata.
|
|
312
|
+
|
|
313
|
+
No-op when the manifest already records the pinned version and the binary is
|
|
314
|
+
present on disk. Otherwise downloads the pinned release for the current
|
|
315
|
+
platform, extracts the binary into ``~/.kodo/bin/<name>/``, and writes the
|
|
316
|
+
manifest.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
kodo_dir (Path): User-level ``~/.kodo`` directory.
|
|
320
|
+
name (str): Util name; must be a key of :data:`UTIL_SPECS`.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
UtilInstall: Metadata for the installed util.
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
KeyError: If *name* is not a known util.
|
|
327
|
+
RuntimeError: If the platform is unsupported, or download/extraction
|
|
328
|
+
fails.
|
|
329
|
+
"""
|
|
330
|
+
spec = UTIL_SPECS[name]
|
|
331
|
+
bin_path = _binary_path(kodo_dir, spec)
|
|
332
|
+
|
|
333
|
+
meta = _read_manifest(kodo_dir, name)
|
|
334
|
+
if meta is not None and str(meta.get("version")) == spec.version and bin_path.exists():
|
|
335
|
+
_log.debug("%s %s already present", name, spec.version)
|
|
336
|
+
return UtilInstall(name=name, version=spec.version, path=bin_path)
|
|
337
|
+
|
|
338
|
+
archive_name, url = _resolve_asset(spec)
|
|
339
|
+
ext = "zip" if _IS_WINDOWS else "tar.gz"
|
|
340
|
+
util_dir = _util_dir(kodo_dir, name)
|
|
341
|
+
util_dir.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
archive_path = _bin_root(kodo_dir) / archive_name
|
|
343
|
+
|
|
344
|
+
_log.info("Installing %s %s (%s)", name, spec.version, archive_name)
|
|
345
|
+
try:
|
|
346
|
+
_download(url, archive_path)
|
|
347
|
+
_extract_binary(archive_path, ext, _binary_name(spec), bin_path)
|
|
348
|
+
finally:
|
|
349
|
+
archive_path.unlink(missing_ok=True)
|
|
350
|
+
|
|
351
|
+
if not _IS_WINDOWS:
|
|
352
|
+
bin_path.chmod(bin_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
353
|
+
|
|
354
|
+
_write_manifest(kodo_dir, name, spec.version, bin_path, url)
|
|
355
|
+
_log.info("%s %s installed at %s", name, spec.version, bin_path)
|
|
356
|
+
return UtilInstall(name=name, version=spec.version, path=bin_path)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def ensure_all_utils(kodo_dir: Path) -> dict[str, UtilInstall]:
|
|
360
|
+
"""Ensure every util in :data:`UTIL_SPECS` is installed (best-effort).
|
|
361
|
+
|
|
362
|
+
Each util is installed independently; a failure on one is logged and does
|
|
363
|
+
not prevent the others. Intended to be called once at server startup.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
kodo_dir (Path): User-level ``~/.kodo`` directory.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
dict[str, UtilInstall]: Successfully installed utils keyed by name.
|
|
370
|
+
"""
|
|
371
|
+
installed: dict[str, UtilInstall] = {}
|
|
372
|
+
for name in UTIL_SPECS:
|
|
373
|
+
try:
|
|
374
|
+
installed[name] = ensure_util(kodo_dir, name)
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
_log.warning("Failed to ensure util %r: %s", name, exc)
|
|
377
|
+
return installed
|
kodo/common/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Common shared types and protocols for Kōdo — no intra-kodo dependencies."""
|
|
2
|
+
|
|
3
|
+
from ._envelope import Envelope, MessageKind
|
|
4
|
+
from ._protocols import ApiKey, ApiKeyProvider, MessageSink, ResponseChannel
|
|
5
|
+
from ._tempdir import system_temp_roots
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Envelope",
|
|
9
|
+
"MessageKind",
|
|
10
|
+
"ApiKey",
|
|
11
|
+
"ApiKeyProvider",
|
|
12
|
+
"MessageSink",
|
|
13
|
+
"ResponseChannel",
|
|
14
|
+
"system_temp_roots",
|
|
15
|
+
]
|
kodo/common/_envelope.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Wire-protocol envelope: ``{kind, id, correlation_id?, payload}``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Literal, cast
|
|
9
|
+
|
|
10
|
+
__all__ = ["Envelope", "MessageKind"]
|
|
11
|
+
|
|
12
|
+
MessageKind = Literal[
|
|
13
|
+
"request",
|
|
14
|
+
"response",
|
|
15
|
+
"event",
|
|
16
|
+
"stream_chunk",
|
|
17
|
+
"thinking_chunk",
|
|
18
|
+
"toolgen_chunk",
|
|
19
|
+
"stream_end",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _new_id() -> str:
|
|
24
|
+
return uuid.uuid4().hex
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Envelope:
|
|
29
|
+
"""Single WebSocket frame — the atomic unit of the Kōdo wire protocol.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
kind: Frame type discriminator.
|
|
33
|
+
payload: Message-specific data.
|
|
34
|
+
id: Unique frame identifier (hex UUID).
|
|
35
|
+
correlation_id: ID of the request this frame responds to (optional).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
kind: MessageKind
|
|
39
|
+
payload: dict[str, object]
|
|
40
|
+
id: str = field(default_factory=_new_id)
|
|
41
|
+
correlation_id: str | None = None
|
|
42
|
+
|
|
43
|
+
def to_json(self) -> str:
|
|
44
|
+
"""Serialise this envelope to a JSON string.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: JSON-encoded envelope.
|
|
48
|
+
"""
|
|
49
|
+
d: dict[str, object] = {
|
|
50
|
+
"kind": self.kind,
|
|
51
|
+
"id": self.id,
|
|
52
|
+
"payload": self.payload,
|
|
53
|
+
}
|
|
54
|
+
if self.correlation_id is not None:
|
|
55
|
+
d["correlation_id"] = self.correlation_id
|
|
56
|
+
return json.dumps(d)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_json(cls, raw: str) -> Envelope:
|
|
60
|
+
"""Deserialise an envelope from a JSON string.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
raw (str): JSON-encoded envelope.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Envelope: Parsed envelope.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
KeyError: If required fields are absent.
|
|
70
|
+
ValueError: If JSON is invalid.
|
|
71
|
+
"""
|
|
72
|
+
d = cast(dict[str, object], json.loads(raw))
|
|
73
|
+
return cls(
|
|
74
|
+
kind=cast(MessageKind, d["kind"]),
|
|
75
|
+
payload=cast(dict[str, object], d.get("payload", {})),
|
|
76
|
+
id=str(d.get("id", _new_id())),
|
|
77
|
+
correlation_id=str(d["correlation_id"]) if "correlation_id" in d else None,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def make_response(cls, correlation_id: str, payload: dict[str, object]) -> Envelope:
|
|
82
|
+
"""Create a response envelope for a given request.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
correlation_id (str): ID of the originating request.
|
|
86
|
+
payload (dict[str, object]): Response payload.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Envelope: A ``response`` envelope.
|
|
90
|
+
"""
|
|
91
|
+
return cls(kind="response", payload=payload, correlation_id=correlation_id)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def make_event(cls, event_type: str, payload: dict[str, object]) -> Envelope:
|
|
95
|
+
"""Create an unsolicited server-push event envelope.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
event_type (str): Event discriminator (e.g. ``'agent.started'``).
|
|
99
|
+
payload (dict[str, object]): Event-specific fields.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Envelope: An ``event`` envelope with ``payload.type`` set.
|
|
103
|
+
"""
|
|
104
|
+
return cls(kind="event", payload={"type": event_type, **payload})
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def make_stream_chunk(cls, correlation_id: str, text: str) -> Envelope:
|
|
108
|
+
"""Create a streaming-token chunk envelope.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
correlation_id (str): Stream identifier.
|
|
112
|
+
text (str): Token text fragment.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Envelope: A ``stream_chunk`` envelope.
|
|
116
|
+
"""
|
|
117
|
+
return cls(
|
|
118
|
+
kind="stream_chunk",
|
|
119
|
+
payload={"type": "agent.tokens", "text": text},
|
|
120
|
+
correlation_id=correlation_id,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def make_thinking_chunk(cls, correlation_id: str, text: str) -> Envelope:
|
|
125
|
+
"""Create a thinking-token chunk envelope.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
correlation_id (str): Stream identifier.
|
|
129
|
+
text (str): Thinking text fragment.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Envelope: A ``thinking_chunk`` envelope.
|
|
133
|
+
"""
|
|
134
|
+
return cls(
|
|
135
|
+
kind="thinking_chunk",
|
|
136
|
+
payload={"type": "agent.thinking", "text": text},
|
|
137
|
+
correlation_id=correlation_id,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def make_toolgen_chunk(cls, correlation_id: str, tool_name: str, text: str) -> Envelope:
|
|
142
|
+
"""Create a tool-call-argument streaming chunk envelope.
|
|
143
|
+
|
|
144
|
+
Emitted as the model decodes a tool call's arguments (which can be
|
|
145
|
+
large, e.g. a whole file), so the client can show a live "generating"
|
|
146
|
+
indicator. Display-only; the assembled call still arrives separately.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
correlation_id (str): Stream identifier.
|
|
150
|
+
tool_name (str): Tool whose arguments are streaming (may be empty
|
|
151
|
+
on the first fragment).
|
|
152
|
+
text (str): Raw argument-text fragment.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Envelope: A ``toolgen_chunk`` envelope.
|
|
156
|
+
"""
|
|
157
|
+
return cls(
|
|
158
|
+
kind="toolgen_chunk",
|
|
159
|
+
payload={"type": "agent.toolgen", "tool_name": tool_name, "text": text},
|
|
160
|
+
correlation_id=correlation_id,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def make_stream_end(cls, correlation_id: str) -> Envelope:
|
|
165
|
+
"""Create a stream terminator envelope.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
correlation_id (str): Stream identifier matching the prior chunks.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Envelope: A ``stream_end`` envelope.
|
|
172
|
+
"""
|
|
173
|
+
return cls(kind="stream_end", payload={}, correlation_id=correlation_id)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Shared protocols and data types for decoupling consumers from implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from ._envelope import Envelope
|
|
10
|
+
|
|
11
|
+
__all__ = ["ApiKey", "ApiKeyProvider", "MessageSink", "ResponseChannel"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ApiKey:
|
|
16
|
+
"""Result of an API key request.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
vendor: Vendor identifier (e.g. ``'anthropic'``).
|
|
20
|
+
api_key: The key value; empty when ``error`` is set.
|
|
21
|
+
error: Non-``None`` when the request was rejected or cancelled.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
vendor: str
|
|
25
|
+
api_key: str
|
|
26
|
+
error: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MessageSink(Protocol):
|
|
30
|
+
"""Accepts outbound envelopes for delivery to the connected client."""
|
|
31
|
+
|
|
32
|
+
async def send(self, env: Envelope) -> None:
|
|
33
|
+
"""Send an envelope to the client.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
env (Envelope): The envelope to deliver.
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ResponseChannel(Protocol):
|
|
42
|
+
"""A per-session channel for server-initiated request/response round-trips.
|
|
43
|
+
|
|
44
|
+
Used by the key broker and gate orchestrator: send a ``kind=request`` frame
|
|
45
|
+
to the session's window and register a future to be resolved when the
|
|
46
|
+
matching ``kind=response`` arrives. Satisfied by the per-session transport
|
|
47
|
+
channel.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
async def send(self, env: Envelope) -> None: ...
|
|
51
|
+
|
|
52
|
+
def register_response_future(
|
|
53
|
+
self, request_id: str, future: asyncio.Future[dict[str, object]]
|
|
54
|
+
) -> None: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ApiKeyProvider(Protocol):
|
|
58
|
+
"""Requests API keys from the connected client (e.g. VSIX SecretStorage)."""
|
|
59
|
+
|
|
60
|
+
async def get_key(self, vendor: str) -> ApiKey:
|
|
61
|
+
"""Request the API key for *vendor* from the client.
|
|
62
|
+
|
|
63
|
+
Blocks until the client responds. Returns an :class:`ApiKey` with
|
|
64
|
+
``error`` set if the user cancelled or the connection dropped.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
vendor (str): Vendor identifier (e.g. ``'anthropic'``).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
ApiKey: Key result; check ``error`` before using ``api_key``.
|
|
71
|
+
"""
|
|
72
|
+
...
|