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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/adapters/jsonl.py
ADDED
|
@@ -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__}")
|