kolega-code 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 (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,157 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from kolega_code.cli.skills import activated_skill_names, discover_skills
6
+ from kolega_code.llm.models import Message, TextBlock, ToolResult
7
+
8
+
9
+ def write_skill(root: Path, name: str, description: str = "Use this skill for testing.", body: str = "Do the work.") -> Path:
10
+ skill_dir = root / name
11
+ skill_dir.mkdir(parents=True)
12
+ skill_file = skill_dir / "SKILL.md"
13
+ skill_file.write_text(
14
+ f"---\nname: {name}\ndescription: {description}\n---\n\n{body}\n",
15
+ encoding="utf-8",
16
+ )
17
+ return skill_dir
18
+
19
+
20
+ def test_discover_skills_loads_user_and_project_spec_paths(tmp_path: Path) -> None:
21
+ project = tmp_path / "project"
22
+ user_home = tmp_path / "home"
23
+ project.mkdir()
24
+
25
+ user_skills = user_home / ".agents" / "skills"
26
+ project_skills = project / ".agents" / "skills"
27
+ write_skill(user_skills, "user-skill", "Use this user skill.")
28
+ write_skill(project_skills, "project-skill", "Use this project skill.")
29
+
30
+ catalog = discover_skills(project, user_home=user_home)
31
+
32
+ assert list(catalog.skills) == ["project-skill", "user-skill"]
33
+ assert catalog.skills["project-skill"].scope == "project"
34
+ assert catalog.skills["user-skill"].scope == "user"
35
+
36
+
37
+ def test_discover_skills_does_not_scan_singular_project_agent_path(tmp_path: Path) -> None:
38
+ project = tmp_path / "project"
39
+ user_home = tmp_path / "home"
40
+ project.mkdir()
41
+ write_skill(project / ".agent" / "skills", "ignored-skill", "Use this ignored skill.")
42
+
43
+ catalog = discover_skills(project, user_home=user_home)
44
+
45
+ assert "ignored-skill" not in catalog.skills
46
+
47
+
48
+ def test_project_skill_overrides_user_skill_with_same_name(tmp_path: Path) -> None:
49
+ project = tmp_path / "project"
50
+ user_home = tmp_path / "home"
51
+ project.mkdir()
52
+ write_skill(user_home / ".agents" / "skills", "shared-skill", "Use the user skill.", body="user body")
53
+ write_skill(project / ".agents" / "skills", "shared-skill", "Use the project skill.", body="project body")
54
+
55
+ catalog = discover_skills(project, user_home=user_home)
56
+
57
+ assert catalog.skills["shared-skill"].scope == "project"
58
+ assert "project body" in catalog.activation_content("shared-skill")
59
+ assert any("overrides user skill" in diagnostic.message for diagnostic in catalog.diagnostics)
60
+
61
+
62
+ def test_discover_skills_supports_folded_yaml_description(tmp_path: Path) -> None:
63
+ project = tmp_path / "project"
64
+ user_home = tmp_path / "home"
65
+ skills_root = project / ".agents" / "skills"
66
+ skill_dir = skills_root / "folded-skill"
67
+ skill_dir.mkdir(parents=True)
68
+ (skill_dir / "SKILL.md").write_text(
69
+ """---
70
+ name: folded-skill
71
+ description: >
72
+ Use this folded skill
73
+ when testing YAML descriptions.
74
+ ---
75
+
76
+ Follow folded instructions.
77
+ """,
78
+ encoding="utf-8",
79
+ )
80
+
81
+ catalog = discover_skills(project, user_home=user_home)
82
+
83
+ assert catalog.skills["folded-skill"].description == "Use this folded skill when testing YAML descriptions."
84
+
85
+
86
+ def test_discover_skills_skips_missing_description_and_malformed_yaml(tmp_path: Path) -> None:
87
+ project = tmp_path / "project"
88
+ user_home = tmp_path / "home"
89
+ skills_root = project / ".agents" / "skills"
90
+ missing = skills_root / "missing-description"
91
+ malformed = skills_root / "malformed-skill"
92
+ missing.mkdir(parents=True)
93
+ malformed.mkdir(parents=True)
94
+ (missing / "SKILL.md").write_text("---\nname: missing-description\n---\nbody\n", encoding="utf-8")
95
+ (malformed / "SKILL.md").write_text(
96
+ "---\nname: malformed-skill\ndescription: Use when: yaml breaks\n---\nbody\n",
97
+ encoding="utf-8",
98
+ )
99
+
100
+ catalog = discover_skills(project, user_home=user_home)
101
+
102
+ assert catalog.skills == {}
103
+ assert len(catalog.diagnostics) == 2
104
+ assert all(diagnostic.severity == "error" for diagnostic in catalog.diagnostics)
105
+
106
+
107
+ def test_activation_content_wraps_body_and_lists_resources(tmp_path: Path) -> None:
108
+ project = tmp_path / "project"
109
+ user_home = tmp_path / "home"
110
+ skill_dir = write_skill(project / ".agents" / "skills", "resource-skill", "Use resources.", body="# Steps")
111
+ (skill_dir / "references").mkdir()
112
+ (skill_dir / "references" / "guide.md").write_text("reference content", encoding="utf-8")
113
+ (skill_dir / "scripts").mkdir()
114
+ (skill_dir / "scripts" / "run.py").write_text("print('ok')", encoding="utf-8")
115
+
116
+ catalog = discover_skills(project, user_home=user_home)
117
+ content = catalog.activation_content("resource-skill")
118
+
119
+ assert '<skill_content name="resource-skill">' in content
120
+ assert "# Steps" in content
121
+ assert "Skill directory:" in content
122
+ assert "<file>references/guide.md</file>" in content
123
+ assert "<file>scripts/run.py</file>" in content
124
+ assert "name: resource-skill" not in content
125
+
126
+
127
+ def test_read_resource_rejects_path_traversal_and_caps_content(tmp_path: Path) -> None:
128
+ project = tmp_path / "project"
129
+ user_home = tmp_path / "home"
130
+ skill_dir = write_skill(project / ".agents" / "skills", "read-skill")
131
+ (skill_dir / "big.txt").write_text("a" * 20, encoding="utf-8")
132
+
133
+ catalog = discover_skills(project, user_home=user_home)
134
+
135
+ assert catalog.read_resource("read-skill", "big.txt", max_chars=5).startswith("aaaaa\n\n[truncated")
136
+ with pytest.raises(ValueError, match="inside the skill directory"):
137
+ catalog.read_resource("read-skill", "../SKILL.md")
138
+
139
+
140
+ def test_activated_skill_names_scans_text_and_tool_results() -> None:
141
+ history = [
142
+ Message("user", [TextBlock('<skill_content name="one">body</skill_content>')]),
143
+ Message("user", '<skill_content name="two">body</skill_content>'),
144
+ Message(
145
+ "user",
146
+ [
147
+ ToolResult(
148
+ tool_use_id="toolu_123",
149
+ content=[TextBlock('<skill_content name="three">body</skill_content>')],
150
+ name="activate_skill",
151
+ is_error=False,
152
+ )
153
+ ],
154
+ ),
155
+ ]
156
+
157
+ assert activated_skill_names(history) == {"one", "two", "three"}
@@ -0,0 +1,88 @@
1
+ from pathlib import Path
2
+
3
+ from ..skills import SkillCatalog, SkillRecord
4
+ from ..slash_commands import (
5
+ SKILLS_LIST_COMMAND,
6
+ THREAD_RESET_COMMANDS,
7
+ TUI_COMMAND_NAMES,
8
+ CommandScope,
9
+ agent_command_names,
10
+ all_command_entries,
11
+ search_commands,
12
+ )
13
+
14
+
15
+ def _catalog(*names: str) -> SkillCatalog:
16
+ skills = {
17
+ name: SkillRecord(
18
+ name=name,
19
+ description=f"Description of {name}",
20
+ skill_dir=Path("/tmp") / name,
21
+ skill_file=Path("/tmp") / name / "SKILL.md",
22
+ scope="project",
23
+ )
24
+ for name in names
25
+ }
26
+ return SkillCatalog(skills=skills)
27
+
28
+
29
+ def test_agent_command_names_match_command_processor():
30
+ from kolega_code.agent.utils.commands import CommandProcessor
31
+
32
+ assert agent_command_names() == {spec.name for spec in CommandProcessor.SPECS}
33
+ assert THREAD_RESET_COMMANDS <= agent_command_names()
34
+ assert SKILLS_LIST_COMMAND in TUI_COMMAND_NAMES
35
+
36
+
37
+ def test_all_command_entries_unique_with_descriptions():
38
+ entries = all_command_entries(_catalog("demo-skill"))
39
+ names = [entry.name for entry in entries]
40
+ assert len(names) == len(set(names))
41
+ assert all(entry.description for entry in entries)
42
+ assert all(entry.token == f"/{entry.name}" for entry in entries)
43
+
44
+
45
+ def test_all_command_entries_include_each_scope():
46
+ entries = all_command_entries(_catalog("demo-skill"))
47
+ by_name = {entry.name: entry for entry in entries}
48
+ assert by_name["help"].scope is CommandScope.AGENT
49
+ assert by_name["plan"].scope is CommandScope.TUI
50
+ assert by_name["demo-skill"].scope is CommandScope.SKILL
51
+
52
+
53
+ def test_builtin_command_shadows_skill_with_same_name():
54
+ entries = all_command_entries(_catalog("plan"))
55
+ matches = [entry for entry in entries if entry.name == "plan"]
56
+ assert len(matches) == 1
57
+ assert matches[0].scope is CommandScope.TUI
58
+
59
+
60
+ def test_search_commands_prefix_matches_first():
61
+ results = search_commands("c", _catalog())
62
+ names = [entry.name for entry in results]
63
+ prefix = [name for name in names if name.startswith("c")]
64
+ assert prefix == names[: len(prefix)]
65
+ assert "clear" in prefix and "compress" in prefix and "context" in prefix and "copy" in prefix
66
+
67
+
68
+ def test_search_commands_substring_after_prefix():
69
+ results = search_commands("ui", _catalog())
70
+ names = [entry.name for entry in results]
71
+ assert "quit" in names
72
+
73
+
74
+ def test_search_commands_empty_query_lists_all_up_to_limit():
75
+ catalog = _catalog("a-skill", "b-skill")
76
+ assert len(search_commands("", catalog, limit=5)) == 5
77
+ everything = search_commands("", catalog, limit=100)
78
+ assert {entry.name for entry in everything} >= {"help", "plan", "a-skill", "b-skill"}
79
+
80
+
81
+ def test_search_commands_includes_skills_dynamically():
82
+ results = search_commands("demo", _catalog("demo-skill"))
83
+ assert [entry.name for entry in results] == ["demo-skill"]
84
+ assert results[0].description == "Description of demo-skill"
85
+
86
+
87
+ def test_search_commands_no_match():
88
+ assert search_commands("zzz", _catalog()) == []
@@ -0,0 +1,180 @@
1
+ """Visual design tokens for the Kolega Code CLI.
2
+
3
+ This module is the single source of truth for colors, glyphs, spacing, and
4
+ truncation limits used by both the Textual TUI (app.py) and the plain CLI
5
+ commands (main.py). It must stay importable without rich or textual installed,
6
+ so those libraries are only imported lazily inside helpers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from functools import lru_cache
13
+ from typing import Optional
14
+
15
+
16
+ class Color:
17
+ """Semantic color roles as Rich style strings."""
18
+
19
+ ACCENT = "cyan"
20
+ SUCCESS = "green"
21
+ WARNING = "yellow"
22
+ ERROR = "red"
23
+ MUTED = "bright_black"
24
+ USER = "cyan"
25
+ AGENT = "magenta"
26
+ TOOL = "blue"
27
+ THINKING = "bright_black"
28
+
29
+
30
+ class Glyph:
31
+ """Unicode glyphs used in the UI. Use g() to apply ASCII fallbacks."""
32
+
33
+ USER = "❯" # ❯
34
+ AGENT = "●" # ●
35
+ STATUS = "●" # ●
36
+ TOOL = "⏺" # ⏺
37
+ SUB_AGENT = "◆" # ◆
38
+ PLAN = "◆" # ◆
39
+ QUESTION = "?"
40
+ INSET_BAR = "│" # │
41
+ INSET_ELBOW = "└" # └
42
+ ELLIPSIS = "…" # …
43
+ BULLET_SEP = "·" # ·
44
+ BAR_FILLED = "█" # █
45
+ BAR_EMPTY = "░" # ░
46
+ CHECK = "✓" # ✓
47
+ CROSS = "✗" # ✗
48
+ DOWN = "↓" # ↓
49
+
50
+
51
+ ASCII_FALLBACKS = {
52
+ Glyph.USER: ">",
53
+ Glyph.AGENT: "*",
54
+ Glyph.TOOL: "*",
55
+ Glyph.SUB_AGENT: "*",
56
+ Glyph.INSET_BAR: "|",
57
+ Glyph.INSET_ELBOW: "`-",
58
+ Glyph.ELLIPSIS: "...",
59
+ Glyph.BULLET_SEP: "-",
60
+ Glyph.BAR_FILLED: "#",
61
+ Glyph.BAR_EMPTY: "-",
62
+ Glyph.CHECK: "ok",
63
+ Glyph.CROSS: "x",
64
+ Glyph.DOWN: "v",
65
+ }
66
+
67
+ SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" # ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
68
+ SPINNER_FRAMES_ASCII = "|/-\\"
69
+ SPINNER_INTERVAL = 0.25
70
+
71
+ # Truncation and layout limits
72
+ TOOL_RESULT_PREVIEW_CHARS = 500
73
+ TOOL_STREAM_PREVIEW_CHARS = 4_000
74
+ TOOL_FULL_CONTENT_CAP_CHARS = 50_000
75
+ SUB_AGENT_TAIL_CHARS = 200
76
+ SUB_AGENT_TASK_PREVIEW_CHARS = 120
77
+ CONTEXT_BAR_WIDTH = 18
78
+ INSET_WIDTH = 2
79
+ MARKDOWN_CODE_THEME = "monokai"
80
+ RENDER_COALESCE_INTERVAL = 0.05
81
+
82
+
83
+ @lru_cache(maxsize=None)
84
+ def supports_unicode(encoding: Optional[str] = None) -> bool:
85
+ """Whether the output encoding can represent the glyphs above.
86
+
87
+ Probes the original stdout (sys.__stdout__) because Textual redirects
88
+ sys.stdout while the app is running.
89
+ """
90
+ if encoding is None:
91
+ encoding = getattr(sys.__stdout__, "encoding", None)
92
+ if encoding is None:
93
+ import locale
94
+
95
+ encoding = locale.getpreferredencoding(False) or "ascii"
96
+ try:
97
+ Glyph.TOOL.encode(encoding)
98
+ SPINNER_FRAMES.encode(encoding)
99
+ except (UnicodeEncodeError, LookupError):
100
+ return False
101
+ return True
102
+
103
+
104
+ def g(glyph: str) -> str:
105
+ """Return the glyph, or its ASCII fallback on limited encodings."""
106
+ if supports_unicode():
107
+ return glyph
108
+ return ASCII_FALLBACKS.get(glyph, glyph)
109
+
110
+
111
+ def spinner_frames() -> str:
112
+ return SPINNER_FRAMES if supports_unicode() else SPINNER_FRAMES_ASCII
113
+
114
+
115
+ LOG_LEVEL_COLORS = {
116
+ "debug": Color.MUTED,
117
+ "info": Color.MUTED,
118
+ "ok": Color.SUCCESS,
119
+ "warn": Color.WARNING,
120
+ "warning": Color.WARNING,
121
+ "error": Color.ERROR,
122
+ "critical": Color.ERROR,
123
+ }
124
+
125
+
126
+ def log_level_color(level: str) -> str:
127
+ """Semantic color for a log level, defaulting to muted."""
128
+ return LOG_LEVEL_COLORS.get(level.lower(), Color.MUTED)
129
+
130
+
131
+ def styled(text: str, style: str) -> str:
132
+ """Wrap text in Rich markup for the given style."""
133
+ return f"[{style}]{text}[/{style}]"
134
+
135
+
136
+ def role_header(
137
+ glyph: str,
138
+ label: str,
139
+ color: str,
140
+ *,
141
+ label_style: str = "bold",
142
+ state: Optional[str] = None,
143
+ detail: Optional[str] = None,
144
+ ) -> str:
145
+ """Render the shared entry-header grammar.
146
+
147
+ GRAMMAR: <colored glyph> <bold label> [ · state] [ · detail]
148
+ The glyph carries the semantic color; state and detail are muted.
149
+ """
150
+ parts = [styled(g(glyph), color), styled(label, label_style)]
151
+ sep = g(Glyph.BULLET_SEP)
152
+ if state:
153
+ parts.append(styled(f"{sep} {state}", "dim"))
154
+ if detail:
155
+ parts.append(styled(f"{sep} {detail}", "dim"))
156
+ return " ".join(parts)
157
+
158
+
159
+ def context_bar(usage_percentage: float, width: int = CONTEXT_BAR_WIDTH) -> str:
160
+ """Render a usage bar like █████░░░░ for the status dashboard."""
161
+ filled = max(0, min(width, round((usage_percentage / 100) * width)))
162
+ return g(Glyph.BAR_FILLED) * filled + g(Glyph.BAR_EMPTY) * (width - filled)
163
+
164
+
165
+ def build_rich_theme():
166
+ """Build a rich Theme mapping semantic names to the palette (lazy import)."""
167
+ from rich.theme import Theme
168
+
169
+ return Theme(
170
+ {
171
+ "accent": Color.ACCENT,
172
+ "success": Color.SUCCESS,
173
+ "warning": Color.WARNING,
174
+ "error": Color.ERROR,
175
+ "muted": Color.MUTED,
176
+ "user": Color.USER,
177
+ "agent": Color.AGENT,
178
+ "tool": Color.TOOL,
179
+ }
180
+ )
kolega_code/config.py ADDED
@@ -0,0 +1,154 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+
6
+
7
+ class ModelProvider(str, Enum):
8
+ """Supported model providers."""
9
+
10
+ ANTHROPIC = "anthropic"
11
+ OPENAI = "openai"
12
+ GOOGLE = "google"
13
+ GROQ = "groq"
14
+ TOGETHER = "together"
15
+ FIREWORKS = "fireworks"
16
+ XAI = "xai"
17
+ LLAMA = "llama"
18
+ DASHSCOPE = "dashscope"
19
+ MOONSHOT = "moonshot"
20
+ DEEPSEEK = "deepseek"
21
+
22
+
23
+ class RateLimitConfig(BaseModel):
24
+ """Rate limit configuration for a specific LLM."""
25
+
26
+ requests_per_minute: int = Field(default=60, description="Maximum number of requests allowed per minute", gt=0)
27
+
28
+ tokens_per_minute: int = Field(default=80_000, description="Maximum number of tokens allowed per minute", gt=0)
29
+
30
+ max_retries: int = Field(default=3, description="Maximum number of retries for failed requests", ge=0)
31
+
32
+
33
+ class ModelConfig(BaseModel):
34
+ """Configuration for a specific model type (long context, fast, or thinking)."""
35
+
36
+ provider: ModelProvider = Field(description="Provider to use for this model configuration")
37
+
38
+ model: str = Field(description="Model identifier to use")
39
+
40
+ rate_limits: RateLimitConfig = Field(default_factory=RateLimitConfig, description="Rate limits for this model")
41
+
42
+ # Only used for thinking config
43
+ thinking_tokens: Optional[int] = Field(
44
+ default=None, description="Number of tokens to reserve for thinking operations", gt=0
45
+ )
46
+
47
+
48
+ class AgentConfig(BaseModel):
49
+ """Configuration for the agent system.
50
+
51
+ This class contains all configuration parameters needed to run the agent,
52
+ including API keys for different providers and model configurations for
53
+ various operational modes (long context, fast, and thinking).
54
+
55
+ Usage:
56
+ # Create a default configuration
57
+ config = AgentConfig()
58
+
59
+ # Create a custom configuration
60
+ config = AgentConfig(
61
+ anthropic_api_key="your_anthropic_key",
62
+ long_context_config=ModelConfig(
63
+ provider=ModelProvider.ANTHROPIC,
64
+ model="claude-3-7-sonnet-20250219"
65
+ )
66
+ )
67
+
68
+ # Get an API key for a specific provider
69
+ api_key = config.get_api_key(ModelProvider.ANTHROPIC)
70
+
71
+ # Access model configurations
72
+ long_context_model = config.long_context_config
73
+ fast_model = config.fast_config
74
+ thinking_model = config.thinking_config
75
+
76
+ API keys can be set directly or loaded from environment variables.
77
+ Model configurations define which models to use for different operational
78
+ contexts and their respective token limits and rate limits.
79
+ """
80
+
81
+ # API Keys for different providers
82
+ anthropic_api_key: Optional[str] = Field(default=None, description="API key for Anthropic")
83
+ openai_api_key: Optional[str] = Field(default=None, description="API key for OpenAI")
84
+ google_api_key: Optional[str] = Field(default=None, description="API key for Google")
85
+ groq_api_key: Optional[str] = Field(default=None, description="API key for Groq")
86
+ together_api_key: Optional[str] = Field(default=None, description="API key for Together.ai")
87
+ fireworks_api_key: Optional[str] = Field(default=None, description="API key for Fireworks.ai")
88
+ xai_api_key: Optional[str] = Field(default=None, description="API key for X.ai")
89
+ dashscope_api_key: Optional[str] = Field(default=None, description="API key for Dashscope (Alibaba Model Studio)")
90
+ moonshot_api_key: Optional[str] = Field(default=None, description="API key for Moonshot.ai")
91
+ deepseek_api_key: Optional[str] = Field(default=None, description="API key for DeepSeek")
92
+
93
+ # Langfuse configuration
94
+ langfuse_enabled: bool = Field(default=False, description="Enable Langfuse tracing")
95
+ langfuse_host: Optional[str] = Field(default=None, description="Langfuse host URL")
96
+ langfuse_public_key: Optional[str] = Field(default=None, description="Langfuse public key")
97
+ langfuse_secret_key: Optional[str] = Field(default=None, description="Langfuse secret key")
98
+ environment: Optional[str] = Field(default="development", description="Environment name (development, production)")
99
+
100
+ # Model configurations
101
+ long_context_config: ModelConfig = Field(
102
+ default_factory=lambda: ModelConfig(provider=ModelProvider.ANTHROPIC, model="claude-opus-4-7"),
103
+ description="Configuration for long context operations",
104
+ )
105
+
106
+ fast_config: ModelConfig = Field(
107
+ default_factory=lambda: ModelConfig(provider=ModelProvider.ANTHROPIC, model="claude-haiku-4-5-20251001"),
108
+ description="Configuration for fast operations",
109
+ )
110
+
111
+ edit_model_config: ModelConfig = Field(
112
+ default_factory=lambda: ModelConfig(provider=ModelProvider.ANTHROPIC, model="claude-sonnet-4-6"),
113
+ description="Configuration for applying edits",
114
+ )
115
+
116
+ thinking_config: ModelConfig = Field(
117
+ default_factory=lambda: ModelConfig(
118
+ provider=ModelProvider.ANTHROPIC, model="claude-opus-4-7", thinking_tokens=1024
119
+ ),
120
+ description="Configuration for thinking operations",
121
+ )
122
+
123
+ def get_api_key(self, provider: ModelProvider) -> Optional[str]:
124
+ """Get the API key for a specific provider."""
125
+ api_key_map = {
126
+ ModelProvider.ANTHROPIC: self.anthropic_api_key,
127
+ ModelProvider.OPENAI: self.openai_api_key,
128
+ ModelProvider.GOOGLE: self.google_api_key,
129
+ ModelProvider.GROQ: self.groq_api_key,
130
+ ModelProvider.TOGETHER: self.together_api_key,
131
+ ModelProvider.FIREWORKS: self.fireworks_api_key,
132
+ ModelProvider.XAI: self.xai_api_key,
133
+ ModelProvider.DASHSCOPE: self.dashscope_api_key,
134
+ ModelProvider.MOONSHOT: self.moonshot_api_key,
135
+ ModelProvider.DEEPSEEK: self.deepseek_api_key,
136
+ ModelProvider.LLAMA: None, # Local model, no API key needed
137
+ }
138
+ return api_key_map[provider]
139
+
140
+ @model_validator(mode="after")
141
+ def validate_provider_api_key(self) -> "AgentConfig":
142
+ """Validates that if a model provider is specified, the corresponding API key is provided."""
143
+ configs = [
144
+ (self.long_context_config, "long context"),
145
+ (self.fast_config, "fast"),
146
+ (self.thinking_config, "thinking"),
147
+ (self.edit_model_config, "edit model"),
148
+ ]
149
+
150
+ for config, config_name in configs:
151
+ if config.provider != ModelProvider.LLAMA and self.get_api_key(config.provider) is None:
152
+ raise ValueError(f"Missing API key for {config_name} provider '{config.provider.value}'")
153
+
154
+ return self