mycode-coding-agent 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 (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,38 @@
1
+ """Application-layer runtime assembly and single-turn use cases."""
2
+
3
+ from mycode.application.agent_session import (
4
+ AgentApplicationSession,
5
+ CompactResult,
6
+ ContextStatus,
7
+ start_agent_application_session,
8
+ )
9
+ from mycode.application.events import RuntimeEvent, RuntimeEventType
10
+ from mycode.application.runtime import (
11
+ build_agent_runner,
12
+ context_budget_from_config,
13
+ run_agent_turn,
14
+ )
15
+ from mycode.application.sessions import (
16
+ ActiveProjectSession,
17
+ SessionStartRequest,
18
+ delete_project_session,
19
+ list_project_sessions,
20
+ start_project_session,
21
+ )
22
+
23
+ __all__ = [
24
+ "ActiveProjectSession",
25
+ "AgentApplicationSession",
26
+ "CompactResult",
27
+ "ContextStatus",
28
+ "RuntimeEvent",
29
+ "RuntimeEventType",
30
+ "SessionStartRequest",
31
+ "build_agent_runner",
32
+ "context_budget_from_config",
33
+ "delete_project_session",
34
+ "list_project_sessions",
35
+ "run_agent_turn",
36
+ "start_agent_application_session",
37
+ "start_project_session",
38
+ ]
@@ -0,0 +1,367 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterator
4
+ from dataclasses import dataclass, field
5
+ from typing import Literal
6
+
7
+ from mycode.agent.events import AgentEvent
8
+ from mycode.agent.outcome import AgentRunOutcome
9
+ from mycode.agent.runner import AgentRunner
10
+ from mycode.application.events import RuntimeEvent
11
+ from mycode.application.runtime import build_agent_runner, run_agent_turn
12
+ from mycode.context.budget import ModelContext
13
+ from mycode.error_handling import error_summary
14
+ from mycode.application.sessions import (
15
+ ActiveProjectSession,
16
+ SessionStartRequest,
17
+ start_project_session,
18
+ )
19
+ from mycode.config import LLMConfig
20
+ from mycode.mcp import MCPConfig, MCPManager
21
+ from mycode.observability import ObservationSink
22
+ from mycode.permissions import Confirmer
23
+ from mycode.persistence.session_store import SessionStore
24
+ from mycode.project import ProjectIdentity
25
+ from mycode.subagents.observability import CompositeSubAgentObserver, SubAgentObserver
26
+ from mycode.subagents.persistence import SessionSubAgentObserver
27
+
28
+
29
+ RuntimeEventHandler = Callable[[RuntimeEvent], None]
30
+ CompactResultStatus = Literal["compacted", "skipped", "failed"]
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ContextStatus:
35
+ """Content-free, serializable inspection of the current context."""
36
+
37
+ estimated_input_tokens: int
38
+ context_window_tokens: int
39
+ max_input_tokens: int
40
+ reserved_output_tokens: int
41
+ safety_margin_tokens: int
42
+ estimate_source: str
43
+ last_provider_prompt_tokens: int | None
44
+ source_message_count: int
45
+ model_visible_message_count: int
46
+ memory_entry_count: int
47
+ memory_estimated_tokens: int
48
+ compact_status: str
49
+ compact_covered_message_count: int
50
+ compressed_tool_result_count: int
51
+
52
+ def to_dict(self) -> dict[str, object]:
53
+ return {
54
+ "estimated": True,
55
+ "estimated_input_tokens": self.estimated_input_tokens,
56
+ "context_window_tokens": self.context_window_tokens,
57
+ "max_input_tokens": self.max_input_tokens,
58
+ "reserved_output_tokens": self.reserved_output_tokens,
59
+ "safety_margin_tokens": self.safety_margin_tokens,
60
+ "estimate_source": self.estimate_source,
61
+ "last_provider_prompt_tokens": self.last_provider_prompt_tokens,
62
+ "source_message_count": self.source_message_count,
63
+ "model_visible_message_count": self.model_visible_message_count,
64
+ "memory_entry_count": self.memory_entry_count,
65
+ "memory_estimated_tokens": self.memory_estimated_tokens,
66
+ "compact_status": self.compact_status,
67
+ "compact_covered_message_count": self.compact_covered_message_count,
68
+ "compressed_tool_result_count": self.compressed_tool_result_count,
69
+ }
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class CompactResult:
74
+ status: CompactResultStatus
75
+ reason: str | None
76
+ before: ContextStatus | None
77
+ after: ContextStatus | None
78
+
79
+ def to_dict(self) -> dict[str, object]:
80
+ return {
81
+ "status": self.status,
82
+ "reason": self.reason,
83
+ "before": None if self.before is None else self.before.to_dict(),
84
+ "after": None if self.after is None else self.after.to_dict(),
85
+ }
86
+
87
+
88
+ def _context_status(
89
+ context: ModelContext,
90
+ *,
91
+ context_window_tokens: int,
92
+ reserved_output_tokens: int,
93
+ safety_margin_tokens: int,
94
+ last_provider_prompt_tokens: int | None,
95
+ ) -> ContextStatus:
96
+ memory = context.memory_stats
97
+ compact = context.compact_stats
98
+ if compact is None or compact.boundary_id is None:
99
+ compact_status = "none"
100
+ compact_covered_message_count = 0
101
+ elif compact.summary_visible:
102
+ compact_status = "active"
103
+ compact_covered_message_count = compact.compacted_message_count
104
+ else:
105
+ compact_status = compact.status
106
+ compact_covered_message_count = compact.compacted_message_count
107
+ return ContextStatus(
108
+ estimated_input_tokens=context.estimate.estimated_input_tokens,
109
+ context_window_tokens=context_window_tokens,
110
+ max_input_tokens=context.estimate.max_input_tokens,
111
+ reserved_output_tokens=reserved_output_tokens,
112
+ safety_margin_tokens=safety_margin_tokens,
113
+ estimate_source=context.estimate.token_estimate_source,
114
+ last_provider_prompt_tokens=last_provider_prompt_tokens,
115
+ source_message_count=context.source_message_count,
116
+ model_visible_message_count=context.selected_message_count,
117
+ memory_entry_count=(0 if memory is None else memory.included_entry_count),
118
+ memory_estimated_tokens=(0 if memory is None else memory.estimated_tokens),
119
+ compact_status=compact_status,
120
+ compact_covered_message_count=compact_covered_message_count,
121
+ compressed_tool_result_count=context.compressed_tool_result_count,
122
+ )
123
+
124
+
125
+ @dataclass
126
+ class AgentApplicationSession:
127
+ runner: AgentRunner
128
+ active_project_session: ActiveProjectSession
129
+ mcp_manager: MCPManager
130
+ _cleaned_up: bool = field(default=False, init=False, repr=False)
131
+
132
+ @property
133
+ def compact_state_recovered(self) -> bool:
134
+ return self.active_project_session.compact_state_recovered
135
+
136
+ @property
137
+ def mcp_statuses(self):
138
+ return self.mcp_manager.statuses
139
+
140
+ def get_context_status(self) -> ContextStatus:
141
+ """Recompute current Context statistics without invoking an LLM."""
142
+ self._ensure_active()
143
+ context = self.runner.inspect_context()
144
+ return _context_status(
145
+ context,
146
+ context_window_tokens=self.runner.context_budget.context_window_tokens,
147
+ reserved_output_tokens=self.runner.context_budget.reserved_output_tokens,
148
+ safety_margin_tokens=self.runner.context_budget.safety_margin_tokens,
149
+ last_provider_prompt_tokens=(
150
+ None
151
+ if self.runner.last_token_usage is None
152
+ else self.runner.last_token_usage.prompt_tokens
153
+ ),
154
+ )
155
+
156
+ def compact_context(self) -> CompactResult:
157
+ """Run one manual Compact and return a safe structured outcome."""
158
+ self._ensure_active()
159
+ before: ContextStatus | None = None
160
+ try:
161
+ before = self.get_context_status()
162
+ context = self.runner.compact_context()
163
+ compact = context.compact_stats
164
+ if compact is None:
165
+ raise RuntimeError("Compact returned no statistics.")
166
+ operation_status = compact.status
167
+ if operation_status == "compacted":
168
+ result_status: CompactResultStatus = "compacted"
169
+ reason = None
170
+ elif operation_status in {
171
+ "insufficient_history",
172
+ "cooldown",
173
+ "circuit_open",
174
+ }:
175
+ result_status = "skipped"
176
+ reason = operation_status
177
+ elif operation_status in {"failed", "invalid_boundary"}:
178
+ result_status = "failed"
179
+ compactor = getattr(self.runner, "compactor", None)
180
+ reason = (
181
+ None
182
+ if compactor is None
183
+ else compactor.state.last_failure_reason
184
+ ) or operation_status
185
+ else:
186
+ raise RuntimeError(
187
+ f"Unexpected manual Compact status: {operation_status}."
188
+ )
189
+ after = _context_status(
190
+ context,
191
+ context_window_tokens=self.runner.context_budget.context_window_tokens,
192
+ reserved_output_tokens=self.runner.context_budget.reserved_output_tokens,
193
+ safety_margin_tokens=self.runner.context_budget.safety_margin_tokens,
194
+ last_provider_prompt_tokens=(
195
+ None
196
+ if self.runner.last_token_usage is None
197
+ else self.runner.last_token_usage.prompt_tokens
198
+ ),
199
+ )
200
+ return CompactResult(
201
+ status=result_status,
202
+ reason=reason,
203
+ before=before,
204
+ after=after,
205
+ )
206
+ except Exception as error: # noqa: BLE001 - application command boundary
207
+ return CompactResult(
208
+ status="failed",
209
+ reason=error_summary(error),
210
+ before=before,
211
+ after=(
212
+ self._safe_context_status(before)
213
+ if before is not None
214
+ else None
215
+ ),
216
+ )
217
+
218
+ def _ensure_active(self) -> None:
219
+ if self._cleaned_up:
220
+ raise RuntimeError(
221
+ "AgentApplicationSession is already closed or interrupted."
222
+ )
223
+
224
+ def _safe_context_status(self, fallback: ContextStatus) -> ContextStatus:
225
+ try:
226
+ return self.get_context_status()
227
+ except Exception:
228
+ return fallback
229
+
230
+ def startup_events(self) -> Iterator[RuntimeEvent]:
231
+ yield RuntimeEvent(
232
+ type="runtime_ready",
233
+ session_id=self.active_project_session.record.id,
234
+ session_title=self.active_project_session.record.title,
235
+ session_created=self.active_project_session.created,
236
+ compact_state_recovered=self.compact_state_recovered,
237
+ instruction_sources=tuple(
238
+ getattr(self.runner, "instruction_sources", ())
239
+ ),
240
+ instruction_warnings=tuple(
241
+ getattr(self.runner, "instruction_warnings", ())
242
+ ),
243
+ skill_warnings=tuple(getattr(self.runner, "skill_warnings", ())),
244
+ )
245
+ for status in self.mcp_statuses:
246
+ yield RuntimeEvent(type="mcp_status", mcp_status=status)
247
+
248
+ def run_turn(
249
+ self,
250
+ content: str,
251
+ *,
252
+ turn_id: str | None = None,
253
+ event_handler: RuntimeEventHandler | None = None,
254
+ ) -> AgentRunOutcome:
255
+ if self._cleaned_up:
256
+ raise RuntimeError(
257
+ "AgentApplicationSession is already closed or interrupted."
258
+ )
259
+
260
+ def handle_agent_event(agent_event: AgentEvent) -> None:
261
+ if event_handler is not None:
262
+ event_handler(
263
+ RuntimeEvent(
264
+ type="agent",
265
+ turn_id=turn_id,
266
+ agent_event=agent_event,
267
+ )
268
+ )
269
+
270
+ outcome = run_agent_turn(
271
+ self.runner,
272
+ content,
273
+ event_handler=handle_agent_event,
274
+ )
275
+ if event_handler is not None:
276
+ event_handler(
277
+ RuntimeEvent(
278
+ type="turn_finished",
279
+ turn_id=turn_id,
280
+ outcome=outcome,
281
+ )
282
+ )
283
+ return outcome
284
+
285
+ def close(self) -> None:
286
+ self._cleanup("closed")
287
+
288
+ def interrupt(self) -> None:
289
+ """Finalize this session as interrupted and release its resources.
290
+
291
+ This does not asynchronously cancel a currently running Agent turn.
292
+ """
293
+ self._cleanup("interrupted")
294
+
295
+ def _cleanup(self, status: Literal["closed", "interrupted"]) -> None:
296
+ if self._cleaned_up:
297
+ return
298
+ try:
299
+ if status == "closed":
300
+ self.active_project_session.close()
301
+ else:
302
+ self.active_project_session.interrupt()
303
+ finally:
304
+ try:
305
+ self.mcp_manager.close()
306
+ finally:
307
+ self._cleaned_up = True
308
+
309
+
310
+ def start_agent_application_session(
311
+ store: SessionStore,
312
+ project: ProjectIdentity,
313
+ *,
314
+ request: SessionStartRequest,
315
+ mcp_config: MCPConfig | None = None,
316
+ confirmer: Confirmer | None = None,
317
+ external_observer: SubAgentObserver | None = None,
318
+ llm_config: LLMConfig | None = None,
319
+ observability_sink: ObservationSink | None = None,
320
+ ) -> AgentApplicationSession:
321
+ active_session = start_project_session(store, project, request=request)
322
+ mcp_manager: MCPManager | None = None
323
+ try:
324
+ conversation_history = active_session.load_history()
325
+ compact_state = active_session.load_compact_state()
326
+ mcp_manager = MCPManager(
327
+ MCPConfig() if mcp_config is None else mcp_config,
328
+ observability_sink=observability_sink,
329
+ )
330
+ mcp_manager.start()
331
+ session_observer = SessionSubAgentObserver(session=active_session.writer)
332
+ subagent_observer: SubAgentObserver = session_observer
333
+ if external_observer is not None:
334
+ subagent_observer = CompositeSubAgentObserver(
335
+ observers=(session_observer, external_observer),
336
+ )
337
+ runner = build_agent_runner(
338
+ workspace_path=project.workspace_root,
339
+ confirmer=confirmer,
340
+ conversation_history=conversation_history,
341
+ on_message_added=active_session.persist_message,
342
+ compact_state=compact_state,
343
+ on_compact_state_changed=active_session.persist_compact_state,
344
+ artifact_directory=active_session.artifact_directory,
345
+ subagent_observer=subagent_observer,
346
+ llm_config=llm_config,
347
+ llm_session_id=active_session.record.id,
348
+ observability_sink=observability_sink,
349
+ )
350
+ for tool in mcp_manager.tools:
351
+ runner.tool_registry.register(tool)
352
+ return AgentApplicationSession(
353
+ runner=runner,
354
+ active_project_session=active_session,
355
+ mcp_manager=mcp_manager,
356
+ )
357
+ except BaseException:
358
+ if mcp_manager is not None:
359
+ try:
360
+ mcp_manager.close()
361
+ except BaseException:
362
+ pass
363
+ try:
364
+ active_session.interrupt()
365
+ except BaseException:
366
+ pass
367
+ raise
@@ -0,0 +1,59 @@
1
+ """Application-facing runtime events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ from mycode.agent.events import AgentEvent
9
+ from mycode.agent.outcome import AgentRunOutcome
10
+ from mycode.mcp.models import MCPServerStatus
11
+
12
+
13
+ RuntimeEventType = Literal[
14
+ "runtime_ready",
15
+ "mcp_status",
16
+ "agent",
17
+ "turn_finished",
18
+ ]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class RuntimeEvent:
23
+ """Small application boundary wrapper around existing domain events."""
24
+
25
+ type: RuntimeEventType
26
+ turn_id: str | None = None
27
+ agent_event: AgentEvent | None = None
28
+ outcome: AgentRunOutcome | None = None
29
+ mcp_status: MCPServerStatus | None = None
30
+ session_id: str | None = None
31
+ session_title: str | None = None
32
+ session_created: bool | None = None
33
+ compact_state_recovered: bool | None = None
34
+ instruction_sources: tuple[str, ...] = ()
35
+ instruction_warnings: tuple[str, ...] = ()
36
+ skill_warnings: tuple[str, ...] = ()
37
+
38
+ def __post_init__(self) -> None:
39
+ if self.type not in {
40
+ "runtime_ready",
41
+ "mcp_status",
42
+ "agent",
43
+ "turn_finished",
44
+ }:
45
+ raise ValueError(f"Unsupported runtime event type: {self.type}")
46
+ object.__setattr__(self, "instruction_sources", tuple(self.instruction_sources))
47
+ object.__setattr__(self, "instruction_warnings", tuple(self.instruction_warnings))
48
+ object.__setattr__(self, "skill_warnings", tuple(self.skill_warnings))
49
+
50
+ if self.type == "agent" and not isinstance(self.agent_event, AgentEvent):
51
+ raise ValueError("agent runtime events require agent_event")
52
+ if self.type == "mcp_status" and not isinstance(
53
+ self.mcp_status, MCPServerStatus
54
+ ):
55
+ raise ValueError("mcp_status runtime events require mcp_status")
56
+ if self.type == "turn_finished" and not isinstance(
57
+ self.outcome, AgentRunOutcome
58
+ ):
59
+ raise ValueError("turn_finished runtime events require outcome")
@@ -0,0 +1,211 @@
1
+ """Application-layer use cases shared by interactive and automated frontends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from uuid import uuid4
8
+
9
+ from mycode.agent.events import AgentEvent, AgentStopReason
10
+ from mycode.context.artifacts import (
11
+ ReadArtifactTool,
12
+ ToolResultArtifactStore,
13
+ )
14
+ from mycode.config import LLMConfig, load_llm_config
15
+ from mycode.context.budget import ContextBudget
16
+ from mycode.context.compact import CompactState, ConversationCompactor
17
+ from mycode.conversation import Conversation
18
+ from mycode.instructions import load_instruction_bundle
19
+ from mycode.llm import OpenAICompatibleLLMClient
20
+ from mycode.memory import MemoryStore
21
+ from mycode.memory_context import MemoryContextSelector, MemoryRecallPolicy
22
+ from mycode.messages import Message
23
+ from mycode.observability import ObservationSink
24
+ from mycode.permissions import Confirmer
25
+ from mycode.project import ProjectIdentity
26
+ from mycode.prompts import build_agent_system_prompt
27
+ from mycode.agent.outcome import AgentRunOutcome
28
+ from mycode.agent.runner import AgentRunner
29
+ from mycode.skills import ActiveSkillState, SkillRegistry
30
+ from mycode.subagents.delegate import DelegateTaskTool
31
+ from mycode.subagents.delegation import DelegationToolBatchHandler
32
+ from mycode.subagents.observability import SubAgentObserver
33
+ from mycode.subagents.runtime import SubAgentRuntime
34
+ from mycode.tools import (
35
+ LoadSkillTool,
36
+ ReadSkillResourceTool,
37
+ RunSkillScriptTool,
38
+ Workspace,
39
+ create_default_tool_registry,
40
+ )
41
+
42
+
43
+ AgentEventHandler = Callable[[AgentEvent], None]
44
+
45
+
46
+ def build_agent_runner(
47
+ workspace_path: Path | None = None,
48
+ *,
49
+ confirmer: Confirmer | None = None,
50
+ conversation_history: Conversation | None = None,
51
+ on_message_added: Callable[[Message], None] | None = None,
52
+ compact_state: CompactState | None = None,
53
+ on_compact_state_changed: Callable[[CompactState], None] | None = None,
54
+ artifact_directory: Path | None = None,
55
+ memory_store: MemoryStore | None = None,
56
+ subagent_observer: SubAgentObserver | None = None,
57
+ llm_config: LLMConfig | None = None,
58
+ llm_session_id: str | None = None,
59
+ observability_sink: ObservationSink | None = None,
60
+ ) -> AgentRunner:
61
+ """Assemble one Agent runtime without coupling it to a presentation layer."""
62
+ workspace_root = Path.cwd() if workspace_path is None else workspace_path
63
+ workspace = Workspace(workspace_root)
64
+ project = ProjectIdentity.from_workspace(workspace.root)
65
+ effective_memory_store = (
66
+ MemoryStore(project) if memory_store is None else memory_store
67
+ )
68
+ instruction_bundle = load_instruction_bundle(workspace.root)
69
+ skill_registry = SkillRegistry.discover(workspace.root)
70
+ active_skill_state = ActiveSkillState()
71
+ config = (
72
+ load_llm_config(workspace_root=workspace.root)
73
+ if llm_config is None
74
+ else llm_config
75
+ )
76
+ effective_llm_session_id = llm_session_id or uuid4().hex
77
+ client = OpenAICompatibleLLMClient(
78
+ config=config,
79
+ session_id=effective_llm_session_id,
80
+ )
81
+ summary_client = OpenAICompatibleLLMClient(
82
+ config=config,
83
+ model=config.compact_model,
84
+ thinking_enabled=False,
85
+ session_id=effective_llm_session_id,
86
+ )
87
+ context_budget = context_budget_from_config(config)
88
+ memory_recall_policy = MemoryRecallPolicy(
89
+ max_tokens=config.memory_context_tokens,
90
+ )
91
+ subagent_runtime = SubAgentRuntime(
92
+ workspace=workspace,
93
+ llm_client_factory=lambda: OpenAICompatibleLLMClient(
94
+ config=config,
95
+ model=config.subagent_model,
96
+ session_id=effective_llm_session_id,
97
+ ),
98
+ confirmer=confirmer,
99
+ memory_store=effective_memory_store,
100
+ memory_recall_policy=memory_recall_policy,
101
+ context_budget=context_budget,
102
+ observability_sink=observability_sink,
103
+ )
104
+ history_messages = (
105
+ [] if conversation_history is None else conversation_history.get_messages()
106
+ )
107
+ if any(message.role == "system" for message in history_messages):
108
+ raise ValueError("conversation_history must not contain system messages.")
109
+ conversation = Conversation.from_messages(
110
+ [
111
+ Message(
112
+ role="system",
113
+ content=build_agent_system_prompt(
114
+ instruction_bundle.to_prompt_text(),
115
+ memory_enabled=True,
116
+ delegation_enabled=True,
117
+ skill_catalog=skill_registry.get_catalog(),
118
+ ),
119
+ ),
120
+ *history_messages,
121
+ ],
122
+ on_message_added=on_message_added,
123
+ )
124
+ artifact_store = (
125
+ None
126
+ if artifact_directory is None
127
+ else ToolResultArtifactStore(
128
+ root=artifact_directory,
129
+ threshold_chars=context_budget.tool_result_compression_threshold_chars,
130
+ )
131
+ )
132
+ extra_tools = []
133
+ if artifact_store is not None:
134
+ extra_tools.append(ReadArtifactTool(artifact_store.root))
135
+ extra_tools.append(
136
+ DelegateTaskTool(
137
+ subagent_runtime,
138
+ observer=subagent_observer,
139
+ )
140
+ )
141
+ if skill_registry.list_skills():
142
+ extra_tools.extend(
143
+ [
144
+ LoadSkillTool(skill_registry, active_skill_state),
145
+ ReadSkillResourceTool(skill_registry, active_skill_state),
146
+ RunSkillScriptTool(workspace, skill_registry, active_skill_state),
147
+ ]
148
+ )
149
+ tool_registry = create_default_tool_registry(
150
+ workspace,
151
+ confirmer=confirmer,
152
+ memory_store=effective_memory_store,
153
+ extra_tools=extra_tools,
154
+ )
155
+
156
+ return AgentRunner(
157
+ llm_client=client,
158
+ tool_registry=tool_registry,
159
+ conversation=conversation,
160
+ context_budget=context_budget,
161
+ instruction_sources=tuple(
162
+ source.label for source in instruction_bundle.sources
163
+ ),
164
+ instruction_warnings=tuple(
165
+ issue.display for issue in instruction_bundle.issues
166
+ ),
167
+ skill_warnings=tuple(warning.display for warning in skill_registry.warnings),
168
+ active_skill_state=active_skill_state,
169
+ memory_context_selector=MemoryContextSelector(
170
+ effective_memory_store,
171
+ policy=memory_recall_policy,
172
+ ),
173
+ tool_batch_handler=DelegationToolBatchHandler(),
174
+ compactor=ConversationCompactor(
175
+ llm_client=summary_client,
176
+ state=CompactState() if compact_state is None else compact_state,
177
+ on_state_changed=on_compact_state_changed,
178
+ observability_sink=observability_sink,
179
+ observability_scope="compact",
180
+ ),
181
+ tool_result_artifact_store=artifact_store,
182
+ observability_sink=observability_sink,
183
+ observability_scope="main",
184
+ )
185
+
186
+
187
+ def run_agent_turn(
188
+ runner: AgentRunner,
189
+ content: str,
190
+ *,
191
+ event_handler: AgentEventHandler | None = None,
192
+ ) -> AgentRunOutcome:
193
+ """Run one user request, optionally forwarding events to its caller."""
194
+ stop_reasons: list[AgentStopReason] = []
195
+ for event in runner.run(content):
196
+ if event.type == "stop" and event.stop_reason is not None:
197
+ stop_reasons.append(event.stop_reason)
198
+ if event_handler is not None:
199
+ event_handler(event)
200
+
201
+ if len(stop_reasons) != 1:
202
+ return AgentRunOutcome.from_stop_reason(None)
203
+ return AgentRunOutcome.from_stop_reason(stop_reasons[0])
204
+
205
+
206
+ def context_budget_from_config(config: LLMConfig) -> ContextBudget:
207
+ return ContextBudget(
208
+ context_window_tokens=config.context_window_tokens,
209
+ reserved_output_tokens=config.reserved_output_tokens,
210
+ safety_margin_tokens=config.context_safety_margin_tokens,
211
+ )