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,692 @@
1
+ """Versioned JSONL adapter for the application runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import TextIO
11
+ from uuid import uuid4
12
+
13
+ from mycode.agent.events import (
14
+ AgentEvent,
15
+ AgentModelRetry,
16
+ AgentProgressSnapshot,
17
+ AgentToolCall,
18
+ )
19
+ from mycode.application.agent_session import AgentApplicationSession, start_agent_application_session
20
+ from mycode.application.events import RuntimeEvent
21
+ from mycode.application.sessions import SessionStartRequest
22
+ from mycode.config import LLMConfig
23
+ from mycode.mcp import (
24
+ MCPConfig,
25
+ MCPConfigError,
26
+ MCPServerStatus,
27
+ load_mcp_config_layers,
28
+ resolve_project_mcp_trust,
29
+ )
30
+ from mycode.mcp.trust import MCPTrustRequest, MCPTrustServer, MCPTrustWarning
31
+ from mycode.permissions import (
32
+ ConfirmationRequest,
33
+ ConfirmationResult,
34
+ Confirmer,
35
+ )
36
+ from mycode.persistence.session_store import (
37
+ SessionInUseError,
38
+ SessionNotFoundError,
39
+ SessionStore,
40
+ SessionStoreError,
41
+ )
42
+ from mycode.project import ProjectIdentity
43
+ from mycode.tools import ToolResult
44
+ from mycode.tools.workspace import Workspace
45
+
46
+
47
+ JSONL_PROTOCOL_VERSION = 1
48
+ _PERMISSION_DECISIONS = {"once", "task", "session", "reject"}
49
+
50
+
51
+ class JsonlProtocolError(ValueError):
52
+ def __init__(self, code: str, message: str) -> None:
53
+ super().__init__(message)
54
+ self.code = code
55
+
56
+
57
+ @dataclass
58
+ class JsonlChannel:
59
+ input_stream: TextIO
60
+ output_stream: TextIO
61
+ error_stream: TextIO | None = None
62
+
63
+ def emit(self, message: Mapping[str, object]) -> None:
64
+ if not isinstance(message, Mapping):
65
+ raise TypeError("JSONL output must be an object")
66
+ payload = {"version": JSONL_PROTOCOL_VERSION, **dict(message)}
67
+ if payload.get("version") != JSONL_PROTOCOL_VERSION or isinstance(
68
+ payload.get("version"), bool
69
+ ):
70
+ raise ValueError("JSONL output version must be 1")
71
+ if not isinstance(payload.get("type"), str):
72
+ raise ValueError("JSONL output type must be a string")
73
+ encoded = json.dumps(
74
+ payload,
75
+ ensure_ascii=False,
76
+ separators=(",", ":"),
77
+ sort_keys=True,
78
+ allow_nan=False,
79
+ )
80
+ self.output_stream.write(encoded + "\n")
81
+ self.output_stream.flush()
82
+
83
+ def read_message(self) -> dict[str, object] | None:
84
+ line = self.input_stream.readline()
85
+ if line == "":
86
+ return None
87
+ try:
88
+ payload = json.loads(line)
89
+ except json.JSONDecodeError as error:
90
+ raise JsonlProtocolError("invalid_json", "Input is not valid JSON.") from error
91
+ if not isinstance(payload, dict):
92
+ raise JsonlProtocolError("invalid_message", "Input message must be a JSON object.")
93
+ if payload.get("version") != JSONL_PROTOCOL_VERSION or isinstance(
94
+ payload.get("version"), bool
95
+ ):
96
+ raise JsonlProtocolError("unsupported_version", "Unsupported JSONL protocol version.")
97
+ if not isinstance(payload.get("type"), str):
98
+ raise JsonlProtocolError("missing_type", "Input message requires a string type.")
99
+ return payload
100
+
101
+ def diagnostic(self, message: str) -> None:
102
+ if self.error_stream is not None:
103
+ self.error_stream.write(message + "\n")
104
+ self.error_stream.flush()
105
+
106
+
107
+ class JsonlConfirmer(Confirmer):
108
+ def __init__(self, channel: JsonlChannel) -> None:
109
+ self.channel = channel
110
+
111
+ def confirm(self, request: ConfirmationRequest) -> ConfirmationResult:
112
+ request_id = uuid4().hex
113
+ permission_request = request.permission_request
114
+ self.channel.emit(
115
+ {
116
+ "type": "permission_request",
117
+ "request_id": request_id,
118
+ "tool_name": permission_request.tool_name,
119
+ "capability": permission_request.capability,
120
+ "action": permission_request.action,
121
+ "target": permission_request.target,
122
+ "reason": request.permission_decision.reason,
123
+ "prompt": request.prompt,
124
+ "arguments": _json_safe(permission_request.arguments),
125
+ "metadata": _json_safe(request.metadata),
126
+ }
127
+ )
128
+ while True:
129
+ try:
130
+ message = self.channel.read_message()
131
+ except JsonlProtocolError as error:
132
+ self._protocol_error(error.code, str(error))
133
+ continue
134
+ if message is None:
135
+ return ConfirmationResult.rejected(
136
+ message="Permission confirmation unavailable.",
137
+ metadata={"input": "eof"},
138
+ )
139
+ if message.get("type") != "permission_response":
140
+ self._protocol_error(
141
+ "unexpected_message",
142
+ "Expected permission_response.",
143
+ )
144
+ continue
145
+ if message.get("request_id") != request_id:
146
+ self._protocol_error(
147
+ "request_id_mismatch",
148
+ "Permission response request_id does not match.",
149
+ )
150
+ continue
151
+ decision = message.get("decision")
152
+ if not isinstance(decision, str) or decision not in _PERMISSION_DECISIONS:
153
+ self._protocol_error(
154
+ "invalid_permission_decision",
155
+ "Permission decision must be once, task, session, or reject.",
156
+ )
157
+ continue
158
+ if decision == "reject":
159
+ return ConfirmationResult.rejected(
160
+ message="Permission confirmation rejected.",
161
+ metadata={"input": decision},
162
+ )
163
+ return ConfirmationResult.approved(
164
+ scope=decision,
165
+ message="Permission confirmation approved.",
166
+ metadata={"input": decision},
167
+ )
168
+
169
+ def _protocol_error(self, code: str, message: str) -> None:
170
+ self.channel.emit(
171
+ {
172
+ "type": "runtime_error",
173
+ "code": code,
174
+ "message": message,
175
+ }
176
+ )
177
+
178
+
179
+ class JsonlMCPTrustConfirmer:
180
+ def __init__(self, channel: JsonlChannel) -> None:
181
+ self.channel = channel
182
+
183
+ def confirm(self, request: MCPTrustRequest) -> bool:
184
+ request_id = uuid4().hex
185
+ self.channel.emit(
186
+ {
187
+ "type": "mcp_trust_request",
188
+ "request_id": request_id,
189
+ "servers": [_serialize_mcp_trust_server(server) for server in request.servers],
190
+ }
191
+ )
192
+ while True:
193
+ try:
194
+ message = self.channel.read_message()
195
+ except JsonlProtocolError as error:
196
+ self._protocol_error(error.code, str(error))
197
+ continue
198
+ if message is None:
199
+ return False
200
+ if message.get("type") != "mcp_trust_response":
201
+ self._protocol_error(
202
+ "unexpected_message",
203
+ "Expected mcp_trust_response.",
204
+ )
205
+ continue
206
+ if message.get("request_id") != request_id:
207
+ self._protocol_error(
208
+ "request_id_mismatch",
209
+ "MCP trust response request_id does not match.",
210
+ )
211
+ continue
212
+ approved = message.get("approved")
213
+ if not isinstance(approved, bool):
214
+ self._protocol_error(
215
+ "invalid_mcp_trust_response",
216
+ "MCP trust approved must be a boolean.",
217
+ )
218
+ continue
219
+ return approved
220
+
221
+ def report_warning(self, warning: MCPTrustWarning) -> None:
222
+ self.channel.emit(
223
+ {
224
+ "type": "runtime_warning",
225
+ "code": warning.code,
226
+ "message": warning.message,
227
+ }
228
+ )
229
+
230
+ def _protocol_error(self, code: str, message: str) -> None:
231
+ self.channel.emit(
232
+ {
233
+ "type": "runtime_error",
234
+ "code": code,
235
+ "message": message,
236
+ }
237
+ )
238
+
239
+
240
+ def run_jsonl_runtime(
241
+ *,
242
+ workspace_path: Path | None = None,
243
+ session_request: SessionStartRequest | None = None,
244
+ session_store: SessionStore | None = None,
245
+ llm_config: LLMConfig | None = None,
246
+ mcp_config: MCPConfig | None = None,
247
+ input_stream: TextIO | None = None,
248
+ output_stream: TextIO | None = None,
249
+ error_stream: TextIO | None = None,
250
+ ) -> int:
251
+ channel = JsonlChannel(
252
+ input_stream=sys.stdin if input_stream is None else input_stream,
253
+ output_stream=sys.stdout if output_stream is None else output_stream,
254
+ error_stream=sys.stderr if error_stream is None else error_stream,
255
+ )
256
+ workspace = Workspace(Path.cwd() if workspace_path is None else workspace_path)
257
+ project = ProjectIdentity.from_workspace(workspace.root)
258
+ store = SessionStore() if session_store is None else session_store
259
+ request = (
260
+ SessionStartRequest(mode="continue")
261
+ if session_request is None
262
+ else session_request
263
+ )
264
+
265
+ effective_mcp_config = mcp_config
266
+ if effective_mcp_config is None:
267
+ effective_mcp_config = _resolve_machine_mcp_config(channel, workspace, project)
268
+
269
+ confirmer = JsonlConfirmer(channel)
270
+ try:
271
+ application_session = start_agent_application_session(
272
+ store,
273
+ project,
274
+ request=request,
275
+ mcp_config=effective_mcp_config,
276
+ confirmer=confirmer,
277
+ llm_config=llm_config,
278
+ )
279
+ except (SessionNotFoundError, SessionInUseError, SessionStoreError) as error:
280
+ channel.emit(
281
+ {
282
+ "type": "runtime_error",
283
+ "code": "session_start_failed",
284
+ "message": str(error),
285
+ }
286
+ )
287
+ return 1
288
+ except Exception as error: # noqa: BLE001 - adapter startup boundary
289
+ channel.diagnostic(f"runtime startup failed: {type(error).__name__}: {error}")
290
+ channel.emit(
291
+ {
292
+ "type": "runtime_error",
293
+ "code": "startup_failed",
294
+ "message": "Runtime startup failed.",
295
+ }
296
+ )
297
+ return 1
298
+
299
+ try:
300
+ for event in application_session.startup_events():
301
+ channel.emit(serialize_runtime_event(event))
302
+ return _run_message_loop(channel, application_session)
303
+ except KeyboardInterrupt:
304
+ _interrupt_application_session(channel, application_session)
305
+ return 130
306
+ except Exception as error: # noqa: BLE001 - runtime boundary
307
+ channel.diagnostic(f"runtime failed: {type(error).__name__}: {error}")
308
+ _interrupt_application_session(channel, application_session)
309
+ return 1
310
+
311
+
312
+ def _resolve_machine_mcp_config(
313
+ channel: JsonlChannel,
314
+ workspace: Workspace,
315
+ project: ProjectIdentity,
316
+ ) -> MCPConfig:
317
+ try:
318
+ loaded = load_mcp_config_layers(workspace_root=workspace.root)
319
+ except MCPConfigError as error:
320
+ channel.emit(
321
+ {
322
+ "type": "runtime_error",
323
+ "code": "mcp_config_error",
324
+ "message": str(error),
325
+ }
326
+ )
327
+ return MCPConfig()
328
+
329
+ try:
330
+ return resolve_project_mcp_trust(
331
+ loaded,
332
+ project,
333
+ confirmer=JsonlMCPTrustConfirmer(channel),
334
+ ).config
335
+ except Exception as error: # noqa: BLE001 - trust startup boundary
336
+ channel.diagnostic(f"mcp trust resolution failed: {type(error).__name__}: {error}")
337
+ channel.emit(
338
+ {
339
+ "type": "runtime_error",
340
+ "code": "mcp_trust_failed",
341
+ "message": "MCP trust resolution failed.",
342
+ }
343
+ )
344
+ return MCPConfig()
345
+
346
+
347
+ def _run_message_loop(
348
+ channel: JsonlChannel,
349
+ application_session: AgentApplicationSession,
350
+ ) -> int:
351
+ while True:
352
+ try:
353
+ message = channel.read_message()
354
+ except JsonlProtocolError as error:
355
+ channel.emit(
356
+ {
357
+ "type": "runtime_error",
358
+ "code": error.code,
359
+ "message": str(error),
360
+ }
361
+ )
362
+ continue
363
+ if message is None:
364
+ if not _close_application_session(channel, application_session):
365
+ return 1
366
+ channel.emit({"type": "runtime_closed"})
367
+ return 0
368
+
369
+ message_type = message.get("type")
370
+ if message_type == "close":
371
+ if not _close_application_session(channel, application_session):
372
+ return 1
373
+ channel.emit({"type": "runtime_closed"})
374
+ return 0
375
+ if message_type == "turn":
376
+ if not _run_turn_message(channel, application_session, message):
377
+ return 1
378
+ continue
379
+ if message_type == "context_status":
380
+ _run_context_status_message(channel, application_session)
381
+ continue
382
+ if message_type == "compact":
383
+ _run_compact_message(channel, application_session)
384
+ continue
385
+ channel.emit(
386
+ {
387
+ "type": "runtime_error",
388
+ "code": "unexpected_message",
389
+ "message": "Expected turn, context_status, compact, or close.",
390
+ }
391
+ )
392
+
393
+
394
+ def _run_turn_message(
395
+ channel: JsonlChannel,
396
+ application_session: AgentApplicationSession,
397
+ message: dict[str, object],
398
+ ) -> bool:
399
+ turn_id = message.get("turn_id")
400
+ content = message.get("content")
401
+ if not isinstance(turn_id, str) or not turn_id.strip():
402
+ channel.emit(
403
+ {
404
+ "type": "runtime_error",
405
+ "code": "missing_turn_id",
406
+ "message": "turn requires a non-empty turn_id.",
407
+ }
408
+ )
409
+ return True
410
+ if not isinstance(content, str) or not content.strip():
411
+ channel.emit(
412
+ {
413
+ "type": "runtime_error",
414
+ "code": "invalid_turn_content",
415
+ "message": "turn content must be a string.",
416
+ }
417
+ )
418
+ return True
419
+
420
+ try:
421
+ application_session.run_turn(
422
+ content,
423
+ turn_id=turn_id,
424
+ event_handler=lambda event: channel.emit(serialize_runtime_event(event)),
425
+ )
426
+ except Exception as error: # noqa: BLE001 - turn boundary
427
+ channel.diagnostic(f"turn failed: {type(error).__name__}: {error}")
428
+ channel.emit(
429
+ {
430
+ "type": "runtime_error",
431
+ "code": "turn_failed",
432
+ "turn_id": turn_id,
433
+ "message": "Turn failed.",
434
+ }
435
+ )
436
+ _interrupt_application_session(channel, application_session)
437
+ return False
438
+ return True
439
+
440
+
441
+ def _run_context_status_message(
442
+ channel: JsonlChannel,
443
+ application_session: AgentApplicationSession,
444
+ ) -> None:
445
+ try:
446
+ status = application_session.get_context_status()
447
+ except Exception as error: # noqa: BLE001 - control boundary
448
+ channel.diagnostic(
449
+ f"context status failed: {type(error).__name__}: {error}"
450
+ )
451
+ channel.emit(
452
+ {
453
+ "type": "runtime_error",
454
+ "code": "context_status_failed",
455
+ "message": "Context status inspection failed.",
456
+ }
457
+ )
458
+ return
459
+ channel.emit({"type": "context_status", **status.to_dict()})
460
+
461
+
462
+ def _run_compact_message(
463
+ channel: JsonlChannel,
464
+ application_session: AgentApplicationSession,
465
+ ) -> None:
466
+ try:
467
+ result = application_session.compact_context()
468
+ except Exception as error: # noqa: BLE001 - control boundary
469
+ channel.diagnostic(f"compact failed: {type(error).__name__}: {error}")
470
+ channel.emit(
471
+ {
472
+ "type": "runtime_error",
473
+ "code": "compact_failed",
474
+ "message": "Context Compact failed.",
475
+ }
476
+ )
477
+ return
478
+ channel.emit({"type": "compact_result", **result.to_dict()})
479
+
480
+
481
+ def _close_application_session(
482
+ channel: JsonlChannel,
483
+ application_session: AgentApplicationSession,
484
+ ) -> bool:
485
+ try:
486
+ application_session.close()
487
+ except BaseException as error: # noqa: BLE001 - lifecycle boundary
488
+ channel.diagnostic(
489
+ f"runtime close failed: {type(error).__name__}: {error}"
490
+ )
491
+ _emit_runtime_error_safely(
492
+ channel,
493
+ code="lifecycle_failed",
494
+ message="Runtime cleanup failed.",
495
+ )
496
+ _interrupt_application_session(channel, application_session, emit_error=False)
497
+ return False
498
+ return True
499
+
500
+
501
+ def _interrupt_application_session(
502
+ channel: JsonlChannel,
503
+ application_session: AgentApplicationSession,
504
+ *,
505
+ emit_error: bool = True,
506
+ ) -> None:
507
+ try:
508
+ application_session.interrupt()
509
+ except BaseException as error: # noqa: BLE001 - lifecycle boundary
510
+ channel.diagnostic(
511
+ f"runtime cleanup failed: {type(error).__name__}: {error}"
512
+ )
513
+ if emit_error:
514
+ _emit_runtime_error_safely(
515
+ channel,
516
+ code="lifecycle_failed",
517
+ message="Runtime cleanup failed.",
518
+ )
519
+
520
+
521
+ def _emit_runtime_error_safely(
522
+ channel: JsonlChannel,
523
+ *,
524
+ code: str,
525
+ message: str,
526
+ ) -> None:
527
+ try:
528
+ channel.emit(
529
+ {
530
+ "type": "runtime_error",
531
+ "code": code,
532
+ "message": message,
533
+ }
534
+ )
535
+ except BaseException as error: # noqa: BLE001 - error reporting boundary
536
+ channel.diagnostic(
537
+ f"runtime error reporting failed: {type(error).__name__}: {error}"
538
+ )
539
+
540
+
541
+ def serialize_runtime_event(event: RuntimeEvent) -> dict[str, object]:
542
+ if event.type == "runtime_ready":
543
+ return {
544
+ "type": "runtime_ready",
545
+ "session_id": event.session_id,
546
+ "session_title": event.session_title,
547
+ "session_created": event.session_created,
548
+ "compact_state_recovered": event.compact_state_recovered,
549
+ "instruction_sources": list(event.instruction_sources),
550
+ "instruction_warnings": list(event.instruction_warnings),
551
+ "skill_warnings": list(event.skill_warnings),
552
+ }
553
+ if event.type == "mcp_status":
554
+ if event.mcp_status is None:
555
+ raise ValueError("mcp_status event has no status")
556
+ return {"type": "mcp_status", **serialize_mcp_status(event.mcp_status)}
557
+ if event.type == "agent":
558
+ if event.agent_event is None:
559
+ raise ValueError("agent event has no AgentEvent")
560
+ return {
561
+ "type": "agent_event",
562
+ "turn_id": event.turn_id,
563
+ "event": serialize_agent_event(event.agent_event),
564
+ }
565
+ if event.type == "turn_finished":
566
+ if event.outcome is None:
567
+ raise ValueError("turn_finished event has no outcome")
568
+ return {
569
+ "type": "turn_finished",
570
+ "turn_id": event.turn_id,
571
+ "status": event.outcome.status,
572
+ "stop_reason": event.outcome.stop_reason,
573
+ }
574
+ raise ValueError(f"Unsupported runtime event type: {event.type}")
575
+
576
+
577
+ def serialize_agent_event(event: AgentEvent) -> dict[str, object]:
578
+ payload: dict[str, object] = {"type": event.type}
579
+ if event.content:
580
+ payload["content"] = event.content
581
+ if event.turn_number is not None:
582
+ payload["turn_number"] = event.turn_number
583
+ if event.max_turns is not None:
584
+ payload["max_turns"] = event.max_turns
585
+ if event.progress is not None:
586
+ payload["progress"] = _serialize_progress(event.progress)
587
+ if event.model_retry is not None:
588
+ payload["model_retry"] = _serialize_model_retry(event.model_retry)
589
+ if event.tool_call is not None:
590
+ payload["tool_call"] = _serialize_tool_call(event.tool_call)
591
+ if event.tool_result is not None:
592
+ payload["tool_result"] = _serialize_tool_result(event.tool_result)
593
+ if event.stop_reason is not None:
594
+ payload["stop_reason"] = event.stop_reason
595
+ if event.error is not None:
596
+ payload["error"] = event.error
597
+ if event.type == "reasoning_state":
598
+ payload["reasoning_state"] = event.reasoning_state
599
+ return payload
600
+
601
+
602
+ def serialize_mcp_status(status: MCPServerStatus) -> dict[str, object]:
603
+ return {
604
+ "alias": status.alias,
605
+ "status": status.status,
606
+ "tool_count": status.tool_count,
607
+ "error_type": status.error_type,
608
+ "error_summary": status.error_summary,
609
+ }
610
+
611
+
612
+ def _serialize_progress(progress: AgentProgressSnapshot) -> dict[str, object]:
613
+ return {
614
+ "stagnation_turns": progress.stagnation_turns,
615
+ "same_tool_repeat": progress.same_tool_repeat,
616
+ "same_result_repeat": progress.same_result_repeat,
617
+ "resource_repeat": progress.resource_repeat,
618
+ "convergence_guided": progress.convergence_guided,
619
+ "reason": progress.reason,
620
+ }
621
+
622
+
623
+ def _serialize_model_retry(retry: AgentModelRetry) -> dict[str, object]:
624
+ return {
625
+ "attempt": retry.attempt,
626
+ "max_retries": retry.max_retries,
627
+ "delay_seconds": retry.delay_seconds,
628
+ "error_type": retry.error_type,
629
+ "error_code": retry.error_code,
630
+ "retryable": retry.retryable,
631
+ "call_kind": retry.call_kind,
632
+ "stream_started": retry.stream_started,
633
+ "partial_output_chars": retry.partial_output_chars,
634
+ }
635
+
636
+
637
+ def _serialize_tool_call(tool_call: AgentToolCall) -> dict[str, object]:
638
+ return {
639
+ "id": tool_call.id,
640
+ "name": tool_call.name,
641
+ "arguments": _json_safe(tool_call.arguments),
642
+ }
643
+
644
+
645
+ def _serialize_tool_result(result: ToolResult) -> dict[str, object]:
646
+ return {
647
+ "ok": result.ok,
648
+ "content": result.content,
649
+ "error": result.error,
650
+ "metadata": _json_safe(result.metadata),
651
+ }
652
+
653
+
654
+ def _serialize_mcp_trust_server(server: MCPTrustServer) -> dict[str, object]:
655
+ payload: dict[str, object] = {
656
+ "alias": server.alias,
657
+ "transport": server.transport,
658
+ }
659
+ if server.transport == "stdio":
660
+ payload.update(
661
+ {
662
+ "command": server.command,
663
+ "args": list(server.args),
664
+ "env_keys": list(server.env_keys),
665
+ }
666
+ )
667
+ else:
668
+ payload.update(
669
+ {
670
+ "url_template": server.url_template,
671
+ "destination": server.destination,
672
+ "header_keys": list(server.header_keys),
673
+ }
674
+ )
675
+ return payload
676
+
677
+
678
+ def _json_safe(value: object) -> object:
679
+ if value is None or isinstance(value, (bool, int, float, str)):
680
+ return value
681
+ if isinstance(value, Path):
682
+ return str(value)
683
+ if isinstance(value, Mapping):
684
+ result: dict[str, object] = {}
685
+ for key, item in value.items():
686
+ if not isinstance(key, str):
687
+ raise TypeError("JSON object keys must be strings")
688
+ result[key] = _json_safe(item)
689
+ return result
690
+ if isinstance(value, (list, tuple)):
691
+ return [_json_safe(item) for item in value]
692
+ raise TypeError(f"Unsupported JSON value type: {type(value).__name__}")