kodelet-sdk 0.2.0__tar.gz → 0.2.2__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.
Files changed (57) hide show
  1. kodelet_sdk-0.2.2/PKG-INFO +425 -0
  2. kodelet_sdk-0.2.2/README.md +412 -0
  3. kodelet_sdk-0.2.2/VERSION.txt +1 -0
  4. kodelet_sdk-0.2.2/examples/sdk/README.md +26 -0
  5. kodelet_sdk-0.2.2/examples/sdk/inline-extension-session +48 -0
  6. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/__init__.py +2 -1
  7. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/agent/__init__.py +0 -13
  8. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/agent/client.py +29 -90
  9. kodelet_sdk-0.2.2/src/kodelet_sdk/agent/relay.py +243 -0
  10. kodelet_sdk-0.2.2/src/kodelet_sdk/agent/rpc.py +393 -0
  11. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/agent/session.py +3 -12
  12. kodelet_sdk-0.2.2/src/kodelet_sdk/agent/transport.py +55 -0
  13. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/agent/types.py +8 -1
  14. kodelet_sdk-0.2.2/src/kodelet_sdk/child.py +226 -0
  15. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/runtime.py +169 -80
  16. kodelet_sdk-0.2.2/tests/test_acp_transport.py +613 -0
  17. kodelet_sdk-0.2.2/tests/test_agent.py +1702 -0
  18. kodelet_sdk-0.2.2/tests/test_child.py +498 -0
  19. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/tests/test_examples.py +17 -0
  20. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/tests/test_runtime.py +534 -4
  21. kodelet_sdk-0.2.0/PKG-INFO +0 -431
  22. kodelet_sdk-0.2.0/README.md +0 -418
  23. kodelet_sdk-0.2.0/VERSION.txt +0 -1
  24. kodelet_sdk-0.2.0/examples/sdk/README.md +0 -22
  25. kodelet_sdk-0.2.0/examples/sdk/inline-extension-session +0 -72
  26. kodelet_sdk-0.2.0/src/kodelet_sdk/agent/bridge.py +0 -618
  27. kodelet_sdk-0.2.0/src/kodelet_sdk/agent/rpc.py +0 -259
  28. kodelet_sdk-0.2.0/src/kodelet_sdk/agent/transport.py +0 -196
  29. kodelet_sdk-0.2.0/src/kodelet_sdk/child.py +0 -111
  30. kodelet_sdk-0.2.0/tests/test_agent.py +0 -1395
  31. kodelet_sdk-0.2.0/tests/test_child.py +0 -85
  32. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/.github/workflows/ci.yml +0 -0
  33. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/.github/workflows/release.yml +0 -0
  34. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/.gitignore +0 -0
  35. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/AGENTS.md +0 -0
  36. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/LICENSE +0 -0
  37. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/Makefile +0 -0
  38. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/examples/review/kodelet-extension-review +0 -0
  39. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/examples/review/reviewer-prompt.md +0 -0
  40. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/examples/sdk/basic-agent-session +0 -0
  41. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/examples/sdk/streaming-agent-session +0 -0
  42. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/examples/workspace/kodelet-extension-workspace +0 -0
  43. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/pyproject.toml +0 -0
  44. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/_utils.py +0 -0
  45. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/api.py +0 -0
  46. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/context.py +0 -0
  47. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/execution.py +0 -0
  48. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/py.typed +0 -0
  49. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/schemas.py +0 -0
  50. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/task_progress.py +0 -0
  51. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/template.py +0 -0
  52. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/src/kodelet_sdk/test_harness.py +0 -0
  53. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/tests/fixtures/workspace_policy_extension.py +0 -0
  54. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/tests/test_api.py +0 -0
  55. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/tests/test_persistent_ui.py +0 -0
  56. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/tests/test_task_progress.py +0 -0
  57. {kodelet_sdk-0.2.0 → kodelet_sdk-0.2.2}/uv.lock +0 -0
