kodelet-sdk 0.2.2__tar.gz → 0.4.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.
Files changed (48) hide show
  1. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/PKG-INFO +34 -42
  2. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/README.md +33 -41
  3. kodelet_sdk-0.4.0/VERSION.txt +1 -0
  4. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/__init__.py +1 -6
  5. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/client.py +4 -2
  6. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/types.py +2 -1
  7. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/api.py +53 -12
  8. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/context.py +1 -3
  9. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/execution.py +49 -31
  10. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_agent.py +98 -5
  11. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_api.py +331 -0
  12. kodelet_sdk-0.4.0/tests/test_execution.py +66 -0
  13. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_runtime.py +30 -45
  14. kodelet_sdk-0.2.2/VERSION.txt +0 -1
  15. kodelet_sdk-0.2.2/src/kodelet_sdk/child.py +0 -226
  16. kodelet_sdk-0.2.2/tests/test_child.py +0 -498
  17. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/.github/workflows/ci.yml +0 -0
  18. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/.github/workflows/release.yml +0 -0
  19. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/.gitignore +0 -0
  20. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/AGENTS.md +0 -0
  21. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/LICENSE +0 -0
  22. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/Makefile +0 -0
  23. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/review/kodelet-extension-review +0 -0
  24. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/review/reviewer-prompt.md +0 -0
  25. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/README.md +0 -0
  26. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/basic-agent-session +0 -0
  27. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/inline-extension-session +0 -0
  28. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/streaming-agent-session +0 -0
  29. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/workspace/kodelet-extension-workspace +0 -0
  30. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/pyproject.toml +0 -0
  31. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/_utils.py +0 -0
  32. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/__init__.py +0 -0
  33. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/relay.py +0 -0
  34. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/rpc.py +0 -0
  35. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/session.py +0 -0
  36. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/transport.py +0 -0
  37. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/py.typed +0 -0
  38. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/runtime.py +0 -0
  39. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/schemas.py +0 -0
  40. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/task_progress.py +0 -0
  41. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/template.py +0 -0
  42. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/test_harness.py +0 -0
  43. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/fixtures/workspace_policy_extension.py +0 -0
  44. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_acp_transport.py +0 -0
  45. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_examples.py +0 -0
  46. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_persistent_ui.py +0 -0
  47. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_task_progress.py +0 -0
  48. {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kodelet-sdk
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for authoring Kodelet extensions
5
5
  Project-URL: Repository, https://github.com/jingkaihe/kodelet
6
6
  Author: Kodelet
@@ -108,7 +108,30 @@ Session options include:
108
108
  | `cwd` | Working directory on the runner |
109
109
  | `resume` | An existing conversation ID |
110
110
 
111
- Inline settings cannot include provider secrets, endpoints, or local prompt paths.
111
+ Pass an `ExecutionOptions` instance directly as `create_session(options=...)` for model settings, execution limits, and tool selection. The optional `profile="work"` selects a model profile already configured on the daemon; omit it to use the daemon's default. Inline settings cannot include provider secrets, endpoints, or local prompt paths.
112
+
113
+ ### Extension model profiles
114
+
115
+ ```python
116
+ ext = Extension()
117
+ SEARCH_PROFILE = ext.register_profile(
118
+ "code-search",
119
+ provider="openai",
120
+ model="gpt-5.6-luna",
121
+ reasoning_effort="none",
122
+ hidden=True,
123
+ )
124
+ CLAUDE_PROFILE = ext.register_profile(
125
+ "claude",
126
+ provider="anthropic",
127
+ model="claude-sonnet-4-6",
128
+ anthropic={"platform": "anthropic"},
129
+ anthropic_api_access="subscription",
130
+ )
131
+ session = await client.create_session(profile=SEARCH_PROFILE)
132
+ ```
133
+
134
+ `register_profile(name, *, provider, model, hidden=False, **options)` returns the flat name unchanged. Names are ASCII slugs (1–128 characters; `default` is reserved). `hidden=True` hides the profile from pickers.
112
135
 
113
136
  ### Inline extensions
114
137
 
@@ -149,7 +172,9 @@ if __name__ == "__main__":
149
172
 
150
173
  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
174
 
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.
175
+ To customize the prompt, attach an inline `agent.init` handler that returns a `systemPrompt` patch and leave extensions enabled. Prompt hooks work alongside typed session options.
176
+
177
+ Host calls such as `ctx.update()` and `ctx.fork_conversation()` 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
178
 
154
179
  ### Steering
155
180
 
@@ -179,7 +204,6 @@ Create an `Extension(name=..., version=...)`, then register synchronous or async
179
204
  | `@ext.command(...)` | Slash command or recipe, with optional aliases |
180
205
  | `@ext.on(event, ...)` | Lifecycle handler, such as `session.start`, `tool.call`, or `agent.end` |
181
206
  | `@ext.shortcut(...)` | Native TUI keyboard shortcut |
182
- | `ext.register_profile(...)` | Child execution preset |
183
207
  | `ext.run_sync()` / `await ext.run()` | Serve an executable extension over stdio |
184
208
 
185
209
  ### Tool results and progress
@@ -292,54 +316,22 @@ Handlers receive `ctx` with call metadata and these helpers:
292
316
  - `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
293
317
  - `ctx.env.get(...)` for environment access.
294
318
  - `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
295
- - `ctx.children.start(...)` for delegated agent work.
319
+ - `await ctx.fork_conversation(name=...)` to snapshot the active tool's conversation for an ACP session.
296
320
  - `await ctx.acquire_background_task(...)` to keep runtime resources alive after a handler returns.
297
321
  - `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
298
322
  - `ctx.ui.append_transcript(...)`, `ctx.ui.set_widget(...)`, and `ctx.ui.open_surface(...)` for capability-gated persistent native-TUI content.
299
323
 
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.
324
+ ### Agent work inside tools
333
325
 
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.
326
+ Use `Client.create_session(options=ExecutionOptions(...))` with normal client credentials and server/runner targeting. Attach `TaskProgress` for activity updates and await client/progress cleanup on every exit path. A cancelled response is not success, even with partial text.
335
327
 
336
- For a history snapshot without execution, use `ctx.fork_conversation()`. It raises `ConversationForkUnavailableError` when unavailable. `Client.create_session(inherit_context=...)` is unsupported.
328
+ Sessions start fresh. For inherited context, call `ctx.fork_conversation(name="worker-name")` inside the active tool, then pass the returned ID as `resume`. `inherit_context` remains unsupported. Reuse or resume the session for follow-ups; steering only affects a running turn.
337
329
 
338
330
  ### Background work
339
331
 
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.
332
+ To let extension work outlive its handler, acquire a lease with `await ctx.acquire_background_task(...)` while the handler is active. Retain and manage the background task yourself, close any ACP client it owns, and release the lease after the work and final UI updates finish. A lease keeps extension resources alive; it does not authorize ACP sessions or keep a completed tool's `ctx.update()` channel open.
341
333
 
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.
334
+ Runner leases last at most one hour and end on release, cancellation, or runner/extension shutdown. They keep resources alive, not Python task state; background tasks must be recreated after a restart.
343
335
 
344
336
  ### User input
345
337
 
@@ -95,7 +95,30 @@ Session options include:
95
95
  | `cwd` | Working directory on the runner |
96
96
  | `resume` | An existing conversation ID |
97
97
 
98
- Inline settings cannot include provider secrets, endpoints, or local prompt paths.
98
+ Pass an `ExecutionOptions` instance directly as `create_session(options=...)` for model settings, execution limits, and tool selection. The optional `profile="work"` selects a model profile already configured on the daemon; omit it to use the daemon's default. Inline settings cannot include provider secrets, endpoints, or local prompt paths.
99
+
100
+ ### Extension model profiles
101
+
102
+ ```python
103
+ ext = Extension()
104
+ SEARCH_PROFILE = ext.register_profile(
105
+ "code-search",
106
+ provider="openai",
107
+ model="gpt-5.6-luna",
108
+ reasoning_effort="none",
109
+ hidden=True,
110
+ )
111
+ CLAUDE_PROFILE = ext.register_profile(
112
+ "claude",
113
+ provider="anthropic",
114
+ model="claude-sonnet-4-6",
115
+ anthropic={"platform": "anthropic"},
116
+ anthropic_api_access="subscription",
117
+ )
118
+ session = await client.create_session(profile=SEARCH_PROFILE)
119
+ ```
120
+
121
+ `register_profile(name, *, provider, model, hidden=False, **options)` returns the flat name unchanged. Names are ASCII slugs (1–128 characters; `default` is reserved). `hidden=True` hides the profile from pickers.
99
122
 
100
123
  ### Inline extensions
101
124
 
@@ -136,7 +159,9 @@ if __name__ == "__main__":
136
159
 
137
160
  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.
138
161
 
139
- 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.
162
+ To customize the prompt, attach an inline `agent.init` handler that returns a `systemPrompt` patch and leave extensions enabled. Prompt hooks work alongside typed session options.
163
+
164
+ Host calls such as `ctx.update()` and `ctx.fork_conversation()` 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.
140
165
 
141
166
  ### Steering
142
167
 
@@ -166,7 +191,6 @@ Create an `Extension(name=..., version=...)`, then register synchronous or async
166
191
  | `@ext.command(...)` | Slash command or recipe, with optional aliases |
167
192
  | `@ext.on(event, ...)` | Lifecycle handler, such as `session.start`, `tool.call`, or `agent.end` |
168
193
  | `@ext.shortcut(...)` | Native TUI keyboard shortcut |
169
- | `ext.register_profile(...)` | Child execution preset |
170
194
  | `ext.run_sync()` / `await ext.run()` | Serve an executable extension over stdio |
171
195
 
172
196
  ### Tool results and progress
@@ -279,54 +303,22 @@ Handlers receive `ctx` with call metadata and these helpers:
279
303
  - `ctx.process.exec(...)` and `ctx.process.spawn(...)` for async process execution.
280
304
  - `ctx.env.get(...)` for environment access.
281
305
  - `ctx.log.debug/info/warn/error(...)` for JSON logs to stderr.
282
- - `ctx.children.start(...)` for delegated agent work.
306
+ - `await ctx.fork_conversation(name=...)` to snapshot the active tool's conversation for an ACP session.
283
307
  - `await ctx.acquire_background_task(...)` to keep runtime resources alive after a handler returns.
284
308
  - `ctx.ui.input/confirm/select/notify(...)` for host UI reverse-RPC calls.
285
309
  - `ctx.ui.append_transcript(...)`, `ctx.ui.set_widget(...)`, and `ctx.ui.open_surface(...)` for capability-gated persistent native-TUI content.
286
310
 
287
- ### Child agents
288
-
289
- 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:
290
-
291
- ```python
292
- class TaskInput(BaseModel):
293
- task: str
294
-
295
-
296
- ext.register_profile({
297
- "name": "code_search",
298
- "options": {
299
- "allowed_tools": ["file_read", "grep_tool", "glob_tool"],
300
- "no_extensions": True,
301
- "no_skills": True,
302
- "enable_fs_search_tools": True,
303
- "max_turns": 3,
304
- },
305
- })
306
-
307
-
308
- @ext.tool("code_search", description="Search the repository", input_schema=TaskInput)
309
- async def code_search(input: TaskInput, ctx: ToolContext) -> str:
310
- child = await ctx.children.start(profile="code_search", message=input.task)
311
- result = await child.wait(on_event=lambda event: ctx.update(event.get("text") or event["kind"]))
312
- return result["output"]
313
- ```
314
-
315
- - `read()` checks progress; `cancel()` cancels only that child. Cancelling `wait()` also cancels the child.
316
- - Children start with fresh context. Use `context_mode="fork"` inside the active tool handler to copy the parent's history.
317
- - 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.
318
- - `child.steer(message)` queues guidance without starting a turn. Like session steering, it returns `injected` or `promptRequired`.
319
- - Child settings cannot expand the parent's permissions or limits. `system_prompt_path` in a preset is resolved on the runner.
311
+ ### Agent work inside tools
320
312
 
321
- 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.
313
+ Use `Client.create_session(options=ExecutionOptions(...))` with normal client credentials and server/runner targeting. Attach `TaskProgress` for activity updates and await client/progress cleanup on every exit path. A cancelled response is not success, even with partial text.
322
314
 
323
- For a history snapshot without execution, use `ctx.fork_conversation()`. It raises `ConversationForkUnavailableError` when unavailable. `Client.create_session(inherit_context=...)` is unsupported.
315
+ Sessions start fresh. For inherited context, call `ctx.fork_conversation(name="worker-name")` inside the active tool, then pass the returned ID as `resume`. `inherit_context` remains unsupported. Reuse or resume the session for follow-ups; steering only affects a running turn.
324
316
 
325
317
  ### Background work
326
318
 
327
- 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.
319
+ To let extension work outlive its handler, acquire a lease with `await ctx.acquire_background_task(...)` while the handler is active. Retain and manage the background task yourself, close any ACP client it owns, and release the lease after the work and final UI updates finish. A lease keeps extension resources alive; it does not authorize ACP sessions or keep a completed tool's `ctx.update()` channel open.
328
320
 
329
- 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.
321
+ Runner leases last at most one hour and end on release, cancellation, or runner/extension shutdown. They keep resources alive, not Python task state; background tasks must be recreated after a restart.
330
322
 
331
323
  ### User input
332
324
 
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -65,7 +65,6 @@ from .api import (
65
65
  define_extension,
66
66
  on,
67
67
  )
68
- from .child import ChildClient, ChildExecution, ChildSteerResult
69
68
  from .context import (
70
69
  BackgroundTaskLease,
71
70
  CommandContext,
@@ -115,7 +114,7 @@ from .context import (
115
114
  run_with_host_rpc_client,
116
115
  set_active_host_rpc_client,
117
116
  )
118
- from .execution import ExecutionOptions, ExecutionProfile
117
+ from .execution import ExecutionOptions
119
118
  from .runtime import run_extension
120
119
  from .schemas import JSONSchema, ToolInputSchema
121
120
  from .task_progress import (
@@ -153,9 +152,6 @@ __all__ = [
153
152
  "BackgroundTaskLease",
154
153
  "BaseModel",
155
154
  "BridgeTransport",
156
- "ChildClient",
157
- "ChildExecution",
158
- "ChildSteerResult",
159
155
  "Client",
160
156
  "ClientOptions",
161
157
  "CommandContext",
@@ -176,7 +172,6 @@ __all__ = [
176
172
  "EventResult",
177
173
  "ExecResult",
178
174
  "ExecutionOptions",
179
- "ExecutionProfile",
180
175
  "Extension",
181
176
  "ExtensionEvent",
182
177
  "ExtensionHost",
@@ -79,8 +79,10 @@ class Client:
79
79
 
80
80
  merged_options: dict[str, Any] = {**dict(session_options or {}), **kwargs}
81
81
  if merged_options.get("inherit_context") is not None:
82
- raise ValueError("Use ctx.children for scoped execution; live forking remains "
83
- "available separately through ctx.fork_conversation")
82
+ raise ValueError(
83
+ "inherit_context is unsupported; call ctx.fork_conversation() "
84
+ "and pass the returned conversation ID as resume"
85
+ )
84
86
  extensions = tuple(
85
87
  cast(Sequence[Entrypoint | Extension], merged_options.get("extensions") or ())
86
88
  )
@@ -78,7 +78,8 @@ class CreateSessionOptions(TypedDict, total=False):
78
78
  protocol; supply them again on resume in the same order. ``ui`` services
79
79
  input, confirm, select and notify requests locally when handlers are supplied.
80
80
  ``extension_transport`` accepts legacy unix/tcp values but is now a no-op.
81
- ``inherit_context`` remains unsupported; use ``ctx.children`` instead.
81
+ ``inherit_context`` remains unsupported; call ``ctx.fork_conversation()``
82
+ explicitly and pass its returned conversation ID as ``resume`` instead.
82
83
  """
83
84
 
84
85
  profile: str | Profile | ProfileInput
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import re
4
5
  from collections.abc import Awaitable, Callable, Mapping, Sequence
5
6
  from dataclasses import dataclass
6
7
  from typing import (
@@ -36,7 +37,7 @@ from .context import (
36
37
  create_shortcut_context,
37
38
  create_tool_context,
38
39
  )
39
- from .execution import ExecutionProfile
40
+ from .execution import ExtensionProfileOptions
40
41
  from .schemas import SchemaAdapter, SchemaLike, infer_schema_from_callable
41
42
 
42
43
  _MISSING = object()
@@ -326,8 +327,8 @@ class Extension:
326
327
  self._metadata["name"] = name
327
328
  if version is not None:
328
329
  self._metadata["version"] = version
329
- self._tools: dict[str, ToolRegistration] = {}
330
330
  self._profiles: dict[str, dict[str, Any]] = {}
331
+ self._tools: dict[str, ToolRegistration] = {}
331
332
  self._commands_by_name: dict[str, CommandRegistration] = {}
332
333
  self._command_registrations: list[CommandRegistration] = []
333
334
  self._shortcuts: dict[str, ShortcutRegistration] = {}
@@ -352,6 +353,49 @@ class Extension:
352
353
  if value is not None:
353
354
  self._metadata[key] = value
354
355
 
356
+ def register_profile(
357
+ self,
358
+ name: str,
359
+ *,
360
+ provider: Literal["openai", "anthropic"],
361
+ model: str,
362
+ hidden: bool = False,
363
+ **options: Any,
364
+ ) -> str:
365
+ """Declare a model profile and return its name unchanged.
366
+
367
+ Names are 1–128-character ASCII slugs; default is reserved (case-insensitive).
368
+ Requires host acceptance before ACP selection.
369
+ Optional settings: weak_model, max_tokens, weak_model_max_tokens,
370
+ thinking_budget_tokens, reasoning_effort, openai, anthropic, and
371
+ anthropic_api_access (camelCase also accepted at the top level).
372
+ Provider blocks use daemon config keys unchanged; the daemon validates them.
373
+ """
374
+ if not isinstance(name, str) or not re.fullmatch(
375
+ r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", name,
376
+ ):
377
+ raise ValueError(
378
+ "Extension profile names must be ASCII slugs "
379
+ "matching [A-Za-z0-9][A-Za-z0-9._-]* (1–128 characters)"
380
+ )
381
+ if name.lower() == "default":
382
+ raise ValueError('Extension profile name "default" is reserved')
383
+ if name in self._profiles:
384
+ raise ValueError(f"Duplicate extension profile registration: {name}")
385
+ if not isinstance(hidden, bool):
386
+ raise ValueError("Extension profile hidden must be a boolean")
387
+ validated = ExtensionProfileOptions.model_validate({
388
+ "provider": provider,
389
+ "model": model,
390
+ **options,
391
+ })
392
+ self._profiles[name] = {
393
+ "name": name,
394
+ "options": validated.to_wire(),
395
+ "hidden": hidden,
396
+ }
397
+ return name
398
+
355
399
  def register_tool(
356
400
  self,
357
401
  *,
@@ -650,16 +694,6 @@ class Extension:
650
694
  return decorator(cast(HandlerT, handler))
651
695
  return decorator
652
696
 
653
- def register_profile(self, profile: ExecutionProfile | Mapping[str, Any]) -> None:
654
- """Register an extension-scoped child preset without daemon YAML setup."""
655
- snapshot = ExecutionProfile.model_validate(profile).to_wire()
656
- name = str(snapshot["name"])
657
- if name in self._profiles:
658
- raise ValueError(f"Duplicate execution preset: {name}")
659
- if len(self._profiles) >= 32:
660
- raise ValueError("An extension supports at most 32 execution presets")
661
- self._profiles[name] = snapshot
662
-
663
697
  def initialize(self, params: Mapping[str, Any]) -> dict[str, Any]:
664
698
  """Handle Kodelet's ``extension.initialize`` JSON-RPC request.
665
699
 
@@ -671,6 +705,13 @@ class Extension:
671
705
  registrations, shortcut registrations, and event subscriptions.
672
706
  """
673
707
 
708
+ capabilities = _mapping_or_empty(params.get("capabilities"))
709
+ profiles_capability = _mapping_or_empty(capabilities.get("profiles"))
710
+ if self._profiles and profiles_capability.get("remote") is not True:
711
+ raise RuntimeError(
712
+ "Remote extension profiles are not supported by this host; update the Kodelet "
713
+ "daemon and runner (capabilities.profiles.remote is required)"
714
+ )
674
715
  self._init_params = params
675
716
  extension = params.get("extension")
676
717
  extension_id = extension.get("id") if isinstance(extension, Mapping) else None
@@ -14,8 +14,6 @@ from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
  from typing import Any, Literal, NotRequired, Protocol, Required, TypeAlias, TypedDict, cast
16
16
 
17
- from .child import ChildClient
18
-
19
17
  CommandFlagValue: TypeAlias = str | bool | list[str]
20
18
 
21
19
 
@@ -1432,6 +1430,7 @@ class SharedContext:
1432
1430
  )
1433
1431
  data_dir = Path(str(data_dir_value)).resolve(strict=False)
1434
1432
  self.session_id = _optional_str(context.get("sessionId"))
1433
+ self.runner_id = _optional_str(extension.get("runnerId"))
1435
1434
  self.conversation_id = _optional_str(context.get("conversationId"))
1436
1435
  self.ui_scope_id = _normalize_ui_scope_id(context.get("uiScopeId")) or None
1437
1436
  self.cwd = str(cwd)
@@ -1447,7 +1446,6 @@ class SharedContext:
1447
1446
  self.env = EnvContext()
1448
1447
  self.log = LogContext(_optional_str(extension.get("id")))
1449
1448
  self._host_rpc_client = _current_host_rpc_client()
1450
- self.children = ChildClient(self._host_rpc_client)
1451
1449
  self._background_tasks_enabled = _runtime_capability_supported(
1452
1450
  init,
1453
1451
  "backgroundTasks",
@@ -1,4 +1,4 @@
1
- """Credential-free execution options and runner-owned preset registrations."""
1
+ """Typed ACP execution and extension profile options."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -6,9 +6,9 @@ import csv
6
6
  import io
7
7
  import re
8
8
  from collections.abc import Mapping
9
- from typing import Annotated, Any, Literal
9
+ from typing import Annotated, Any, Literal, Self
10
10
 
11
- from pydantic import BaseModel, ConfigDict, Field, model_validator
11
+ from pydantic import BaseModel, ConfigDict, Field, JsonValue, model_validator
12
12
  from pydantic.alias_generators import to_camel
13
13
 
14
14
 
@@ -45,6 +45,52 @@ class ExecutionOptions(BaseModel):
45
45
  return self.model_dump(by_alias=True, exclude_none=True)
46
46
 
47
47
 
48
+ class ExtensionProfileOptions(BaseModel):
49
+ """Registration-only settings; nested provider configuration is validated by the daemon."""
50
+
51
+ model_config = ConfigDict(
52
+ extra="forbid",
53
+ strict=True,
54
+ alias_generator=to_camel,
55
+ populate_by_name=True,
56
+ )
57
+
58
+ provider: Literal["openai", "anthropic"]
59
+ model: str = Field(min_length=1)
60
+ weak_model: str | None = Field(default=None, min_length=1)
61
+ max_tokens: int | None = Field(default=None, gt=0)
62
+ weak_model_max_tokens: int | None = Field(default=None, gt=0)
63
+ thinking_budget_tokens: int | None = Field(default=None, ge=0)
64
+ reasoning_effort: str | None = Field(default=None, min_length=1)
65
+ openai: dict[str, JsonValue] | None = None
66
+ anthropic: dict[str, JsonValue] | None = None
67
+ anthropic_api_access: Literal["auto", "subscription", "api-key"] | None = Field(
68
+ default=None,
69
+ alias="anthropicAPIAccess",
70
+ )
71
+
72
+ @model_validator(mode="before")
73
+ @classmethod
74
+ def reject_null(cls, value: Any) -> Any:
75
+ if isinstance(value, Mapping) and any(item is None for item in value.values()):
76
+ raise ValueError("Omit absent profile settings; null is not inheritance")
77
+ return value
78
+
79
+ @model_validator(mode="after")
80
+ def validate_provider_settings(self) -> Self:
81
+ if self.provider != "openai" and self.openai is not None:
82
+ raise ValueError("openai settings require the openai provider")
83
+ if self.provider != "anthropic" and (
84
+ self.anthropic is not None or self.anthropic_api_access is not None
85
+ ):
86
+ raise ValueError("anthropic settings require the anthropic provider")
87
+ return self
88
+
89
+ def to_wire(self) -> dict[str, Any]:
90
+ """Return an independent wire snapshot, preserving nested provider config keys."""
91
+ return self.model_dump(by_alias=True, exclude_none=True)
92
+
93
+
48
94
  def execution_args(options: ExecutionOptions) -> list[str]:
49
95
  """Encode typed options for the daemon-only ACP client, including empty/false values."""
50
96
  args: list[str] = []
@@ -62,31 +108,3 @@ def execution_args(options: ExecutionOptions) -> list[str]:
62
108
  encoded = str(value)
63
109
  args.append(f"--{flag}={encoded}")
64
110
  return args
65
-
66
-
67
- class ExecutionProfile(BaseModel):
68
- """Extension-owned preset; prompt paths are resolved on the runner."""
69
-
70
- model_config = ConfigDict(extra="forbid", strict=True, alias_generator=to_camel,
71
- populate_by_name=True)
72
- name: str = Field(min_length=1, max_length=128, pattern=r"^[^/\\\x00]+$")
73
- options: ExecutionOptions | None = None
74
- system_prompt_path: str | None = Field(default=None, max_length=8192)
75
- system_prompt: str | None = Field(default=None, max_length=256 * 1024)
76
-
77
- @model_validator(mode="before")
78
- @classmethod
79
- def reject_null(cls, value: Any) -> Any:
80
- if isinstance(value, Mapping) and any(item is None for item in value.values()):
81
- raise ValueError("Omit absent preset fields instead of null")
82
- return value
83
-
84
- @model_validator(mode="after")
85
- def check_prompt(self) -> ExecutionProfile:
86
- if self.system_prompt and self.system_prompt_path:
87
- raise ValueError("Use prompt content or a prompt path, not both")
88
- return self
89
-
90
- def to_wire(self) -> dict[str, Any]:
91
- """Return the serializable registration for extension.initialize."""
92
- return self.model_dump(by_alias=True, exclude_none=True)