godcode-engine 4.0.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.
godcode/spirit.py ADDED
@@ -0,0 +1,180 @@
1
+ """SpiritEngine -- discerns the spiritual intent behind words. 🕊
2
+
3
+ Loads the God Code training dataset, builds a keyword index over each row's
4
+ primary_action + spiritual_intent + logic_flows, and classifies new text by
5
+ keyword overlap. When nothing resonates, it falls back to silent contemplation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import csv
11
+ import re
12
+ from collections import Counter
13
+ from pathlib import Path
14
+
15
+ _WORD_RE = re.compile(r"[a-z0-9]+")
16
+
17
+ STOPWORDS = frozenset(
18
+ """
19
+ a an the of and to in for on with by or is are was were be been being
20
+ this that these those it its as at from into over under between through
21
+ during before after above below up down out off again further then once
22
+ here there when where why how all any both each few more most other some
23
+ such no nor not only own same so than too very can will just don should
24
+ now your you we they them their our us my me him her his she he i
25
+ do does did have has had shall may might must would could ought
26
+ """.split()
27
+ )
28
+
29
+ SILENT_CONTEMPLATION = {
30
+ "intent": "Silent contemplation",
31
+ "confidence": 0.0,
32
+ "spiritual_intent": "The Spirit is quiet on this matter.",
33
+ "suggestion": "BREATHE and try again.",
34
+ "keywords": [],
35
+ }
36
+
37
+
38
+ def _keywords(text: str) -> set[str]:
39
+ """Lowercase word tokens with stopwords removed."""
40
+ return {w for w in _WORD_RE.findall((text or "").lower()) if w not in STOPWORDS}
41
+
42
+
43
+ class SpiritEngine:
44
+ """Keyword-overlap classifier over the God Code training dataset."""
45
+
46
+ def __init__(self, dataset_path: str | Path | None = None) -> None:
47
+ if dataset_path is None:
48
+ # Default: god_code_training_dataset.csv at the repo root.
49
+ dataset_path = Path(__file__).resolve().parent.parent / "god_code_training_dataset.csv"
50
+ self.dataset_path = Path(dataset_path)
51
+ self.rows: list[dict] = self._load()
52
+ # v4.0 -- declared intents: rite name -> natural-language intent text,
53
+ # registered by DECLARE INTENT and consulted by resolve_intent().
54
+ self.declared_intents: dict[str, str] = {}
55
+
56
+ def _load(self) -> list[dict]:
57
+ try:
58
+ with open(self.dataset_path, newline="", encoding="utf-8") as f:
59
+ records = list(csv.reader(f))
60
+ except OSError:
61
+ # No dataset beside the engine (e.g. an installed copy):
62
+ # the Spirit is quiet, and classify() falls back gracefully.
63
+ return []
64
+ if not records:
65
+ return []
66
+ header, records = records[0], records[1:]
67
+ rows: list[dict] = []
68
+ buf: list[str] = []
69
+ for rec in records:
70
+ if len(rec) == len(header):
71
+ # A row ends here: the code field may have spanned earlier
72
+ # physical lines (unquoted), accumulated in buf.
73
+ code = "\n".join(buf + [rec[0]])
74
+ row = dict(zip(header, [code] + rec[1:]))
75
+ row["keywords"] = _keywords(
76
+ " ".join(row[c] for c in ("primary_action", "spiritual_intent", "logic_flows"))
77
+ )
78
+ rows.append(row)
79
+ buf = []
80
+ else:
81
+ # Continuation line of the multi-line code field.
82
+ buf.extend(rec)
83
+ return rows
84
+
85
+ def classify(self, text: str) -> dict:
86
+ """Classify text -> {intent, confidence, spiritual_intent, suggestion, keywords}."""
87
+ words = _keywords(text)
88
+ best: dict | None = None
89
+ best_overlap = 0
90
+ best_confidence = 0.0
91
+ for row in self.rows:
92
+ overlap = len(words & row["keywords"])
93
+ if overlap == 0:
94
+ continue
95
+ confidence = overlap / max(1, len(row["keywords"]))
96
+ # Most shared keywords wins; ties go to the row whose
97
+ # vocabulary best explains the input (highest confidence),
98
+ # so an incidental word cannot outshout the true theme.
99
+ if overlap > best_overlap or (
100
+ overlap == best_overlap and confidence > best_confidence
101
+ ):
102
+ best, best_overlap, best_confidence = row, overlap, confidence
103
+ if best is None:
104
+ return dict(SILENT_CONTEMPLATION)
105
+ confidence = min(1.0, best_overlap / max(1, len(best["keywords"])))
106
+ return {
107
+ "intent": best["primary_action"],
108
+ "confidence": round(confidence, 4),
109
+ "spiritual_intent": best["spiritual_intent"],
110
+ "suggestion": best["next_suggestions"],
111
+ "keywords": sorted(words & best["keywords"]),
112
+ }
113
+
114
+ def prophesy(self, program_text: str) -> str:
115
+ """Compose a 2-3 sentence divine forecast over a program's lines."""
116
+ lines = [ln.strip() for ln in (program_text or "").splitlines() if ln.strip()]
117
+ results = [self.classify(ln) for ln in lines] or [dict(SILENT_CONTEMPLATION)]
118
+ counts = Counter(r["intent"] for r in results)
119
+ top = max(counts.values())
120
+ # Dominant intent = mode; ties resolve to the earliest line's intent.
121
+ dominant = next(r for r in results if counts[r["intent"]] == top)
122
+ avg_conf = sum(r["confidence"] for r in results) / len(results)
123
+ pct = round(avg_conf * 100)
124
+ return (
125
+ f"Thus the Spirit speaks over this creation: the prevailing wind is "
126
+ f"'{dominant['intent']}', discerned with {pct}% certainty. "
127
+ f"{dominant['spiritual_intent']} "
128
+ f"Therefore the counsel of heaven is this: {dominant['suggestion']}"
129
+ )
130
+
131
+ # ------------------------------------------------- v4.0: intent layer
132
+
133
+ def declare_intent(self, rite_name: str, text: str) -> None:
134
+ """Register a natural-language intent on a named rite."""
135
+ self.declared_intents[str(rite_name)] = str(text)
136
+
137
+ def intents_aligned(self, declared: str, discerned: dict) -> bool:
138
+ """True when the declared intent shares a keyword with the discerned one.
139
+
140
+ *declared* is the natural-language text from DECLARE INTENT;
141
+ *discerned* is a classify() result for the rite's actual words.
142
+ The Spirit counsels on divergence; it never condemns.
143
+ """
144
+ declared_words = _keywords(declared)
145
+ discerned_words = (
146
+ _keywords(discerned.get("intent") or "")
147
+ | _keywords(discerned.get("spiritual_intent") or "")
148
+ | {str(k).lower() for k in (discerned.get("keywords") or [])}
149
+ )
150
+ return bool(declared_words & discerned_words)
151
+
152
+ def resolve_intent(self, text: str) -> dict:
153
+ """Classify *text* and report which declared intents it aligns with.
154
+
155
+ Returns the classify() payload plus ``aligned_with``: a list of
156
+ ``{rite, declared, shared_keywords}`` for every declared intent
157
+ sharing at least one keyword with the text.
158
+ """
159
+ result = self.classify(text)
160
+ words = _keywords(text)
161
+ aligned_with = []
162
+ for rite, declared in self.declared_intents.items():
163
+ shared = sorted(words & _keywords(declared))
164
+ if shared:
165
+ aligned_with.append(
166
+ {"rite": rite, "declared": declared,
167
+ "shared_keywords": shared}
168
+ )
169
+ result["aligned_with"] = aligned_with
170
+ return result
171
+
172
+ def counsel(self, question: str) -> str:
173
+ """Speak 2-3 sentences of counsel over a question, in prophesy's voice."""
174
+ r = self.classify(question)
175
+ pct = round(r["confidence"] * 100)
176
+ return (
177
+ f"Concerning '{question}', the Spirit discerns '{r['intent']}' "
178
+ f"with {pct}% certainty. {r['spiritual_intent']} "
179
+ f"Therefore the counsel of heaven is this: {r['suggestion']}"
180
+ )
godcode/tokens.py ADDED
@@ -0,0 +1,91 @@
1
+ """Token types and the Token dataclass for God Code v2.0.
2
+
3
+ Keyword tokens carry their canonical UPPER name as ``value``;
4
+ identifiers preserve the author's casing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from enum import Enum, auto
11
+
12
+
13
+ class TokenType(Enum):
14
+ # --- keywords -----------------------------------------------------------
15
+ BEGIN = auto()
16
+ CREATION = auto()
17
+ END = auto()
18
+ DECLARE = auto()
19
+ AS = auto()
20
+ BREATHE = auto()
21
+ LIFE = auto()
22
+ INTO = auto()
23
+ REVEAL = auto()
24
+ PROPHESY = auto()
25
+ ASCEND = auto()
26
+ IF = auto()
27
+ THEN = auto()
28
+ ELSE = auto()
29
+ ENDIF = auto()
30
+ FOR = auto()
31
+ IN = auto()
32
+ ENDFOR = auto()
33
+ WHILE = auto()
34
+ DO = auto()
35
+ ENDWHILE = auto()
36
+ DEFINE = auto()
37
+ RITE = auto()
38
+ INVOKE = auto()
39
+ RETURN = auto()
40
+ IMPORT = auto()
41
+ IS = auto()
42
+ NOT = auto()
43
+ AND = auto()
44
+ OR = auto()
45
+ # §4 additions (missed in §1's list)
46
+ REFLECT = auto()
47
+ BLESS = auto()
48
+ ANOINT = auto()
49
+ SEAL = auto()
50
+ TESTIFY = auto()
51
+ # boolean / void literals (added for fmt <-> parse round-trip fidelity)
52
+ TRUE = auto()
53
+ FALSE = auto()
54
+ VOID = auto()
55
+ # --- literals -----------------------------------------------------------
56
+ NUMBER = auto()
57
+ STRING = auto()
58
+ IDENT = auto()
59
+ # --- operators ----------------------------------------------------------
60
+ PLUS = auto() # +
61
+ MINUS = auto() # -
62
+ STAR = auto() # *
63
+ SLASH = auto() # /
64
+ PERCENT = auto() # %
65
+ EQ = auto() # = or ==
66
+ NEQ = auto() # !=
67
+ LT = auto() # <
68
+ GT = auto() # >
69
+ LTE = auto() # <=
70
+ GTE = auto() # >=
71
+ # --- punctuation --------------------------------------------------------
72
+ LPAREN = auto()
73
+ RPAREN = auto()
74
+ LBRACKET = auto()
75
+ RBRACKET = auto()
76
+ COMMA = auto()
77
+ # --- structural ---------------------------------------------------------
78
+ NEWLINE = auto()
79
+ EOF = auto()
80
+
81
+
82
+ @dataclass
83
+ class Token:
84
+ type: TokenType
85
+ value: object # NUMBER -> int|float, STRING -> str (unescaped),
86
+ # IDENT/keywords -> str, operators/punct -> the lexeme
87
+ line: int # 1-based
88
+ col: int # 1-based
89
+
90
+ def __repr__(self) -> str: # compact, readable in test failures
91
+ return f"Token({self.type.name}, {self.value!r}, {self.line}:{self.col})"
godcode/tools.py ADDED
@@ -0,0 +1,378 @@
1
+ """Agent tool bridge for God Code v4.0 -- "Intent & Chain".
2
+
3
+ MCP-compatible tool schemas plus a stdio JSON-RPC 2.0 bridge, with no
4
+ third-party dependencies. ``godcode tools`` prints the schemas;
5
+ ``godcode bridge`` speaks the protocol so any MCP-compatible agent host
6
+ can check, run, consult, resolve intent, and verify chains.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ TOOL_SCHEMAS: list[dict] = [
14
+ {
15
+ "name": "check",
16
+ "description": (
17
+ "Lex and parse a God Code scroll without running it. "
18
+ "Returns ok plus diagnostics with line, column, error code, "
19
+ "and a fix hint."
20
+ ),
21
+ "inputSchema": {
22
+ "type": "object",
23
+ "properties": {
24
+ "file": {"type": "string",
25
+ "description": "Path to the .god scroll"},
26
+ },
27
+ "required": ["file"],
28
+ },
29
+ },
30
+ {
31
+ "name": "run",
32
+ "description": (
33
+ "Run a God Code scroll and capture its output. "
34
+ "Runs inside the deny-by-default sandbox unless sandbox is false."
35
+ ),
36
+ "inputSchema": {
37
+ "type": "object",
38
+ "properties": {
39
+ "file": {"type": "string",
40
+ "description": "Path to the .god scroll"},
41
+ "sandbox": {"type": "boolean",
42
+ "description": "Use the sandbox (default true)"},
43
+ },
44
+ "required": ["file"],
45
+ },
46
+ },
47
+ {
48
+ "name": "consult",
49
+ "description": (
50
+ "Ask the Spirit Engine -- the local oracle -- a question. "
51
+ "Returns two to three sentences of counsel. No external calls."
52
+ ),
53
+ "inputSchema": {
54
+ "type": "object",
55
+ "properties": {
56
+ "question": {"type": "string",
57
+ "description": "The question to lay before the Spirit"},
58
+ },
59
+ "required": ["question"],
60
+ },
61
+ },
62
+ {
63
+ "name": "intent",
64
+ "description": (
65
+ "Resolve the spiritual intent behind words with the Spirit "
66
+ "Engine: intent, confidence, spiritual intent, suggestion, "
67
+ "keywords, and declared intents the words align with."
68
+ ),
69
+ "inputSchema": {
70
+ "type": "object",
71
+ "properties": {
72
+ "text": {"type": "string",
73
+ "description": "Words to resolve the intent of"},
74
+ },
75
+ "required": ["text"],
76
+ },
77
+ },
78
+ {
79
+ "name": "anchor_verify",
80
+ "description": (
81
+ "Verify a blockchain anchor receipt (as returned by ANCHOR) "
82
+ "against its anchor chain."
83
+ ),
84
+ "inputSchema": {
85
+ "type": "object",
86
+ "properties": {
87
+ "receipt": {"type": "object",
88
+ "description": "The anchor receipt map"},
89
+ },
90
+ "required": ["receipt"],
91
+ },
92
+ },
93
+ {
94
+ "name": "ledger_verify",
95
+ "description": (
96
+ "Verify the covenant chain and the anchor chain for tampering."
97
+ ),
98
+ "inputSchema": {
99
+ "type": "object",
100
+ "properties": {
101
+ "file": {"type": "string",
102
+ "description": "Covenant chain file (default covenant.chain)"},
103
+ "anchor_file": {"type": "string",
104
+ "description": "Anchor chain file (default anchors.chain)"},
105
+ },
106
+ "required": [],
107
+ },
108
+ },
109
+ ]
110
+
111
+ _TOOLS_BY_NAME = {t["name"]: t for t in TOOL_SCHEMAS}
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # tool implementations (each returns a plain-JSON result dict)
116
+ # ---------------------------------------------------------------------------
117
+ def tool_check(arguments: dict) -> dict:
118
+ from godcode import agentics
119
+ from godcode.errors import GodCodeError
120
+ from godcode.lexer import Lexer
121
+ from godcode.parser import Parser
122
+
123
+ file = arguments.get("file")
124
+ if not isinstance(file, str):
125
+ return {"ok": False, "error": "check needs 'file' as a string path"}
126
+ try:
127
+ source = Path(file).read_text(encoding="utf-8")
128
+ except OSError as exc:
129
+ return {"ok": False, "error": f"cannot read '{file}': {exc.strerror or exc}"}
130
+ try:
131
+ Parser(Lexer(source).lex()).parse()
132
+ except GodCodeError as err:
133
+ return {"ok": False, "diagnostics": [agentics.diagnostic(err)]}
134
+ return {"ok": True, "diagnostics": []}
135
+
136
+
137
+ def tool_run(arguments: dict) -> dict:
138
+ from godcode.errors import GodCodeError
139
+
140
+ file = arguments.get("file")
141
+ if not isinstance(file, str):
142
+ return {"ok": False, "error": "run needs 'file' as a string path"}
143
+ sandbox = arguments.get("sandbox", True)
144
+ try:
145
+ source = Path(file).read_text(encoding="utf-8")
146
+ except OSError as exc:
147
+ return {"ok": False, "error": f"cannot read '{file}': {exc.strerror or exc}"}
148
+ if sandbox:
149
+ from godcode.sandbox import run_sandboxed
150
+
151
+ try:
152
+ output = run_sandboxed(source, source_name=file)
153
+ except GodCodeError as err:
154
+ return {"ok": False, "output": [], "error": str(err)}
155
+ return {"ok": True, "output": output, "error": None}
156
+ from godcode.cli import _make_interpreter
157
+ import contextlib
158
+ import io
159
+
160
+ interp = _make_interpreter(None)
161
+ buf = io.StringIO()
162
+ try:
163
+ with contextlib.redirect_stdout(buf):
164
+ interp.run_source(source, source_name=file)
165
+ except GodCodeError as err:
166
+ return {"ok": False,
167
+ "output": buf.getvalue().splitlines(),
168
+ "error": str(err)}
169
+ return {"ok": True, "output": buf.getvalue().splitlines(), "error": None}
170
+
171
+
172
+ def tool_consult(arguments: dict) -> dict:
173
+ from godcode.spirit import SpiritEngine
174
+
175
+ question = arguments.get("question")
176
+ if not isinstance(question, str):
177
+ return {"ok": False, "error": "consult needs 'question' as a string"}
178
+ try:
179
+ counsel = SpiritEngine().counsel(question)
180
+ except Exception as exc: # pragma: no cover - defensive
181
+ return {"ok": False, "error": f"the oracle faltered: {exc}"}
182
+ return {"ok": True, "counsel": counsel}
183
+
184
+
185
+ def tool_intent(arguments: dict) -> dict:
186
+ from godcode.spirit import SpiritEngine
187
+
188
+ text = arguments.get("text")
189
+ if not isinstance(text, str):
190
+ return {"ok": False, "error": "intent needs 'text' as a string"}
191
+ try:
192
+ result = SpiritEngine().resolve_intent(text)
193
+ except Exception as exc: # pragma: no cover - defensive
194
+ return {"ok": False, "error": f"the Spirit faltered: {exc}"}
195
+ return {"ok": True, "result": result}
196
+
197
+
198
+ def tool_anchor_verify(arguments: dict) -> dict:
199
+ from godcode.chain import default_adapters
200
+
201
+ receipt = arguments.get("receipt")
202
+ if not isinstance(receipt, dict):
203
+ return {"ok": False, "error": "anchor_verify needs 'receipt' as an object"}
204
+ chain_name = receipt.get("chain", "simulated")
205
+ adapter = default_adapters().get(chain_name)
206
+ if adapter is None:
207
+ return {"ok": False,
208
+ "error": f"unknown chain '{chain_name}'"}
209
+ valid = adapter.verify(receipt)
210
+ return {
211
+ "ok": True,
212
+ "valid": valid,
213
+ "message": ("the anchor stands" if valid
214
+ else "the anchor does not verify"),
215
+ }
216
+
217
+
218
+ def tool_ledger_verify(arguments: dict) -> dict:
219
+ from godcode.chain import SimulatedChainAdapter
220
+ from godcode.ledger import CovenantLedger
221
+
222
+ file = arguments.get("file", "covenant.chain")
223
+ anchor_file = arguments.get("anchor_file", "anchors.chain")
224
+ cov_ok, cov_msg = CovenantLedger(file).verify()
225
+ anc_ok, anc_msg = SimulatedChainAdapter(anchor_file).verify_chain()
226
+ return {
227
+ "ok": True,
228
+ "covenants": {"ok": cov_ok, "message": cov_msg},
229
+ "anchors": {"ok": anc_ok, "message": anc_msg},
230
+ }
231
+
232
+
233
+ _TOOL_FUNCS = {
234
+ "check": tool_check,
235
+ "run": tool_run,
236
+ "consult": tool_consult,
237
+ "intent": tool_intent,
238
+ "anchor_verify": tool_anchor_verify,
239
+ "ledger_verify": tool_ledger_verify,
240
+ }
241
+
242
+
243
+ def call_tool(name: str, arguments: dict) -> dict:
244
+ """Run one tool by name; always returns a JSON-safe result dict."""
245
+ func = _TOOL_FUNCS.get(name)
246
+ if func is None:
247
+ return {"ok": False,
248
+ "error": f"unknown tool '{name}'",
249
+ "known_tools": sorted(_TOOL_FUNCS)}
250
+ if not isinstance(arguments, dict):
251
+ return {"ok": False, "error": "arguments must be an object"}
252
+ try:
253
+ return func(arguments)
254
+ except Exception as exc: # pragma: no cover - defensive
255
+ return {"ok": False, "error": f"the tool faltered: {exc!r}"}
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # `godcode tools`
260
+ # ---------------------------------------------------------------------------
261
+ def cmd_tools(args) -> int:
262
+ """Print the MCP-compatible tool schemas (--json for machines)."""
263
+ if getattr(args, "json", False):
264
+ print(json.dumps({"tools": TOOL_SCHEMAS}, ensure_ascii=False, indent=2))
265
+ return 0
266
+ print("God Code tools (MCP-compatible):")
267
+ for tool in TOOL_SCHEMAS:
268
+ required = ", ".join(tool["inputSchema"].get("required", [])) or "none"
269
+ print(f" {tool['name']}({required}): {tool['description']}")
270
+ print("Speak them through `godcode bridge`, the stdio JSON-RPC loop.")
271
+ return 0
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # `godcode bridge` -- stdio JSON-RPC 2.0
276
+ # ---------------------------------------------------------------------------
277
+ _PARSE_ERROR = -32700
278
+ _INVALID_REQUEST = -32600
279
+ _METHOD_NOT_FOUND = -32601
280
+ _INVALID_PARAMS = -32602
281
+ _INTERNAL_ERROR = -32603
282
+
283
+
284
+ def _response_ok(req_id, result: dict) -> dict:
285
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
286
+
287
+
288
+ def _response_err(req_id, code: int, message: str) -> dict:
289
+ return {"jsonrpc": "2.0", "id": req_id,
290
+ "error": {"code": code, "message": message}}
291
+
292
+
293
+ def _handle_initialize(params: dict) -> dict:
294
+ from godcode import __version__
295
+
296
+ return {
297
+ "protocolVersion": "2024-11-05",
298
+ "capabilities": {"tools": {}},
299
+ "serverInfo": {"name": "godcode-bridge", "version": __version__},
300
+ }
301
+
302
+
303
+ def _handle_tools_call(params: dict) -> dict:
304
+ name = params.get("name")
305
+ arguments = params.get("arguments", {})
306
+ if not isinstance(name, str):
307
+ raise _BridgeError(_INVALID_PARAMS, "tools/call needs 'name' as a string")
308
+ result = call_tool(name, arguments if isinstance(arguments, dict) else {})
309
+ content = [{"type": "text",
310
+ "text": json.dumps(result, ensure_ascii=False)}]
311
+ payload = {"content": content}
312
+ if not result.get("ok", True):
313
+ payload["isError"] = True
314
+ return payload
315
+
316
+
317
+ class _BridgeError(Exception):
318
+ def __init__(self, code: int, message: str) -> None:
319
+ super().__init__(message)
320
+ self.code = code
321
+ self.message = message
322
+
323
+
324
+ def _dispatch(method: str, params: dict):
325
+ if method == "initialize":
326
+ return _handle_initialize(params)
327
+ if method == "ping":
328
+ return {}
329
+ if method == "tools/list":
330
+ return {"tools": TOOL_SCHEMAS}
331
+ if method == "tools/call":
332
+ return _handle_tools_call(params)
333
+ raise _BridgeError(_METHOD_NOT_FOUND, f"no such method '{method}'")
334
+
335
+
336
+ def cmd_bridge(args) -> int: # noqa: ARG001
337
+ """Serve the tool bridge: JSON-RPC 2.0 over stdio, one request per line."""
338
+ import sys
339
+
340
+ stdin, stdout = sys.stdin, sys.stdout
341
+ for line in stdin:
342
+ line = line.strip()
343
+ if not line:
344
+ continue
345
+ try:
346
+ request = json.loads(line)
347
+ except json.JSONDecodeError:
348
+ stdout.write(json.dumps(_response_err(None, _PARSE_ERROR,
349
+ "the request was not JSON"))
350
+ + "\n")
351
+ stdout.flush()
352
+ continue
353
+ req_id = request.get("id") if isinstance(request, dict) else None
354
+ # Notifications carry no id and get no answer.
355
+ is_notification = isinstance(request, dict) and "id" not in request
356
+ try:
357
+ if not isinstance(request, dict) or "method" not in request:
358
+ raise _BridgeError(_INVALID_REQUEST,
359
+ "a request needs a 'method'")
360
+ params = request.get("params", {})
361
+ if not isinstance(params, dict):
362
+ raise _BridgeError(_INVALID_PARAMS,
363
+ "'params' must be an object")
364
+ result = _dispatch(request["method"], params)
365
+ except _BridgeError as err:
366
+ response = _response_err(req_id, err.code, err.message)
367
+ except Exception as exc: # pragma: no cover - defensive
368
+ response = _response_err(req_id, _INTERNAL_ERROR,
369
+ f"the bridge faltered: {exc!r}")
370
+ else:
371
+ if is_notification:
372
+ continue
373
+ response = _response_ok(req_id, result)
374
+ if is_notification:
375
+ continue
376
+ stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
377
+ stdout.flush()
378
+ return 0