kryth 0.0.36__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 (132) hide show
  1. agent/__init__.py +6 -0
  2. agent/agent.py +253 -0
  3. agent/events.py +161 -0
  4. agent/session.py +294 -0
  5. agent/store.py +362 -0
  6. client/__init__.py +23 -0
  7. client/llm_client.py +376 -0
  8. client/response.py +87 -0
  9. config/__init__.py +6 -0
  10. config/config.py +288 -0
  11. config/credentials.py +54 -0
  12. config/keyring_store.py +264 -0
  13. config/loader.py +207 -0
  14. config/oauth.py +137 -0
  15. config/providers.py +167 -0
  16. config/setup_wizard.py +387 -0
  17. context/__init__.py +30 -0
  18. context/compaction.py +106 -0
  19. context/loop_detector.py +51 -0
  20. context/manager.py +357 -0
  21. context/project_context.py +245 -0
  22. context/workspace.py +260 -0
  23. hooks/__init__.py +1 -0
  24. hooks/hook_system.py +139 -0
  25. kryth-0.0.36.dist-info/METADATA +219 -0
  26. kryth-0.0.36.dist-info/RECORD +132 -0
  27. kryth-0.0.36.dist-info/WHEEL +5 -0
  28. kryth-0.0.36.dist-info/entry_points.txt +2 -0
  29. kryth-0.0.36.dist-info/licenses/LICENSE +674 -0
  30. kryth-0.0.36.dist-info/top_level.txt +11 -0
  31. main.py +454 -0
  32. prompts/__init__.py +31 -0
  33. prompts/agents_md.py +14 -0
  34. prompts/compaction.py +27 -0
  35. prompts/environment.py +44 -0
  36. prompts/identity.py +13 -0
  37. prompts/instructions.py +33 -0
  38. prompts/loop_breaker.py +15 -0
  39. prompts/operational.py +77 -0
  40. prompts/security.py +15 -0
  41. prompts/skills.py +30 -0
  42. prompts/system.py +63 -0
  43. prompts/tool_guidelines.py +71 -0
  44. safety/__init__.py +1 -0
  45. safety/approval.py +245 -0
  46. tools/__init__.py +26 -0
  47. tools/base.py +217 -0
  48. tools/core/__init__.py +70 -0
  49. tools/core/apply_patch.py +645 -0
  50. tools/core/bash.py +344 -0
  51. tools/core/code_integrity.py +258 -0
  52. tools/core/dependency_validator.py +321 -0
  53. tools/core/directories.py +65 -0
  54. tools/core/edit.py +245 -0
  55. tools/core/error_recovery.py +474 -0
  56. tools/core/glob.py +92 -0
  57. tools/core/grep.py +128 -0
  58. tools/core/long_running.py +174 -0
  59. tools/core/persistent_terminal.py +454 -0
  60. tools/core/plan.py +145 -0
  61. tools/core/project.py +550 -0
  62. tools/core/read.py +126 -0
  63. tools/core/shells.py +867 -0
  64. tools/core/terminal.py +382 -0
  65. tools/core/tool_supervisor.py +337 -0
  66. tools/core/write.py +135 -0
  67. tools/discovery.py +89 -0
  68. tools/mcp/client.py +127 -0
  69. tools/mcp/manager.py +80 -0
  70. tools/mcp/mcp_tool.py +54 -0
  71. tools/memory/__init__.py +0 -0
  72. tools/memory/memory.py +189 -0
  73. tools/network/__init__.py +0 -0
  74. tools/network/fetch.py +74 -0
  75. tools/network/headers.py +14 -0
  76. tools/network/search.py +68 -0
  77. tools/registry.py +189 -0
  78. tools/subagents/subagents.py +318 -0
  79. ui/__init__.py +6 -0
  80. ui/components/__init__.py +112 -0
  81. ui/components/args_table.py +14 -0
  82. ui/components/confirmation.py +86 -0
  83. ui/components/diff_viewer.py +37 -0
  84. ui/components/gutter.py +26 -0
  85. ui/components/logo.py +91 -0
  86. ui/components/markdown.py +201 -0
  87. ui/components/memory.py +29 -0
  88. ui/components/notification.py +26 -0
  89. ui/components/permission_prompt.py +59 -0
  90. ui/components/plan.py +35 -0
  91. ui/components/shimmer.py +38 -0
  92. ui/components/spinner.py +84 -0
  93. ui/components/status_bar.py +47 -0
  94. ui/components/terminal_approval.py +367 -0
  95. ui/components/thinking.py +47 -0
  96. ui/components/tool_call.py +458 -0
  97. ui/components/tool_progress.py +33 -0
  98. ui/components/transcript.py +70 -0
  99. ui/components/usage.py +29 -0
  100. ui/components/welcome.py +150 -0
  101. ui/console.py +12 -0
  102. ui/format/__init__.py +33 -0
  103. ui/format/args.py +80 -0
  104. ui/format/code.py +63 -0
  105. ui/format/diff.py +50 -0
  106. ui/format/elapsed.py +32 -0
  107. ui/motion.py +16 -0
  108. ui/repl/__init__.py +6 -0
  109. ui/repl/app.py +292 -0
  110. ui/repl/banner.py +52 -0
  111. ui/repl/commands/__init__.py +17 -0
  112. ui/repl/commands/base.py +40 -0
  113. ui/repl/commands/dispatch.py +217 -0
  114. ui/repl/commands/help.py +47 -0
  115. ui/repl/commands/inspect.py +80 -0
  116. ui/repl/commands/sessions.py +376 -0
  117. ui/repl/commands/settings.py +140 -0
  118. ui/repl/completer.py +37 -0
  119. ui/repl/keys.py +177 -0
  120. ui/repl/prompt.py +130 -0
  121. ui/setup_tui.py +435 -0
  122. ui/stream.py +95 -0
  123. ui/theme/__init__.py +24 -0
  124. ui/theme/palette.py +50 -0
  125. ui/theme/styles.py +56 -0
  126. ui/theme/syntax.py +69 -0
  127. ui/tui.py +649 -0
  128. utils/__init__.py +17 -0
  129. utils/development_supervisor.py +160 -0
  130. utils/errors.py +47 -0
  131. utils/paths.py +184 -0
  132. utils/text.py +74 -0
