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.
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/PKG-INFO +34 -42
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/README.md +33 -41
- kodelet_sdk-0.4.0/VERSION.txt +1 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/__init__.py +1 -6
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/client.py +4 -2
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/types.py +2 -1
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/api.py +53 -12
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/context.py +1 -3
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/execution.py +49 -31
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_agent.py +98 -5
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_api.py +331 -0
- kodelet_sdk-0.4.0/tests/test_execution.py +66 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_runtime.py +30 -45
- kodelet_sdk-0.2.2/VERSION.txt +0 -1
- kodelet_sdk-0.2.2/src/kodelet_sdk/child.py +0 -226
- kodelet_sdk-0.2.2/tests/test_child.py +0 -498
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/.github/workflows/ci.yml +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/.github/workflows/release.yml +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/.gitignore +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/AGENTS.md +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/LICENSE +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/Makefile +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/review/kodelet-extension-review +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/review/reviewer-prompt.md +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/README.md +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/basic-agent-session +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/inline-extension-session +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/sdk/streaming-agent-session +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/examples/workspace/kodelet-extension-workspace +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/pyproject.toml +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/_utils.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/__init__.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/relay.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/rpc.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/session.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/agent/transport.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/py.typed +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/runtime.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/schemas.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/task_progress.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/template.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/src/kodelet_sdk/test_harness.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/fixtures/workspace_policy_extension.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_acp_transport.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_examples.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_persistent_ui.py +0 -0
- {kodelet_sdk-0.2.2 → kodelet_sdk-0.4.0}/tests/test_task_progress.py +0 -0
- {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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
83
|
-
|
|
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;
|
|
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
|
|
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
|
-
"""
|
|
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)
|