windcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- windcode/__init__.py +6 -0
- windcode/__main__.py +4 -0
- windcode/auth/__init__.py +11 -0
- windcode/auth/store.py +90 -0
- windcode/cli.py +181 -0
- windcode/config/__init__.py +41 -0
- windcode/config/loader.py +117 -0
- windcode/config/models.py +203 -0
- windcode/config/writer.py +74 -0
- windcode/context/__init__.py +18 -0
- windcode/context/compactor.py +72 -0
- windcode/context/estimator.py +89 -0
- windcode/context/truncation.py +87 -0
- windcode/domain/__init__.py +1 -0
- windcode/domain/errors.py +33 -0
- windcode/domain/events.py +597 -0
- windcode/domain/messages.py +226 -0
- windcode/domain/models.py +73 -0
- windcode/domain/subagents.py +263 -0
- windcode/domain/tools.py +55 -0
- windcode/extensions/__init__.py +27 -0
- windcode/extensions/commands.py +25 -0
- windcode/extensions/discovery.py +139 -0
- windcode/extensions/events.py +58 -0
- windcode/extensions/hooks/__init__.py +4 -0
- windcode/extensions/hooks/dispatcher.py +107 -0
- windcode/extensions/hooks/executor.py +49 -0
- windcode/extensions/hooks/loader.py +101 -0
- windcode/extensions/hooks/models.py +109 -0
- windcode/extensions/mcp/__init__.py +10 -0
- windcode/extensions/mcp/adapter.py +101 -0
- windcode/extensions/mcp/catalog.py +153 -0
- windcode/extensions/mcp/client.py +273 -0
- windcode/extensions/mcp/runtime.py +144 -0
- windcode/extensions/mcp/tools.py +570 -0
- windcode/extensions/models.py +168 -0
- windcode/extensions/paths.py +71 -0
- windcode/extensions/plugins/__init__.py +3 -0
- windcode/extensions/plugins/installer.py +93 -0
- windcode/extensions/plugins/manifest.py +166 -0
- windcode/extensions/runtime.py +366 -0
- windcode/extensions/service.py +437 -0
- windcode/extensions/skills/__init__.py +3 -0
- windcode/extensions/skills/loader.py +67 -0
- windcode/extensions/skills/parser.py +57 -0
- windcode/extensions/skills/tools.py +213 -0
- windcode/extensions/snapshot.py +57 -0
- windcode/extensions/state.py +158 -0
- windcode/instructions/__init__.py +8 -0
- windcode/instructions/loader.py +50 -0
- windcode/memory/__init__.py +57 -0
- windcode/memory/extraction.py +83 -0
- windcode/memory/models.py +218 -0
- windcode/memory/refiner.py +189 -0
- windcode/memory/security.py +23 -0
- windcode/memory/service.py +179 -0
- windcode/memory/store.py +362 -0
- windcode/observability/__init__.py +4 -0
- windcode/observability/redaction.py +95 -0
- windcode/observability/trace.py +115 -0
- windcode/policy/__init__.py +22 -0
- windcode/policy/engine.py +137 -0
- windcode/policy/models.py +69 -0
- windcode/providers/__init__.py +29 -0
- windcode/providers/_utils.py +19 -0
- windcode/providers/anthropic.py +215 -0
- windcode/providers/base.py +46 -0
- windcode/providers/catalog.py +119 -0
- windcode/providers/errors.py +96 -0
- windcode/providers/openai_compat.py +231 -0
- windcode/providers/openai_responses.py +189 -0
- windcode/providers/registry.py +105 -0
- windcode/runtime/__init__.py +15 -0
- windcode/runtime/control.py +89 -0
- windcode/runtime/event_bus.py +67 -0
- windcode/runtime/loop.py +510 -0
- windcode/runtime/prompts.py +132 -0
- windcode/runtime/report.py +64 -0
- windcode/runtime/retry.py +73 -0
- windcode/runtime/scheduler.py +194 -0
- windcode/runtime/subagents/__init__.py +28 -0
- windcode/runtime/subagents/approvals.py +88 -0
- windcode/runtime/subagents/budgets.py +79 -0
- windcode/runtime/subagents/coordinator.py +714 -0
- windcode/runtime/subagents/factory.py +311 -0
- windcode/runtime/subagents/roles.py +74 -0
- windcode/runtime/subagents/verification.py +53 -0
- windcode/sandbox/__init__.py +3 -0
- windcode/sandbox/bwrap.py +79 -0
- windcode/sdk.py +1259 -0
- windcode/sessions/__init__.py +23 -0
- windcode/sessions/artifacts.py +69 -0
- windcode/sessions/models.py +104 -0
- windcode/sessions/store.py +167 -0
- windcode/sessions/tree.py +35 -0
- windcode/tools/__init__.py +10 -0
- windcode/tools/apply_patch.py +191 -0
- windcode/tools/ask_user.py +58 -0
- windcode/tools/builtins.py +44 -0
- windcode/tools/edit_file.py +54 -0
- windcode/tools/filesystem.py +77 -0
- windcode/tools/glob.py +35 -0
- windcode/tools/grep.py +66 -0
- windcode/tools/memory.py +400 -0
- windcode/tools/read_file.py +54 -0
- windcode/tools/registry.py +120 -0
- windcode/tools/shell.py +159 -0
- windcode/tools/subagents/__init__.py +31 -0
- windcode/tools/subagents/cancel.py +35 -0
- windcode/tools/subagents/integrate.py +54 -0
- windcode/tools/subagents/list.py +46 -0
- windcode/tools/subagents/spawn.py +86 -0
- windcode/tools/subagents/wait.py +74 -0
- windcode/tools/write_file.py +61 -0
- windcode/tui/__init__.py +3 -0
- windcode/tui/app.py +1015 -0
- windcode/tui/commands.py +90 -0
- windcode/tui/permission_display.py +24 -0
- windcode/tui/styles.tcss +591 -0
- windcode/tui/widgets/__init__.py +31 -0
- windcode/tui/widgets/approval.py +98 -0
- windcode/tui/widgets/command_menu.py +90 -0
- windcode/tui/widgets/extensions.py +48 -0
- windcode/tui/widgets/input.py +110 -0
- windcode/tui/widgets/memory.py +143 -0
- windcode/tui/widgets/messages.py +299 -0
- windcode/tui/widgets/models.py +565 -0
- windcode/tui/widgets/question.py +46 -0
- windcode/tui/widgets/sessions.py +68 -0
- windcode/tui/widgets/status.py +46 -0
- windcode/tui/widgets/subagents.py +195 -0
- windcode/tui/widgets/tools.py +66 -0
- windcode/tui/widgets/welcome.py +149 -0
- windcode/types.py +80 -0
- windcode/worktrees/__init__.py +24 -0
- windcode/worktrees/git.py +92 -0
- windcode/worktrees/manager.py +265 -0
- windcode/worktrees/models.py +62 -0
- windcode-0.1.0.dist-info/METADATA +207 -0
- windcode-0.1.0.dist-info/RECORD +143 -0
- windcode-0.1.0.dist-info/WHEEL +4 -0
- windcode-0.1.0.dist-info/entry_points.txt +2 -0
- windcode-0.1.0.dist-info/licenses/LICENSE +201 -0
windcode/sdk.py
ADDED
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import shutil
|
|
5
|
+
from collections.abc import AsyncIterator, Mapping
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any, Self, cast
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from platformdirs import user_state_path
|
|
13
|
+
|
|
14
|
+
from windcode.auth import CredentialStore, FileCredentialStore
|
|
15
|
+
from windcode.config import AppConfig, PermissionMode, save_memory_config, save_model_config
|
|
16
|
+
from windcode.context import TokenEstimator
|
|
17
|
+
from windcode.domain.events import (
|
|
18
|
+
AgentEventType,
|
|
19
|
+
ApprovalResponse,
|
|
20
|
+
MemoryEvent,
|
|
21
|
+
RunRequest,
|
|
22
|
+
RunResponse,
|
|
23
|
+
RunResult,
|
|
24
|
+
)
|
|
25
|
+
from windcode.domain.messages import (
|
|
26
|
+
Message,
|
|
27
|
+
Role,
|
|
28
|
+
TextBlock,
|
|
29
|
+
heal_dangling_tool_calls,
|
|
30
|
+
message_from_dict,
|
|
31
|
+
)
|
|
32
|
+
from windcode.domain.subagents import SubagentRecord, SubagentResult
|
|
33
|
+
from windcode.domain.tools import Tool, ToolContext, ToolEffect
|
|
34
|
+
from windcode.extensions.commands import CommandRoute
|
|
35
|
+
from windcode.extensions.hooks.models import HookContext, HookEvent
|
|
36
|
+
from windcode.extensions.mcp.catalog import McpToolDefinition
|
|
37
|
+
from windcode.extensions.mcp.tools import (
|
|
38
|
+
register_mcp_management_tools,
|
|
39
|
+
register_mcp_status_tool,
|
|
40
|
+
)
|
|
41
|
+
from windcode.extensions.models import (
|
|
42
|
+
CapabilityKind,
|
|
43
|
+
CapabilityRecord,
|
|
44
|
+
ExtensionSnapshot,
|
|
45
|
+
ManagementResult,
|
|
46
|
+
)
|
|
47
|
+
from windcode.extensions.plugins.installer import InstallResult
|
|
48
|
+
from windcode.extensions.runtime import RunExtensions
|
|
49
|
+
from windcode.extensions.service import ExtensionService
|
|
50
|
+
from windcode.extensions.skills.loader import SkillLoader
|
|
51
|
+
from windcode.extensions.skills.tools import (
|
|
52
|
+
SkillCatalog,
|
|
53
|
+
SkillSearchResult,
|
|
54
|
+
register_skill_tools,
|
|
55
|
+
)
|
|
56
|
+
from windcode.extensions.state import ExtensionStateStore, ManagementAuditRecord
|
|
57
|
+
from windcode.instructions import load_instructions
|
|
58
|
+
from windcode.memory import (
|
|
59
|
+
MemoryActivation,
|
|
60
|
+
MemoryKind,
|
|
61
|
+
MemoryRecord,
|
|
62
|
+
MemoryScope,
|
|
63
|
+
MemoryService,
|
|
64
|
+
MemorySource,
|
|
65
|
+
MemoryStatus,
|
|
66
|
+
assess_core_project_fact,
|
|
67
|
+
assess_experience,
|
|
68
|
+
classify_memory_intent,
|
|
69
|
+
explicitly_always_project_fact,
|
|
70
|
+
is_project_fact,
|
|
71
|
+
refine_memory,
|
|
72
|
+
should_assess_experience,
|
|
73
|
+
)
|
|
74
|
+
from windcode.observability import DynamicRedactor, TraceStore
|
|
75
|
+
from windcode.policy import PolicyEngine, PolicyRequest
|
|
76
|
+
from windcode.providers import ModelTarget, ModelTransport, TransportRegistry
|
|
77
|
+
from windcode.runtime.control import RunBudgets, RunControl
|
|
78
|
+
from windcode.runtime.event_bus import EventBus
|
|
79
|
+
from windcode.runtime.loop import AgentLoop
|
|
80
|
+
from windcode.runtime.prompts import build_system_prompt
|
|
81
|
+
from windcode.runtime.scheduler import ScheduledCall, ToolScheduler
|
|
82
|
+
from windcode.runtime.subagents import (
|
|
83
|
+
ChildRuntimeFactory,
|
|
84
|
+
SubagentCoordinator,
|
|
85
|
+
VerificationRunner,
|
|
86
|
+
)
|
|
87
|
+
from windcode.sandbox import BubblewrapSandbox, detect_bubblewrap
|
|
88
|
+
from windcode.sessions import (
|
|
89
|
+
ArtifactStore,
|
|
90
|
+
EventRecord,
|
|
91
|
+
SessionMetadata,
|
|
92
|
+
SessionStore,
|
|
93
|
+
ancestor_chain,
|
|
94
|
+
create_branch,
|
|
95
|
+
)
|
|
96
|
+
from windcode.tools import (
|
|
97
|
+
ToolRegistry,
|
|
98
|
+
add_subagent_tools,
|
|
99
|
+
create_builtin_registry,
|
|
100
|
+
register_memory_tools,
|
|
101
|
+
)
|
|
102
|
+
from windcode.tools.shell import ShellTool
|
|
103
|
+
from windcode.worktrees import WorktreeManager
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RunHandle:
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
task: asyncio.Task[RunResult],
|
|
110
|
+
event_bus: EventBus,
|
|
111
|
+
control: RunControl,
|
|
112
|
+
*,
|
|
113
|
+
after_sequence: int = 0,
|
|
114
|
+
coordinator: SubagentCoordinator,
|
|
115
|
+
policy: PolicyEngine,
|
|
116
|
+
loop: AgentLoop,
|
|
117
|
+
) -> None:
|
|
118
|
+
self._task = task
|
|
119
|
+
self._event_bus = event_bus
|
|
120
|
+
self._control = control
|
|
121
|
+
self._after_sequence = after_sequence
|
|
122
|
+
self._coordinator = coordinator
|
|
123
|
+
self._policy = policy
|
|
124
|
+
self._loop = loop
|
|
125
|
+
self._result: RunResult | None = None
|
|
126
|
+
self._result_lock = asyncio.Lock()
|
|
127
|
+
|
|
128
|
+
def __aiter__(self) -> AsyncIterator[AgentEventType]:
|
|
129
|
+
return self._event_bus.subscribe(after_sequence=self._after_sequence)
|
|
130
|
+
|
|
131
|
+
async def respond(self, response: RunResponse) -> None:
|
|
132
|
+
try:
|
|
133
|
+
self._control.respond(response)
|
|
134
|
+
except ValueError:
|
|
135
|
+
if not isinstance(response, ApprovalResponse):
|
|
136
|
+
raise
|
|
137
|
+
self._coordinator.approvals.respond(response)
|
|
138
|
+
|
|
139
|
+
async def cancel(self) -> None:
|
|
140
|
+
await self._coordinator.shutdown("parent run cancelled")
|
|
141
|
+
self._control.cancel()
|
|
142
|
+
if not self._task.done():
|
|
143
|
+
self._task.cancel()
|
|
144
|
+
await self.result()
|
|
145
|
+
|
|
146
|
+
async def result(self) -> RunResult:
|
|
147
|
+
if self._result is not None:
|
|
148
|
+
return self._result
|
|
149
|
+
async with self._result_lock:
|
|
150
|
+
if self._result is None:
|
|
151
|
+
self._result = await self._task
|
|
152
|
+
return self._result
|
|
153
|
+
|
|
154
|
+
async def compact(self) -> None:
|
|
155
|
+
if self.done:
|
|
156
|
+
raise RuntimeError("cannot compact a completed run")
|
|
157
|
+
self._control.request_compaction()
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def permission_mode(self) -> PermissionMode:
|
|
161
|
+
return self._policy.mode
|
|
162
|
+
|
|
163
|
+
def set_permission_mode(self, mode: PermissionMode | str) -> PermissionMode:
|
|
164
|
+
selected = PermissionMode(mode)
|
|
165
|
+
previous = self._policy.mode
|
|
166
|
+
self._policy.set_mode(selected)
|
|
167
|
+
self._loop.system_prompt = self._loop.system_prompt.replace(
|
|
168
|
+
f"权限模式: {previous.value}.",
|
|
169
|
+
f"权限模式: {selected.value}.",
|
|
170
|
+
)
|
|
171
|
+
self._coordinator.set_permission_mode(selected)
|
|
172
|
+
return selected
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def done(self) -> bool:
|
|
176
|
+
return self._task.done()
|
|
177
|
+
|
|
178
|
+
def subagents(self) -> tuple[SubagentRecord, ...]:
|
|
179
|
+
return self._coordinator.list()
|
|
180
|
+
|
|
181
|
+
async def cancel_subagent(self, subagent_id: str) -> None:
|
|
182
|
+
if self.done:
|
|
183
|
+
raise RuntimeError("cannot cancel a subagent after the parent run has ended")
|
|
184
|
+
await self._coordinator.cancel(subagent_id)
|
|
185
|
+
|
|
186
|
+
async def integrate_subagent(
|
|
187
|
+
self,
|
|
188
|
+
subagent_id: str,
|
|
189
|
+
*,
|
|
190
|
+
verification_commands: tuple[str, ...] = (),
|
|
191
|
+
) -> SubagentResult:
|
|
192
|
+
if self.done:
|
|
193
|
+
raise RuntimeError("cannot integrate a subagent after the parent run has ended")
|
|
194
|
+
return await self._coordinator.integrate(subagent_id, verification_commands)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Windcode:
|
|
198
|
+
"""Public asynchronous SDK client and runtime owner."""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
config: AppConfig,
|
|
203
|
+
*,
|
|
204
|
+
state_root: Path | None = None,
|
|
205
|
+
credential_store: CredentialStore | None = None,
|
|
206
|
+
workspace: Path | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
self.config = config
|
|
209
|
+
self.credential_store = credential_store or FileCredentialStore()
|
|
210
|
+
self.workspace = (workspace or Path.cwd()).expanduser().resolve()
|
|
211
|
+
self.state_root = self._resolve_state_root(state_root)
|
|
212
|
+
self.transport_registry = TransportRegistry()
|
|
213
|
+
self.tool_registry: ToolRegistry | None = None
|
|
214
|
+
self._default_chain: list[str] = []
|
|
215
|
+
self._handles: set[RunHandle] = set()
|
|
216
|
+
self._entered = False
|
|
217
|
+
self.extension_service: ExtensionService | None = None
|
|
218
|
+
self._client_extensions: RunExtensions | None = None
|
|
219
|
+
self._mcp_tool_catalogs: dict[str, tuple[McpToolDefinition, ...]] = {}
|
|
220
|
+
self._mcp_selected_tools: set[str] = set()
|
|
221
|
+
self._mcp_direct_servers: tuple[str, ...] = ()
|
|
222
|
+
self._mcp_start_task: asyncio.Task[None] | None = None
|
|
223
|
+
self._mcp_retirement_tasks: set[asyncio.Task[None]] = set()
|
|
224
|
+
self.memory_service: MemoryService | None = None
|
|
225
|
+
|
|
226
|
+
def _resolve_state_root(self, explicit_root: Path | None) -> Path:
|
|
227
|
+
if explicit_root is not None:
|
|
228
|
+
return explicit_root.expanduser().resolve()
|
|
229
|
+
legacy_root = user_state_path("windcode").expanduser().resolve()
|
|
230
|
+
configured_user_root = self.config.storage.user_storage_root
|
|
231
|
+
user_root = (
|
|
232
|
+
self._configured_state_path(configured_user_root)
|
|
233
|
+
if configured_user_root is not None
|
|
234
|
+
else legacy_root / "state"
|
|
235
|
+
)
|
|
236
|
+
source_root = user_root if user_root.exists() else legacy_root
|
|
237
|
+
configured = self.config.storage.project_state_root
|
|
238
|
+
if configured is None:
|
|
239
|
+
self._migrate_state_root(source_root, user_root)
|
|
240
|
+
return user_root
|
|
241
|
+
project_root = self._configured_state_path(configured)
|
|
242
|
+
self._migrate_state_root(source_root, project_root)
|
|
243
|
+
return project_root
|
|
244
|
+
|
|
245
|
+
def _configured_state_path(self, value: str) -> Path:
|
|
246
|
+
project_root = Path(value).expanduser()
|
|
247
|
+
if not project_root.is_absolute():
|
|
248
|
+
project_root = self.workspace / project_root
|
|
249
|
+
return project_root.resolve()
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _state_manifest(root: Path) -> dict[str, int]:
|
|
253
|
+
return {
|
|
254
|
+
str(path.relative_to(root)): path.stat().st_size
|
|
255
|
+
for path in root.rglob("*")
|
|
256
|
+
if path.is_file() and not path.is_symlink()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def _migrate_state_root(cls, source: Path, target: Path) -> None:
|
|
261
|
+
"""Copy the complete legacy state once, validate it, then atomically install it."""
|
|
262
|
+
if source == target or target.exists():
|
|
263
|
+
return
|
|
264
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
temporary_parent = source.parent if target.is_relative_to(source) else target.parent
|
|
266
|
+
temporary = temporary_parent / f".{target.name}.migrate-{uuid4().hex}"
|
|
267
|
+
try:
|
|
268
|
+
if source.exists():
|
|
269
|
+
shutil.copytree(source, temporary, copy_function=shutil.copy2)
|
|
270
|
+
if cls._state_manifest(source) != cls._state_manifest(temporary):
|
|
271
|
+
raise OSError("project state migration validation failed")
|
|
272
|
+
else:
|
|
273
|
+
temporary.mkdir(parents=True)
|
|
274
|
+
temporary.replace(target)
|
|
275
|
+
finally:
|
|
276
|
+
if temporary.exists():
|
|
277
|
+
shutil.rmtree(temporary)
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def open(
|
|
281
|
+
cls,
|
|
282
|
+
config: AppConfig | Mapping[str, Any] | None = None,
|
|
283
|
+
*,
|
|
284
|
+
state_root: Path | None = None,
|
|
285
|
+
credential_store: CredentialStore | None = None,
|
|
286
|
+
workspace: Path | None = None,
|
|
287
|
+
) -> Self:
|
|
288
|
+
parsed = config if isinstance(config, AppConfig) else AppConfig.model_validate(config or {})
|
|
289
|
+
return cls(
|
|
290
|
+
parsed,
|
|
291
|
+
state_root=state_root,
|
|
292
|
+
credential_store=credential_store,
|
|
293
|
+
workspace=workspace,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def __aenter__(self) -> Self:
|
|
297
|
+
if self._entered:
|
|
298
|
+
raise RuntimeError("Windcode client is already open")
|
|
299
|
+
self._entered = True
|
|
300
|
+
self.state_root.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
if self.config.memory.enabled:
|
|
302
|
+
self.memory_service = MemoryService(self.state_root, self.workspace)
|
|
303
|
+
if self.config.providers:
|
|
304
|
+
self.transport_registry = TransportRegistry.from_config(
|
|
305
|
+
self.config,
|
|
306
|
+
credential_store=self.credential_store,
|
|
307
|
+
allow_missing=True,
|
|
308
|
+
)
|
|
309
|
+
if self.config.primary_provider is not None:
|
|
310
|
+
self._default_chain = [
|
|
311
|
+
alias
|
|
312
|
+
for alias in (self.config.primary_provider, *self.config.fallback_chain)
|
|
313
|
+
if alias in self.transport_registry.aliases
|
|
314
|
+
]
|
|
315
|
+
self.tool_registry = create_builtin_registry(
|
|
316
|
+
shell_timeout=self.config.budgets.shell_timeout_seconds,
|
|
317
|
+
)
|
|
318
|
+
extension_root = self.state_root / "extensions"
|
|
319
|
+
self.extension_service = ExtensionService(
|
|
320
|
+
self.config.extensions,
|
|
321
|
+
self.workspace,
|
|
322
|
+
ExtensionStateStore(extension_root / "state.json"),
|
|
323
|
+
extension_root / "plugins",
|
|
324
|
+
)
|
|
325
|
+
await self.extension_service.reload()
|
|
326
|
+
self._client_extensions = self._create_client_extensions()
|
|
327
|
+
self._mcp_start_task = asyncio.create_task(self._start_required_mcp())
|
|
328
|
+
return self
|
|
329
|
+
|
|
330
|
+
def _create_client_extensions(self) -> RunExtensions:
|
|
331
|
+
if self.extension_service is None:
|
|
332
|
+
raise RuntimeError("extension service is not initialized")
|
|
333
|
+
return RunExtensions.create(
|
|
334
|
+
self.extension_service.snapshot,
|
|
335
|
+
session_id="client",
|
|
336
|
+
run_id="startup",
|
|
337
|
+
credential_store=self.credential_store,
|
|
338
|
+
max_content_bytes=self.config.extensions.max_content_bytes,
|
|
339
|
+
connect_timeout=self.config.extensions.connect_timeout_seconds,
|
|
340
|
+
call_timeout=self.config.extensions.call_timeout_seconds,
|
|
341
|
+
network_enabled=self.config.sandbox.network_enabled,
|
|
342
|
+
mcp_tool_catalogs=self._mcp_tool_catalogs,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
async def _retire_client_extensions(
|
|
346
|
+
self,
|
|
347
|
+
extensions: RunExtensions,
|
|
348
|
+
handles: tuple[RunHandle, ...],
|
|
349
|
+
startup: asyncio.Task[None] | None,
|
|
350
|
+
) -> None:
|
|
351
|
+
await asyncio.gather(*(handle.result() for handle in handles), return_exceptions=True)
|
|
352
|
+
if startup is not None:
|
|
353
|
+
if not startup.done():
|
|
354
|
+
startup.cancel()
|
|
355
|
+
await asyncio.gather(startup, return_exceptions=True)
|
|
356
|
+
extensions.mcp.observer = None
|
|
357
|
+
await extensions.aclose()
|
|
358
|
+
|
|
359
|
+
async def _start_required_mcp(self) -> None:
|
|
360
|
+
if self._client_extensions is None or self.tool_registry is None:
|
|
361
|
+
return
|
|
362
|
+
await self._client_extensions.mcp.activate_required()
|
|
363
|
+
registered = await self._client_extensions.mcp_capabilities.register_direct_tools(
|
|
364
|
+
self.tool_registry,
|
|
365
|
+
direct_tool_limit=self.config.extensions.direct_tool_limit,
|
|
366
|
+
)
|
|
367
|
+
if registered:
|
|
368
|
+
self._mcp_direct_servers = self._client_extensions.mcp.required_server_ids
|
|
369
|
+
|
|
370
|
+
async def wait_for_required_mcp(self) -> None:
|
|
371
|
+
"""Wait for the single client-level MCP startup task."""
|
|
372
|
+
if self._mcp_start_task is not None:
|
|
373
|
+
await self._mcp_start_task
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def required_mcp_loading(self) -> bool:
|
|
377
|
+
return self._mcp_start_task is not None and not self._mcp_start_task.done()
|
|
378
|
+
|
|
379
|
+
def _extensions(self) -> ExtensionService:
|
|
380
|
+
if not self._entered or self.extension_service is None:
|
|
381
|
+
raise RuntimeError("manage extensions inside the Windcode async context")
|
|
382
|
+
return self.extension_service
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def extension_snapshot(self) -> ExtensionSnapshot:
|
|
386
|
+
return self._extensions().snapshot
|
|
387
|
+
|
|
388
|
+
async def list_extensions(self) -> tuple[CapabilityRecord, ...]:
|
|
389
|
+
return await self._extensions().list_capabilities()
|
|
390
|
+
|
|
391
|
+
async def inspect_extension(self, identifier: str) -> tuple[CapabilityRecord, ...]:
|
|
392
|
+
return await self._extensions().inspect(identifier)
|
|
393
|
+
|
|
394
|
+
async def install_extension(self, path: Path, *, enable: bool = False) -> InstallResult:
|
|
395
|
+
return await self._extensions().install_local(path, enable=enable)
|
|
396
|
+
|
|
397
|
+
async def set_extension_enabled(self, identifier: str, enabled: bool) -> ManagementResult:
|
|
398
|
+
return await self._extensions().set_enabled(identifier, enabled)
|
|
399
|
+
|
|
400
|
+
async def trust_extension_workspace(
|
|
401
|
+
self, workspace: Path, trusted: bool = True
|
|
402
|
+
) -> ManagementResult:
|
|
403
|
+
return await self._extensions().trust_workspace(workspace, trusted)
|
|
404
|
+
|
|
405
|
+
async def reload_extensions(self) -> ManagementResult:
|
|
406
|
+
result = await self._extensions().reload()
|
|
407
|
+
previous = self._client_extensions
|
|
408
|
+
previous_startup = self._mcp_start_task
|
|
409
|
+
active_handles = tuple(handle for handle in self._handles if not handle.done)
|
|
410
|
+
self._mcp_tool_catalogs.clear()
|
|
411
|
+
self._mcp_selected_tools.clear()
|
|
412
|
+
self._mcp_direct_servers = ()
|
|
413
|
+
self._client_extensions = self._create_client_extensions()
|
|
414
|
+
self._mcp_start_task = asyncio.create_task(self._start_required_mcp())
|
|
415
|
+
if previous is not None:
|
|
416
|
+
retirement = asyncio.create_task(
|
|
417
|
+
self._retire_client_extensions(previous, active_handles, previous_startup)
|
|
418
|
+
)
|
|
419
|
+
self._mcp_retirement_tasks.add(retirement)
|
|
420
|
+
retirement.add_done_callback(self._mcp_retirement_tasks.discard)
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
def extension_commands(
|
|
424
|
+
self, *, reserved: frozenset[str] = frozenset()
|
|
425
|
+
) -> tuple[CommandRoute, ...]:
|
|
426
|
+
return self._extensions().command_routes(reserved=reserved)
|
|
427
|
+
|
|
428
|
+
def search_skills(self, query: str = "") -> tuple[SkillSearchResult, ...]:
|
|
429
|
+
"""Return enabled, trusted, unshadowed Skills from the current snapshot."""
|
|
430
|
+
catalog = SkillCatalog(
|
|
431
|
+
self.extension_snapshot,
|
|
432
|
+
SkillLoader(max_content_bytes=self.config.extensions.max_content_bytes),
|
|
433
|
+
)
|
|
434
|
+
return catalog.search(query)
|
|
435
|
+
|
|
436
|
+
def extension_audit(self) -> tuple[ManagementAuditRecord, ...]:
|
|
437
|
+
return self._extensions().audit_records
|
|
438
|
+
|
|
439
|
+
async def __aexit__(
|
|
440
|
+
self,
|
|
441
|
+
exc_type: type[BaseException] | None,
|
|
442
|
+
exc: BaseException | None,
|
|
443
|
+
traceback: TracebackType | None,
|
|
444
|
+
) -> None:
|
|
445
|
+
del exc_type, exc, traceback
|
|
446
|
+
await self.aclose()
|
|
447
|
+
|
|
448
|
+
def register_tool(self, tool: Tool, *, replace_existing: bool = False) -> None:
|
|
449
|
+
if self.tool_registry is None:
|
|
450
|
+
raise RuntimeError("register tools inside the Windcode async context")
|
|
451
|
+
self.tool_registry.register(tool, replace=replace_existing)
|
|
452
|
+
|
|
453
|
+
def register_transport(
|
|
454
|
+
self,
|
|
455
|
+
alias: str,
|
|
456
|
+
model: str,
|
|
457
|
+
transport: ModelTransport,
|
|
458
|
+
*,
|
|
459
|
+
replace_existing: bool = False,
|
|
460
|
+
primary: bool = False,
|
|
461
|
+
) -> None:
|
|
462
|
+
self.transport_registry.register(alias, model, transport, replace=replace_existing)
|
|
463
|
+
if primary or not self._default_chain:
|
|
464
|
+
self._default_chain = [alias]
|
|
465
|
+
|
|
466
|
+
async def reconfigure_models(self, config: AppConfig, *, config_file: Path) -> None:
|
|
467
|
+
if any(not handle.done for handle in self._handles):
|
|
468
|
+
raise RuntimeError("cannot configure models while a run is active")
|
|
469
|
+
registry = (
|
|
470
|
+
TransportRegistry.from_config(
|
|
471
|
+
config,
|
|
472
|
+
credential_store=self.credential_store,
|
|
473
|
+
allow_missing=True,
|
|
474
|
+
)
|
|
475
|
+
if config.providers
|
|
476
|
+
else TransportRegistry()
|
|
477
|
+
)
|
|
478
|
+
try:
|
|
479
|
+
save_model_config(config_file, self.config, config)
|
|
480
|
+
except Exception:
|
|
481
|
+
await registry.aclose()
|
|
482
|
+
raise
|
|
483
|
+
|
|
484
|
+
previous_registry = self.transport_registry
|
|
485
|
+
self.transport_registry = registry
|
|
486
|
+
self.config = config
|
|
487
|
+
configured_chain = (
|
|
488
|
+
(config.primary_provider, *config.fallback_chain)
|
|
489
|
+
if config.primary_provider is not None
|
|
490
|
+
else ()
|
|
491
|
+
)
|
|
492
|
+
self._default_chain = [
|
|
493
|
+
alias for alias in configured_chain if alias in self.transport_registry.aliases
|
|
494
|
+
]
|
|
495
|
+
await previous_registry.aclose()
|
|
496
|
+
|
|
497
|
+
def _model_chain(self, requested: str | None) -> tuple[ModelTarget, ...]:
|
|
498
|
+
if requested is not None and requested in self.transport_registry.aliases:
|
|
499
|
+
return (self.transport_registry.get(requested),)
|
|
500
|
+
if not self._default_chain:
|
|
501
|
+
raise RuntimeError("no model transport is configured")
|
|
502
|
+
chain = tuple(self.transport_registry.get(alias) for alias in self._default_chain)
|
|
503
|
+
if requested is not None:
|
|
504
|
+
chain = (replace(chain[0], model=requested), *chain[1:])
|
|
505
|
+
return chain
|
|
506
|
+
|
|
507
|
+
def _memory(self) -> MemoryService:
|
|
508
|
+
if not self.config.memory.enabled or self.memory_service is None:
|
|
509
|
+
raise RuntimeError("long-term memory is disabled")
|
|
510
|
+
return self.memory_service
|
|
511
|
+
|
|
512
|
+
def list_memories(self, *, status: MemoryStatus | None = None) -> tuple[MemoryRecord, ...]:
|
|
513
|
+
service = self._memory()
|
|
514
|
+
return service.store.list(status=status, project_id=service.project_id)
|
|
515
|
+
|
|
516
|
+
def search_memories(self, query: str, *, limit: int | None = None) -> tuple[MemoryRecord, ...]:
|
|
517
|
+
service = self._memory()
|
|
518
|
+
results = service.store.search(
|
|
519
|
+
query,
|
|
520
|
+
project_id=service.project_id,
|
|
521
|
+
limit=limit or self.config.memory.recall_limit,
|
|
522
|
+
statuses=(MemoryStatus.ACTIVE, MemoryStatus.CANDIDATE),
|
|
523
|
+
)
|
|
524
|
+
return tuple(result.record for result in results)
|
|
525
|
+
|
|
526
|
+
def get_memory(self, memory_id: str) -> MemoryRecord:
|
|
527
|
+
return self._memory().store.get(memory_id)
|
|
528
|
+
|
|
529
|
+
def create_memory_candidate(
|
|
530
|
+
self,
|
|
531
|
+
*,
|
|
532
|
+
kind: MemoryKind,
|
|
533
|
+
scope: MemoryScope,
|
|
534
|
+
title: str,
|
|
535
|
+
summary: str,
|
|
536
|
+
body: str,
|
|
537
|
+
source: MemorySource | None = None,
|
|
538
|
+
tags: tuple[str, ...] = (),
|
|
539
|
+
evidence: tuple[str, ...] = (),
|
|
540
|
+
confidence: float = 0.5,
|
|
541
|
+
activation: MemoryActivation | None = None,
|
|
542
|
+
priority: int | None = None,
|
|
543
|
+
) -> MemoryRecord:
|
|
544
|
+
return self._memory().create_candidate(
|
|
545
|
+
kind=kind,
|
|
546
|
+
scope=scope,
|
|
547
|
+
title=title,
|
|
548
|
+
summary=summary,
|
|
549
|
+
body=body,
|
|
550
|
+
source=source,
|
|
551
|
+
tags=tags,
|
|
552
|
+
evidence=evidence,
|
|
553
|
+
confidence=confidence,
|
|
554
|
+
activation=activation,
|
|
555
|
+
priority=priority,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
def confirm_memory(self, memory_id: str) -> MemoryRecord:
|
|
559
|
+
return self._memory().store.transition(memory_id, MemoryStatus.ACTIVE)
|
|
560
|
+
|
|
561
|
+
def reject_memory(self, memory_id: str) -> MemoryRecord:
|
|
562
|
+
return self._memory().store.transition(memory_id, MemoryStatus.REJECTED)
|
|
563
|
+
|
|
564
|
+
def archive_memory(self, memory_id: str) -> MemoryRecord:
|
|
565
|
+
return self._memory().store.transition(memory_id, MemoryStatus.ARCHIVED)
|
|
566
|
+
|
|
567
|
+
def update_memory(self, memory_id: str, **changes: Any) -> MemoryRecord:
|
|
568
|
+
return self._memory().store.update(memory_id, **changes)
|
|
569
|
+
|
|
570
|
+
def set_memory_activation(
|
|
571
|
+
self, memory_id: str, activation: MemoryActivation | str
|
|
572
|
+
) -> MemoryRecord:
|
|
573
|
+
value = (
|
|
574
|
+
activation if isinstance(activation, MemoryActivation) else MemoryActivation(activation)
|
|
575
|
+
)
|
|
576
|
+
return self._memory().store.update(memory_id, activation=value)
|
|
577
|
+
|
|
578
|
+
def delete_memory(self, memory_id: str) -> None:
|
|
579
|
+
self._memory().store.delete(memory_id)
|
|
580
|
+
|
|
581
|
+
def rebuild_memory_index(self) -> int:
|
|
582
|
+
return self._memory().store.rebuild()
|
|
583
|
+
|
|
584
|
+
def export_project_memories(self, destination: Path) -> tuple[Path, ...]:
|
|
585
|
+
service = self._memory()
|
|
586
|
+
return service.store.export_project(service.project_id, destination)
|
|
587
|
+
|
|
588
|
+
def draft_skill_from_memory(self, memory_id: str) -> str:
|
|
589
|
+
return self._memory().draft_skill(memory_id)
|
|
590
|
+
|
|
591
|
+
def set_memory_enabled(self, enabled: bool, *, config_file: Path) -> None:
|
|
592
|
+
updated_memory = self.config.memory.model_copy(update={"enabled": enabled})
|
|
593
|
+
updated = self.config.model_copy(update={"memory": updated_memory})
|
|
594
|
+
save_memory_config(config_file, updated)
|
|
595
|
+
self.config = updated
|
|
596
|
+
self.memory_service = MemoryService(self.state_root, self.workspace) if enabled else None
|
|
597
|
+
|
|
598
|
+
@staticmethod
|
|
599
|
+
def _session_summary(prompt: str, *, limit: int = 60) -> str:
|
|
600
|
+
summary = " ".join(prompt.split())
|
|
601
|
+
if len(summary) <= limit:
|
|
602
|
+
return summary
|
|
603
|
+
return summary[: limit - 3].rstrip() + "..."
|
|
604
|
+
|
|
605
|
+
def _session_store(self, session_id: str) -> SessionStore:
|
|
606
|
+
return SessionStore.open(self.state_root / "sessions", session_id)
|
|
607
|
+
|
|
608
|
+
def session_exists(self, session_id: str) -> bool:
|
|
609
|
+
return (self.state_root / "sessions" / session_id / "meta.json").is_file()
|
|
610
|
+
|
|
611
|
+
def load_session_records(self, session_id: str) -> tuple[EventRecord, ...]:
|
|
612
|
+
store = self._session_store(session_id)
|
|
613
|
+
if store.metadata.head_record_id is None:
|
|
614
|
+
return ()
|
|
615
|
+
return ancestor_chain(store.load_records(), store.metadata.head_record_id)
|
|
616
|
+
|
|
617
|
+
def load_session_messages(self, session_id: str) -> tuple[Message, ...]:
|
|
618
|
+
return heal_dangling_tool_calls(
|
|
619
|
+
tuple(
|
|
620
|
+
message_from_dict(record.payload)
|
|
621
|
+
for record in self.load_session_records(session_id)
|
|
622
|
+
if record.record_type == "conversation_message"
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def _ensure_session_summary(self, store: SessionStore) -> SessionMetadata:
|
|
627
|
+
if store.metadata.summary:
|
|
628
|
+
return store.metadata
|
|
629
|
+
for message in self.load_session_messages(store.metadata.session_id):
|
|
630
|
+
if message.role is not Role.USER:
|
|
631
|
+
continue
|
|
632
|
+
text = "".join(
|
|
633
|
+
block.text for block in message.content if isinstance(block, TextBlock)
|
|
634
|
+
).strip()
|
|
635
|
+
if text:
|
|
636
|
+
store.set_summary(self._session_summary(text))
|
|
637
|
+
break
|
|
638
|
+
return store.metadata
|
|
639
|
+
|
|
640
|
+
def start_run(self, request: RunRequest) -> RunHandle:
|
|
641
|
+
if not self._entered or self.tool_registry is None:
|
|
642
|
+
raise RuntimeError("start runs inside the Windcode async context")
|
|
643
|
+
workspace = request.workspace.expanduser().resolve()
|
|
644
|
+
if not workspace.is_dir():
|
|
645
|
+
raise ValueError(f"workspace is not a directory: {workspace}")
|
|
646
|
+
sessions_root = self.state_root / "sessions"
|
|
647
|
+
existing_session = (
|
|
648
|
+
request.session_id is not None
|
|
649
|
+
and (sessions_root / request.session_id / "meta.json").exists()
|
|
650
|
+
)
|
|
651
|
+
if existing_session:
|
|
652
|
+
assert request.session_id is not None
|
|
653
|
+
session = SessionStore.open(sessions_root, request.session_id)
|
|
654
|
+
else:
|
|
655
|
+
session = SessionStore.create(sessions_root, request.session_id)
|
|
656
|
+
if not session.metadata.summary:
|
|
657
|
+
session.set_summary(self._session_summary(request.prompt))
|
|
658
|
+
initial_messages: tuple[Message, ...] = ()
|
|
659
|
+
if existing_session and session.metadata.head_record_id is not None:
|
|
660
|
+
records = ancestor_chain(
|
|
661
|
+
session.load_records(),
|
|
662
|
+
session.metadata.head_record_id,
|
|
663
|
+
)
|
|
664
|
+
initial_messages = heal_dangling_tool_calls(
|
|
665
|
+
tuple(
|
|
666
|
+
message_from_dict(record.payload)
|
|
667
|
+
for record in records
|
|
668
|
+
if record.record_type == "conversation_message"
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
run_id = uuid4().hex
|
|
672
|
+
artifact_store = ArtifactStore(session.session_dir)
|
|
673
|
+
extension_snapshot = self._extensions().snapshot
|
|
674
|
+
extension_redactor = DynamicRedactor()
|
|
675
|
+
run_extensions = RunExtensions.create(
|
|
676
|
+
extension_snapshot,
|
|
677
|
+
session_id=session.metadata.session_id,
|
|
678
|
+
run_id=run_id,
|
|
679
|
+
credential_store=self.credential_store,
|
|
680
|
+
max_content_bytes=self.config.extensions.max_content_bytes,
|
|
681
|
+
connect_timeout=self.config.extensions.connect_timeout_seconds,
|
|
682
|
+
call_timeout=self.config.extensions.call_timeout_seconds,
|
|
683
|
+
observe_secret=extension_redactor.register,
|
|
684
|
+
artifact_store=artifact_store,
|
|
685
|
+
network_enabled=self.config.sandbox.network_enabled,
|
|
686
|
+
mcp_runtime=(None if self._client_extensions is None else self._client_extensions.mcp),
|
|
687
|
+
mcp_tool_catalogs=self._mcp_tool_catalogs,
|
|
688
|
+
)
|
|
689
|
+
trace = TraceStore(
|
|
690
|
+
run_id,
|
|
691
|
+
root=self.state_root / "traces",
|
|
692
|
+
enabled=self.config.trace.enabled,
|
|
693
|
+
include_tool_arguments=self.config.trace.include_tool_arguments,
|
|
694
|
+
include_transient_events=self.config.trace.include_transient_events,
|
|
695
|
+
retention_days=self.config.trace.retention_days,
|
|
696
|
+
max_total_mb=self.config.trace.max_total_mb,
|
|
697
|
+
)
|
|
698
|
+
bus = EventBus(session, trace)
|
|
699
|
+
run_extensions.event_observer = lambda event: bus.publish(event, durable=True)
|
|
700
|
+
mode = (
|
|
701
|
+
PermissionMode(request.permission_mode)
|
|
702
|
+
if request.permission_mode is not None
|
|
703
|
+
else self.config.permission.mode
|
|
704
|
+
)
|
|
705
|
+
sandbox_status = detect_bubblewrap()
|
|
706
|
+
sandbox = (
|
|
707
|
+
BubblewrapSandbox(workspace, sandbox_status)
|
|
708
|
+
if self.config.sandbox.enabled and sandbox_status.available
|
|
709
|
+
else None
|
|
710
|
+
)
|
|
711
|
+
run_registry = self.tool_registry.clone()
|
|
712
|
+
register_skill_tools(run_registry, run_extensions.skills, run_extensions.activate_skill)
|
|
713
|
+
register_mcp_status_tool(
|
|
714
|
+
run_registry,
|
|
715
|
+
extension_snapshot.capabilities,
|
|
716
|
+
self._mcp_tool_catalogs,
|
|
717
|
+
self._mcp_selected_tools,
|
|
718
|
+
)
|
|
719
|
+
if run_extensions.mcp.server_ids:
|
|
720
|
+
register_mcp_management_tools(
|
|
721
|
+
run_registry, run_extensions.mcp_capabilities, self._mcp_selected_tools
|
|
722
|
+
)
|
|
723
|
+
run_registry.register(
|
|
724
|
+
ShellTool(
|
|
725
|
+
sandbox=sandbox,
|
|
726
|
+
default_timeout=self.config.budgets.shell_timeout_seconds,
|
|
727
|
+
),
|
|
728
|
+
replace=True,
|
|
729
|
+
)
|
|
730
|
+
policy = PolicyEngine(
|
|
731
|
+
mode,
|
|
732
|
+
sandbox_enabled=self.config.sandbox.enabled,
|
|
733
|
+
sandbox_available=sandbox_status.available,
|
|
734
|
+
)
|
|
735
|
+
for record in session.load_records():
|
|
736
|
+
if record.record_type != "session_approval":
|
|
737
|
+
continue
|
|
738
|
+
if record.payload.get("workspace") != str(workspace):
|
|
739
|
+
continue
|
|
740
|
+
tool_name = record.payload.get("tool_name")
|
|
741
|
+
raw_effects = record.payload.get("effects")
|
|
742
|
+
if not isinstance(tool_name, str) or not isinstance(raw_effects, list):
|
|
743
|
+
continue
|
|
744
|
+
try:
|
|
745
|
+
effects = frozenset(
|
|
746
|
+
ToolEffect(str(effect)) for effect in cast(list[object], raw_effects)
|
|
747
|
+
)
|
|
748
|
+
except ValueError:
|
|
749
|
+
continue
|
|
750
|
+
policy.restore_session_approval(tool_name, effects)
|
|
751
|
+
child_tools = run_registry.clone()
|
|
752
|
+
instructions = load_instructions(workspace, workspace_root=workspace)
|
|
753
|
+
run_memory = (
|
|
754
|
+
MemoryService(self.state_root, workspace) if self.config.memory.enabled else None
|
|
755
|
+
)
|
|
756
|
+
tool_memory_id: str | None = None
|
|
757
|
+
if run_memory is not None:
|
|
758
|
+
|
|
759
|
+
async def observe_memory_tool(action: str, details: dict[str, Any]) -> None:
|
|
760
|
+
nonlocal tool_memory_id
|
|
761
|
+
memory_id = details.get("memory_id")
|
|
762
|
+
if action in {"activated", "candidate_created", "already_exists"} and isinstance(
|
|
763
|
+
memory_id, str
|
|
764
|
+
):
|
|
765
|
+
tool_memory_id = memory_id
|
|
766
|
+
await bus.publish(
|
|
767
|
+
MemoryEvent(
|
|
768
|
+
event_id=uuid4().hex,
|
|
769
|
+
session_id=session.metadata.session_id,
|
|
770
|
+
run_id=run_id,
|
|
771
|
+
turn=0,
|
|
772
|
+
action=action,
|
|
773
|
+
memory_id=memory_id if isinstance(memory_id, str) else None,
|
|
774
|
+
memory_kind=str(details.get("kind", "")) or None,
|
|
775
|
+
scope=str(details.get("scope", "")) or None,
|
|
776
|
+
status=str(details.get("status", "")),
|
|
777
|
+
details=details,
|
|
778
|
+
),
|
|
779
|
+
durable=True,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
register_memory_tools(
|
|
783
|
+
run_registry,
|
|
784
|
+
run_memory,
|
|
785
|
+
observe_memory_tool,
|
|
786
|
+
max_chars=self.config.memory.recall_max_chars,
|
|
787
|
+
user_prompt=request.prompt,
|
|
788
|
+
source=MemorySource(session.metadata.session_id, run_id),
|
|
789
|
+
enabled_kinds=frozenset(
|
|
790
|
+
kind
|
|
791
|
+
for kind, enabled in {
|
|
792
|
+
MemoryKind.USER_PROFILE: self.config.memory.user_profile_enabled,
|
|
793
|
+
MemoryKind.PROJECT_KNOWLEDGE: self.config.memory.project_knowledge_enabled,
|
|
794
|
+
MemoryKind.EXPERIENCE: self.config.memory.experience_enabled,
|
|
795
|
+
MemoryKind.SOP: self.config.memory.sop_enabled,
|
|
796
|
+
MemoryKind.REFERENCE: self.config.memory.reference_enabled,
|
|
797
|
+
}.items()
|
|
798
|
+
if enabled
|
|
799
|
+
),
|
|
800
|
+
)
|
|
801
|
+
memory_context = ""
|
|
802
|
+
if run_memory is not None:
|
|
803
|
+
memory_context = run_memory.build_context(
|
|
804
|
+
request.prompt,
|
|
805
|
+
baseline_max_records=self.config.memory.baseline_max_records,
|
|
806
|
+
baseline_max_chars=self.config.memory.baseline_max_chars,
|
|
807
|
+
search_limit=self.config.memory.recall_limit,
|
|
808
|
+
search_max_chars=self.config.memory.recall_max_chars,
|
|
809
|
+
)
|
|
810
|
+
budgets = RunBudgets(
|
|
811
|
+
max_model_steps=self.config.budgets.max_model_steps,
|
|
812
|
+
max_tool_calls=self.config.budgets.max_tool_calls,
|
|
813
|
+
max_runtime_seconds=self.config.budgets.max_runtime_seconds,
|
|
814
|
+
)
|
|
815
|
+
control = RunControl(budgets)
|
|
816
|
+
if request.compact_before_run:
|
|
817
|
+
control.request_compaction()
|
|
818
|
+
factory = ChildRuntimeFactory(
|
|
819
|
+
config=self.config,
|
|
820
|
+
state_root=self.state_root,
|
|
821
|
+
parent_tools=child_tools,
|
|
822
|
+
model_chain=lambda model: self._model_chain(model or request.model),
|
|
823
|
+
extension_snapshot=extension_snapshot,
|
|
824
|
+
)
|
|
825
|
+
coordinator = SubagentCoordinator(
|
|
826
|
+
parent_session_id=session.metadata.session_id,
|
|
827
|
+
parent_run_id=run_id,
|
|
828
|
+
workspace=workspace,
|
|
829
|
+
permission_mode=mode,
|
|
830
|
+
config=self.config.subagents,
|
|
831
|
+
event_bus=bus,
|
|
832
|
+
factory=factory,
|
|
833
|
+
worktrees=WorktreeManager(worktrees_root=self.state_root / "worktrees"),
|
|
834
|
+
verification=VerificationRunner(
|
|
835
|
+
sandbox=sandbox,
|
|
836
|
+
timeout_seconds=self.config.budgets.shell_timeout_seconds,
|
|
837
|
+
),
|
|
838
|
+
network_enabled=self.config.sandbox.network_enabled,
|
|
839
|
+
event_observer=run_extensions.subagent_lifecycle,
|
|
840
|
+
)
|
|
841
|
+
add_subagent_tools(run_registry, coordinator)
|
|
842
|
+
|
|
843
|
+
unavailable_mcp_servers = tuple(
|
|
844
|
+
(
|
|
845
|
+
record.public_name,
|
|
846
|
+
"未信任当前工作区, 需要执行 extensions trust 后 reload",
|
|
847
|
+
)
|
|
848
|
+
for record in extension_snapshot.capabilities
|
|
849
|
+
if record.kind is CapabilityKind.MCP_SERVER and record.enabled and not record.trusted
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
def make_system_prompt(
|
|
853
|
+
direct_servers: tuple[str, ...], search_servers: tuple[str, ...]
|
|
854
|
+
) -> str:
|
|
855
|
+
prompt = build_system_prompt(
|
|
856
|
+
workspace=workspace,
|
|
857
|
+
permission_mode=policy.mode,
|
|
858
|
+
instructions=instructions,
|
|
859
|
+
tools=run_registry,
|
|
860
|
+
delegation_mode=self.config.subagents.mode,
|
|
861
|
+
skills=run_extensions.skills.search(),
|
|
862
|
+
mcp_direct_servers=direct_servers,
|
|
863
|
+
mcp_search_servers=search_servers,
|
|
864
|
+
mcp_unavailable_servers=unavailable_mcp_servers,
|
|
865
|
+
memory_enabled=run_memory is not None,
|
|
866
|
+
)
|
|
867
|
+
if memory_context:
|
|
868
|
+
prompt += f"\n\n{memory_context}"
|
|
869
|
+
return prompt
|
|
870
|
+
|
|
871
|
+
# Direct tools are not registered until run start (after activation), so
|
|
872
|
+
# build a provisional prompt now and refine it once we know which servers
|
|
873
|
+
# expose their tools directly versus needing the search/select flow.
|
|
874
|
+
system_prompt = make_system_prompt((), run_extensions.mcp.server_ids)
|
|
875
|
+
|
|
876
|
+
def record_session_approval(request: PolicyRequest) -> None:
|
|
877
|
+
session.append(
|
|
878
|
+
"session_approval",
|
|
879
|
+
{
|
|
880
|
+
"workspace": str(workspace),
|
|
881
|
+
"tool_name": request.tool_name,
|
|
882
|
+
"effects": sorted(effect.value for effect in request.effects),
|
|
883
|
+
},
|
|
884
|
+
durable=True,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
scheduler = ToolScheduler(
|
|
888
|
+
run_registry,
|
|
889
|
+
policy,
|
|
890
|
+
before_policy=run_extensions.before_policy,
|
|
891
|
+
permission_observer=run_extensions.permission_requested,
|
|
892
|
+
after_execute=run_extensions.after_execute,
|
|
893
|
+
session_approval_recorder=record_session_approval,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
async def run_hook_command(command: str, origin: str, hook_context: HookContext) -> str:
|
|
897
|
+
del hook_context
|
|
898
|
+
scheduled = ScheduledCall(
|
|
899
|
+
uuid4().hex,
|
|
900
|
+
"shell",
|
|
901
|
+
{"command": command},
|
|
902
|
+
origin=origin,
|
|
903
|
+
)
|
|
904
|
+
results = await scheduler.execute(
|
|
905
|
+
(scheduled,),
|
|
906
|
+
ToolContext(workspace, run_id, lambda: control.cancelled),
|
|
907
|
+
)
|
|
908
|
+
result = results[0].result
|
|
909
|
+
if result.is_error:
|
|
910
|
+
raise RuntimeError(result.output)
|
|
911
|
+
return result.output
|
|
912
|
+
|
|
913
|
+
run_extensions.hooks.executor.command_runner = run_hook_command
|
|
914
|
+
model_chain = self._model_chain(request.model)
|
|
915
|
+
|
|
916
|
+
async def extract_memories(result: RunResult) -> None:
|
|
917
|
+
if (
|
|
918
|
+
not self.config.memory.enabled
|
|
919
|
+
or not self.config.memory.extraction_enabled
|
|
920
|
+
or run_memory is None
|
|
921
|
+
):
|
|
922
|
+
return
|
|
923
|
+
enabled_kinds = {
|
|
924
|
+
MemoryKind.USER_PROFILE: self.config.memory.user_profile_enabled,
|
|
925
|
+
MemoryKind.PROJECT_KNOWLEDGE: self.config.memory.project_knowledge_enabled,
|
|
926
|
+
MemoryKind.EXPERIENCE: self.config.memory.experience_enabled,
|
|
927
|
+
MemoryKind.SOP: self.config.memory.sop_enabled,
|
|
928
|
+
MemoryKind.REFERENCE: self.config.memory.reference_enabled,
|
|
929
|
+
}
|
|
930
|
+
explicit_experience_id: str | None = None
|
|
931
|
+
if tool_memory_id is not None:
|
|
932
|
+
tool_memory = run_memory.store.get(tool_memory_id)
|
|
933
|
+
if tool_memory.kind is MemoryKind.EXPERIENCE:
|
|
934
|
+
explicit_experience_id = tool_memory_id
|
|
935
|
+
intent_kind = classify_memory_intent(request.prompt)
|
|
936
|
+
if tool_memory_id is None and intent_kind is not None and enabled_kinds[intent_kind]:
|
|
937
|
+
project_fact = is_project_fact(request.prompt)
|
|
938
|
+
scope = (
|
|
939
|
+
MemoryScope.USER
|
|
940
|
+
if intent_kind is MemoryKind.USER_PROFILE
|
|
941
|
+
or (intent_kind is MemoryKind.REFERENCE and not project_fact)
|
|
942
|
+
else MemoryScope.PROJECT
|
|
943
|
+
)
|
|
944
|
+
refined = await refine_memory(
|
|
945
|
+
model_chain[0],
|
|
946
|
+
text=request.prompt,
|
|
947
|
+
kind=intent_kind,
|
|
948
|
+
max_output_tokens=self.config.memory.extraction_max_output_tokens,
|
|
949
|
+
)
|
|
950
|
+
activation: MemoryActivation | None = None
|
|
951
|
+
if intent_kind is MemoryKind.PROJECT_KNOWLEDGE:
|
|
952
|
+
core = explicitly_always_project_fact(
|
|
953
|
+
request.prompt
|
|
954
|
+
) or await assess_core_project_fact(
|
|
955
|
+
model_chain[0],
|
|
956
|
+
text=request.prompt,
|
|
957
|
+
max_output_tokens=min(256, self.config.memory.extraction_max_output_tokens),
|
|
958
|
+
)
|
|
959
|
+
activation = MemoryActivation.ALWAYS if core else MemoryActivation.MANUAL
|
|
960
|
+
priority = 60 if activation is MemoryActivation.ALWAYS else None
|
|
961
|
+
candidate = run_memory.create_candidate(
|
|
962
|
+
kind=intent_kind,
|
|
963
|
+
scope=scope,
|
|
964
|
+
title=refined.title,
|
|
965
|
+
summary=refined.summary,
|
|
966
|
+
body=refined.body,
|
|
967
|
+
source=MemorySource(session.metadata.session_id, run_id),
|
|
968
|
+
tags=refined.tags,
|
|
969
|
+
evidence=(
|
|
970
|
+
() if intent_kind is MemoryKind.SOP else (f"用户原话: {request.prompt}",)
|
|
971
|
+
),
|
|
972
|
+
confidence=0.8,
|
|
973
|
+
activation=activation,
|
|
974
|
+
priority=priority,
|
|
975
|
+
)
|
|
976
|
+
if intent_kind is MemoryKind.SOP:
|
|
977
|
+
saved = candidate
|
|
978
|
+
action = "candidate_created"
|
|
979
|
+
policy = "explicit_sop_candidate"
|
|
980
|
+
else:
|
|
981
|
+
saved = run_memory.store.transition(candidate.memory_id, MemoryStatus.ACTIVE)
|
|
982
|
+
if intent_kind is MemoryKind.EXPERIENCE:
|
|
983
|
+
explicit_experience_id = saved.memory_id
|
|
984
|
+
action = "activated"
|
|
985
|
+
policy = "explicit_or_stable_fact"
|
|
986
|
+
await bus.publish(
|
|
987
|
+
MemoryEvent(
|
|
988
|
+
event_id=uuid4().hex,
|
|
989
|
+
session_id=session.metadata.session_id,
|
|
990
|
+
run_id=run_id,
|
|
991
|
+
turn=0,
|
|
992
|
+
action=action,
|
|
993
|
+
memory_id=saved.memory_id,
|
|
994
|
+
memory_kind=saved.kind.value,
|
|
995
|
+
scope=saved.scope.value,
|
|
996
|
+
status=saved.status.value,
|
|
997
|
+
details={"policy": policy},
|
|
998
|
+
),
|
|
999
|
+
durable=True,
|
|
1000
|
+
)
|
|
1001
|
+
if self.config.memory.experience_enabled and should_assess_experience(
|
|
1002
|
+
status=result.status,
|
|
1003
|
+
changed_files=result.changed_files,
|
|
1004
|
+
verification=result.verification,
|
|
1005
|
+
):
|
|
1006
|
+
experience_text = (
|
|
1007
|
+
f"用户请求:\n{request.prompt}\n\n"
|
|
1008
|
+
f"变更文件:\n{chr(10).join(result.changed_files)}\n\n"
|
|
1009
|
+
f"任务结果:\n{result.final_text}"
|
|
1010
|
+
)[: self.config.memory.extraction_max_chars]
|
|
1011
|
+
assessment = await assess_experience(
|
|
1012
|
+
model_chain[0],
|
|
1013
|
+
text=experience_text,
|
|
1014
|
+
evidence=result.verification,
|
|
1015
|
+
max_output_tokens=self.config.memory.extraction_max_output_tokens,
|
|
1016
|
+
)
|
|
1017
|
+
if not assessment.should_store or assessment.memory is None:
|
|
1018
|
+
return
|
|
1019
|
+
refined = assessment.memory
|
|
1020
|
+
duplicates = tuple(
|
|
1021
|
+
record
|
|
1022
|
+
for record in run_memory.store.list(
|
|
1023
|
+
status=MemoryStatus.ACTIVE,
|
|
1024
|
+
project_id=run_memory.project_id,
|
|
1025
|
+
)
|
|
1026
|
+
if record.kind is MemoryKind.EXPERIENCE
|
|
1027
|
+
and (
|
|
1028
|
+
record.title.casefold() == refined.title.casefold()
|
|
1029
|
+
or record.summary.casefold() == refined.summary.casefold()
|
|
1030
|
+
)
|
|
1031
|
+
)
|
|
1032
|
+
if duplicates:
|
|
1033
|
+
existing = duplicates[0]
|
|
1034
|
+
if explicit_experience_id is not None:
|
|
1035
|
+
run_memory.store.delete(explicit_experience_id)
|
|
1036
|
+
evidence = tuple(dict.fromkeys((*existing.evidence, *result.verification)))
|
|
1037
|
+
run_memory.store.update(existing.memory_id, evidence=evidence)
|
|
1038
|
+
run_memory.store.record_outcome(existing.memory_id, success=True)
|
|
1039
|
+
return
|
|
1040
|
+
if explicit_experience_id is not None:
|
|
1041
|
+
experience = run_memory.store.update(
|
|
1042
|
+
explicit_experience_id,
|
|
1043
|
+
title=refined.title,
|
|
1044
|
+
summary=refined.summary,
|
|
1045
|
+
body=refined.body,
|
|
1046
|
+
tags=refined.tags,
|
|
1047
|
+
evidence=result.verification,
|
|
1048
|
+
confidence=0.8,
|
|
1049
|
+
)
|
|
1050
|
+
else:
|
|
1051
|
+
experience = run_memory.create_candidate(
|
|
1052
|
+
kind=MemoryKind.EXPERIENCE,
|
|
1053
|
+
scope=MemoryScope.PROJECT,
|
|
1054
|
+
title=refined.title,
|
|
1055
|
+
summary=refined.summary,
|
|
1056
|
+
body=refined.body,
|
|
1057
|
+
source=MemorySource(session.metadata.session_id, run_id),
|
|
1058
|
+
tags=refined.tags,
|
|
1059
|
+
evidence=result.verification,
|
|
1060
|
+
confidence=0.7,
|
|
1061
|
+
)
|
|
1062
|
+
verified = run_memory.store.transition(experience.memory_id, MemoryStatus.ACTIVE)
|
|
1063
|
+
await bus.publish(
|
|
1064
|
+
MemoryEvent(
|
|
1065
|
+
event_id=uuid4().hex,
|
|
1066
|
+
session_id=session.metadata.session_id,
|
|
1067
|
+
run_id=run_id,
|
|
1068
|
+
turn=0,
|
|
1069
|
+
action="activated",
|
|
1070
|
+
memory_id=verified.memory_id,
|
|
1071
|
+
memory_kind=verified.kind.value,
|
|
1072
|
+
scope=verified.scope.value,
|
|
1073
|
+
status=verified.status.value,
|
|
1074
|
+
details={"verified": True, "policy": "no_execution_no_memory"},
|
|
1075
|
+
),
|
|
1076
|
+
durable=True,
|
|
1077
|
+
)
|
|
1078
|
+
if self.config.memory.sop_enabled and assessment.sop is not None:
|
|
1079
|
+
sop = assessment.sop
|
|
1080
|
+
sop_candidate = run_memory.create_candidate(
|
|
1081
|
+
kind=MemoryKind.SOP,
|
|
1082
|
+
scope=MemoryScope.PROJECT,
|
|
1083
|
+
title=sop.title,
|
|
1084
|
+
summary=sop.summary,
|
|
1085
|
+
body=sop.body,
|
|
1086
|
+
source=MemorySource(session.metadata.session_id, run_id),
|
|
1087
|
+
tags=sop.tags,
|
|
1088
|
+
evidence=result.verification,
|
|
1089
|
+
confidence=0.7,
|
|
1090
|
+
)
|
|
1091
|
+
await bus.publish(
|
|
1092
|
+
MemoryEvent(
|
|
1093
|
+
event_id=uuid4().hex,
|
|
1094
|
+
session_id=session.metadata.session_id,
|
|
1095
|
+
run_id=run_id,
|
|
1096
|
+
turn=0,
|
|
1097
|
+
action="candidate_created",
|
|
1098
|
+
memory_id=sop_candidate.memory_id,
|
|
1099
|
+
memory_kind=sop_candidate.kind.value,
|
|
1100
|
+
scope=sop_candidate.scope.value,
|
|
1101
|
+
status=sop_candidate.status.value,
|
|
1102
|
+
details={"verified": True, "policy": "experience_sop_candidate"},
|
|
1103
|
+
),
|
|
1104
|
+
durable=True,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
loop = AgentLoop(
|
|
1108
|
+
session_id=session.metadata.session_id,
|
|
1109
|
+
run_id=run_id,
|
|
1110
|
+
model_chain=model_chain,
|
|
1111
|
+
scheduler=scheduler,
|
|
1112
|
+
control=control,
|
|
1113
|
+
event_bus=bus,
|
|
1114
|
+
system_prompt=system_prompt,
|
|
1115
|
+
token_estimator=TokenEstimator(
|
|
1116
|
+
self.config.context.window_tokens,
|
|
1117
|
+
compaction_threshold=self.config.context.compaction_threshold,
|
|
1118
|
+
),
|
|
1119
|
+
artifact_store=artifact_store,
|
|
1120
|
+
preserve_recent_turns=self.config.context.preserve_recent_turns,
|
|
1121
|
+
max_tool_result_chars=self.config.context.max_tool_result_chars,
|
|
1122
|
+
close_event_bus=False,
|
|
1123
|
+
sourced_context_provider=run_extensions.drain_context,
|
|
1124
|
+
compact_observer=run_extensions.compact_lifecycle,
|
|
1125
|
+
completion_observer=extract_memories,
|
|
1126
|
+
)
|
|
1127
|
+
after_sequence = session.metadata.next_sequence - 1
|
|
1128
|
+
|
|
1129
|
+
async def run_with_subagents() -> RunResult:
|
|
1130
|
+
try:
|
|
1131
|
+
await self.wait_for_required_mcp()
|
|
1132
|
+
await run_extensions.mcp_capabilities.register_direct_tools(
|
|
1133
|
+
run_registry,
|
|
1134
|
+
direct_tool_limit=self.config.extensions.direct_tool_limit,
|
|
1135
|
+
)
|
|
1136
|
+
await run_extensions.mcp_capabilities.register_direct_tools(
|
|
1137
|
+
child_tools,
|
|
1138
|
+
direct_tool_limit=self.config.extensions.direct_tool_limit,
|
|
1139
|
+
)
|
|
1140
|
+
await run_extensions.mcp_capabilities.register_selected_tools(
|
|
1141
|
+
run_registry, self._mcp_selected_tools
|
|
1142
|
+
)
|
|
1143
|
+
await run_extensions.mcp_capabilities.register_selected_tools(
|
|
1144
|
+
child_tools, self._mcp_selected_tools
|
|
1145
|
+
)
|
|
1146
|
+
direct_servers = self._mcp_direct_servers
|
|
1147
|
+
search_servers = tuple(
|
|
1148
|
+
server_id
|
|
1149
|
+
for server_id in run_extensions.mcp.server_ids
|
|
1150
|
+
if server_id not in set(direct_servers)
|
|
1151
|
+
)
|
|
1152
|
+
loop.system_prompt = make_system_prompt(direct_servers, search_servers)
|
|
1153
|
+
if memory_context:
|
|
1154
|
+
await bus.publish(
|
|
1155
|
+
MemoryEvent(
|
|
1156
|
+
event_id=uuid4().hex,
|
|
1157
|
+
session_id=session.metadata.session_id,
|
|
1158
|
+
run_id=run_id,
|
|
1159
|
+
turn=0,
|
|
1160
|
+
action="recalled",
|
|
1161
|
+
status="active",
|
|
1162
|
+
details={"characters": len(memory_context)},
|
|
1163
|
+
)
|
|
1164
|
+
)
|
|
1165
|
+
if not existing_session:
|
|
1166
|
+
await run_extensions.lifecycle(HookEvent.SESSION_START)
|
|
1167
|
+
await run_extensions.lifecycle(HookEvent.USER_SUBMIT)
|
|
1168
|
+
await run_extensions.lifecycle(HookEvent.RUN_START)
|
|
1169
|
+
prompt_parts = request.prompt.strip().split(maxsplit=1)
|
|
1170
|
+
if prompt_parts and prompt_parts[0].startswith("$"):
|
|
1171
|
+
await run_extensions.activate_skill(prompt_parts[0])
|
|
1172
|
+
elif prompt_parts and prompt_parts[0].startswith("@prompt:"):
|
|
1173
|
+
await run_extensions.activate_prompt(prompt_parts[0].removeprefix("@prompt:"))
|
|
1174
|
+
elif prompt_parts and prompt_parts[0].startswith("@capability:"):
|
|
1175
|
+
run_extensions.activate_capability(prompt_parts[0].removeprefix("@capability:"))
|
|
1176
|
+
if existing_session:
|
|
1177
|
+
await coordinator.recover()
|
|
1178
|
+
result = await loop.run(request.prompt, workspace, initial_messages)
|
|
1179
|
+
await run_extensions.lifecycle(HookEvent.RUN_END, status=result.status)
|
|
1180
|
+
return result
|
|
1181
|
+
except BaseException:
|
|
1182
|
+
await run_extensions.lifecycle(HookEvent.RUN_ERROR, status="error")
|
|
1183
|
+
raise
|
|
1184
|
+
finally:
|
|
1185
|
+
await coordinator.shutdown("parent run ended")
|
|
1186
|
+
await run_extensions.lifecycle(HookEvent.SESSION_END)
|
|
1187
|
+
await run_extensions.aclose()
|
|
1188
|
+
# The MCP runtime outlives this run; do not retain its closed event bus.
|
|
1189
|
+
run_extensions.mcp.observer = None
|
|
1190
|
+
extension_redactor.clear()
|
|
1191
|
+
await bus.close()
|
|
1192
|
+
|
|
1193
|
+
task = asyncio.create_task(run_with_subagents())
|
|
1194
|
+
handle = RunHandle(
|
|
1195
|
+
task,
|
|
1196
|
+
bus,
|
|
1197
|
+
control,
|
|
1198
|
+
after_sequence=after_sequence,
|
|
1199
|
+
coordinator=coordinator,
|
|
1200
|
+
policy=policy,
|
|
1201
|
+
loop=loop,
|
|
1202
|
+
)
|
|
1203
|
+
self._handles.add(handle)
|
|
1204
|
+
task.add_done_callback(lambda _task: self._handles.discard(handle))
|
|
1205
|
+
return handle
|
|
1206
|
+
|
|
1207
|
+
def list_sessions(self) -> tuple[SessionMetadata, ...]:
|
|
1208
|
+
sessions_root = self.state_root / "sessions"
|
|
1209
|
+
if not sessions_root.exists():
|
|
1210
|
+
return ()
|
|
1211
|
+
sessions: list[SessionMetadata] = []
|
|
1212
|
+
for path in sessions_root.iterdir():
|
|
1213
|
+
if not path.is_dir() or not (path / "meta.json").is_file():
|
|
1214
|
+
continue
|
|
1215
|
+
store = SessionStore.open(sessions_root, path.name)
|
|
1216
|
+
sessions.append(self._ensure_session_summary(store))
|
|
1217
|
+
return tuple(sorted(sessions, key=lambda item: item.updated_at, reverse=True))
|
|
1218
|
+
|
|
1219
|
+
def rewind_session(self, session_id: str, record_id: str) -> EventRecord:
|
|
1220
|
+
store = SessionStore.open(self.state_root / "sessions", session_id)
|
|
1221
|
+
return create_branch(
|
|
1222
|
+
store,
|
|
1223
|
+
record_id,
|
|
1224
|
+
"branch_point",
|
|
1225
|
+
{"source_record_id": record_id},
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
async def aclose(self) -> None:
|
|
1229
|
+
if not self._entered:
|
|
1230
|
+
return
|
|
1231
|
+
handles = tuple(self._handles)
|
|
1232
|
+
await asyncio.gather(*(handle.cancel() for handle in handles))
|
|
1233
|
+
if self._mcp_start_task is not None:
|
|
1234
|
+
if not self._mcp_start_task.done():
|
|
1235
|
+
self._mcp_start_task.cancel()
|
|
1236
|
+
await asyncio.gather(self._mcp_start_task, return_exceptions=True)
|
|
1237
|
+
self._mcp_start_task = None
|
|
1238
|
+
if self._mcp_retirement_tasks:
|
|
1239
|
+
await asyncio.gather(*tuple(self._mcp_retirement_tasks), return_exceptions=True)
|
|
1240
|
+
self._mcp_retirement_tasks.clear()
|
|
1241
|
+
if self._client_extensions is not None:
|
|
1242
|
+
self._client_extensions.mcp.observer = None
|
|
1243
|
+
extension_close = (
|
|
1244
|
+
self._client_extensions.aclose()
|
|
1245
|
+
if self._client_extensions is not None
|
|
1246
|
+
else asyncio.sleep(0)
|
|
1247
|
+
)
|
|
1248
|
+
try:
|
|
1249
|
+
await asyncio.gather(
|
|
1250
|
+
extension_close,
|
|
1251
|
+
self.transport_registry.aclose(),
|
|
1252
|
+
return_exceptions=True,
|
|
1253
|
+
)
|
|
1254
|
+
finally:
|
|
1255
|
+
self._client_extensions = None
|
|
1256
|
+
self._entered = False
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
__all__ = ["RunHandle", "Windcode"]
|