@team-agent/installer 0.1.0

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 (36) hide show
  1. package/README.md +201 -0
  2. package/crates/team-agent-core/Cargo.toml +12 -0
  3. package/crates/team-agent-core/src/lib.rs +287 -0
  4. package/crates/team-agent-core/src/main.rs +152 -0
  5. package/examples/team.spec.yaml +206 -0
  6. package/examples/team_state.md +35 -0
  7. package/npm/install.mjs +266 -0
  8. package/package.json +28 -0
  9. package/pyproject.toml +18 -0
  10. package/schemas/result-envelope.schema.json +76 -0
  11. package/schemas/team.schema.json +241 -0
  12. package/scripts/install.py +88 -0
  13. package/scripts/run_regression_tests.py +79 -0
  14. package/skills/team-agent/SKILL.md +173 -0
  15. package/src/team_agent/__init__.py +3 -0
  16. package/src/team_agent/__main__.py +5 -0
  17. package/src/team_agent/cli.py +857 -0
  18. package/src/team_agent/compiler.py +269 -0
  19. package/src/team_agent/coordinator.py +62 -0
  20. package/src/team_agent/errors.py +10 -0
  21. package/src/team_agent/events.py +37 -0
  22. package/src/team_agent/fake_worker.py +80 -0
  23. package/src/team_agent/mcp_server.py +579 -0
  24. package/src/team_agent/message_store.py +497 -0
  25. package/src/team_agent/paths.py +45 -0
  26. package/src/team_agent/permissions.py +123 -0
  27. package/src/team_agent/profiles.py +882 -0
  28. package/src/team_agent/providers.py +1045 -0
  29. package/src/team_agent/routing.py +84 -0
  30. package/src/team_agent/runtime.py +5213 -0
  31. package/src/team_agent/rust_core.py +156 -0
  32. package/src/team_agent/simple_yaml.py +236 -0
  33. package/src/team_agent/spec.py +308 -0
  34. package/src/team_agent/state.py +112 -0
  35. package/src/team_agent/task_graph.py +80 -0
  36. package/templates/team_state.md +32 -0
