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
|
@@ -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
|