@@ -0,0 +1,425 @@
1
+ Metadata-Version: 2.5
2
+ Name: kodelet-sdk
3
+ Version: 0.2.2
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
+ Run [Kodelet](https://github.com/jingkaihe/kodelet) sessions and write tools, commands, and event handlers in Python.
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ uv add kodelet-sdk
22
+ ```
23
+
24
+ An executable extension registers handlers and serves them over stdio:
25
+
26
+ ```python
27
+ from kodelet_sdk import BaseModel, Extension, ToolContext, ToolExecutionResult
28
+
29
+ ext = Extension(name="weather", version="0.1.0")
30
+
31
+
32
+ class WeatherInput(BaseModel):
33
+ location: str
34
+
35
+
36
+ @ext.tool("get_weather", description="Get weather", input_schema=WeatherInput)
37
+ async def get_weather(input: WeatherInput, ctx: ToolContext) -> ToolExecutionResult:
38
+ return {"content": f"Weather for {input.location}"}
39
+
40
+
41
+ @ext.on("session.start")
42
+ async def session_start(event, ctx):
43
+ ctx.log.info("extension started")
44
+
45
+
46
+ if __name__ == "__main__":
47
+ ext.run_sync()
48
+ ```
49
+
50
+ ## Agent sessions
51
+
52
+ `Client` uses your configured `kodelet` CLI to connect to a daemon and runner. Set `server` and `runner` on `Client` to select them explicitly. Provider credentials stay on the daemon; session `cwd` refers to the runner's workspace.
53
+
54
+ Run the following snippets inside an async function. The inline-extension example below includes a complete script.
55
+
56
+ ```python
57
+ from kodelet_sdk import Client
58
+
59
+ client = Client()
60
+ session = await client.create_session()
61
+ response = await session.run_and_wait(message="what is the meaning of life?")
62
+
63
+ print(response.content)
64
+ await client.close()
65
+ ```
66
+
67
+ ### Streaming
68
+
69
+ Choose a profile and subscribe to session events:
70
+
71
+ ```python
72
+ from kodelet_sdk import Client
73
+
74
+ client = Client()
75
+ session = await client.create_session(
76
+ profile="work", # A model profile configured on your daemon.
77
+ max_turns=4,
78
+ streaming=True,
79
+ )
80
+
81
+ session.on(
82
+ "assistant.message_delta",
83
+ lambda event: print(event.data.deltaContent, end="", flush=True),
84
+ )
85
+ session.on(
86
+ "tool.update",
87
+ lambda event: print(f"partial {event.data.toolCallId}: {event.data.result}"),
88
+ )
89
+ session.on(
90
+ "tool.result",
91
+ lambda event: print(f"final {event.data.toolCallId}: {event.data.result}"),
92
+ )
93
+
94
+ response = await session.run_and_wait(message="help me choose an approach")
95
+ print("\nfinal:", response.content)
96
+ await client.close()
97
+ ```
98
+
99
+ `tool.update` replaces the previous snapshot for that tool call; it is not a delta. Listeners receive every update, while `response.events` keeps only the latest snapshot and the final result.
100
+
101
+ Session options include:
102
+
103
+ | Option | Purpose |
104
+ | --- | --- |
105
+ | `profile` | A daemon profile name, `Profile`, or inline model settings |
106
+ | `options` | An `ExecutionOptions` instance or mapping of execution limits and restrictions |
107
+ | `environment_profile` | A runner-owned environment profile |
108
+ | `cwd` | Working directory on the runner |
109
+ | `resume` | An existing conversation ID |
110
+
111
+ Inline settings cannot include provider secrets, endpoints, or local prompt paths.
112
+
113
+ ### Inline extensions
114
+
115
+ Pass `extensions=[ext]` to expose local Python callbacks as agent tools. This calculator runs in your Python process; the agent runs on the selected daemon and runner.
116
+
117
+ ```python
118
+ import asyncio
119
+
120
+ from kodelet_sdk import BaseModel, Client, Extension
121
+
122
+
123
+ ext = Extension(name="calculator", version="0.1.0")
124
+
125
+
126
+ class CalculatorInput(BaseModel):
127
+ a: int
128
+ b: int
129
+
130
+
131
+ @ext.tool("calculator", description="Add two integers", input_schema=CalculatorInput)
132
+ async def calculator(input: CalculatorInput) -> str:
133
+ return str(input.a + input.b)
134
+
135
+
136
+ async def main() -> None:
137
+ client = Client()
138
+ try:
139
+ session = await client.create_session(extensions=[ext])
140
+ response = await session.run_and_wait("Use calculator to add 123 and 456")
141
+ print(response.content)
142
+ finally:
143
+ await client.close()
144
+
145
+
146
+ if __name__ == "__main__":
147
+ asyncio.run(main())
148
+ ```
149
+
150
+ Inline extensions require ACP session-extension protocol v1; older hosts fail with a clear error. On resume, reattach extensions in the same order. Callbacks are not saved or replayed, and pending work is cancelled when the channel or session closes.
151
+
152
+ Host calls such as `ctx.update()` and `ctx.children` go to the runner. File, process, and storage helpers remain local. Legacy `extension_transport="unix"` and `"tcp"` options are accepted but ignored; ACP manages the connection.
153
+
154
+ ### Steering
155
+
156
+ Call `session.steer()` after an event confirms the run is active:
157
+
158
+ ```python
159
+ import asyncio
160
+
161
+ run_active = asyncio.Event()
162
+ session.once("assistant.thinking_start", lambda _event: run_active.set())
163
+ run_task = asyncio.create_task(session.run_and_wait("Review the persistence implementation"))
164
+
165
+ await run_active.wait()
166
+ steered = await session.steer("Also check transaction boundaries")
167
+ response = await run_task
168
+ ```
169
+
170
+ Steering requires host support. `injected` means guidance was queued, not necessarily consumed. If the turn has just ended, the result is `promptRequired`; steering never starts a new turn automatically.
171
+
172
+ ## Extension registration
173
+
174
+ Create an `Extension(name=..., version=...)`, then register synchronous or asynchronous handlers:
175
+
176
+ | API | Purpose |
177
+ | --- | --- |
178
+ | `@ext.tool(...)` | Model-callable tool with an input schema |
179
+ | `@ext.command(...)` | Slash command or recipe, with optional aliases |
180
+ | `@ext.on(event, ...)` | Lifecycle handler, such as `session.start`, `tool.call`, or `agent.end` |
181
+ | `@ext.shortcut(...)` | Native TUI keyboard shortcut |
182
+ | `ext.register_profile(...)` | Child execution preset |
183
+ | `ext.run_sync()` / `await ext.run()` | Serve an executable extension over stdio |
184
+
185
+ ### Tool results and progress
186
+
187
+ Return a string or a mapping with `content` and optional `data` and `error` fields. Use `data["presentation"]` to customize the displayed result without changing what the model receives:
188
+
189
+ ```python
190
+ from kodelet_sdk import ToolExecutionResult, ToolPresentation
191
+
192
+ presentation: ToolPresentation = {
193
+ "summary": "Found 2 matches",
194
+ "body": "- `src/api.py`\n- `tests/test_api.py`",
195
+ "format": "markdown",
196
+ }
197
+ result: ToolExecutionResult = {
198
+ "content": "Found 2 matches.",
199
+ "data": {"presentation": presentation},
200
+ }
201
+ ```
202
+
203
+ `summary` is required; `body` is optional and supports `text` or `markdown`. Hosts may sanitize or truncate display content.
204
+
205
+ For live progress, call `ctx.update()`. Updates replace earlier snapshots; only the final return value is persisted and sent to the model:
206
+
207
+ ```python
208
+ @ext.tool("search", description="Search a project")
209
+ async def search(input, ctx: ToolContext) -> str:
210
+ await ctx.update("Searching code", {"filesScanned": 12})
211
+ return "Search complete"
212
+ ```
213
+
214
+ `ctx.update()` is a no-op on hosts without progress support. For multi-step tasks, `TaskProgress` tracks activities and can attach to session events; `await progress.finish(...)` ends tracking and detaches listeners.
215
+
216
+ ### Commands and events
217
+
218
+ ```python
219
+ from kodelet_sdk import (
220
+ CommandContext,
221
+ CommandResult,
222
+ EventContext,
223
+ ToolCallEvent,
224
+ ToolUpdateEvent,
225
+ )
226
+
227
+
228
+ @ext.command("doctor", description="Check health", input_schema=WeatherInput)
229
+ async def doctor(input: WeatherInput, ctx: CommandContext) -> CommandResult:
230
+ return {"action": "respond", "response": ctx.input["commandName"]}
231
+
232
+
233
+ @ext.on("tool.call")
234
+ def approve(event: ToolCallEvent, ctx: EventContext):
235
+ return {"message": event.tool.name}
236
+
237
+
238
+ @ext.on("tool.update")
239
+ def sanitize_partial_output(event: ToolUpdateEvent, ctx: EventContext):
240
+ return {"output": event.tool.output}
241
+ ```
242
+
243
+ Commands return one of:
244
+
245
+ - `{"action": "pass"}` — let another route handle the command.
246
+ - `{"action": "respond", "response": "..."}` — respond directly to the user.
247
+ - `{"action": "runAgent", "prompt": "..."}` — run the agent with a replacement prompt; optional `display` controls the visible user message.
248
+
249
+ If you sanitize `tool.result`, apply the same policy to `tool.update` so partial output is also safe to display. Without an update handler, Kodelet suppresses partial output for result-subscribing extensions.
250
+
251
+ ### Keyboard shortcuts
252
+
253
+ Shortcuts run in local native `kodelet chat` sessions:
254
+
255
+ ```python
256
+ from kodelet_sdk import ShortcutContext
257
+
258
+
259
+ @ext.shortcut("ctrl+alt+r", description="Refresh project context")
260
+ async def refresh(ctx: ShortcutContext) -> None:
261
+ await ctx.ui.notify("Project context refreshed")
262
+ ```
263
+
264
+ Supported chords are `ctrl+<letter>`, `alt+<letter-or-digit>`, `ctrl+alt+<letter>`, and `f1`–`f12` (ASCII, case-insensitive). `ctrl+i` and `ctrl+m` are excluded because terminals treat them as Tab and Enter. Reserved host bindings take precedence.
265
+
266
+ Handlers can return `{"action": "submit", "message": "/dictate"}` when the host supports shortcut submission.
267
+
268
+ ## Schemas and templates
269
+
270
+ The SDK re-exports Pydantic types and provides Jinja2 rendering:
271
+
272
+ ```python
273
+ from kodelet_sdk import BaseModel, Field, render_template
274
+
275
+
276
+ class ReviewInput(BaseModel):
277
+ target: str = Field(min_length=1)
278
+
279
+
280
+ assert render_template("Review {{ target }}", {"target": "main"}) == "Review main"
281
+ ```
282
+
283
+ Pydantic schemas validate inputs before handlers run and generate JSON Schema for the host. Invalid command inputs return `{"action": "pass"}`. Raw JSON Schema mappings are also accepted, but do not perform local input validation.
284
+
285
+ ## Context helpers
286
+
287
+ Handlers receive `ctx` with call metadata and these helpers:
288
+
289
+ - `ctx.storage.read_text/write_text/read_json/write_json(...)` for extension data files.
290
+ - `ctx.path.resolve_workspace_path(...)` and `ctx.path.relative_to_workspace(...)`.
291
+ - `ctx.fs.exists/read_text/write_text/list(...)` for workspace file access.
292
+ - `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
293
+ - `ctx.env.get(...)` for environment access.
294
+ - `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
295
+ - `ctx.children.start(...)` for delegated agent work.
296
+ - `await ctx.acquire_background_task(...)` to keep runtime resources alive after a handler returns.
297
+ - `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
298
+ - `ctx.ui.append_transcript(...)`, `ctx.ui.set_widget(...)`, and `ctx.ui.open_surface(...)` for capability-gated persistent native-TUI content.
299
+
300
+ ### Child agents
301
+
302
+ Use `ctx.children` for agent work inside a tool, rather than creating a nested `Client`. Register a preset on the extension, then start a child with that preset:
303
+
304
+ ```python
305
+ class TaskInput(BaseModel):
306
+ task: str
307
+
308
+
309
+ ext.register_profile({
310
+ "name": "code_search",
311
+ "options": {
312
+ "allowed_tools": ["file_read", "grep_tool", "glob_tool"],
313
+ "no_extensions": True,
314
+ "no_skills": True,
315
+ "enable_fs_search_tools": True,
316
+ "max_turns": 3,
317
+ },
318
+ })
319
+
320
+
321
+ @ext.tool("code_search", description="Search the repository", input_schema=TaskInput)
322
+ async def code_search(input: TaskInput, ctx: ToolContext) -> str:
323
+ child = await ctx.children.start(profile="code_search", message=input.task)
324
+ result = await child.wait(on_event=lambda event: ctx.update(event.get("text") or event["kind"]))
325
+ return result["output"]
326
+ ```
327
+
328
+ - `read()` checks progress; `cancel()` cancels only that child. Cancelling `wait()` also cancels the child.
329
+ - Children start with fresh context. Use `context_mode="fork"` inside the active tool handler to copy the parent's history.
330
+ - Use `resume=child.conversation_id` for a follow-up. It creates a new run; old handles still target the old run. Resume cannot be combined with fork.
331
+ - `child.steer(message)` queues guidance without starting a turn. Like session steering, it returns `injected` or `promptRequired`.
332
+ - Child settings cannot expand the parent's permissions or limits. `system_prompt_path` in a preset is resolved on the runner.
333
+
334
+ Start and steer calls do not retry automatically. Supply a stable `request_id` when you need to reconcile an uncertain submission; reuse it only for the same input. A disconnected start may already be running, so keep its ID and any lease until cancellation or cleanup is confirmed.
335
+
336
+ For a history snapshot without execution, use `ctx.fork_conversation()`. It raises `ConversationForkUnavailableError` when unavailable. `Client.create_session(inherit_context=...)` is unsupported.
337
+
338
+ ### Background work
339
+
340
+ Children normally end with their tool handler. To let work outlive it, acquire a lease with `await ctx.acquire_background_task(...)` and pass `lease=lease` when first starting the child inside the tool. Close the lease after the work and final updates finish.
341
+
342
+ Runner leases last at most one hour and end on release, cancellation, or runner/extension shutdown. They keep resources alive, not Python task state. Resuming after a restart requires a new authorized tool call; saved IDs alone are insufficient.
343
+
344
+ ### User input
345
+
346
+ Executable extensions use the host's UI. For inline extensions, pass `ui={"select": handler, ...}` to `create_session()` to handle requests locally. A local handler needs no terminal unless it uses one; requests without a handler depend on the runner's UI support.
347
+
348
+ ```python
349
+ from kodelet_sdk import UIInputRequest, UISelectRequest
350
+
351
+ input_request: UIInputRequest = {"title": "Branch name", "required": True}
352
+ select_request: UISelectRequest = {"title": "Mode", "options": ["fast", "thorough"]}
353
+
354
+ branch = await ctx.ui.input(input_request)
355
+ mode = await ctx.ui.select(select_request)
356
+ ```
357
+
358
+ ### Persistent TUI content
359
+
360
+ Use transcript entries, widgets, and surfaces for longer-lived UI. Widget and surface IDs are scoped to the originating conversation.
361
+
362
+ ```python
363
+ await ctx.ui.append_transcript({"title": "Drawing saved", "message": "./drawing.png"})
364
+ await ctx.ui.set_widget("status", ["Indexing repository…"])
365
+
366
+ surface = await ctx.ui.open_surface(
367
+ {
368
+ "id": "preview",
369
+ "initialLines": ["Loading…"],
370
+ "width": "75%",
371
+ "height": "80%",
372
+ "anchor": "center",
373
+ }
374
+ )
375
+
376
+ surface.on_resize(
377
+ lambda event: surface.update([f"Surface size: {event['width']}×{event['height']}"])
378
+ )
379
+ surface.update(["Preview ready"])
380
+ await surface.close()
381
+ await ctx.ui.set_widget("status", None) # Remove the widget.
382
+ ```
383
+
384
+ `append_transcript()` and `set_widget()` are no-ops without host support; `open_surface()` raises `RuntimeError`. Surface updates replace existing content. If `surface.close()` fails, the handle remains valid for retry.
385
+
386
+ ## Runtime behavior
387
+
388
+ - Requests run concurrently. Cancellation or disconnection raises `asyncio.CancelledError` in async handlers; late request-scoped UI calls and updates are rejected.
389
+ - Persistent widgets and surfaces can outlive the handler that opened them, but remain scoped to their conversation.
390
+ - ACP messages are limited to 64 MiB each. Pipe failures and oversized messages fail pending requests and stop the subprocess.
391
+
392
+ ## Testing extensions
393
+
394
+ Test handlers without a subprocess:
395
+
396
+ ```python
397
+ from kodelet_sdk import Extension, create_test_harness
398
+
399
+
400
+ async def test_tool():
401
+ ext = Extension(name="example")
402
+
403
+ @ext.tool("echo", description="Echo", input_schema={"type": "object"})
404
+ async def echo(input, ctx):
405
+ return {"content": input["text"]}
406
+
407
+ harness = await create_test_harness(ext)
408
+ result = await harness.execute_tool({"name": "echo", "input": {"text": "hi"}})
409
+ assert result == {"content": "hi"}
410
+ ```
411
+
412
+ ## Examples
413
+
414
+ Run the inline calculator from a checkout with a configured Kodelet daemon:
415
+
416
+ ```bash
417
+ uv run -s examples/sdk/inline-extension-session
418
+ ```
419
+
420
+ Other examples:
421
+
422
+ - `examples/sdk/basic-agent-session` — one prompt and its final response.
423
+ - `examples/sdk/streaming-agent-session` — live assistant and tool output.
424
+ - `examples/review/kodelet-extension-review` — review command extension.
425
+ - `examples/workspace/kodelet-extension-workspace` — workspace helper/policy extension.