kodelet-sdk 0.1.0__tar.gz
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.
- kodelet_sdk-0.1.0/.github/workflows/ci.yml +39 -0
- kodelet_sdk-0.1.0/.github/workflows/release.yml +88 -0
- kodelet_sdk-0.1.0/.gitignore +9 -0
- kodelet_sdk-0.1.0/AGENTS.md +72 -0
- kodelet_sdk-0.1.0/LICENSE +21 -0
- kodelet_sdk-0.1.0/Makefile +58 -0
- kodelet_sdk-0.1.0/PKG-INFO +134 -0
- kodelet_sdk-0.1.0/README.md +121 -0
- kodelet_sdk-0.1.0/VERSION.txt +1 -0
- kodelet_sdk-0.1.0/examples/review/kodelet-extension-review +56 -0
- kodelet_sdk-0.1.0/examples/review/reviewer-prompt.md +118 -0
- kodelet_sdk-0.1.0/examples/workspace/kodelet-extension-workspace +235 -0
- kodelet_sdk-0.1.0/pyproject.toml +51 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/__init__.py +66 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/_utils.py +64 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/api.py +684 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/context.py +554 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/py.typed +0 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/runtime.py +246 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/schemas.py +57 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/template.py +30 -0
- kodelet_sdk-0.1.0/src/kodelet_sdk/test_harness.py +95 -0
- kodelet_sdk-0.1.0/tests/test_api.py +523 -0
- kodelet_sdk-0.1.0/tests/test_examples.py +189 -0
- kodelet_sdk-0.1.0/uv.lock +425 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
python-sdk:
|
|
10
|
+
name: Python SDK
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Check out repository
|
|
14
|
+
uses: actions/checkout@v6
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.14"
|
|
20
|
+
|
|
21
|
+
- name: Set up uv
|
|
22
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --locked
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run -- ruff check
|
|
31
|
+
|
|
32
|
+
- name: Type check
|
|
33
|
+
run: uv run -- ty check
|
|
34
|
+
|
|
35
|
+
- name: Test
|
|
36
|
+
run: uv run -- pytest -q
|
|
37
|
+
|
|
38
|
+
- name: Build
|
|
39
|
+
run: uv build
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
name: Publish to PyPI
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment:
|
|
17
|
+
name: pypi
|
|
18
|
+
url: https://pypi.org/p/kodelet-sdk
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out repository
|
|
21
|
+
uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.14"
|
|
27
|
+
|
|
28
|
+
- name: Set up uv
|
|
29
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
30
|
+
with:
|
|
31
|
+
enable-cache: true
|
|
32
|
+
|
|
33
|
+
- name: Install dependencies
|
|
34
|
+
run: uv sync --locked
|
|
35
|
+
|
|
36
|
+
- name: Verify tag matches VERSION.txt
|
|
37
|
+
run: |
|
|
38
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
39
|
+
FILE_VERSION="$(cat VERSION.txt)"
|
|
40
|
+
if [ "${TAG_VERSION}" != "${FILE_VERSION}" ]; then
|
|
41
|
+
echo "tag version ${TAG_VERSION} does not match VERSION.txt ${FILE_VERSION}" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
uv run -- python - <<'PY' "${FILE_VERSION}"
|
|
45
|
+
import sys
|
|
46
|
+
from packaging.version import Version
|
|
47
|
+
version = sys.argv[1]
|
|
48
|
+
parsed = Version(version)
|
|
49
|
+
if str(parsed) != version:
|
|
50
|
+
raise SystemExit(f"version must be normalized PEP 440; got {version!r}, expected {parsed!s}")
|
|
51
|
+
PY
|
|
52
|
+
|
|
53
|
+
- name: Lint
|
|
54
|
+
run: uv run -- ruff check
|
|
55
|
+
|
|
56
|
+
- name: Type check
|
|
57
|
+
run: uv run -- ty check
|
|
58
|
+
|
|
59
|
+
- name: Test
|
|
60
|
+
run: uv run -- pytest -q
|
|
61
|
+
|
|
62
|
+
- name: Build distributions
|
|
63
|
+
run: uv build
|
|
64
|
+
|
|
65
|
+
- name: Check built metadata version
|
|
66
|
+
run: |
|
|
67
|
+
uv run -- python - <<'PY'
|
|
68
|
+
import zipfile
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
|
|
71
|
+
expected = Path("VERSION.txt").read_text(encoding="utf-8").strip()
|
|
72
|
+
wheels = sorted(Path("dist").glob("kodelet_sdk-*.whl"))
|
|
73
|
+
if len(wheels) != 1:
|
|
74
|
+
raise SystemExit(f"expected one wheel, found {wheels}")
|
|
75
|
+
with zipfile.ZipFile(wheels[0]) as wheel:
|
|
76
|
+
metadata_name = next(name for name in wheel.namelist() if name.endswith(".dist-info/METADATA"))
|
|
77
|
+
metadata = wheel.read(metadata_name).decode("utf-8")
|
|
78
|
+
if f"Version: {expected}\n" not in metadata:
|
|
79
|
+
raise SystemExit(f"wheel metadata does not contain Version: {expected}")
|
|
80
|
+
PY
|
|
81
|
+
|
|
82
|
+
- name: Publish to PyPI
|
|
83
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
84
|
+
|
|
85
|
+
- name: Upload release artifacts
|
|
86
|
+
uses: softprops/action-gh-release@v2
|
|
87
|
+
with:
|
|
88
|
+
files: dist/*
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# kodelet-sdk Python SDK
|
|
2
|
+
|
|
3
|
+
## Project overview
|
|
4
|
+
|
|
5
|
+
This repository contains `kodelet-sdk`, a Python SDK for authoring Kodelet extensions. It mirrors the TypeScript SDK protocol shape while using Python idioms: asyncio runtime, decorators, Pydantic schemas, and Jinja2 templates.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
src/kodelet_sdk/ # SDK source
|
|
11
|
+
api.py # Extension registration/dispatch API
|
|
12
|
+
context.py # Tool/command/event context helpers
|
|
13
|
+
runtime.py # stdio JSON-RPC runtime
|
|
14
|
+
schemas.py # Pydantic/JSON Schema bridge
|
|
15
|
+
template.py # Jinja2 rendering helper
|
|
16
|
+
test_harness.py # In-process extension test harness
|
|
17
|
+
examples/ # Runnable example extensions
|
|
18
|
+
review/ # Review command extension
|
|
19
|
+
workspace/ # Workspace helper/policy extension
|
|
20
|
+
tests/ # pytest coverage
|
|
21
|
+
.github/workflows/ # GitHub Actions CI
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Tooling
|
|
25
|
+
|
|
26
|
+
Use `uv` for dependency management and command execution. Do not use raw `pip` or ad-hoc virtualenv commands.
|
|
27
|
+
|
|
28
|
+
Common commands:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync
|
|
32
|
+
uv run -- ruff check
|
|
33
|
+
uv run -- ty check
|
|
34
|
+
uv run -- pytest -q
|
|
35
|
+
uv build
|
|
36
|
+
make check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run all gates before considering a change complete:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run -- ruff check && uv run -- ty check && uv run -- pytest -q && uv build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Generated build artifacts under `dist/`, virtualenvs, caches, and `__pycache__` should not be committed.
|
|
46
|
+
|
|
47
|
+
## Releases
|
|
48
|
+
|
|
49
|
+
Package versions are sourced from `VERSION.txt` via Hatchling dynamic version metadata. Edit `VERSION.txt` manually, commit it, then use the Makefile helper:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git add VERSION.txt pyproject.toml uv.lock
|
|
53
|
+
git commit -m "chore: release v0.1.0"
|
|
54
|
+
make release
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`make release` runs `make check`, creates the `v$(cat VERSION.txt)` tag, and pushes the branch and tag. The tag push triggers `.github/workflows/release.yml`, which publishes to PyPI through trusted publishing/OIDC (`id-token: write`) and uploads artifacts to the GitHub release.
|
|
58
|
+
|
|
59
|
+
## Coding conventions
|
|
60
|
+
|
|
61
|
+
- Keep public APIs documented with useful docstrings and parameter descriptions.
|
|
62
|
+
- Prefer asyncio-compatible implementations for runtime/context features.
|
|
63
|
+
- Use Pydantic for schema validation and JSON Schema generation.
|
|
64
|
+
- Use Jinja2 for template rendering.
|
|
65
|
+
- Keep examples importable without side effects; guard runtime startup with `if __name__ == "__main__"`.
|
|
66
|
+
- Example entrypoints should be executable `kodelet-extension-*` wrappers; keep importable implementation code in adjacent `*_extension.py` files.
|
|
67
|
+
- Prefer SDK re-exports in examples (`from kodelet_sdk import BaseModel, Extension, Field, render_template`) so examples are self-contained.
|
|
68
|
+
- When adding public functionality, add focused pytest coverage and keep type/lint checks green.
|
|
69
|
+
|
|
70
|
+
## README guidance
|
|
71
|
+
|
|
72
|
+
Keep `README.md` focused on users of the SDK: quick start, public API shape, examples, and runtime behavior. Put contributor/agent workflow details here in `AGENTS.md` instead of expanding the README.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jingkai He
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
VERSION := $(shell cat VERSION.txt)
|
|
2
|
+
TAG := v$(VERSION)
|
|
3
|
+
|
|
4
|
+
.PHONY: help sync lint typecheck test build check version tag release clean
|
|
5
|
+
|
|
6
|
+
help:
|
|
7
|
+
@printf '%s\n' \
|
|
8
|
+
'Targets:' \
|
|
9
|
+
' sync Install/update the uv environment' \
|
|
10
|
+
' lint Run ruff' \
|
|
11
|
+
' typecheck Run ty' \
|
|
12
|
+
' test Run pytest' \
|
|
13
|
+
' build Build sdist and wheel' \
|
|
14
|
+
' check Run lint, typecheck, tests, and build' \
|
|
15
|
+
' version Print VERSION.txt' \
|
|
16
|
+
' tag Create git tag v$$(cat VERSION.txt)' \
|
|
17
|
+
' release Run check, create tag, and push branch+tag' \
|
|
18
|
+
' clean Remove build/test caches'
|
|
19
|
+
|
|
20
|
+
sync:
|
|
21
|
+
uv sync
|
|
22
|
+
|
|
23
|
+
lint:
|
|
24
|
+
uv run -- ruff check
|
|
25
|
+
|
|
26
|
+
typecheck:
|
|
27
|
+
uv run -- ty check
|
|
28
|
+
|
|
29
|
+
test:
|
|
30
|
+
uv run -- pytest -q
|
|
31
|
+
|
|
32
|
+
build:
|
|
33
|
+
uv build
|
|
34
|
+
|
|
35
|
+
check: lint typecheck test build
|
|
36
|
+
|
|
37
|
+
version:
|
|
38
|
+
@cat VERSION.txt
|
|
39
|
+
|
|
40
|
+
tag:
|
|
41
|
+
@uv run -- python -c 'from packaging.version import Version; import pathlib; v=pathlib.Path("VERSION.txt").read_text().strip(); assert str(Version(v)) == v, f"version must be normalized PEP 440: {v}"'
|
|
42
|
+
@if ! git diff --quiet -- VERSION.txt pyproject.toml uv.lock; then \
|
|
43
|
+
echo 'version-related files have uncommitted changes; commit them before tagging' >&2; \
|
|
44
|
+
exit 2; \
|
|
45
|
+
fi
|
|
46
|
+
@if git rev-parse '$(TAG)' >/dev/null 2>&1; then \
|
|
47
|
+
echo 'tag $(TAG) already exists' >&2; \
|
|
48
|
+
exit 2; \
|
|
49
|
+
fi
|
|
50
|
+
git tag '$(TAG)'
|
|
51
|
+
|
|
52
|
+
release: check tag
|
|
53
|
+
git push origin HEAD
|
|
54
|
+
git push origin '$(TAG)'
|
|
55
|
+
|
|
56
|
+
clean:
|
|
57
|
+
rm -rf dist build *.egg-info .pytest_cache .ruff_cache
|
|
58
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kodelet-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for authoring Kodelet extensions
|
|
5
|
+
Project-URL: Repository, https://github.com/jingkaihe/kodelet
|
|
6
|
+
Author: Kodelet
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
11
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# kodelet-sdk
|
|
15
|
+
|
|
16
|
+
Python SDK for authoring [Kodelet](https://github.com/jingkaihe/kodelet) extensions.
|
|
17
|
+
|
|
18
|
+
The SDK speaks Kodelet's JSON-RPC extension protocol over stdio and provides an asyncio-first API for registering tools, commands, and event handlers.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from kodelet_sdk import BaseModel, Extension
|
|
24
|
+
|
|
25
|
+
ext = Extension(name="weather", version="0.1.0")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WeatherInput(BaseModel):
|
|
29
|
+
location: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@ext.tool("get_weather", description="Get weather", input_schema=WeatherInput)
|
|
33
|
+
async def get_weather(input: WeatherInput, ctx):
|
|
34
|
+
return {"content": f"Weather for {input.location}"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@ext.on("session.start")
|
|
38
|
+
async def session_start(event, ctx):
|
|
39
|
+
ctx.log.info("extension started")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
ext.run_sync()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Public API
|
|
47
|
+
|
|
48
|
+
### Extension registration
|
|
49
|
+
|
|
50
|
+
- `Extension(name=None, version=None)` creates an extension host.
|
|
51
|
+
- `@ext.tool(name=None, description=None, input_schema=None, timeout_in_sec=None)` registers a tool.
|
|
52
|
+
- `@ext.command(name=None, description=None, input_schema=None, aliases=None, kind=None, timeout_in_sec=None)` registers a command.
|
|
53
|
+
- `@ext.on(event, priority=0, timeout_in_sec=None)` registers an event handler such as `session.start`, `tool.call`, or `agent.end`.
|
|
54
|
+
- `await ext.run()` starts the async stdio runtime; `ext.run_sync()` is a synchronous entrypoint convenience.
|
|
55
|
+
|
|
56
|
+
Handlers may be synchronous or asynchronous. Tool handlers may return a string, which is converted to `{ "content": ... }`, or a protocol-shaped mapping. Command handlers return `{ "action": "pass" }`, `{ "action": "respond", "response": ... }`, or `{ "action": "runAgent", "prompt": ... }`.
|
|
57
|
+
|
|
58
|
+
### Pydantic and Jinja2 bridge dependencies
|
|
59
|
+
|
|
60
|
+
`kodelet-sdk` depends on Pydantic and Jinja2 and re-exports common entry points so extensions can be self-contained:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from kodelet_sdk import BaseModel, Field, Jinja2, Pydantic, render_template
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ReviewInput(BaseModel):
|
|
67
|
+
target: str = Field(min_length=1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
assert render_template("Review {{ target }}", {"target": "main"}) == "Review main"
|
|
71
|
+
assert Jinja2.Template("Hello {{ name }}").render(name="Kodelet") == "Hello Kodelet"
|
|
72
|
+
assert Pydantic.TypeAdapter(int).validate_python("1") == 1
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pydantic input schemas are converted to JSON Schema during initialization and validate incoming tool/command inputs before handlers run. Commands with validation failures return `{"action": "pass"}` so another command route can handle the invocation.
|
|
76
|
+
|
|
77
|
+
### Context helpers
|
|
78
|
+
|
|
79
|
+
Handlers receive `ctx` with Kodelet call metadata and helper namespaces:
|
|
80
|
+
|
|
81
|
+
- `ctx.storage.read_text/write_text/read_json/write_json(...)` for extension data files.
|
|
82
|
+
- `ctx.path.resolve_workspace_path(...)` and `ctx.path.relative_to_workspace(...)`.
|
|
83
|
+
- `ctx.fs.exists/read_text/write_text/list(...)` for workspace file access.
|
|
84
|
+
- `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
|
|
85
|
+
- `ctx.env.get(...)` for environment access.
|
|
86
|
+
- `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
|
|
87
|
+
- `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
|
|
88
|
+
|
|
89
|
+
### Testing extensions
|
|
90
|
+
|
|
91
|
+
Use `create_test_harness` to exercise registrations without spawning a subprocess:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from kodelet_sdk import Extension, create_test_harness
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def test_tool():
|
|
98
|
+
ext = Extension(name="example")
|
|
99
|
+
|
|
100
|
+
@ext.tool("echo", description="Echo", input_schema={"type": "object"})
|
|
101
|
+
async def echo(input, ctx):
|
|
102
|
+
return {"content": input["text"]}
|
|
103
|
+
|
|
104
|
+
harness = await create_test_harness(ext)
|
|
105
|
+
result = await harness.execute_tool({"name": "echo", "input": {"text": "hi"}})
|
|
106
|
+
assert result == {"content": "hi"}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
Runnable example extensions live in `examples/`:
|
|
112
|
+
|
|
113
|
+
- `examples/review/kodelet-extension-review` is a review command extension.
|
|
114
|
+
- `examples/workspace/kodelet-extension-workspace` is a workspace helper/policy extension.
|
|
115
|
+
|
|
116
|
+
From a checked-out SDK repository, run an example with:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv run -s examples/review/kodelet-extension-review
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The `kodelet-extension-*` files are executable wrappers so Kodelet can discover and launch them directly.
|
|
123
|
+
|
|
124
|
+
## Releases
|
|
125
|
+
|
|
126
|
+
Package versions are read from `VERSION.txt`. To publish a release, configure PyPI Trusted Publishing for the `Release` workflow, then update and commit `VERSION.txt` manually:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git add VERSION.txt pyproject.toml uv.lock
|
|
130
|
+
git commit -m "chore: release v0.1.0"
|
|
131
|
+
make release
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Pushing the `vX.Y.Z` tag runs the GitHub Actions release workflow, builds the package, and publishes to PyPI using OIDC trusted publishing.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# kodelet-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for authoring [Kodelet](https://github.com/jingkaihe/kodelet) extensions.
|
|
4
|
+
|
|
5
|
+
The SDK speaks Kodelet's JSON-RPC extension protocol over stdio and provides an asyncio-first API for registering tools, commands, and event handlers.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from kodelet_sdk import BaseModel, Extension
|
|
11
|
+
|
|
12
|
+
ext = Extension(name="weather", version="0.1.0")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WeatherInput(BaseModel):
|
|
16
|
+
location: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@ext.tool("get_weather", description="Get weather", input_schema=WeatherInput)
|
|
20
|
+
async def get_weather(input: WeatherInput, ctx):
|
|
21
|
+
return {"content": f"Weather for {input.location}"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@ext.on("session.start")
|
|
25
|
+
async def session_start(event, ctx):
|
|
26
|
+
ctx.log.info("extension started")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
ext.run_sync()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Public API
|
|
34
|
+
|
|
35
|
+
### Extension registration
|
|
36
|
+
|
|
37
|
+
- `Extension(name=None, version=None)` creates an extension host.
|
|
38
|
+
- `@ext.tool(name=None, description=None, input_schema=None, timeout_in_sec=None)` registers a tool.
|
|
39
|
+
- `@ext.command(name=None, description=None, input_schema=None, aliases=None, kind=None, timeout_in_sec=None)` registers a command.
|
|
40
|
+
- `@ext.on(event, priority=0, timeout_in_sec=None)` registers an event handler such as `session.start`, `tool.call`, or `agent.end`.
|
|
41
|
+
- `await ext.run()` starts the async stdio runtime; `ext.run_sync()` is a synchronous entrypoint convenience.
|
|
42
|
+
|
|
43
|
+
Handlers may be synchronous or asynchronous. Tool handlers may return a string, which is converted to `{ "content": ... }`, or a protocol-shaped mapping. Command handlers return `{ "action": "pass" }`, `{ "action": "respond", "response": ... }`, or `{ "action": "runAgent", "prompt": ... }`.
|
|
44
|
+
|
|
45
|
+
### Pydantic and Jinja2 bridge dependencies
|
|
46
|
+
|
|
47
|
+
`kodelet-sdk` depends on Pydantic and Jinja2 and re-exports common entry points so extensions can be self-contained:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from kodelet_sdk import BaseModel, Field, Jinja2, Pydantic, render_template
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ReviewInput(BaseModel):
|
|
54
|
+
target: str = Field(min_length=1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
assert render_template("Review {{ target }}", {"target": "main"}) == "Review main"
|
|
58
|
+
assert Jinja2.Template("Hello {{ name }}").render(name="Kodelet") == "Hello Kodelet"
|
|
59
|
+
assert Pydantic.TypeAdapter(int).validate_python("1") == 1
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pydantic input schemas are converted to JSON Schema during initialization and validate incoming tool/command inputs before handlers run. Commands with validation failures return `{"action": "pass"}` so another command route can handle the invocation.
|
|
63
|
+
|
|
64
|
+
### Context helpers
|
|
65
|
+
|
|
66
|
+
Handlers receive `ctx` with Kodelet call metadata and helper namespaces:
|
|
67
|
+
|
|
68
|
+
- `ctx.storage.read_text/write_text/read_json/write_json(...)` for extension data files.
|
|
69
|
+
- `ctx.path.resolve_workspace_path(...)` and `ctx.path.relative_to_workspace(...)`.
|
|
70
|
+
- `ctx.fs.exists/read_text/write_text/list(...)` for workspace file access.
|
|
71
|
+
- `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
|
|
72
|
+
- `ctx.env.get(...)` for environment access.
|
|
73
|
+
- `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
|
|
74
|
+
- `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
|
|
75
|
+
|
|
76
|
+
### Testing extensions
|
|
77
|
+
|
|
78
|
+
Use `create_test_harness` to exercise registrations without spawning a subprocess:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from kodelet_sdk import Extension, create_test_harness
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def test_tool():
|
|
85
|
+
ext = Extension(name="example")
|
|
86
|
+
|
|
87
|
+
@ext.tool("echo", description="Echo", input_schema={"type": "object"})
|
|
88
|
+
async def echo(input, ctx):
|
|
89
|
+
return {"content": input["text"]}
|
|
90
|
+
|
|
91
|
+
harness = await create_test_harness(ext)
|
|
92
|
+
result = await harness.execute_tool({"name": "echo", "input": {"text": "hi"}})
|
|
93
|
+
assert result == {"content": "hi"}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
Runnable example extensions live in `examples/`:
|
|
99
|
+
|
|
100
|
+
- `examples/review/kodelet-extension-review` is a review command extension.
|
|
101
|
+
- `examples/workspace/kodelet-extension-workspace` is a workspace helper/policy extension.
|
|
102
|
+
|
|
103
|
+
From a checked-out SDK repository, run an example with:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv run -s examples/review/kodelet-extension-review
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The `kodelet-extension-*` files are executable wrappers so Kodelet can discover and launch them directly.
|
|
110
|
+
|
|
111
|
+
## Releases
|
|
112
|
+
|
|
113
|
+
Package versions are read from `VERSION.txt`. To publish a release, configure PyPI Trusted Publishing for the `Release` workflow, then update and commit `VERSION.txt` manually:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git add VERSION.txt pyproject.toml uv.lock
|
|
117
|
+
git commit -m "chore: release v0.1.0"
|
|
118
|
+
make release
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Pushing the `vX.Y.Z` tag runs the GitHub Actions release workflow, builds the package, and publishes to PyPI using OIDC trusted publishing.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = ["kodelet-sdk"]
|
|
5
|
+
# [tool.uv.sources]
|
|
6
|
+
# kodelet-sdk = { path = "../..", editable = true }
|
|
7
|
+
# ///
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from kodelet_sdk import BaseModel, Extension, Field, render_template
|
|
13
|
+
|
|
14
|
+
PROMPT_PATH = Path(__file__).with_name("reviewer-prompt.md")
|
|
15
|
+
|
|
16
|
+
ext = Extension(name="review", version="0.1.0")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReviewChangesInput(BaseModel):
|
|
20
|
+
target: str = Field(default="HEAD", description="Git ref or branch to compare against")
|
|
21
|
+
focus: str = Field(
|
|
22
|
+
default="correctness, tests, and maintainability",
|
|
23
|
+
description="What the review should emphasize",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@ext.command(
|
|
28
|
+
"review",
|
|
29
|
+
aliases=["/review"],
|
|
30
|
+
description="Review local git changes against a target ref",
|
|
31
|
+
input_schema=ReviewChangesInput,
|
|
32
|
+
kind="recipe",
|
|
33
|
+
)
|
|
34
|
+
async def review_changes(input: ReviewChangesInput, ctx):
|
|
35
|
+
status = await ctx.process.exec("git", ["status", "--short"], {"timeoutMs": 2_000})
|
|
36
|
+
diff = await ctx.process.exec("git", ["diff", "--stat", input.target], {"timeoutMs": 5_000})
|
|
37
|
+
prompt_template = await ctx.fs.read_text(str(PROMPT_PATH))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"action": "runAgent",
|
|
41
|
+
"recipeName": "review",
|
|
42
|
+
"prompt": render_template(
|
|
43
|
+
prompt_template,
|
|
44
|
+
{
|
|
45
|
+
"target": input.target,
|
|
46
|
+
"focus": input.focus,
|
|
47
|
+
"gitStatus": status.stdout.strip()
|
|
48
|
+
or "No changes reported by git status --short.",
|
|
49
|
+
"diffStat": diff.stdout.strip() or f"No diff stat against {input.target}.",
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
ext.run_sync()
|