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.
- {aisc_cli-0.1.2/src/aisc_cli.egg-info → aisc_cli-0.1.3}/PKG-INFO +1 -1
- aisc_cli-0.1.3/src/aisc/VERSION +1 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/conversation.py +290 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/docker_lifecycle.py +1249 -1028
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/session.py +28 -0
- aisc_cli-0.1.3/src/aisc/application/worklog.py +258 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/build.py +55 -4
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/cc_switch.py +11 -7
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/conversation.py +23 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve.py +18 -2
- aisc_cli-0.1.3/src/aisc/cli/commands/worklog.py +74 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/main.py +156 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/models.py +5 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3/src/aisc_cli.egg-info}/PKG-INFO +1 -1
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/SOURCES.txt +2 -0
- aisc_cli-0.1.2/src/aisc/VERSION +0 -1
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/LICENSE +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/MANIFEST.in +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/README.md +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/pyproject.toml +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/setup.cfg +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/__main__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/config_reader.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/container_registry.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/data_root_store.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/docker_.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/docker_gateway.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/docker_sdk_backed.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/maintenance_lock.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/state_file.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/system.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/windows_config_reader.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/adapters/workspace_lease_store.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/artifact.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/bundle_fetch.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_provider.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_resolver.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/config_service.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/data_migration.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/data_root.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/doctor.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/legacy_scan.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/network_subscription.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/profile_service.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/provider.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/repair.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/resources.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/runtime.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/tool_versions.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/toolchain.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/update.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/usage.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/version.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/web_gateway.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/application/workspace_reconcile.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/applog.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/build_progress_parser.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/agents.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/artifact.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/bundle.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/config.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/container.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/data_root.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/logs.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/network.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/profile.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/provider.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/run.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/runs.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/runtime.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve_fs.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/session.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/update.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/usage.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/wizard.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/commands/workspaces.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/help_i18n.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/output.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/cli/tutorial.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/artifacts.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/cc_switch_release.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/config.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/data_migration.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/data_root.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/docker_ownership.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/gateway.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/toolchain.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/web_services.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/domain/workspace_lease.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/schemas/__init__.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc/schemas/config_schema.py +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/dependency_links.txt +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/entry_points.txt +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/requires.txt +0 -0
- {aisc_cli-0.1.2 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/top_level.txt +0 -0
|
@@ -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
|
+
}
|