agent/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Agent module for the Kryth AI coding agent."""
2
+
3
+ from agent.agent import Agent
4
+ from agent.events import AgentEvent, AgentEventType
5
+
6
+ __all__ = ['Agent', 'AgentEvent', 'AgentEventType']
agent/agent.py ADDED
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from typing import AsyncGenerator, Awaitable, Callable
4
+ from agent.session import Session
5
+ from agent.store import SessionRecord
6
+ from config.config import Config
7
+ from agent.events import AgentEvent, AgentEventType
8
+ from client.response import StreamEventType, TokenUsage, ToolCall, ToolResultMessage
9
+ from prompts import create_loop_breaker_prompt
10
+ from tools.base import ToolConfirmation
11
+
12
+ class Agent:
13
+ def __init__(
14
+ self,
15
+ config: Config,
16
+ confirmation_callback: Callable[[ToolConfirmation], Awaitable[bool]] | None = None,
17
+ resume: str | None = None,
18
+ progress_callback: Callable[[str], None] | None = None,
19
+ ):
20
+ self.config = config
21
+ self.session: Session | None = Session(self.config)
22
+ self.session.approval_manager.confirmation_callback = confirmation_callback
23
+
24
+ self.progress_callback = progress_callback
25
+
26
+ # Resolved on entry, once the session has a context manager to fill.
27
+ self._resume = resume
28
+ self.resumed: SessionRecord | None = None
29
+
30
+ async def run(self, message: str):
31
+ await self.session.hook_system.trigger_before_agent(message)
32
+
33
+ # PHASE 1 — Workspace ambiguity check. An empty/ambiguous workspace does
34
+ # not automatically mean the user wants a new project. Ask for
35
+ # clarification instead of silently creating nested project directories.
36
+ if self.session.project_context and self.session.project_context.is_ambiguous:
37
+ hint = (
38
+ self.session.project_context_hint
39
+ or f"Workspace `{self.session.config.cwd}` appears to be empty or new."
40
+ )
41
+ yield AgentEvent.agent_error(
42
+ "Clarification needed: "
43
+ + hint
44
+ + "\n\nDid you mean to start a new project here, or point KRYTH at a "
45
+ "different directory?",
46
+ details={
47
+ "ambiguous_workspace": True,
48
+ "workspace": str(self.session.config.cwd),
49
+ },
50
+ )
51
+ return
52
+
53
+ yield AgentEvent.agent_start(message)
54
+ self.session.inc_turn()
55
+ self.session.note_prompt(message)
56
+ self.session.reset_turn_usage()
57
+ self.session.context_manager.add_user_message(message)
58
+
59
+ final_response: str | None = None
60
+
61
+ async for event in self._agentic_loop():
62
+ yield event
63
+
64
+ if event.type == AgentEventType.TEXT_COMPLETE:
65
+ final_response = event.data.get("content")
66
+
67
+ await self.session.hook_system.trigger_after_agent(message, final_response)
68
+ self.session.save_checkpoint()
69
+ yield AgentEvent.agent_end(final_response, self.session.last_usage)
70
+
71
+ async def _agentic_loop(self) -> AsyncGenerator[AgentEvent, None]:
72
+
73
+ max_turns = self.config.max_turns
74
+
75
+ for i in range(max_turns):
76
+
77
+ response_text = ""
78
+ reasoning_text = ""
79
+ reasoning_done = False
80
+
81
+ # Prune before compacting: a tool-heavy turn can blow past the context
82
+ # window without ever reaching the end of the loop.
83
+ self.session.context_manager.prune_tool_outputs()
84
+
85
+ if self.session.context_manager.needs_compression():
86
+ summary, usage = await self.session.chat_compactor.compress(
87
+ self.session.context_manager
88
+ )
89
+
90
+ if summary:
91
+ self.session.context_manager.replace_with_summary(summary)
92
+ self.session.context_manager.set_latest_usage(usage)
93
+ self.session.context_manager.add_usage(usage)
94
+
95
+ tool_schemas = self.session.tool_registry.get_schemas()
96
+
97
+ tool_calls: list[ToolCall] = []
98
+ usage: TokenUsage | None = None
99
+
100
+ async for event in self.session.client.chat_completion(
101
+ self.session.context_manager.get_messages(),
102
+ tools=tool_schemas if tool_schemas else None,
103
+ stream=True
104
+ ):
105
+ if event.type == StreamEventType.REASONING_DELTA:
106
+ if not event.reasoning_delta:
107
+ continue
108
+ reasoning_text += event.reasoning_delta
109
+ yield AgentEvent.reasoning_delta(event.reasoning_delta)
110
+ elif event.type == StreamEventType.TEXT_DELTA:
111
+ if not event.text_delta:
112
+ continue
113
+ content = event.text_delta.content
114
+ # Thinking always ends where the answer begins.
115
+ if reasoning_text and not reasoning_done:
116
+ reasoning_done = True
117
+ yield AgentEvent.reasoning_complete(reasoning_text)
118
+ response_text += content
119
+ yield AgentEvent.text_delta(content)
120
+ elif event.type == StreamEventType.TOOL_CALL_COMPLETE:
121
+ if event.tool_call:
122
+ tool_calls.append(event.tool_call)
123
+ elif event.type == StreamEventType.MESSAGE_COMPLETE:
124
+ if event.reasoning_delta and not reasoning_text:
125
+ reasoning_text = event.reasoning_delta
126
+ yield AgentEvent.reasoning_delta(event.reasoning_delta)
127
+ if event.usage:
128
+ usage = event.usage
129
+ self.session.last_usage = event.usage
130
+ self.session.turn_usage += event.usage
131
+ yield AgentEvent.usage(self.session.turn_usage)
132
+ if event.text_delta:
133
+ if reasoning_text and not reasoning_done:
134
+ reasoning_done = True
135
+ yield AgentEvent.reasoning_complete(reasoning_text)
136
+ response_text += event.text_delta.content
137
+ yield AgentEvent.text_delta(event.text_delta.content)
138
+ if event.tool_calls:
139
+ tool_calls.extend(event.tool_calls)
140
+ elif event.type == StreamEventType.ERROR:
141
+ if reasoning_text and not reasoning_done:
142
+ reasoning_done = True
143
+ yield AgentEvent.reasoning_complete(reasoning_text)
144
+ yield AgentEvent.agent_error(event.error or "Unknown error occurred.")
145
+ return
146
+
147
+ # Reasoning with no answer after it: the model went straight to
148
+ # tool calls, so nothing else will close the block.
149
+ if reasoning_text and not reasoning_done:
150
+ yield AgentEvent.reasoning_complete(reasoning_text)
151
+
152
+ self.session.context_manager.add_assistant_message(
153
+ response_text or None,
154
+ [
155
+ {
156
+ 'id': tc.call_id,
157
+ 'type': 'function',
158
+ 'function': {
159
+ 'name': tc.name,
160
+ 'arguments': json.dumps(tc.arguments),
161
+ },
162
+ }
163
+ for tc in tool_calls
164
+ ]
165
+ if tool_calls
166
+ else None
167
+ )
168
+
169
+ if response_text:
170
+ yield AgentEvent.text_complete(response_text)
171
+ self.session.loop_detector.record_action(
172
+ 'response',
173
+ text=response_text
174
+ )
175
+
176
+ if not tool_calls:
177
+ if usage:
178
+ self.session.context_manager.set_latest_usage(usage)
179
+ self.session.context_manager.add_usage(usage)
180
+
181
+ return
182
+
183
+ tool_call_results: list[ToolResultMessage] = []
184
+
185
+ for tool_call in tool_calls:
186
+
187
+ yield AgentEvent.tool_call_start(
188
+ tool_call.call_id,
189
+ tool_call.name,
190
+ tool_call.arguments
191
+ )
192
+
193
+ self.session.loop_detector.record_action(
194
+ "tool_call",
195
+ tool_name=tool_call.name,
196
+ args=tool_call.arguments
197
+ )
198
+
199
+ result = await self.session.tool_registry.invoke(
200
+ tool_call.name,
201
+ tool_call.arguments,
202
+ self.config.cwd,
203
+ self.session.hook_system,
204
+ self.session.approval_manager,
205
+ progress_callback=self.progress_callback,
206
+ )
207
+
208
+ yield AgentEvent.tool_call_complete(
209
+ tool_call.call_id,
210
+ tool_call.name,
211
+ result,
212
+ )
213
+
214
+ tool_call_results.append(
215
+ ToolResultMessage(
216
+ tool_call_id=tool_call.call_id,
217
+ content=result.to_model_output(),
218
+ is_error=not result.success
219
+ )
220
+ )
221
+
222
+ for tool_result in tool_call_results:
223
+ self.session.context_manager.add_tool_result(
224
+ tool_result.tool_call_id,
225
+ tool_result.content,
226
+ )
227
+
228
+ loop_detection_err = self.session.loop_detector.check_for_loop()
229
+
230
+ if loop_detection_err:
231
+
232
+ loop_prompt = create_loop_breaker_prompt(loop_detection_err)
233
+
234
+ self.session.context_manager.add_user_message(loop_prompt)
235
+ continue
236
+
237
+ async def __aenter__(self) -> Agent:
238
+ await self.session.initalize(progress=self.progress_callback)
239
+ if self._resume:
240
+ self.resumed = self.session.resume(self._resume)
241
+ return self
242
+
243
+ async def __aexit__(
244
+ self,
245
+ exc_type,
246
+ exc_val,
247
+ exc_tb
248
+ ) -> None:
249
+
250
+ if self.session and self.session.client and self.session.mcp_manager:
251
+ await self.session.client.close()
252
+ await self.session.mcp_manager.shutdown()
253
+ self.session = None # Close session, everything else will be garbage collected
agent/events.py ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+ from enum import Enum
3
+ from typing import Any
4
+ from dataclasses import dataclass, field
5
+ from client.response import TokenUsage
6
+ from tools.base import ToolResult
7
+
8
+ class AgentEventType(str, Enum):
9
+
10
+ # Agentic loops, and err
11
+ AGENT_START="agent_start"
12
+ AGENT_END="agent_end"
13
+ AGENT_ERROR="agent_error"
14
+
15
+ # Token accounting, emitted once per completion
16
+ USAGE = "usage"
17
+
18
+ # Text streaming
19
+ TEXT_DELTA = "text_delta"
20
+ TEXT_COMPLETE = "text_complete"
21
+
22
+ # Reasoning ("thinking") streaming, only for models that expose it
23
+ REASONING_DELTA = "reasoning_delta"
24
+ REASONING_COMPLETE = "reasoning_complete"
25
+
26
+ # Tool Calls
27
+ TOOL_CALL_START = 'tool_call_start'
28
+ TOOL_CALL_COMPLETE = 'tool_call_complete'
29
+
30
+ @dataclass
31
+ class AgentEvent:
32
+ type: AgentEventType
33
+ data: dict[str, Any] = field(default_factory=dict)
34
+
35
+ @classmethod
36
+ def agent_start(
37
+ cls,
38
+ message: str
39
+ ) -> AgentEvent:
40
+ return cls(
41
+ type=AgentEventType.AGENT_START,
42
+ data={
43
+ "message": message
44
+ }
45
+ )
46
+
47
+ @classmethod
48
+ def agent_end(
49
+ cls,
50
+ response: str | None = None,
51
+ usage: TokenUsage | None = None
52
+ ) -> AgentEvent:
53
+ return cls(
54
+ type=AgentEventType.AGENT_END,
55
+ data={
56
+ "response": response,
57
+ 'usage': usage.__dict__ if usage else None
58
+ },
59
+ )
60
+
61
+ @classmethod
62
+ def usage(
63
+ cls,
64
+ turn_usage: TokenUsage
65
+ ) -> AgentEvent:
66
+ return cls(
67
+ type=AgentEventType.USAGE,
68
+ data={
69
+ 'usage': turn_usage.__dict__,
70
+ }
71
+ )
72
+
73
+ @classmethod
74
+ def agent_error(
75
+ cls,
76
+ error: str,
77
+ details: dict[str, Any] | None = None
78
+ ) -> AgentEvent:
79
+ return cls(
80
+ type=AgentEventType.AGENT_ERROR,
81
+ data={
82
+ "error": error,
83
+ 'details': details or {}
84
+ }
85
+ )
86
+
87
+ @classmethod
88
+ def text_delta(
89
+ cls,
90
+ content: str
91
+ ) -> AgentEvent:
92
+ return cls(
93
+ type=AgentEventType.TEXT_DELTA,
94
+ data={
95
+ "content": content
96
+ }
97
+ )
98
+
99
+ @classmethod
100
+ def text_complete(
101
+ cls,
102
+ content: str
103
+ ) -> AgentEvent:
104
+ return cls(
105
+ type=AgentEventType.TEXT_COMPLETE,
106
+ data={
107
+ "content": content
108
+ }
109
+ )
110
+
111
+ @classmethod
112
+ def reasoning_delta(
113
+ cls,
114
+ content: str
115
+ ) -> AgentEvent:
116
+ return cls(
117
+ type=AgentEventType.REASONING_DELTA,
118
+ data={
119
+ "content": content
120
+ }
121
+ )
122
+
123
+ @classmethod
124
+ def reasoning_complete(
125
+ cls,
126
+ content: str
127
+ ) -> AgentEvent:
128
+ return cls(
129
+ type=AgentEventType.REASONING_COMPLETE,
130
+ data={
131
+ "content": content
132
+ }
133
+ )
134
+
135
+ @classmethod
136
+ def tool_call_start(cls, call_id: str, name: str, arguments: dict[str, Any]):
137
+ return cls(
138
+ type=AgentEventType.TOOL_CALL_START,
139
+ data={
140
+ 'call_id': call_id,
141
+ 'name': name,
142
+ 'arguments': arguments
143
+ }
144
+ )
145
+
146
+ @classmethod
147
+ def tool_call_complete(cls, call_id: str, name: str, result: ToolResult):
148
+ return cls(
149
+ type=AgentEventType.TOOL_CALL_COMPLETE,
150
+ data={
151
+ 'call_id': call_id,
152
+ 'name': name,
153
+ 'success': result.success,
154
+ 'output': result.output,
155
+ 'error': result.error,
156
+ 'metadata': result.metadata,
157
+ 'diff': result.diff.create_diff() if result.diff else None,
158
+ 'truncated': result.truncated,
159
+ 'exit_code': result.exit_code,
160
+ }
161
+ )