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
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tomllib
5
+ from pathlib import Path
6
+ from typing import Any, cast
7
+ from uuid import uuid4
8
+
9
+ import tomli_w
10
+
11
+ from windcode.config.models import AppConfig
12
+
13
+
14
+ def _read_config(path: Path) -> dict[str, Any]:
15
+ if not path.exists():
16
+ return {}
17
+ with path.open("rb") as stream:
18
+ value = tomllib.load(stream)
19
+ return value
20
+
21
+
22
+ def save_model_config(path: Path, previous: AppConfig, updated: AppConfig) -> None:
23
+ """Persist model profiles without storing API key values."""
24
+ path = path.expanduser().resolve()
25
+ data = _read_config(path)
26
+ raw_disabled = data.get("disabled_providers", [])
27
+ disabled: set[str] = set()
28
+ if isinstance(raw_disabled, list):
29
+ disabled = {str(alias) for alias in cast(list[object], raw_disabled)}
30
+ disabled.update(previous.providers.keys() - updated.providers.keys())
31
+ disabled.difference_update(updated.providers)
32
+
33
+ data["providers"] = {
34
+ alias: provider.model_dump(mode="json", exclude_none=True)
35
+ for alias, provider in updated.providers.items()
36
+ }
37
+ if updated.primary_provider is None:
38
+ data.pop("primary_provider", None)
39
+ else:
40
+ data["primary_provider"] = updated.primary_provider
41
+ data["fallback_chain"] = list(updated.fallback_chain)
42
+ data["enabled_providers"] = sorted(updated.providers)
43
+ if disabled:
44
+ data["disabled_providers"] = sorted(disabled)
45
+ else:
46
+ data.pop("disabled_providers", None)
47
+
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ temporary = path.with_suffix(f"{path.suffix}.tmp-{uuid4().hex}")
50
+ try:
51
+ with temporary.open("wb") as stream:
52
+ tomli_w.dump(data, stream)
53
+ stream.flush()
54
+ os.fsync(stream.fileno())
55
+ temporary.replace(path)
56
+ finally:
57
+ temporary.unlink(missing_ok=True)
58
+
59
+
60
+ def save_memory_config(path: Path, config: AppConfig) -> None:
61
+ """Persist non-secret memory policy without rewriting unrelated configuration."""
62
+ path = path.expanduser().resolve()
63
+ data = _read_config(path)
64
+ data["memory"] = config.memory.model_dump(mode="json", exclude_none=True)
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ temporary = path.with_suffix(f"{path.suffix}.tmp-{uuid4().hex}")
67
+ try:
68
+ with temporary.open("wb") as stream:
69
+ tomli_w.dump(data, stream)
70
+ stream.flush()
71
+ os.fsync(stream.fileno())
72
+ temporary.replace(path)
73
+ finally:
74
+ temporary.unlink(missing_ok=True)
@@ -0,0 +1,18 @@
1
+ from windcode.context.compactor import (
2
+ CHECKPOINT_SECTIONS,
3
+ CompactionResult,
4
+ compact_context,
5
+ )
6
+ from windcode.context.estimator import ContextBudget, TokenEstimator, estimate_message_tokens
7
+ from windcode.context.truncation import TruncationResult, truncate_context
8
+
9
+ __all__ = [
10
+ "CHECKPOINT_SECTIONS",
11
+ "CompactionResult",
12
+ "ContextBudget",
13
+ "TokenEstimator",
14
+ "TruncationResult",
15
+ "compact_context",
16
+ "estimate_message_tokens",
17
+ "truncate_context",
18
+ ]
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from windcode.domain.messages import Message, Role, TextBlock
6
+ from windcode.domain.models import ModelRequest, TextDelta
7
+ from windcode.providers.base import ModelTransport
8
+
9
+ CHECKPOINT_SECTIONS = (
10
+ "任务目标",
11
+ "关键决策",
12
+ "相关文件",
13
+ "当前进度",
14
+ "未完成事项",
15
+ "验证证据",
16
+ "约束与指令",
17
+ "下一步",
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class CompactionResult:
23
+ messages: tuple[Message, ...]
24
+ checkpoint: str | None
25
+ error: str | None = None
26
+
27
+ @property
28
+ def compacted(self) -> bool:
29
+ return self.checkpoint is not None
30
+
31
+
32
+ def checkpoint_prompt() -> str:
33
+ sections = "\n".join(f"## {section}\n[简明, 具体, 无遗漏]" for section in CHECKPOINT_SECTIONS)
34
+ return f"请根据完整对话生成结构化检查点. 必须准确记录已验证证据, 不要推测成功.\n\n{sections}"
35
+
36
+
37
+ def _valid_checkpoint(text: str) -> bool:
38
+ return all(f"## {section}" in text for section in CHECKPOINT_SECTIONS)
39
+
40
+
41
+ async def compact_context(
42
+ messages: tuple[Message, ...],
43
+ transport: ModelTransport,
44
+ *,
45
+ model: str,
46
+ system_prompt: str,
47
+ preserve_recent_turns: int = 8,
48
+ ) -> CompactionResult:
49
+ request = ModelRequest(
50
+ model=model,
51
+ messages=(*messages, Message(Role.USER, (TextBlock(checkpoint_prompt()),))),
52
+ system_prompt=system_prompt,
53
+ )
54
+ parts: list[str] = []
55
+ try:
56
+ async for event in transport.stream(request):
57
+ if isinstance(event, TextDelta):
58
+ parts.append(event.text)
59
+ except Exception as exc:
60
+ return CompactionResult(messages, None, f"checkpoint generation failed: {exc}")
61
+ checkpoint = "".join(parts).strip()
62
+ if not _valid_checkpoint(checkpoint):
63
+ return CompactionResult(messages, None, "checkpoint response is missing required sections")
64
+
65
+ system_messages = tuple(message for message in messages if message.role is Role.SYSTEM)
66
+ non_system = tuple(message for message in messages if message.role is not Role.SYSTEM)
67
+ recent = non_system[-preserve_recent_turns * 2 :]
68
+ checkpoint_message = Message(
69
+ Role.SYSTEM,
70
+ (TextBlock(f"上下文检查点:\n{checkpoint}"),),
71
+ )
72
+ return CompactionResult((*system_messages, checkpoint_message, *recent), checkpoint)
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+
6
+ from windcode.domain.messages import (
7
+ Message,
8
+ ReasoningBlock,
9
+ TextBlock,
10
+ ToolCallBlock,
11
+ ToolResultBlock,
12
+ )
13
+ from windcode.domain.models import ModelRequest, Usage
14
+
15
+
16
+ def estimate_text_tokens(text: str) -> int:
17
+ if not text:
18
+ return 0
19
+ return max(1, math.ceil(len(text.encode("utf-8")) / 4))
20
+
21
+
22
+ def estimate_message_tokens(message: Message) -> int:
23
+ tokens = 4
24
+ for block in message.content:
25
+ if isinstance(block, TextBlock):
26
+ tokens += estimate_text_tokens(block.text)
27
+ elif isinstance(block, ReasoningBlock):
28
+ tokens += estimate_text_tokens(block.summary)
29
+ elif isinstance(block, ToolCallBlock):
30
+ tokens += estimate_text_tokens(block.name) + estimate_text_tokens(str(block.arguments))
31
+ elif isinstance(block, ToolResultBlock):
32
+ tokens += estimate_text_tokens(block.content)
33
+ else:
34
+ tokens += 256
35
+ return tokens
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class ContextBudget:
40
+ estimated_tokens: int
41
+ compaction_at: int
42
+ reserved_output_tokens: int
43
+ remaining_tokens: int
44
+ should_compact: bool
45
+
46
+
47
+ class TokenEstimator:
48
+ def __init__(
49
+ self,
50
+ window_tokens: int,
51
+ *,
52
+ compaction_threshold: float = 0.8,
53
+ reserved_output_tokens: int = 4_096,
54
+ ) -> None:
55
+ if window_tokens <= reserved_output_tokens:
56
+ raise ValueError("context window must exceed the reserved output budget")
57
+ if not 0 < compaction_threshold < 1:
58
+ raise ValueError("compaction threshold must be between zero and one")
59
+ self.window_tokens = window_tokens
60
+ self.compaction_threshold = compaction_threshold
61
+ self.reserved_output_tokens = reserved_output_tokens
62
+
63
+ def estimate(
64
+ self,
65
+ request: ModelRequest,
66
+ *,
67
+ actual_usage: Usage | None = None,
68
+ incremental_text: str = "",
69
+ ) -> ContextBudget:
70
+ estimated = (
71
+ actual_usage.input_tokens
72
+ if actual_usage is not None and actual_usage.input_tokens > 0
73
+ else estimate_text_tokens(request.system_prompt)
74
+ + sum(estimate_message_tokens(message) for message in request.messages)
75
+ + sum(estimate_text_tokens(str(tool.parameters)) + 8 for tool in request.tools)
76
+ )
77
+ estimated += estimate_text_tokens(incremental_text)
78
+ compaction_at = min(
79
+ int(self.window_tokens * self.compaction_threshold),
80
+ self.window_tokens - self.reserved_output_tokens,
81
+ )
82
+ remaining = max(0, self.window_tokens - self.reserved_output_tokens - estimated)
83
+ return ContextBudget(
84
+ estimated_tokens=estimated,
85
+ compaction_at=compaction_at,
86
+ reserved_output_tokens=self.reserved_output_tokens,
87
+ remaining_tokens=remaining,
88
+ should_compact=estimated >= compaction_at,
89
+ )
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+
5
+ from windcode.domain.messages import (
6
+ AttachmentBlock,
7
+ ContentBlock,
8
+ Message,
9
+ Role,
10
+ ToolCallBlock,
11
+ ToolResultBlock,
12
+ )
13
+ from windcode.sessions import ArtifactReference, ArtifactStore
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class TruncationResult:
18
+ messages: tuple[Message, ...]
19
+ artifacts: tuple[ArtifactReference, ...]
20
+ removed_attachments: int
21
+ changed: bool
22
+
23
+
24
+ def _recent_cutoff(messages: tuple[Message, ...], preserve_recent_turns: int) -> int:
25
+ user_indexes = [index for index, message in enumerate(messages) if message.role is Role.USER]
26
+ if len(user_indexes) <= preserve_recent_turns:
27
+ return 0
28
+ return user_indexes[-preserve_recent_turns]
29
+
30
+
31
+ def truncate_context(
32
+ messages: tuple[Message, ...],
33
+ artifact_store: ArtifactStore,
34
+ *,
35
+ max_tool_result_chars: int = 20_000,
36
+ preserve_recent_turns: int = 8,
37
+ ) -> TruncationResult:
38
+ cutoff = _recent_cutoff(messages, preserve_recent_turns)
39
+ completed_calls = {
40
+ block.call_id
41
+ for message in messages
42
+ for block in message.content
43
+ if isinstance(block, ToolResultBlock)
44
+ }
45
+ open_calls = {
46
+ block.call_id
47
+ for message in messages
48
+ for block in message.content
49
+ if isinstance(block, ToolCallBlock) and block.call_id not in completed_calls
50
+ }
51
+ artifacts: list[ArtifactReference] = []
52
+ removed = 0
53
+ changed = False
54
+ transformed: list[Message] = []
55
+ for index, message in enumerate(messages):
56
+ protected = (
57
+ index >= cutoff
58
+ or message.role is Role.SYSTEM
59
+ or any(
60
+ isinstance(block, ToolCallBlock) and block.call_id in open_calls
61
+ for block in message.content
62
+ )
63
+ )
64
+ content: list[ContentBlock] = []
65
+ for block in message.content:
66
+ if isinstance(block, AttachmentBlock) and not protected:
67
+ removed += 1
68
+ changed = True
69
+ continue
70
+ if (
71
+ isinstance(block, ToolResultBlock)
72
+ and not protected
73
+ and len(block.content) > max_tool_result_chars
74
+ ):
75
+ summary, reference = artifact_store.externalize(
76
+ block.content, threshold=max_tool_result_chars
77
+ )
78
+ if reference is not None:
79
+ artifacts.append(reference)
80
+ content.append(
81
+ replace(block, content=summary, artifact_ref=reference.relative_path)
82
+ )
83
+ changed = True
84
+ continue
85
+ content.append(block)
86
+ transformed.append(replace(message, content=tuple(content)))
87
+ return TruncationResult(tuple(transformed), tuple(artifacts), removed, changed)
@@ -0,0 +1 @@
1
+ """Dependency-free domain contracts."""
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ErrorCategory(StrEnum):
7
+ AUTHENTICATION = "authentication"
8
+ RATE_LIMIT = "rate_limit"
9
+ NETWORK = "network"
10
+ SERVER = "server"
11
+ CONTEXT_OVERFLOW = "context_overflow"
12
+ INVALID_REQUEST = "invalid_request"
13
+ CONTENT_POLICY = "content_policy"
14
+ CANCELLED = "cancelled"
15
+ INTERNAL = "internal"
16
+
17
+
18
+ _RETRYABLE = {ErrorCategory.RATE_LIMIT, ErrorCategory.NETWORK, ErrorCategory.SERVER}
19
+ _FALLBACK_ALLOWED = _RETRYABLE | {ErrorCategory.CONTEXT_OVERFLOW}
20
+
21
+
22
+ class WindcodeError(Exception):
23
+ def __init__(self, message: str, category: ErrorCategory = ErrorCategory.INTERNAL) -> None:
24
+ super().__init__(message)
25
+ self.category = category
26
+
27
+ @property
28
+ def retryable(self) -> bool:
29
+ return self.category in _RETRYABLE
30
+
31
+ @property
32
+ def fallback_allowed(self) -> bool:
33
+ return self.category in _FALLBACK_ALLOWED