juno-code 1.0.33 → 1.0.35
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.
- package/dist/bin/cli.js +207 -79
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +170 -42
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -7
- package/dist/index.mjs.map +1 -1
- package/dist/templates/services/__pycache__/codex.cpython-38.pyc +0 -0
- package/dist/templates/services/claude.py +31 -1
- package/dist/templates/services/codex.py +338 -150
- package/package.json +1 -1
|
Binary file
|
|
@@ -51,6 +51,7 @@ class ClaudeService:
|
|
|
51
51
|
self.verbose = False
|
|
52
52
|
# User message truncation: -1 = no truncation, N = truncate to N lines
|
|
53
53
|
self.user_message_truncate = int(os.environ.get("CLAUDE_USER_MESSAGE_PRETTY_TRUNCATE", "4"))
|
|
54
|
+
self.last_result_event: Optional[Dict[str, Any]] = None
|
|
54
55
|
|
|
55
56
|
def expand_model_shorthand(self, model: str) -> str:
|
|
56
57
|
"""
|
|
@@ -529,6 +530,20 @@ Environment Variables:
|
|
|
529
530
|
print(f"Executing: {' '.join(cmd)}", file=sys.stderr)
|
|
530
531
|
print("-" * 80, file=sys.stderr)
|
|
531
532
|
|
|
533
|
+
capture_path = os.environ.get("JUNO_SUBAGENT_CAPTURE_PATH")
|
|
534
|
+
|
|
535
|
+
def write_capture_file():
|
|
536
|
+
"""Persist the final result event for programmatic capture without affecting screen output."""
|
|
537
|
+
if not capture_path or not self.last_result_event:
|
|
538
|
+
return
|
|
539
|
+
try:
|
|
540
|
+
Path(capture_path).write_text(
|
|
541
|
+
json.dumps(self.last_result_event, ensure_ascii=False),
|
|
542
|
+
encoding="utf-8"
|
|
543
|
+
)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
print(f"Warning: Failed to write capture file: {e}", file=sys.stderr)
|
|
546
|
+
|
|
532
547
|
try:
|
|
533
548
|
# Change to project directory before running
|
|
534
549
|
original_cwd = os.getcwd()
|
|
@@ -549,9 +564,19 @@ Environment Variables:
|
|
|
549
564
|
# This allows users to pipe to jq and see output as it streams
|
|
550
565
|
if process.stdout:
|
|
551
566
|
for line in process.stdout:
|
|
567
|
+
raw_line = line.strip()
|
|
568
|
+
# Capture the raw final result event for programmatic consumption
|
|
569
|
+
try:
|
|
570
|
+
parsed_raw = json.loads(raw_line)
|
|
571
|
+
if isinstance(parsed_raw, dict) and parsed_raw.get("type") == "result":
|
|
572
|
+
self.last_result_event = parsed_raw
|
|
573
|
+
except json.JSONDecodeError:
|
|
574
|
+
# Ignore non-JSON lines here; pretty formatter will handle them
|
|
575
|
+
pass
|
|
576
|
+
|
|
552
577
|
# Apply pretty formatting if enabled
|
|
553
578
|
if pretty:
|
|
554
|
-
formatted_line = self.pretty_format_json(
|
|
579
|
+
formatted_line = self.pretty_format_json(raw_line)
|
|
555
580
|
if formatted_line:
|
|
556
581
|
print(formatted_line, flush=True)
|
|
557
582
|
else:
|
|
@@ -567,6 +592,9 @@ Environment Variables:
|
|
|
567
592
|
if stderr_output:
|
|
568
593
|
print(stderr_output, file=sys.stderr)
|
|
569
594
|
|
|
595
|
+
# Persist the raw final result event for programmatic capture
|
|
596
|
+
write_capture_file()
|
|
597
|
+
|
|
570
598
|
# Restore original working directory
|
|
571
599
|
os.chdir(original_cwd)
|
|
572
600
|
|
|
@@ -577,12 +605,14 @@ Environment Variables:
|
|
|
577
605
|
if process:
|
|
578
606
|
process.terminate()
|
|
579
607
|
process.wait()
|
|
608
|
+
write_capture_file()
|
|
580
609
|
# Restore original working directory
|
|
581
610
|
if 'original_cwd' in locals():
|
|
582
611
|
os.chdir(original_cwd)
|
|
583
612
|
return 130
|
|
584
613
|
except Exception as e:
|
|
585
614
|
print(f"Error executing claude: {e}", file=sys.stderr)
|
|
615
|
+
write_capture_file()
|
|
586
616
|
# Restore original working directory
|
|
587
617
|
if 'original_cwd' in locals():
|
|
588
618
|
os.chdir(original_cwd)
|
|
@@ -34,6 +34,7 @@ class CodexService:
|
|
|
34
34
|
self.prompt = ""
|
|
35
35
|
self.additional_args: List[str] = []
|
|
36
36
|
self.verbose = False
|
|
37
|
+
self._item_counter = 0
|
|
37
38
|
|
|
38
39
|
def expand_model_shorthand(self, model: str) -> str:
|
|
39
40
|
"""
|
|
@@ -128,6 +129,108 @@ Environment Variables:
|
|
|
128
129
|
|
|
129
130
|
return parser.parse_args()
|
|
130
131
|
|
|
132
|
+
def _first_nonempty_str(self, *values: Optional[str]) -> str:
|
|
133
|
+
"""Return the first non-empty string value."""
|
|
134
|
+
for val in values:
|
|
135
|
+
if isinstance(val, str) and val != "":
|
|
136
|
+
return val
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
def _extract_content_text(self, payload: dict) -> str:
|
|
140
|
+
"""Join text-like fields from content arrays (item.* schema)."""
|
|
141
|
+
content = payload.get("content") if isinstance(payload, dict) else None
|
|
142
|
+
parts: List[str] = []
|
|
143
|
+
if isinstance(content, list):
|
|
144
|
+
for entry in content:
|
|
145
|
+
if not isinstance(entry, dict):
|
|
146
|
+
continue
|
|
147
|
+
text_val = (
|
|
148
|
+
entry.get("text")
|
|
149
|
+
or entry.get("message")
|
|
150
|
+
or entry.get("output_text")
|
|
151
|
+
or entry.get("input_text")
|
|
152
|
+
)
|
|
153
|
+
if isinstance(text_val, str) and text_val != "":
|
|
154
|
+
parts.append(text_val)
|
|
155
|
+
return "\n".join(parts) if parts else ""
|
|
156
|
+
|
|
157
|
+
def _extract_command_output_text(self, payload: dict) -> str:
|
|
158
|
+
"""Extract aggregated/command output from various item.* layouts."""
|
|
159
|
+
if not isinstance(payload, dict):
|
|
160
|
+
return ""
|
|
161
|
+
result = payload.get("result") if isinstance(payload.get("result"), dict) else None
|
|
162
|
+
content_text = self._extract_content_text(payload)
|
|
163
|
+
return self._first_nonempty_str(
|
|
164
|
+
payload.get("aggregated_output"),
|
|
165
|
+
payload.get("output"),
|
|
166
|
+
payload.get("formatted_output"),
|
|
167
|
+
result.get("aggregated_output") if result else None,
|
|
168
|
+
result.get("output") if result else None,
|
|
169
|
+
result.get("formatted_output") if result else None,
|
|
170
|
+
content_text,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _extract_reasoning_text(self, payload: dict) -> str:
|
|
174
|
+
"""Extract reasoning text from legacy and item.* schemas."""
|
|
175
|
+
if not isinstance(payload, dict):
|
|
176
|
+
return ""
|
|
177
|
+
reasoning_obj = payload.get("reasoning") if isinstance(payload.get("reasoning"), dict) else None
|
|
178
|
+
result_obj = payload.get("result") if isinstance(payload.get("result"), dict) else None
|
|
179
|
+
content_text = self._extract_content_text(payload)
|
|
180
|
+
return self._first_nonempty_str(
|
|
181
|
+
payload.get("text"),
|
|
182
|
+
payload.get("reasoning_text"),
|
|
183
|
+
reasoning_obj.get("text") if reasoning_obj else None,
|
|
184
|
+
result_obj.get("text") if result_obj else None,
|
|
185
|
+
content_text,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _extract_message_text(self, payload: dict) -> str:
|
|
189
|
+
"""Extract final/assistant message text from item.* schemas."""
|
|
190
|
+
if not isinstance(payload, dict):
|
|
191
|
+
return ""
|
|
192
|
+
result_obj = payload.get("result") if isinstance(payload.get("result"), dict) else None
|
|
193
|
+
content_text = self._extract_content_text(payload)
|
|
194
|
+
return self._first_nonempty_str(
|
|
195
|
+
payload.get("message"),
|
|
196
|
+
payload.get("text"),
|
|
197
|
+
payload.get("final"),
|
|
198
|
+
result_obj.get("message") if result_obj else None,
|
|
199
|
+
result_obj.get("text") if result_obj else None,
|
|
200
|
+
content_text,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def _parse_item_number(self, item_id: str) -> Optional[int]:
|
|
204
|
+
"""Return numeric component from item_{n} ids or None if unparseable."""
|
|
205
|
+
if not isinstance(item_id, str):
|
|
206
|
+
return None
|
|
207
|
+
item_id = item_id.strip()
|
|
208
|
+
if not item_id.startswith("item_"):
|
|
209
|
+
return None
|
|
210
|
+
try:
|
|
211
|
+
return int(item_id.split("item_", 1)[1])
|
|
212
|
+
except Exception:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def _normalize_item_id(self, payload: dict, outer_type: str) -> Optional[str]:
|
|
216
|
+
"""
|
|
217
|
+
Prefer the existing id on item.* payloads; otherwise synthesize sequential item_{n}.
|
|
218
|
+
Maintains a per-run counter so missing ids still expose turn counts.
|
|
219
|
+
"""
|
|
220
|
+
item_id = payload.get("id") if isinstance(payload, dict) else None
|
|
221
|
+
if isinstance(item_id, str) and item_id.strip():
|
|
222
|
+
parsed = self._parse_item_number(item_id)
|
|
223
|
+
if parsed is not None and parsed + 1 > self._item_counter:
|
|
224
|
+
self._item_counter = parsed + 1
|
|
225
|
+
return item_id.strip()
|
|
226
|
+
|
|
227
|
+
if isinstance(outer_type, str) and outer_type.startswith("item."):
|
|
228
|
+
generated = f"item_{self._item_counter}"
|
|
229
|
+
self._item_counter += 1
|
|
230
|
+
return generated
|
|
231
|
+
|
|
232
|
+
return None
|
|
233
|
+
|
|
131
234
|
def read_prompt_file(self, file_path: str) -> str:
|
|
132
235
|
"""Read prompt from a file"""
|
|
133
236
|
try:
|
|
@@ -190,12 +293,18 @@ Environment Variables:
|
|
|
190
293
|
|
|
191
294
|
return cmd
|
|
192
295
|
|
|
193
|
-
def _format_msg_pretty(
|
|
296
|
+
def _format_msg_pretty(
|
|
297
|
+
self,
|
|
298
|
+
msg_type: str,
|
|
299
|
+
payload: dict,
|
|
300
|
+
outer_type: str = "",
|
|
301
|
+
item_id: Optional[str] = None,
|
|
302
|
+
) -> Optional[str]:
|
|
194
303
|
"""
|
|
195
304
|
Pretty format for specific msg types to be human readable while
|
|
196
305
|
preserving a compact JSON header line that includes the msg.type.
|
|
197
306
|
|
|
198
|
-
- agent_message: render
|
|
307
|
+
- agent_message/message/assistant: render message text as multi-line block
|
|
199
308
|
- agent_reasoning: render 'text' field as multi-line text
|
|
200
309
|
- exec_command_end: only output 'formatted_output' (suppress other fields)
|
|
201
310
|
- token_count: fully suppressed (no final summary emission)
|
|
@@ -203,41 +312,96 @@ Environment Variables:
|
|
|
203
312
|
Returns a string to print, or None to fall back to raw printing.
|
|
204
313
|
"""
|
|
205
314
|
try:
|
|
206
|
-
msg = obj.get("msg") or {}
|
|
207
|
-
msg_type = (msg.get("type") or "").strip()
|
|
208
315
|
now = datetime.now().strftime("%I:%M:%S %p")
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
316
|
+
msg_type = (msg_type or "").strip()
|
|
317
|
+
header_type = (outer_type or msg_type).strip()
|
|
318
|
+
base_type = header_type or msg_type or "message"
|
|
319
|
+
|
|
320
|
+
def make_header(type_value: str):
|
|
321
|
+
hdr = {"type": type_value, "datetime": now}
|
|
322
|
+
if item_id:
|
|
323
|
+
hdr["id"] = item_id
|
|
324
|
+
if outer_type and msg_type and outer_type != msg_type:
|
|
325
|
+
hdr["item_type"] = msg_type
|
|
326
|
+
return hdr
|
|
327
|
+
|
|
328
|
+
header = make_header(base_type)
|
|
329
|
+
|
|
330
|
+
if isinstance(payload, dict):
|
|
331
|
+
if item_id and "id" not in payload:
|
|
332
|
+
payload["id"] = item_id
|
|
333
|
+
if payload.get("command"):
|
|
334
|
+
header["command"] = payload.get("command")
|
|
335
|
+
if payload.get("status"):
|
|
336
|
+
header["status"] = payload.get("status")
|
|
337
|
+
if payload.get("state") and not header.get("status"):
|
|
338
|
+
header["status"] = payload.get("state")
|
|
218
339
|
|
|
219
340
|
# agent_reasoning → show 'text' human-readable
|
|
220
|
-
if msg_type
|
|
221
|
-
content =
|
|
222
|
-
header =
|
|
341
|
+
if msg_type in {"agent_reasoning", "reasoning"}:
|
|
342
|
+
content = self._extract_reasoning_text(payload)
|
|
343
|
+
header = make_header(header_type or msg_type)
|
|
223
344
|
if "\n" in content:
|
|
224
345
|
return json.dumps(header, ensure_ascii=False) + "\ntext:\n" + content
|
|
225
346
|
header["text"] = content
|
|
226
347
|
return json.dumps(header, ensure_ascii=False)
|
|
227
348
|
|
|
349
|
+
if msg_type in {"agent_message", "message", "assistant_message", "assistant"}:
|
|
350
|
+
content = self._extract_message_text(payload)
|
|
351
|
+
header = make_header(header_type or msg_type)
|
|
352
|
+
if "\n" in content:
|
|
353
|
+
return json.dumps(header, ensure_ascii=False) + "\nmessage:\n" + content
|
|
354
|
+
if content != "":
|
|
355
|
+
header["message"] = content
|
|
356
|
+
return json.dumps(header, ensure_ascii=False)
|
|
357
|
+
if header_type:
|
|
358
|
+
return json.dumps(header, ensure_ascii=False)
|
|
359
|
+
|
|
228
360
|
# exec_command_end → only show 'formatted_output'
|
|
229
361
|
if msg_type == "exec_command_end":
|
|
230
|
-
formatted_output =
|
|
362
|
+
formatted_output = payload.get("formatted_output", "") if isinstance(payload, dict) else ""
|
|
231
363
|
header = {"type": msg_type, "datetime": now}
|
|
232
364
|
if "\n" in formatted_output:
|
|
233
365
|
return json.dumps(header, ensure_ascii=False) + "\nformatted_output:\n" + formatted_output
|
|
234
366
|
header["formatted_output"] = formatted_output
|
|
235
367
|
return json.dumps(header, ensure_ascii=False)
|
|
236
368
|
|
|
369
|
+
# item.* schema → command_execution blocks
|
|
370
|
+
if msg_type == "command_execution":
|
|
371
|
+
aggregated_output = self._extract_command_output_text(payload)
|
|
372
|
+
if "\n" in aggregated_output:
|
|
373
|
+
return json.dumps(header, ensure_ascii=False) + "\naggregated_output:\n" + aggregated_output
|
|
374
|
+
if aggregated_output:
|
|
375
|
+
header["aggregated_output"] = aggregated_output
|
|
376
|
+
return json.dumps(header, ensure_ascii=False)
|
|
377
|
+
# No output (likely item.started) – still show header if it carries context
|
|
378
|
+
if header_type:
|
|
379
|
+
return json.dumps(header, ensure_ascii=False)
|
|
380
|
+
|
|
237
381
|
return None
|
|
238
382
|
except Exception:
|
|
239
383
|
return None
|
|
240
384
|
|
|
385
|
+
def _normalize_event(self, obj_dict: dict):
|
|
386
|
+
"""
|
|
387
|
+
Normalize legacy (msg-based) and new item.* schemas into a common tuple.
|
|
388
|
+
Returns (msg_type, payload_dict, outer_type).
|
|
389
|
+
"""
|
|
390
|
+
msg = obj_dict.get("msg") if isinstance(obj_dict.get("msg"), dict) else {}
|
|
391
|
+
outer_type = (obj_dict.get("type") or "").strip()
|
|
392
|
+
item = obj_dict.get("item") if isinstance(obj_dict.get("item"), dict) else None
|
|
393
|
+
|
|
394
|
+
msg_type = (msg.get("type") or "").strip() if isinstance(msg, dict) else ""
|
|
395
|
+
payload = msg if isinstance(msg, dict) else {}
|
|
396
|
+
|
|
397
|
+
if not msg_type and item is not None:
|
|
398
|
+
msg_type = (item.get("type") or "").strip() or outer_type
|
|
399
|
+
payload = item
|
|
400
|
+
elif not msg_type:
|
|
401
|
+
msg_type = outer_type
|
|
402
|
+
|
|
403
|
+
return msg_type, payload, outer_type
|
|
404
|
+
|
|
241
405
|
def run_codex(self, cmd: List[str], verbose: bool = False) -> int:
|
|
242
406
|
"""Execute the codex command and stream output with filtering and pretty-printing
|
|
243
407
|
|
|
@@ -260,6 +424,9 @@ Environment Variables:
|
|
|
260
424
|
parts = [p.strip() for p in env_val.split(",") if p.strip()]
|
|
261
425
|
hide_types.update(parts)
|
|
262
426
|
|
|
427
|
+
# Reset per-run item counter for synthesized ids
|
|
428
|
+
self._item_counter = 0
|
|
429
|
+
|
|
263
430
|
# We fully suppress all token_count events (do not emit even at end)
|
|
264
431
|
last_token_count = None
|
|
265
432
|
|
|
@@ -274,119 +441,124 @@ Environment Variables:
|
|
|
274
441
|
universal_newlines=True
|
|
275
442
|
)
|
|
276
443
|
|
|
444
|
+
def split_json_stream(text: str):
|
|
445
|
+
objs = []
|
|
446
|
+
buf: List[str] = []
|
|
447
|
+
depth = 0
|
|
448
|
+
in_str = False
|
|
449
|
+
esc = False
|
|
450
|
+
started = False
|
|
451
|
+
for ch in text:
|
|
452
|
+
if in_str:
|
|
453
|
+
buf.append(ch)
|
|
454
|
+
if esc:
|
|
455
|
+
esc = False
|
|
456
|
+
elif ch == '\\':
|
|
457
|
+
esc = True
|
|
458
|
+
elif ch == '"':
|
|
459
|
+
in_str = False
|
|
460
|
+
continue
|
|
461
|
+
if ch == '"':
|
|
462
|
+
in_str = True
|
|
463
|
+
buf.append(ch)
|
|
464
|
+
continue
|
|
465
|
+
if ch == '{':
|
|
466
|
+
depth += 1
|
|
467
|
+
started = True
|
|
468
|
+
buf.append(ch)
|
|
469
|
+
continue
|
|
470
|
+
if ch == '}':
|
|
471
|
+
depth -= 1
|
|
472
|
+
buf.append(ch)
|
|
473
|
+
if started and depth == 0:
|
|
474
|
+
candidate = ''.join(buf).strip().strip("'\"")
|
|
475
|
+
if candidate:
|
|
476
|
+
objs.append(candidate)
|
|
477
|
+
buf = []
|
|
478
|
+
started = False
|
|
479
|
+
continue
|
|
480
|
+
if started:
|
|
481
|
+
buf.append(ch)
|
|
482
|
+
remainder = ''.join(buf) if buf else ""
|
|
483
|
+
return objs, remainder
|
|
484
|
+
|
|
485
|
+
def handle_obj(obj_dict: dict):
|
|
486
|
+
nonlocal last_token_count
|
|
487
|
+
msg_type_inner, payload_inner, outer_type_inner = self._normalize_event(obj_dict)
|
|
488
|
+
item_id_inner = self._normalize_item_id(payload_inner, outer_type_inner)
|
|
489
|
+
|
|
490
|
+
if (
|
|
491
|
+
item_id_inner
|
|
492
|
+
and isinstance(obj_dict.get("item"), dict)
|
|
493
|
+
and not obj_dict["item"].get("id")
|
|
494
|
+
):
|
|
495
|
+
obj_dict["item"]["id"] = item_id_inner
|
|
496
|
+
|
|
497
|
+
if msg_type_inner == "token_count":
|
|
498
|
+
last_token_count = obj_dict
|
|
499
|
+
return # suppress
|
|
500
|
+
|
|
501
|
+
if msg_type_inner and msg_type_inner in hide_types:
|
|
502
|
+
return # suppress
|
|
503
|
+
|
|
504
|
+
pretty_line_inner = self._format_msg_pretty(
|
|
505
|
+
msg_type_inner,
|
|
506
|
+
payload_inner,
|
|
507
|
+
outer_type_inner,
|
|
508
|
+
item_id=item_id_inner,
|
|
509
|
+
)
|
|
510
|
+
if pretty_line_inner is not None:
|
|
511
|
+
print(pretty_line_inner, flush=True)
|
|
512
|
+
else:
|
|
513
|
+
# print normalized JSON
|
|
514
|
+
print(json.dumps(obj_dict, ensure_ascii=False), flush=True)
|
|
515
|
+
|
|
516
|
+
pending = ""
|
|
517
|
+
|
|
277
518
|
if process.stdout:
|
|
278
519
|
for raw_line in process.stdout:
|
|
279
|
-
|
|
280
|
-
if not
|
|
520
|
+
combined = pending + raw_line
|
|
521
|
+
if not combined.strip():
|
|
522
|
+
pending = ""
|
|
281
523
|
continue
|
|
282
|
-
# Try to parse NDJSON and filter by msg.type
|
|
283
|
-
try:
|
|
284
|
-
obj = None
|
|
285
|
-
s = line.strip()
|
|
286
524
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
lbrace = s.find("{")
|
|
297
|
-
rbrace = s.rfind("}")
|
|
298
|
-
if lbrace != -1 and rbrace != -1 and rbrace > lbrace:
|
|
299
|
-
candidate = s[lbrace:rbrace + 1]
|
|
300
|
-
try:
|
|
301
|
-
obj = json.loads(candidate)
|
|
302
|
-
s = candidate # normalized JSON text
|
|
303
|
-
except Exception:
|
|
304
|
-
obj = None
|
|
305
|
-
|
|
306
|
-
def handle_obj(obj_dict: dict):
|
|
307
|
-
nonlocal last_token_count
|
|
308
|
-
msg = obj_dict.get("msg") or {}
|
|
309
|
-
msg_type_inner = (msg.get("type") or "").strip()
|
|
310
|
-
|
|
311
|
-
if msg_type_inner == "token_count":
|
|
312
|
-
last_token_count = obj_dict
|
|
313
|
-
return # suppress
|
|
314
|
-
|
|
315
|
-
if msg_type_inner and msg_type_inner in hide_types:
|
|
316
|
-
return # suppress
|
|
317
|
-
|
|
318
|
-
pretty_line_inner = self._format_msg_pretty(obj_dict)
|
|
319
|
-
if pretty_line_inner is not None:
|
|
320
|
-
print(pretty_line_inner, flush=True)
|
|
321
|
-
else:
|
|
322
|
-
# print normalized JSON
|
|
323
|
-
print(json.dumps(obj_dict, ensure_ascii=False), flush=True)
|
|
324
|
-
|
|
325
|
-
if isinstance(obj, dict):
|
|
326
|
-
handle_obj(obj)
|
|
525
|
+
# If no braces present at all, treat as plain text (with suppression)
|
|
526
|
+
if "{" not in combined and "}" not in combined:
|
|
527
|
+
lower = combined.lower()
|
|
528
|
+
if (
|
|
529
|
+
'"token_count"' in lower
|
|
530
|
+
or '"exec_command_output_delta"' in lower
|
|
531
|
+
or '"turn_diff"' in lower
|
|
532
|
+
):
|
|
533
|
+
pending = ""
|
|
327
534
|
continue
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
started = True
|
|
356
|
-
buf.append(ch)
|
|
357
|
-
continue
|
|
358
|
-
if ch == '}':
|
|
359
|
-
depth -= 1
|
|
360
|
-
buf.append(ch)
|
|
361
|
-
if started and depth == 0:
|
|
362
|
-
candidate = ''.join(buf).strip().strip("'\"")
|
|
363
|
-
if candidate:
|
|
364
|
-
objs.append(candidate)
|
|
365
|
-
buf = []
|
|
366
|
-
started = False
|
|
367
|
-
continue
|
|
368
|
-
# accumulate only if currently collecting an object
|
|
369
|
-
if started:
|
|
370
|
-
buf.append(ch)
|
|
371
|
-
return objs
|
|
372
|
-
|
|
373
|
-
parts = split_concatenated_json(s)
|
|
374
|
-
if parts:
|
|
375
|
-
for part in parts:
|
|
376
|
-
try:
|
|
377
|
-
sub = json.loads(part)
|
|
378
|
-
if isinstance(sub, dict):
|
|
379
|
-
handle_obj(sub)
|
|
380
|
-
else:
|
|
381
|
-
low = part.lower()
|
|
382
|
-
if (
|
|
383
|
-
'"token_count"' in low
|
|
384
|
-
or '"exec_command_output_delta"' in low
|
|
385
|
-
or '"turn_diff"' in low
|
|
386
|
-
):
|
|
387
|
-
continue
|
|
388
|
-
print(part, flush=True)
|
|
389
|
-
except Exception:
|
|
535
|
+
print(combined, end="" if combined.endswith("\n") else "\n", flush=True)
|
|
536
|
+
pending = ""
|
|
537
|
+
continue
|
|
538
|
+
|
|
539
|
+
# Preserve and emit any prefix before the first brace
|
|
540
|
+
first_brace = combined.find("{")
|
|
541
|
+
if first_brace > 0:
|
|
542
|
+
prefix = combined[:first_brace]
|
|
543
|
+
lower_prefix = prefix.lower()
|
|
544
|
+
if (
|
|
545
|
+
'"token_count"' not in lower_prefix
|
|
546
|
+
and '"exec_command_output_delta"' not in lower_prefix
|
|
547
|
+
and '"turn_diff"' not in lower_prefix
|
|
548
|
+
and prefix.strip()
|
|
549
|
+
):
|
|
550
|
+
print(prefix, end="" if prefix.endswith("\n") else "\n", flush=True)
|
|
551
|
+
combined = combined[first_brace:]
|
|
552
|
+
|
|
553
|
+
parts, pending = split_json_stream(combined)
|
|
554
|
+
|
|
555
|
+
if parts:
|
|
556
|
+
for part in parts:
|
|
557
|
+
try:
|
|
558
|
+
sub = json.loads(part)
|
|
559
|
+
if isinstance(sub, dict):
|
|
560
|
+
handle_obj(sub)
|
|
561
|
+
else:
|
|
390
562
|
low = part.lower()
|
|
391
563
|
if (
|
|
392
564
|
'"token_count"' in low
|
|
@@ -395,31 +567,47 @@ Environment Variables:
|
|
|
395
567
|
):
|
|
396
568
|
continue
|
|
397
569
|
print(part, flush=True)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
570
|
+
except Exception:
|
|
571
|
+
low = part.lower()
|
|
572
|
+
if (
|
|
573
|
+
'"token_count"' in low
|
|
574
|
+
or '"exec_command_output_delta"' in low
|
|
575
|
+
or '"turn_diff"' in low
|
|
576
|
+
):
|
|
577
|
+
continue
|
|
578
|
+
print(part, flush=True)
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
# No complete object found yet; keep buffering if likely in the middle of one
|
|
582
|
+
if pending:
|
|
583
|
+
continue
|
|
584
|
+
|
|
585
|
+
# Fallback for malformed/non-JSON lines that still contain braces
|
|
586
|
+
lower = combined.lower()
|
|
587
|
+
if (
|
|
588
|
+
'"token_count"' in lower
|
|
589
|
+
or '"exec_command_output_delta"' in lower
|
|
590
|
+
or '"turn_diff"' in lower
|
|
591
|
+
):
|
|
592
|
+
continue
|
|
593
|
+
print(combined, end="" if combined.endswith("\n") else "\n", flush=True)
|
|
594
|
+
|
|
595
|
+
# Flush any pending buffered content after the stream ends
|
|
596
|
+
if pending.strip():
|
|
597
|
+
try:
|
|
598
|
+
tail_obj = json.loads(pending)
|
|
599
|
+
if isinstance(tail_obj, dict):
|
|
600
|
+
handle_obj(tail_obj)
|
|
601
|
+
else:
|
|
602
|
+
print(pending, flush=True)
|
|
603
|
+
except Exception:
|
|
604
|
+
low_tail = pending.lower()
|
|
605
|
+
if (
|
|
606
|
+
'"token_count"' not in low_tail
|
|
607
|
+
and '"exec_command_output_delta"' not in low_tail
|
|
608
|
+
and '"turn_diff"' not in low_tail
|
|
609
|
+
):
|
|
610
|
+
print(pending, flush=True)
|
|
423
611
|
|
|
424
612
|
# Wait for process completion
|
|
425
613
|
process.wait()
|