@@ -0,0 +1,579 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from team_agent import runtime
11
+ from team_agent.events import EventLog
12
+ from team_agent.message_store import MessageStore
13
+ from team_agent.state import load_runtime_state, save_runtime_state, write_team_state
14
+
15
+
16
+ class TeamOrchestratorTools:
17
+ def __init__(self, workspace: Path):
18
+ self.workspace = workspace.resolve()
19
+ self.agent_id = _text(os.environ.get("TEAM_AGENT_ID"))
20
+
21
+ def assign_task(self, task: dict[str, Any], message: str | None = None) -> dict[str, Any]:
22
+ state = load_runtime_state(self.workspace)
23
+ tasks = state.setdefault("tasks", [])
24
+ existing = next((item for item in tasks if item.get("id") == task.get("id")), None)
25
+ if existing:
26
+ existing.update(task)
27
+ else:
28
+ tasks.append(task)
29
+ save_runtime_state(self.workspace, state)
30
+ content = message or task.get("description") or task.get("title") or json.dumps(task)
31
+ return _compact_tool_result(runtime.send_message(self.workspace, task.get("assignee"), content, task_id=task["id"]))
32
+
33
+ def send_message(
34
+ self,
35
+ to: str,
36
+ content: str,
37
+ task_id: str | None = None,
38
+ sender: str | None = None,
39
+ requires_ack: bool | None = None,
40
+ ) -> dict[str, Any]:
41
+ effective_sender = sender or self._infer_agent_id(task_id=task_id, target=to) or "unknown"
42
+ effective_requires_ack = requires_ack if requires_ack is not None else to not in {"leader", "Leader"}
43
+ return _compact_tool_result(
44
+ runtime.send_message(
45
+ self.workspace,
46
+ to,
47
+ content,
48
+ task_id=task_id,
49
+ sender=effective_sender,
50
+ requires_ack=effective_requires_ack,
51
+ )
52
+ )
53
+
54
+ def report_result(
55
+ self,
56
+ envelope: dict[str, Any] | None = None,
57
+ summary: str | None = None,
58
+ status: str = "success",
59
+ changes: list[dict[str, Any]] | None = None,
60
+ tests: list[dict[str, Any]] | None = None,
61
+ risks: list[dict[str, Any]] | None = None,
62
+ artifacts: list[dict[str, Any]] | None = None,
63
+ next_actions: list[dict[str, Any]] | None = None,
64
+ task_id: str | None = None,
65
+ agent_id: str | None = None,
66
+ ) -> dict[str, Any]:
67
+ env = dict(envelope or {})
68
+ effective_task = self._infer_task_id(
69
+ agent_id or _text(env.get("agent_id")) or self.agent_id,
70
+ task_id or _text(env.get("task_id")),
71
+ )
72
+ effective_agent = agent_id or _text(env.get("agent_id")) or self._infer_agent_id(task_id=effective_task) or "unknown"
73
+ env.setdefault("schema_version", "result_envelope_v1")
74
+ env.setdefault("agent_id", effective_agent)
75
+ env.setdefault("task_id", effective_task)
76
+ env.setdefault("status", status)
77
+ env.setdefault("summary", summary or env.get("summary") or "completed")
78
+ env.setdefault("changes", changes if changes is not None else [])
79
+ env.setdefault("tests", tests if tests is not None else [])
80
+ env.setdefault("risks", risks if risks is not None else [])
81
+ env.setdefault("artifacts", artifacts if artifacts is not None else [])
82
+ env.setdefault("next_actions", next_actions if next_actions is not None else [])
83
+ env = _normalize_report_envelope(env)
84
+ return _compact_tool_result(runtime.report_result(self.workspace, env))
85
+
86
+ def _infer_agent_id(self, provided: str | None = None, task_id: str | None = None, target: str | None = None) -> str | None:
87
+ if _text(provided):
88
+ return _text(provided)
89
+ if self.agent_id:
90
+ return self.agent_id
91
+ state = load_runtime_state(self.workspace)
92
+ leader_id = state.get("leader", {}).get("id") or "leader"
93
+ runtime_agents = {str(agent_id) for agent_id in state.get("agents", {})}
94
+ task = self._task_for_id(state, task_id)
95
+ if task and task.get("assignee") in runtime_agents:
96
+ return str(task["assignee"])
97
+ messages = MessageStore(self.workspace).messages()
98
+ if task_id:
99
+ for row in reversed(messages):
100
+ if row.get("task_id") != task_id:
101
+ continue
102
+ for key in ("recipient", "sender"):
103
+ candidate = row.get(key)
104
+ if candidate in runtime_agents and candidate not in {leader_id, "leader", "Leader"}:
105
+ return str(candidate)
106
+ active_assignees = {
107
+ str(task_item.get("assignee"))
108
+ for task_item in state.get("tasks", [])
109
+ if task_item.get("assignee") in runtime_agents and task_item.get("status") not in {"done", "failed"}
110
+ }
111
+ if len(active_assignees) == 1:
112
+ return next(iter(active_assignees))
113
+ if len(runtime_agents) == 1:
114
+ return next(iter(runtime_agents))
115
+ for row in reversed(messages):
116
+ for key in ("recipient", "sender"):
117
+ candidate = row.get(key)
118
+ if candidate in runtime_agents and candidate not in {leader_id, "leader", "Leader"}:
119
+ return str(candidate)
120
+ EventLog(self.workspace).write(
121
+ "mcp.identity_inference_failed",
122
+ target=target,
123
+ task_id=task_id,
124
+ runtime_agents=sorted(runtime_agents),
125
+ fallback="unknown",
126
+ )
127
+ return None
128
+
129
+ def _infer_task_id(self, agent_id: str | None, provided: str | None = None) -> str:
130
+ if provided:
131
+ return provided
132
+ state = load_runtime_state(self.workspace)
133
+ for task in reversed(state.get("tasks", [])):
134
+ if agent_id and task.get("assignee") == agent_id and task.get("status") not in {"done", "failed"}:
135
+ return str(task["id"])
136
+ active_tasks = [
137
+ task
138
+ for task in state.get("tasks", [])
139
+ if task.get("assignee") and task.get("status") not in {"done", "failed"}
140
+ ]
141
+ if len(active_tasks) == 1:
142
+ return str(active_tasks[0]["id"])
143
+ messages = MessageStore(self.workspace).messages()
144
+ for row in reversed(messages):
145
+ if agent_id and row.get("recipient") == agent_id and row.get("task_id"):
146
+ return str(row["task_id"])
147
+ for row in reversed(messages):
148
+ if agent_id and row.get("recipient") == agent_id:
149
+ return str(row["message_id"])
150
+ for row in reversed(messages):
151
+ if row.get("task_id"):
152
+ return str(row["task_id"])
153
+ EventLog(self.workspace).write("mcp.task_inference_failed", agent_id=agent_id, fallback="manual")
154
+ return "manual"
155
+
156
+ def _task_for_id(self, state: dict[str, Any], task_id: str | None) -> dict[str, Any] | None:
157
+ if not task_id:
158
+ return None
159
+ return next((task for task in state.get("tasks", []) if task.get("id") == task_id), None)
160
+
161
+ def update_state(self, note: str) -> dict[str, Any]:
162
+ state = load_runtime_state(self.workspace)
163
+ spec_path = Path(state.get("spec_path", self.workspace / "team.spec.yaml"))
164
+ from team_agent.spec import load_spec
165
+
166
+ spec = load_spec(spec_path)
167
+ state.setdefault("notes", []).append(note)
168
+ save_runtime_state(self.workspace, state)
169
+ path = write_team_state(self.workspace, spec, state)
170
+ return {"ok": True, "state_file": str(path)}
171
+
172
+ def get_team_status(self) -> dict[str, Any]:
173
+ return runtime.status(self.workspace, as_json=True)
174
+
175
+ def request_human(self, question: str, task_id: str | None = None, agent_id: str | None = None) -> dict[str, Any]:
176
+ store = MessageStore(self.workspace)
177
+ sender = agent_id or self._infer_agent_id(task_id=task_id, target="leader") or "unknown"
178
+ message_id = store.create_message(task_id, sender, "leader", question, requires_ack=True)
179
+ return {"ok": True, "message_id": message_id, "status": "needs_human"}
180
+
181
+
182
+ TOOLS = [
183
+ {
184
+ "name": "assign_task",
185
+ "description": "Add or update a task in the team graph and deliver it to its assignee.",
186
+ "inputSchema": {
187
+ "type": "object",
188
+ "required": ["task"],
189
+ "properties": {
190
+ "task": {"type": "object"},
191
+ "message": {"type": "string"},
192
+ },
193
+ },
194
+ },
195
+ {
196
+ "name": "send_message",
197
+ "description": "Send a message to a teammate, the leader, or '*' for all other team members. Provide only target and content; Team Agent fills sender, task id, ack policy, and delivery metadata.",
198
+ "inputSchema": {
199
+ "type": "object",
200
+ "required": ["to", "content"],
201
+ "properties": {
202
+ "to": {"type": "string"},
203
+ "content": {"type": "string"},
204
+ },
205
+ "additionalProperties": False,
206
+ },
207
+ },
208
+ {
209
+ "name": "report_result",
210
+ "description": "Report task completion. Provide a short summary and optional status/details; Team Agent fills schema_version, task_id, and agent_id, and normalizes common change/test field aliases.",
211
+ "inputSchema": {
212
+ "type": "object",
213
+ "required": ["summary"],
214
+ "properties": {
215
+ "summary": {"type": "string"},
216
+ "status": {"type": "string", "enum": ["success", "blocked", "failed", "partial"]},
217
+ "changes": {"type": "array", "items": {"type": "object"}},
218
+ "tests": {"type": "array", "items": {"type": "object"}},
219
+ "risks": {"type": "array", "items": {"type": "object"}},
220
+ "artifacts": {"type": "array", "items": {"type": "object"}},
221
+ "next_actions": {"type": "array", "items": {"type": "object"}},
222
+ },
223
+ "additionalProperties": False,
224
+ },
225
+ },
226
+ {
227
+ "name": "update_state",
228
+ "description": "Append a note to team state and rewrite team_state.md.",
229
+ "inputSchema": {
230
+ "type": "object",
231
+ "required": ["note"],
232
+ "properties": {"note": {"type": "string"}},
233
+ },
234
+ },
235
+ {
236
+ "name": "get_team_status",
237
+ "description": "Return machine-readable team status.",
238
+ "inputSchema": {"type": "object", "properties": {}},
239
+ },
240
+ {
241
+ "name": "request_human",
242
+ "description": "Ask the leader/user for human input.",
243
+ "inputSchema": {
244
+ "type": "object",
245
+ "required": ["question"],
246
+ "properties": {
247
+ "question": {"type": "string"},
248
+ "task_id": {"type": "string"},
249
+ "agent_id": {"type": "string"},
250
+ },
251
+ },
252
+ },
253
+ ]
254
+
255
+
256
+ def dispatch(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[str, Any]:
257
+ tool = request.get("tool") or request.get("method")
258
+ args = request.get("arguments") or request.get("params") or {}
259
+ if tool == "assign_task":
260
+ return tools.assign_task(**args)
261
+ if tool == "send_message":
262
+ return tools.send_message(**args)
263
+ if tool == "report_result":
264
+ return tools.report_result(**args)
265
+ if tool == "update_state":
266
+ return tools.update_state(**args)
267
+ if tool == "get_team_status":
268
+ return tools.get_team_status()
269
+ if tool == "request_human":
270
+ return tools.request_human(**args)
271
+ return {"ok": False, "error": f"unknown tool {tool!r}"}
272
+
273
+
274
+ def _compact_tool_result(result: dict[str, Any]) -> dict[str, Any]:
275
+ if result.get("ok") is False:
276
+ keys = ["ok", "status", "reason", "error", "message_id", "to", "targets", "delivered_count", "failed_count", "fallback_path", "suggestion"]
277
+ return {key: result[key] for key in keys if key in result}
278
+ keys = [
279
+ "ok",
280
+ "status",
281
+ "message_id",
282
+ "to",
283
+ "targets",
284
+ "delivered_count",
285
+ "failed_count",
286
+ "submitted",
287
+ "visible",
288
+ "result_id",
289
+ "task_id",
290
+ "agent_id",
291
+ "leader_notified",
292
+ "notification_message_id",
293
+ "notification_status",
294
+ "notification_channel",
295
+ "notification_event_id",
296
+ ]
297
+ compact = {key: result[key] for key in keys if key in result}
298
+ if "acknowledged_messages" in result:
299
+ compact["acknowledged_count"] = len(result.get("acknowledged_messages") or [])
300
+ return compact or {"ok": True}
301
+
302
+
303
+ def _normalize_report_envelope(env: dict[str, Any]) -> dict[str, Any]:
304
+ summary = _text(env.get("summary")) or "completed"
305
+ return {
306
+ "schema_version": "result_envelope_v1",
307
+ "task_id": _text(env.get("task_id")) or "manual",
308
+ "agent_id": _text(env.get("agent_id")) or "unknown",
309
+ "status": _normalize_result_status(env.get("status")),
310
+ "summary": summary,
311
+ "changes": _normalize_changes(env.get("changes"), summary),
312
+ "tests": _normalize_tests(env.get("tests")),
313
+ "risks": _normalize_risks(env.get("risks")),
314
+ "artifacts": _normalize_artifacts(env.get("artifacts")),
315
+ "next_actions": _normalize_next_actions(env.get("next_actions")),
316
+ }
317
+
318
+
319
+ def _items(value: Any) -> list[Any]:
320
+ if value is None:
321
+ return []
322
+ if isinstance(value, list):
323
+ return value
324
+ return [value]
325
+
326
+
327
+ def _text(value: Any) -> str | None:
328
+ if value is None:
329
+ return None
330
+ text = str(value).strip()
331
+ return text or None
332
+
333
+
334
+ def _first_text(item: dict[str, Any], *keys: str) -> str | None:
335
+ for key in keys:
336
+ text = _text(item.get(key))
337
+ if text:
338
+ return text
339
+ return None
340
+
341
+
342
+ def _normalize_result_status(value: Any) -> str:
343
+ text = (_text(value) or "success").lower().replace("-", "_").replace(" ", "_")
344
+ mapping = {
345
+ "ok": "success",
346
+ "done": "success",
347
+ "complete": "success",
348
+ "completed": "success",
349
+ "passed": "success",
350
+ "pass": "success",
351
+ "blocked": "blocked",
352
+ "block": "blocked",
353
+ "failed": "failed",
354
+ "fail": "failed",
355
+ "error": "failed",
356
+ "partial": "partial",
357
+ "partially_done": "partial",
358
+ }
359
+ return mapping.get(text, text if text in {"success", "blocked", "failed", "partial"} else "success")
360
+
361
+
362
+ def _normalize_changes(value: Any, fallback_summary: str) -> list[dict[str, str]]:
363
+ changes: list[dict[str, str]] = []
364
+ for item in _items(value):
365
+ if not isinstance(item, dict):
366
+ continue
367
+ path = _first_text(item, "path", "file", "filepath", "filename")
368
+ if not path:
369
+ continue
370
+ description = _first_text(item, "description", "summary", "detail", "details", "message") or fallback_summary
371
+ changes.append(
372
+ {
373
+ "path": path,
374
+ "kind": _normalize_change_kind(_first_text(item, "kind", "type", "action"), description),
375
+ "description": description,
376
+ }
377
+ )
378
+ return changes
379
+
380
+
381
+ def _normalize_change_kind(value: str | None, description: str) -> str:
382
+ text = (value or "").strip().lower().replace("-", "_").replace(" ", "_")
383
+ if text in {"created", "modified", "deleted", "observed"}:
384
+ return text
385
+ mapping = {
386
+ "create": "created",
387
+ "added": "created",
388
+ "add": "created",
389
+ "new": "created",
390
+ "changed": "modified",
391
+ "change": "modified",
392
+ "updated": "modified",
393
+ "update": "modified",
394
+ "edited": "modified",
395
+ "edit": "modified",
396
+ "removed": "deleted",
397
+ "remove": "deleted",
398
+ "delete": "deleted",
399
+ "observe": "observed",
400
+ "observed": "observed",
401
+ "inspected": "observed",
402
+ "inspect": "observed",
403
+ }
404
+ if text in mapping:
405
+ return mapping[text]
406
+ description_text = description.lower()
407
+ if any(word in description_text for word in ["created", "added", "new file"]):
408
+ return "created"
409
+ if any(word in description_text for word in ["deleted", "removed"]):
410
+ return "deleted"
411
+ if any(word in description_text for word in ["observed", "inspected", "verified"]):
412
+ return "observed"
413
+ return "modified"
414
+
415
+
416
+ def _normalize_tests(value: Any) -> list[dict[str, str]]:
417
+ tests: list[dict[str, str]] = []
418
+ for item in _items(value):
419
+ if not isinstance(item, dict):
420
+ command = _text(item)
421
+ if command:
422
+ tests.append({"command": command, "status": "not_run"})
423
+ continue
424
+ command = _first_text(item, "command", "cmd", "name", "test")
425
+ if not command:
426
+ continue
427
+ test = {"command": command, "status": _normalize_test_status(item.get("status"))}
428
+ detail = _first_text(item, "detail", "output", "stdout", "stderr", "summary", "message")
429
+ if detail:
430
+ test["detail"] = detail
431
+ tests.append(test)
432
+ return tests
433
+
434
+
435
+ def _normalize_test_status(value: Any) -> str:
436
+ text = (_text(value) or "not_run").lower().replace("-", "_").replace(" ", "_")
437
+ mapping = {
438
+ "pass": "passed",
439
+ "ok": "passed",
440
+ "success": "passed",
441
+ "fail": "failed",
442
+ "error": "failed",
443
+ "notrun": "not_run",
444
+ "not_run": "not_run",
445
+ "notrun_yet": "not_run",
446
+ "skip": "skipped",
447
+ }
448
+ return mapping.get(text, text if text in {"passed", "failed", "not_run", "skipped"} else "not_run")
449
+
450
+
451
+ def _normalize_risks(value: Any) -> list[dict[str, str]]:
452
+ risks: list[dict[str, str]] = []
453
+ for item in _items(value):
454
+ if not isinstance(item, dict):
455
+ description = _text(item)
456
+ if description:
457
+ risks.append({"severity": "low", "description": description})
458
+ continue
459
+ description = _first_text(item, "description", "summary", "detail", "message")
460
+ if not description:
461
+ continue
462
+ severity = (_first_text(item, "severity", "level") or "low").lower()
463
+ if severity not in {"low", "medium", "high"}:
464
+ severity = "low"
465
+ risks.append({"severity": severity, "description": description})
466
+ return risks
467
+
468
+
469
+ def _normalize_artifacts(value: Any) -> list[dict[str, str]]:
470
+ artifacts: list[dict[str, str]] = []
471
+ for item in _items(value):
472
+ if not isinstance(item, dict):
473
+ path = _text(item)
474
+ if path:
475
+ artifacts.append({"path": path, "description": path})
476
+ continue
477
+ path = _first_text(item, "path", "file", "filepath", "filename")
478
+ if not path:
479
+ continue
480
+ artifacts.append({"path": path, "description": _first_text(item, "description", "summary", "detail") or path})
481
+ return artifacts
482
+
483
+
484
+ def _normalize_next_actions(value: Any) -> list[dict[str, str]]:
485
+ actions: list[dict[str, str]] = []
486
+ for item in _items(value):
487
+ if isinstance(item, dict):
488
+ description = _first_text(item, "description", "summary", "action", "todo", "message")
489
+ else:
490
+ description = _text(item)
491
+ if description:
492
+ actions.append({"description": description})
493
+ return actions
494
+
495
+
496
+ def handle_mcp(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[str, Any] | None:
497
+ method = request.get("method")
498
+ msg_id = request.get("id")
499
+ if method and method.startswith("notifications/"):
500
+ return None
501
+ if method == "initialize":
502
+ return {
503
+ "jsonrpc": "2.0",
504
+ "id": msg_id,
505
+ "result": {
506
+ "protocolVersion": request.get("params", {}).get("protocolVersion", "2024-11-05"),
507
+ "capabilities": {"tools": {}},
508
+ "serverInfo": {"name": "team_orchestrator", "version": "0.1.0"},
509
+ },
510
+ }
511
+ if method == "tools/list":
512
+ return {"jsonrpc": "2.0", "id": msg_id, "result": {"tools": TOOLS}}
513
+ if method == "tools/call":
514
+ params = request.get("params", {})
515
+ name = params.get("name")
516
+ arguments = params.get("arguments") or {}
517
+ result = dispatch(tools, {"tool": name, "arguments": arguments})
518
+ is_error = result.get("ok") is False
519
+ return {
520
+ "jsonrpc": "2.0",
521
+ "id": msg_id,
522
+ "result": {
523
+ "content": [
524
+ {
525
+ "type": "text",
526
+ "text": json.dumps(result, ensure_ascii=False),
527
+ }
528
+ ],
529
+ "isError": is_error,
530
+ },
531
+ }
532
+ return {
533
+ "jsonrpc": "2.0",
534
+ "id": msg_id,
535
+ "error": {"code": -32601, "message": f"unknown method {method!r}"},
536
+ }
537
+
538
+
539
+ def main(argv: list[str] | None = None) -> None:
540
+ parser = argparse.ArgumentParser(description="TeamSpec team_orchestrator MCP stdio server")
541
+ parser.add_argument("--workspace", default=".", help="Workspace containing .team/runtime")
542
+ args = parser.parse_args(argv)
543
+ tools = TeamOrchestratorTools(Path(args.workspace))
544
+ for line in sys.stdin:
545
+ line = line.strip()
546
+ if not line:
547
+ continue
548
+ try:
549
+ request = json.loads(line)
550
+ if request.get("jsonrpc") == "2.0":
551
+ response = handle_mcp(tools, request)
552
+ if response is None:
553
+ continue
554
+ sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
555
+ sys.stdout.flush()
556
+ continue
557
+ result = dispatch(tools, request)
558
+ sys.stdout.write(json.dumps({"ok": result.get("ok", True), "result": result}, ensure_ascii=False) + "\n")
559
+ sys.stdout.flush()
560
+ except Exception as exc: # MCP transports need errors surfaced on stdout.
561
+ if "request" in locals() and isinstance(request, dict) and request.get("jsonrpc") == "2.0":
562
+ sys.stdout.write(
563
+ json.dumps(
564
+ {
565
+ "jsonrpc": "2.0",
566
+ "id": request.get("id"),
567
+ "error": {"code": -32000, "message": str(exc)},
568
+ },
569
+ ensure_ascii=False,
570
+ )
571
+ + "\n"
572
+ )
573
+ else:
574
+ sys.stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + "\n")
575
+ sys.stdout.flush()
576
+
577
+
578
+ if __name__ == "__main__":
579
+ main()