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.
Files changed (143) hide show
  1. windcode/__init__.py +6 -0
  2. windcode/__main__.py +4 -0
  3. windcode/auth/__init__.py +11 -0
  4. windcode/auth/store.py +90 -0
  5. windcode/cli.py +181 -0
  6. windcode/config/__init__.py +41 -0
  7. windcode/config/loader.py +117 -0
  8. windcode/config/models.py +203 -0
  9. windcode/config/writer.py +74 -0
  10. windcode/context/__init__.py +18 -0
  11. windcode/context/compactor.py +72 -0
  12. windcode/context/estimator.py +89 -0
  13. windcode/context/truncation.py +87 -0
  14. windcode/domain/__init__.py +1 -0
  15. windcode/domain/errors.py +33 -0
  16. windcode/domain/events.py +597 -0
  17. windcode/domain/messages.py +226 -0
  18. windcode/domain/models.py +73 -0
  19. windcode/domain/subagents.py +263 -0
  20. windcode/domain/tools.py +55 -0
  21. windcode/extensions/__init__.py +27 -0
  22. windcode/extensions/commands.py +25 -0
  23. windcode/extensions/discovery.py +139 -0
  24. windcode/extensions/events.py +58 -0
  25. windcode/extensions/hooks/__init__.py +4 -0
  26. windcode/extensions/hooks/dispatcher.py +107 -0
  27. windcode/extensions/hooks/executor.py +49 -0
  28. windcode/extensions/hooks/loader.py +101 -0
  29. windcode/extensions/hooks/models.py +109 -0
  30. windcode/extensions/mcp/__init__.py +10 -0
  31. windcode/extensions/mcp/adapter.py +101 -0
  32. windcode/extensions/mcp/catalog.py +153 -0
  33. windcode/extensions/mcp/client.py +273 -0
  34. windcode/extensions/mcp/runtime.py +144 -0
  35. windcode/extensions/mcp/tools.py +570 -0
  36. windcode/extensions/models.py +168 -0
  37. windcode/extensions/paths.py +71 -0
  38. windcode/extensions/plugins/__init__.py +3 -0
  39. windcode/extensions/plugins/installer.py +93 -0
  40. windcode/extensions/plugins/manifest.py +166 -0
  41. windcode/extensions/runtime.py +366 -0
  42. windcode/extensions/service.py +437 -0
  43. windcode/extensions/skills/__init__.py +3 -0
  44. windcode/extensions/skills/loader.py +67 -0
  45. windcode/extensions/skills/parser.py +57 -0
  46. windcode/extensions/skills/tools.py +213 -0
  47. windcode/extensions/snapshot.py +57 -0
  48. windcode/extensions/state.py +158 -0
  49. windcode/instructions/__init__.py +8 -0
  50. windcode/instructions/loader.py +50 -0
  51. windcode/memory/__init__.py +57 -0
  52. windcode/memory/extraction.py +83 -0
  53. windcode/memory/models.py +218 -0
  54. windcode/memory/refiner.py +189 -0
  55. windcode/memory/security.py +23 -0
  56. windcode/memory/service.py +179 -0
  57. windcode/memory/store.py +362 -0
  58. windcode/observability/__init__.py +4 -0
  59. windcode/observability/redaction.py +95 -0
  60. windcode/observability/trace.py +115 -0
  61. windcode/policy/__init__.py +22 -0
  62. windcode/policy/engine.py +137 -0
  63. windcode/policy/models.py +69 -0
  64. windcode/providers/__init__.py +29 -0
  65. windcode/providers/_utils.py +19 -0
  66. windcode/providers/anthropic.py +215 -0
  67. windcode/providers/base.py +46 -0
  68. windcode/providers/catalog.py +119 -0
  69. windcode/providers/errors.py +96 -0
  70. windcode/providers/openai_compat.py +231 -0
  71. windcode/providers/openai_responses.py +189 -0
  72. windcode/providers/registry.py +105 -0
  73. windcode/runtime/__init__.py +15 -0
  74. windcode/runtime/control.py +89 -0
  75. windcode/runtime/event_bus.py +67 -0
  76. windcode/runtime/loop.py +510 -0
  77. windcode/runtime/prompts.py +132 -0
  78. windcode/runtime/report.py +64 -0
  79. windcode/runtime/retry.py +73 -0
  80. windcode/runtime/scheduler.py +194 -0
  81. windcode/runtime/subagents/__init__.py +28 -0
  82. windcode/runtime/subagents/approvals.py +88 -0
  83. windcode/runtime/subagents/budgets.py +79 -0
  84. windcode/runtime/subagents/coordinator.py +714 -0
  85. windcode/runtime/subagents/factory.py +311 -0
  86. windcode/runtime/subagents/roles.py +74 -0
  87. windcode/runtime/subagents/verification.py +53 -0
  88. windcode/sandbox/__init__.py +3 -0
  89. windcode/sandbox/bwrap.py +79 -0
  90. windcode/sdk.py +1259 -0
  91. windcode/sessions/__init__.py +23 -0
  92. windcode/sessions/artifacts.py +69 -0
  93. windcode/sessions/models.py +104 -0
  94. windcode/sessions/store.py +167 -0
  95. windcode/sessions/tree.py +35 -0
  96. windcode/tools/__init__.py +10 -0
  97. windcode/tools/apply_patch.py +191 -0
  98. windcode/tools/ask_user.py +58 -0
  99. windcode/tools/builtins.py +44 -0
  100. windcode/tools/edit_file.py +54 -0
  101. windcode/tools/filesystem.py +77 -0
  102. windcode/tools/glob.py +35 -0
  103. windcode/tools/grep.py +66 -0
  104. windcode/tools/memory.py +400 -0
  105. windcode/tools/read_file.py +54 -0
  106. windcode/tools/registry.py +120 -0
  107. windcode/tools/shell.py +159 -0
  108. windcode/tools/subagents/__init__.py +31 -0
  109. windcode/tools/subagents/cancel.py +35 -0
  110. windcode/tools/subagents/integrate.py +54 -0
  111. windcode/tools/subagents/list.py +46 -0
  112. windcode/tools/subagents/spawn.py +86 -0
  113. windcode/tools/subagents/wait.py +74 -0
  114. windcode/tools/write_file.py +61 -0
  115. windcode/tui/__init__.py +3 -0
  116. windcode/tui/app.py +1015 -0
  117. windcode/tui/commands.py +90 -0
  118. windcode/tui/permission_display.py +24 -0
  119. windcode/tui/styles.tcss +591 -0
  120. windcode/tui/widgets/__init__.py +31 -0
  121. windcode/tui/widgets/approval.py +98 -0
  122. windcode/tui/widgets/command_menu.py +90 -0
  123. windcode/tui/widgets/extensions.py +48 -0
  124. windcode/tui/widgets/input.py +110 -0
  125. windcode/tui/widgets/memory.py +143 -0
  126. windcode/tui/widgets/messages.py +299 -0
  127. windcode/tui/widgets/models.py +565 -0
  128. windcode/tui/widgets/question.py +46 -0
  129. windcode/tui/widgets/sessions.py +68 -0
  130. windcode/tui/widgets/status.py +46 -0
  131. windcode/tui/widgets/subagents.py +195 -0
  132. windcode/tui/widgets/tools.py +66 -0
  133. windcode/tui/widgets/welcome.py +149 -0
  134. windcode/types.py +80 -0
  135. windcode/worktrees/__init__.py +24 -0
  136. windcode/worktrees/git.py +92 -0
  137. windcode/worktrees/manager.py +265 -0
  138. windcode/worktrees/models.py +62 -0
  139. windcode-0.1.0.dist-info/METADATA +207 -0
  140. windcode-0.1.0.dist-info/RECORD +143 -0
  141. windcode-0.1.0.dist-info/WHEEL +4 -0
  142. windcode-0.1.0.dist-info/entry_points.txt +2 -0
  143. 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"]