aisc-cli 0.1.2__tar.gz → 0.1.3__tar.gz

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 (101) hide show
  1. {aisc_cli-0.1.2/src/aisc_cli.egg-info → aisc_cli-0.1.3}/PKG-INFO +1 -1
  2. aisc_cli-0.1.3/src/aisc/VERSION +1 -0
  3. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/conversation.py +290 -0
  4. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/docker_lifecycle.py +1249 -1028
  5. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/session.py +28 -0
  6. aisc_cli-0.1.3/src/aisc/application/worklog.py +258 -0
  7. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/build.py +55 -4
  8. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/cc_switch.py +11 -7
  9. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/conversation.py +23 -0
  10. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve.py +18 -2
  11. aisc_cli-0.1.3/src/aisc/cli/commands/worklog.py +74 -0
  12. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/main.py +156 -0
  13. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/models.py +5 -0
  14. {aisc_cli-0.1.2 → aisc_cli-0.1.3/src/aisc_cli.egg-info}/PKG-INFO +1 -1
  15. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/SOURCES.txt +2 -0
  16. aisc_cli-0.1.2/src/aisc/VERSION +0 -1
  17. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/LICENSE +0 -0
  18. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/MANIFEST.in +0 -0
  19. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/README.md +0 -0
  20. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/pyproject.toml +0 -0
  21. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/setup.cfg +0 -0
  22. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/__init__.py +0 -0
  23. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/__main__.py +0 -0
  24. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/__init__.py +0 -0
  25. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/config_reader.py +0 -0
  26. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/container_registry.py +0 -0
  27. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/data_root_store.py +0 -0
  28. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/docker_.py +0 -0
  29. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/docker_gateway.py +0 -0
  30. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/docker_sdk_backed.py +0 -0
  31. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/maintenance_lock.py +0 -0
  32. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/state_file.py +0 -0
  33. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/system.py +0 -0
  34. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/windows_config_reader.py +0 -0
  35. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/workspace_lease_store.py +0 -0
  36. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/__init__.py +0 -0
  37. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/artifact.py +0 -0
  38. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/bundle_fetch.py +0 -0
  39. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_provider.py +0 -0
  40. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_resolver.py +0 -0
  41. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/config_service.py +0 -0
  42. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/data_migration.py +0 -0
  43. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/data_root.py +0 -0
  44. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/doctor.py +0 -0
  45. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/legacy_scan.py +0 -0
  46. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/network_subscription.py +0 -0
  47. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/profile_service.py +0 -0
  48. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/provider.py +0 -0
  49. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/repair.py +0 -0
  50. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/resources.py +0 -0
  51. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/runtime.py +0 -0
  52. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/tool_versions.py +0 -0
  53. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/toolchain.py +0 -0
  54. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/update.py +0 -0
  55. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/usage.py +0 -0
  56. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/version.py +0 -0
  57. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/web_gateway.py +0 -0
  58. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/workspace_reconcile.py +0 -0
  59. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/applog.py +0 -0
  60. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/__init__.py +0 -0
  61. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/build_progress_parser.py +0 -0
  62. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/__init__.py +0 -0
  63. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/agents.py +0 -0
  64. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/artifact.py +0 -0
  65. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/bundle.py +0 -0
  66. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/config.py +0 -0
  67. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/container.py +0 -0
  68. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/data_root.py +0 -0
  69. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/logs.py +0 -0
  70. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/network.py +0 -0
  71. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/profile.py +0 -0
  72. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/provider.py +0 -0
  73. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/run.py +0 -0
  74. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/runs.py +0 -0
  75. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/runtime.py +0 -0
  76. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve_fs.py +0 -0
  77. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/session.py +0 -0
  78. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/update.py +0 -0
  79. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/usage.py +0 -0
  80. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/wizard.py +0 -0
  81. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/workspaces.py +0 -0
  82. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/help_i18n.py +0 -0
  83. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/output.py +0 -0
  84. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/tutorial.py +0 -0
  85. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/__init__.py +0 -0
  86. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/artifacts.py +0 -0
  87. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/cc_switch_release.py +0 -0
  88. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/config.py +0 -0
  89. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/data_migration.py +0 -0
  90. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/data_root.py +0 -0
  91. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/docker_ownership.py +0 -0
  92. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/gateway.py +0 -0
  93. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/toolchain.py +0 -0
  94. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/web_services.py +0 -0
  95. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/workspace_lease.py +0 -0
  96. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/schemas/__init__.py +0 -0
  97. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/schemas/config_schema.py +0 -0
  98. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/dependency_links.txt +0 -0
  99. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/entry_points.txt +0 -0
  100. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/requires.txt +0 -0
  101. {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisc-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: AISC CLI — Super Claude workstation management
5
5
  Author: AISC contributors
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ 0.1.3
@@ -514,3 +514,293 @@ def rename_conversation(workspace: str, conversation_id: str, agent: str,
514
514
  "agent": agent,
515
515
  "title": clean,
516
516
  }
517
+
518
+
519
+ # ---------------------------------------------------------------------------
520
+ # Conversation read (批 2 聊天式 UI, 2026-09-21): normalized read-only stream.
521
+ # Provider transcripts stay strictly read-only (module-head rule); this layer
522
+ # only PROJECTS them into a stable rendering schema — no writes anywhere.
523
+ # ---------------------------------------------------------------------------
524
+
525
+ CONVERSATION_READ_SCHEMA = "aisc.conversation-read/v1"
526
+ _READ_TEXT_CAP = 8000 # per user/assistant message (chars)
527
+ _READ_REASONING_CAP = 4000
528
+ _READ_TOOL_CAP = 4000 # per tool_use input / tool_result output
529
+ _READ_MAX_MESSAGES = 4000 # hard bound on the rendered stream
530
+
531
+
532
+ def _read_join_text(content: Any, block_type: str) -> Optional[str]:
533
+ """Join EVERY ``block_type`` text block (unlike _text_from_content, which
534
+ returns the first). A chat bubble must not silently drop the rest of a
535
+ multi-block message."""
536
+ if isinstance(content, str):
537
+ return content
538
+ if isinstance(content, list):
539
+ parts = []
540
+ for block in content:
541
+ if isinstance(block, dict) and block.get("type") == block_type:
542
+ text = block.get("text")
543
+ if isinstance(text, str) and text.strip():
544
+ parts.append(text)
545
+ if parts:
546
+ return "\n\n".join(parts)
547
+ return None
548
+
549
+
550
+ def _read_as_text(value: Any) -> Optional[str]:
551
+ """Best-effort text for tool arguments/outputs: strings pass through,
552
+ structured payloads serialize (bounded by the caller's cap)."""
553
+ if value is None:
554
+ return None
555
+ if isinstance(value, str):
556
+ return value
557
+ try:
558
+ return json.dumps(value, ensure_ascii=False, indent=1)
559
+ except (TypeError, ValueError):
560
+ return str(value)
561
+
562
+
563
+ def _read_cap(text: Any, cap: int) -> str:
564
+ """Coerce to str and hard-cap, marking the cut in the text itself (the
565
+ chat pane renders plain text — a side flag would be invisible there)."""
566
+ out = text if isinstance(text, str) else ("" if text is None else str(text))
567
+ if len(out) > cap:
568
+ return out[:cap] + f"\n…[截断,共 {len(out)} 字符]"
569
+ return out
570
+
571
+
572
+ def _read_summary_text(summary: Any) -> Optional[str]:
573
+ """Codex reasoning payload: ``summary`` is a list of summary blocks."""
574
+ if isinstance(summary, list):
575
+ parts = []
576
+ for block in summary:
577
+ if isinstance(block, dict) and isinstance(block.get("text"), str) \
578
+ and block["text"].strip():
579
+ parts.append(block["text"])
580
+ if parts:
581
+ return "\n\n".join(parts)
582
+ if isinstance(summary, str) and summary.strip():
583
+ return summary
584
+ return None
585
+
586
+
587
+ def _read_entries_codex(obj: Dict[str, Any]) -> List[Dict[str, Any]]:
588
+ """One codex rollout line → zero or more rendered entries. Rendered:
589
+ user/assistant messages, reasoning summaries, function calls/results.
590
+ Dropped: session_meta, event_msg, world_state, turn_context, developer
591
+ messages, injected-context user blocks."""
592
+ if obj.get("type") != "response_item":
593
+ return []
594
+ payload = obj.get("payload")
595
+ if not isinstance(payload, dict):
596
+ return []
597
+ ts = obj.get("timestamp") if isinstance(obj.get("timestamp"), str) else None
598
+ kind = payload.get("type")
599
+ if kind == "message":
600
+ role = payload.get("role")
601
+ if role == "user":
602
+ text = _read_join_text(payload.get("content"), "input_text")
603
+ if not text or not text.strip():
604
+ return []
605
+ if _CONTEXT_TITLE_RE.match(text.lstrip()):
606
+ return []
607
+ return [{"role": "user", "kind": "message", "text": text, "ts": ts}]
608
+ if role == "assistant":
609
+ text = _read_join_text(payload.get("content"), "output_text")
610
+ if not text or not text.strip():
611
+ return []
612
+ return [{"role": "assistant", "kind": "message", "text": text, "ts": ts}]
613
+ return []
614
+ if kind == "reasoning":
615
+ text = _read_summary_text(payload.get("summary"))
616
+ if not text:
617
+ return []
618
+ return [{"role": "assistant", "kind": "reasoning", "text": text, "ts": ts}]
619
+ if kind == "function_call":
620
+ name = payload.get("name")
621
+ call_id = payload.get("call_id")
622
+ return [{
623
+ "role": "assistant", "kind": "tool_use",
624
+ "name": name if isinstance(name, str) else "tool",
625
+ "text": _read_as_text(payload.get("arguments")),
626
+ "call_id": call_id if isinstance(call_id, str) else None,
627
+ "ts": ts,
628
+ }]
629
+ if kind == "function_call_output":
630
+ call_id = payload.get("call_id")
631
+ return [{
632
+ "role": "tool", "kind": "tool_result",
633
+ "text": _read_as_text(payload.get("output")),
634
+ "call_id": call_id if isinstance(call_id, str) else None,
635
+ "ts": ts,
636
+ }]
637
+ return []
638
+
639
+
640
+ def _read_entries_claude(obj: Dict[str, Any]) -> List[Dict[str, Any]]:
641
+ """One Claude session line → zero or more rendered entries. Rendered:
642
+ user/assistant text, thinking, tool_use, tool_result. Dropped:
643
+ queue-operation/attachment/last-prompt/atis-latch and sidechain
644
+ (subagent) transcripts — the main thread only."""
645
+ if obj.get("type") not in ("user", "assistant"):
646
+ return []
647
+ if obj.get("isSidechain") is True:
648
+ return []
649
+ msg = obj.get("message")
650
+ if not isinstance(msg, dict):
651
+ return []
652
+ ts = obj.get("timestamp") if isinstance(obj.get("timestamp"), str) else None
653
+ role = "user" if obj.get("type") == "user" else "assistant"
654
+ content = msg.get("content")
655
+ out: List[Dict[str, Any]] = []
656
+
657
+ def _push_text(text: Any) -> None:
658
+ if isinstance(text, str) and text.strip():
659
+ if role == "user" and _CONTEXT_TITLE_RE.match(text.lstrip()):
660
+ return
661
+ out.append({"role": role, "kind": "message", "text": text, "ts": ts})
662
+
663
+ if isinstance(content, str):
664
+ _push_text(content)
665
+ return out
666
+ if isinstance(content, list):
667
+ for block in content:
668
+ if not isinstance(block, dict):
669
+ continue
670
+ bt = block.get("type")
671
+ if bt == "text":
672
+ _push_text(block.get("text"))
673
+ elif bt == "thinking":
674
+ text = block.get("thinking")
675
+ if isinstance(text, str) and text.strip():
676
+ out.append({"role": "assistant", "kind": "reasoning",
677
+ "text": text, "ts": ts})
678
+ elif bt == "tool_use":
679
+ call_id = block.get("id")
680
+ out.append({
681
+ "role": "assistant", "kind": "tool_use",
682
+ "name": block.get("name") if isinstance(block.get("name"), str) else "tool",
683
+ "text": _read_as_text(block.get("input")),
684
+ "call_id": call_id if isinstance(call_id, str) else None,
685
+ "ts": ts,
686
+ })
687
+ elif bt == "tool_result":
688
+ inner = block.get("content")
689
+ text = inner if isinstance(inner, str) else _read_join_text(inner, "text")
690
+ call_id = block.get("tool_use_id")
691
+ out.append({
692
+ "role": "tool", "kind": "tool_result",
693
+ "text": text,
694
+ "call_id": call_id if isinstance(call_id, str) else None,
695
+ "ts": ts,
696
+ })
697
+ return out
698
+
699
+
700
+ _READ_EXTRACTORS = {
701
+ "codex": _read_entries_codex,
702
+ "claude": _read_entries_claude,
703
+ }
704
+
705
+
706
+ def read_conversation(workspace: str, conversation_id: str, agent: str,
707
+ tail: Optional[int] = None,
708
+ before: Optional[int] = None) -> Dict[str, Any]:
709
+ """Project a provider transcript into the chat rendering stream.
710
+
711
+ Pagination walks the RENDERED stream backwards: ``tail`` returns the
712
+ last ``tail`` entries; ``before`` (0-based exclusive ordinal) returns
713
+ the window immediately preceding it. Ordinals are stable per file
714
+ version — the UI pages with ``before=start`` until start reaches 0.
715
+ Malformed lines are skipped and counted, never fatal (list semantics)."""
716
+ if not is_conversation_uuid(conversation_id):
717
+ raise CliError(
718
+ message=f"Invalid conversation ID: {conversation_id}",
719
+ exit_code=2,
720
+ error_code=ERROR_INVALID_ID,
721
+ )
722
+ if agent not in _AGENTS:
723
+ raise CliError(
724
+ message=f"Unsupported agent: {agent}",
725
+ exit_code=2,
726
+ error_code=ERROR_INVALID_AGENT,
727
+ )
728
+ ws_dir = _workspace_dir(workspace)
729
+ path = _find_conversation_file(ws_dir, agent, conversation_id)
730
+ if path is None:
731
+ raise CliError(
732
+ message=f"Conversation {conversation_id} not found for agent {agent}",
733
+ exit_code=3,
734
+ error_code=ERROR_UNRESUMABLE,
735
+ )
736
+ extract = _READ_EXTRACTORS[agent]
737
+ entries: List[Dict[str, Any]] = []
738
+ malformed_lines = 0
739
+ try:
740
+ fh = path.open("r", encoding="utf-8", errors="replace")
741
+ except OSError as e:
742
+ raise CliError(
743
+ message=f"Conversation file unreadable: {e}",
744
+ exit_code=3,
745
+ error_code=ERROR_UNRESUMABLE,
746
+ )
747
+ with fh:
748
+ for line in fh:
749
+ line = line.strip()
750
+ if not line:
751
+ continue
752
+ try:
753
+ obj = json.loads(line)
754
+ except (ValueError, TypeError):
755
+ malformed_lines += 1
756
+ continue
757
+ if not isinstance(obj, dict):
758
+ continue
759
+ for entry in extract(obj):
760
+ if len(entries) >= _READ_MAX_MESSAGES:
761
+ break # stream bound; the tail window still pages correctly
762
+ entries.append(entry)
763
+ # Pair tool results into their calls (nearest preceding unmatched match);
764
+ # unpaired results stay standalone so nothing is silently dropped.
765
+ open_calls: Dict[str, Dict[str, Any]] = {}
766
+ for entry in entries:
767
+ if entry["kind"] == "tool_use" and entry.get("call_id"):
768
+ open_calls[entry["call_id"]] = entry
769
+ elif entry["kind"] == "tool_result" and entry.get("call_id"):
770
+ call = open_calls.get(entry["call_id"])
771
+ if call is not None and "result" not in call:
772
+ call["result"] = entry.pop("text")
773
+ entry["consumed"] = True
774
+ rendered = [e for e in entries if not e.get("consumed")]
775
+ for i, entry in enumerate(rendered):
776
+ entry["ordinal"] = i
777
+ cap = _READ_TEXT_CAP
778
+ if entry["kind"] == "reasoning":
779
+ cap = _READ_REASONING_CAP
780
+ elif entry["kind"] == "tool_use":
781
+ entry["text"] = _read_cap(entry.get("text"), _READ_TOOL_CAP)
782
+ if "result" in entry:
783
+ entry["result"] = _read_cap(entry["result"], _READ_TOOL_CAP)
784
+ continue
785
+ elif entry["kind"] == "tool_result":
786
+ cap = _READ_TOOL_CAP
787
+ entry["text"] = _read_cap(entry.get("text"), cap)
788
+
789
+ total = len(rendered)
790
+ start = 0
791
+ end = total
792
+ if before is not None:
793
+ end = max(0, min(int(before), total))
794
+ if tail is not None and tail > 0:
795
+ start = max(0, end - int(tail))
796
+ return {
797
+ "schema": CONVERSATION_READ_SCHEMA,
798
+ "conversation_id": conversation_id,
799
+ "agent": agent,
800
+ "file_size": path.stat().st_size,
801
+ "total": total,
802
+ "start": start,
803
+ "messages": rendered[start:end],
804
+ "malformed_lines": malformed_lines,
805
+ "degraded_reason": "malformed" if malformed_lines else None,
806
+ }