trinity-agent 0.7.1__tar.gz → 0.7.2__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.
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/PKG-INFO +8 -4
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/README.en.md +8 -3
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/README.md +7 -3
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/checkpoint.md +2 -2
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/workflow-v0.7.0-guide.md +2 -1
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/pyproject.toml +1 -1
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/__init__.py +1 -1
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/app.py +1 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/prompt.py +25 -2
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/session.py +131 -0
- trinity_agent-0.7.2/src/trinity/workflow/persistence.py +207 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tui_prompt.py +23 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tui_session.py +42 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_workflow_persistence.py +38 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/uv.lock +1 -1
- trinity_agent-0.7.1/src/trinity/workflow/persistence.py +0 -86
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/.github/workflows/publish-pypi.yml +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/.gitignore +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/LICENSE +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/phase-6-plan.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-phase-10-interactive-redesign.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-prompt-compression.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-token-analytics.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-token-optimization-phase7b.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-tui-overhaul.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-v0.6.9-interactive-error-analysis.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-03-phase10-remaining.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-03-v0.7.0-workflow-engine-redesign.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-04-isolated-provider-bootstrap.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-04-v0.7.0-follow-up-implementation-candidates.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/reference-architecture.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/2026-06-04-isolated-provider-bootstrap.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-1-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-10-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-2-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-3-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-4-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-5-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-6-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-7-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-9-T.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/v0.7.0-workflow-engine.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/v070-smoke-checklist.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/troubleshooting-provider-readiness.md +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/__main__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/base.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/claude_agent.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/codex_agent.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/factory.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/gemini_agent.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/response_cleaner.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/cli.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/base.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/hook.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/idle.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/marker.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/prompt.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/config.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/analytics.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/budget.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/compressor.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/monitor.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/rotator.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/shared.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/consensus.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/distributor.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/protocol.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/error_handler.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/health/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/health/checker.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/i18n.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/logging.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/models.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/orchestrator.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/providers/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/providers/bootstrap.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/providers/readiness.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/retry.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/setup/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/setup/detector.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/setup/wizard.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/layout.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/pane.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/session.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/events.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/theme.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/decomposer.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/engine.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/execution.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/ledger.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/lifecycle.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/models.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/review.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/structured.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workspace/__init__.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workspace/isolation.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workspace/managed_home.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/templates/trinity.config.example +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/conftest.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_agent_factory.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_analytics.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_blueprint_decomposer.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_budget.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_claude_agent.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_cli.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_cli_detector.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_cli_v2.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_codex_agent.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_completion.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_compressor.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_config.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_consensus_v2.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_context_monitor.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_deliberation.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_e2e.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_error_handling.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_execution_protocol.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_gemini_agent.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_health_checker.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_i18n.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_interactive_claude.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_lifecycle_guard.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_logging.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_managed_home.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_models.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_multi_provider.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_orchestrator.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_peer_review.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_protocol.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_protocol_compression.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_protocol_v2.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_provider_bootstrap.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_provider_readiness.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_response_cleaner.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_response_contract.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_retry.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_rotator.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_session_handoff.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_session_rotator.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_setup_wizard.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_shared_context.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_shared_ledger.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_structured_consensus.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tmux.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tmux_integration.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tmux_layout.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tui.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_workflow_engine.py +0 -0
- {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trinity-agent
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Three minds, one context — Multi-agent AI orchestrator for Claude Code, Codex, and Gemini CLI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/hongdangmoo49/Trinity
|
|
6
6
|
Project-URL: Repository, https://github.com/hongdangmoo49/Trinity
|
|
@@ -150,7 +150,7 @@ Trinity가 백그라운드에서 다음 단계를 자동으로 수행합니다:
|
|
|
150
150
|
Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제공하여, 에이전트 간의 실시간 토론 과정을 시각적으로 보여줍니다.
|
|
151
151
|
|
|
152
152
|
```
|
|
153
|
-
🧠 Trinity v0.7.
|
|
153
|
+
🧠 Trinity v0.7.2 — 세 개의 두뇌, 하나의 컨텍스트
|
|
154
154
|
|
|
155
155
|
🏗️ claude ✅ ⚙️ codex ✅ 🔍 gemini ✅
|
|
156
156
|
|
|
@@ -228,9 +228,13 @@ Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제
|
|
|
228
228
|
| `/agent <이름> on\|off` | 특정 에이전트를 즉시 활성화하거나 비활성화 |
|
|
229
229
|
| `/history` | 이전 라운드의 토론 히스토리 요약 조회 |
|
|
230
230
|
| `/save` | 현재 토론 세션의 전체 결과를 파일로 영구 저장 |
|
|
231
|
+
| `/resume [N\|latest\|ID]` | 저장된 workflow 세션을 선택해 재개 |
|
|
231
232
|
| `/help` | 사용 가능한 인라인 명령어 도움말 표시 |
|
|
232
233
|
| `/quit` | Trinity 종료 및 백그라운드 리소스 정리 |
|
|
233
234
|
|
|
235
|
+
TUI는 기본적으로 새 workflow 세션으로 시작한다. 이전 active workflow는
|
|
236
|
+
`.trinity/workflow/history/`에 보존되며 `/resume`으로 명시적으로 재개한다.
|
|
237
|
+
|
|
234
238
|
---
|
|
235
239
|
|
|
236
240
|
## ⚙️ 설정
|
|
@@ -408,8 +412,8 @@ uv publish --token <PYPI_TOKEN>
|
|
|
408
412
|
|
|
409
413
|
| 지표 | 수치 |
|
|
410
414
|
| :--- | :--- |
|
|
411
|
-
| **버전** | 0.7.
|
|
412
|
-
| **테스트** |
|
|
415
|
+
| **버전** | 0.7.2 |
|
|
416
|
+
| **테스트** | 915개 테스트 통과 |
|
|
413
417
|
| **커버리지** | 약 87% |
|
|
414
418
|
| **소스 파일** | 50여 개 |
|
|
415
419
|
| **주요 의존성 라이브러리** | `click`, `rich`, `prompt_toolkit`, `tomli` |
|
|
@@ -124,7 +124,7 @@ That's it. Trinity will:
|
|
|
124
124
|
Trinity features a **Rich-based terminal UI** with real-time deliberation visualization.
|
|
125
125
|
|
|
126
126
|
```
|
|
127
|
-
🧠 Trinity v0.7.
|
|
127
|
+
🧠 Trinity v0.7.2 — Three minds, one context
|
|
128
128
|
|
|
129
129
|
🏗️ claude ✅ ⚙️ codex ✅ 🔍 gemini ✅
|
|
130
130
|
|
|
@@ -202,9 +202,14 @@ Inside the interactive TUI (`trinity` with no args):
|
|
|
202
202
|
| `/agent <name> on\|off` | Enable/disable an agent |
|
|
203
203
|
| `/history` | Show deliberation history |
|
|
204
204
|
| `/save` | Save session results to file |
|
|
205
|
+
| `/resume [N\|latest\|ID]` | Select and resume a saved workflow session |
|
|
205
206
|
| `/help` | Show help |
|
|
206
207
|
| `/quit` | Exit Trinity |
|
|
207
208
|
|
|
209
|
+
The TUI starts with a new workflow session by default. The previous active
|
|
210
|
+
workflow is preserved in `.trinity/workflow/history/` and can be resumed
|
|
211
|
+
explicitly with `/resume`.
|
|
212
|
+
|
|
208
213
|
---
|
|
209
214
|
|
|
210
215
|
## ⚙️ Configuration
|
|
@@ -382,8 +387,8 @@ uv publish --token <PYPI_TOKEN>
|
|
|
382
387
|
|
|
383
388
|
| Metric | Value |
|
|
384
389
|
| :--- | :--- |
|
|
385
|
-
| **Version** | 0.7.
|
|
386
|
-
| **Tests** |
|
|
390
|
+
| **Version** | 0.7.2 |
|
|
391
|
+
| **Tests** | 915 passed |
|
|
387
392
|
| **Coverage** | ~87% |
|
|
388
393
|
| **Source files** | 50+ |
|
|
389
394
|
| **Dependencies** | `click`, `rich`, `prompt_toolkit`, `tomli` |
|
|
@@ -122,7 +122,7 @@ Trinity가 백그라운드에서 다음 단계를 자동으로 수행합니다:
|
|
|
122
122
|
Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제공하여, 에이전트 간의 실시간 토론 과정을 시각적으로 보여줍니다.
|
|
123
123
|
|
|
124
124
|
```
|
|
125
|
-
🧠 Trinity v0.7.
|
|
125
|
+
🧠 Trinity v0.7.2 — 세 개의 두뇌, 하나의 컨텍스트
|
|
126
126
|
|
|
127
127
|
🏗️ claude ✅ ⚙️ codex ✅ 🔍 gemini ✅
|
|
128
128
|
|
|
@@ -200,9 +200,13 @@ Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제
|
|
|
200
200
|
| `/agent <이름> on\|off` | 특정 에이전트를 즉시 활성화하거나 비활성화 |
|
|
201
201
|
| `/history` | 이전 라운드의 토론 히스토리 요약 조회 |
|
|
202
202
|
| `/save` | 현재 토론 세션의 전체 결과를 파일로 영구 저장 |
|
|
203
|
+
| `/resume [N\|latest\|ID]` | 저장된 workflow 세션을 선택해 재개 |
|
|
203
204
|
| `/help` | 사용 가능한 인라인 명령어 도움말 표시 |
|
|
204
205
|
| `/quit` | Trinity 종료 및 백그라운드 리소스 정리 |
|
|
205
206
|
|
|
207
|
+
TUI는 기본적으로 새 workflow 세션으로 시작한다. 이전 active workflow는
|
|
208
|
+
`.trinity/workflow/history/`에 보존되며 `/resume`으로 명시적으로 재개한다.
|
|
209
|
+
|
|
206
210
|
---
|
|
207
211
|
|
|
208
212
|
## ⚙️ 설정
|
|
@@ -380,8 +384,8 @@ uv publish --token <PYPI_TOKEN>
|
|
|
380
384
|
|
|
381
385
|
| 지표 | 수치 |
|
|
382
386
|
| :--- | :--- |
|
|
383
|
-
| **버전** | 0.7.
|
|
384
|
-
| **테스트** |
|
|
387
|
+
| **버전** | 0.7.2 |
|
|
388
|
+
| **테스트** | 915개 테스트 통과 |
|
|
385
389
|
| **커버리지** | 약 87% |
|
|
386
390
|
| **소스 파일** | 50여 개 |
|
|
387
391
|
| **주요 의존성 라이브러리** | `click`, `rich`, `prompt_toolkit`, `tomli` |
|
|
@@ -29,7 +29,7 @@ trinity
|
|
|
29
29
|
|
|
30
30
|
새 목표를 입력하면 Trinity는 다음 순서로 진행한다.
|
|
31
31
|
|
|
32
|
-
1.
|
|
32
|
+
1. 새 workflow session을 만든다. 이전 active workflow는 history로 보존된다.
|
|
33
33
|
2. provider readiness를 확인한다.
|
|
34
34
|
3. round 기반 deliberation을 실행한다.
|
|
35
35
|
4. structured consensus가 나오면 blueprint를 저장한다.
|
|
@@ -47,6 +47,7 @@ trinity
|
|
|
47
47
|
| `/decisions` | 사용자 또는 agent가 남긴 decision ledger를 표시한다. |
|
|
48
48
|
| `/packages` | blueprint에서 생성된 work package와 실행 상태를 표시한다. |
|
|
49
49
|
| `/subtasks` | parent agent가 보고한 provider-internal subtask/tool 사용 결과를 표시한다. |
|
|
50
|
+
| `/resume [N\|latest\|ID]` | `.trinity/workflow/history/`에 저장된 workflow를 선택해 재개한다. |
|
|
50
51
|
|
|
51
52
|
## 4. Workflow State
|
|
52
53
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trinity-agent"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Three minds, one context — Multi-agent AI orchestrator for Claude Code, Codex, and Gemini CLI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -725,6 +725,7 @@ class TrinityTUI:
|
|
|
725
725
|
" [cyan]/decisions[/cyan] — Show recorded workflow decisions\n"
|
|
726
726
|
" [cyan]/packages[/cyan] — Show workflow work packages\n"
|
|
727
727
|
" [cyan]/subtasks[/cyan] — Show delegated subtask reports\n"
|
|
728
|
+
" [cyan]/resume [n|latest|id][/cyan] — Resume a saved workflow session\n"
|
|
728
729
|
" [cyan]/help[/cyan] — Show this help\n"
|
|
729
730
|
" [cyan]/quit[/cyan] — Exit Trinity\n"
|
|
730
731
|
)
|
|
@@ -15,7 +15,8 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
from prompt_toolkit import PromptSession
|
|
17
17
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
18
|
-
from prompt_toolkit.completion import
|
|
18
|
+
from prompt_toolkit.completion import Completer, Completion, CompleteEvent
|
|
19
|
+
from prompt_toolkit.document import Document
|
|
19
20
|
from prompt_toolkit.history import FileHistory
|
|
20
21
|
from prompt_toolkit.input import DummyInput
|
|
21
22
|
from prompt_toolkit.output import DummyOutput
|
|
@@ -44,6 +45,7 @@ TRINITY_COMMANDS = [
|
|
|
44
45
|
"/decisions",
|
|
45
46
|
"/packages",
|
|
46
47
|
"/subtasks",
|
|
48
|
+
"/resume",
|
|
47
49
|
"/help",
|
|
48
50
|
"/quit",
|
|
49
51
|
]
|
|
@@ -55,6 +57,27 @@ TRINITY_STYLE = Style.from_dict({
|
|
|
55
57
|
})
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
class SlashCommandCompleter(Completer):
|
|
61
|
+
"""Complete Trinity commands only when the input begins with slash."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, commands: list[str]):
|
|
64
|
+
self.commands = commands
|
|
65
|
+
|
|
66
|
+
def get_completions(
|
|
67
|
+
self,
|
|
68
|
+
document: Document,
|
|
69
|
+
complete_event: CompleteEvent,
|
|
70
|
+
):
|
|
71
|
+
text = document.text_before_cursor
|
|
72
|
+
if not text.startswith("/") or any(char.isspace() for char in text):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
normalized = text.lower()
|
|
76
|
+
for command in self.commands:
|
|
77
|
+
if command.lower().startswith(normalized):
|
|
78
|
+
yield Completion(command, start_position=-len(text))
|
|
79
|
+
|
|
80
|
+
|
|
58
81
|
class TrinityPromptSession:
|
|
59
82
|
"""prompt_toolkit-backed input session with history and completion.
|
|
60
83
|
|
|
@@ -78,7 +101,7 @@ class TrinityPromptSession:
|
|
|
78
101
|
history=FileHistory(history_path) if history_path else None,
|
|
79
102
|
auto_suggest=AutoSuggestFromHistory(),
|
|
80
103
|
multiline=False,
|
|
81
|
-
completer=
|
|
104
|
+
completer=SlashCommandCompleter(TRINITY_COMMANDS),
|
|
82
105
|
style=TRINITY_STYLE,
|
|
83
106
|
)
|
|
84
107
|
try:
|
|
@@ -15,6 +15,7 @@ import json
|
|
|
15
15
|
import logging
|
|
16
16
|
import shlex
|
|
17
17
|
import sys
|
|
18
|
+
import time
|
|
18
19
|
from typing import TYPE_CHECKING
|
|
19
20
|
|
|
20
21
|
from rich.console import Console
|
|
@@ -32,6 +33,7 @@ from trinity.workflow import (
|
|
|
32
33
|
ExecutionResult,
|
|
33
34
|
WorkflowEngine,
|
|
34
35
|
WorkflowInputAction,
|
|
36
|
+
WorkflowPersistence,
|
|
35
37
|
WorkflowState,
|
|
36
38
|
)
|
|
37
39
|
|
|
@@ -62,6 +64,8 @@ class InteractiveSession:
|
|
|
62
64
|
self.running = False
|
|
63
65
|
self._history_file = config.effective_state_dir / "history" / "session_history.json"
|
|
64
66
|
self._prompt_session = TrinityPromptSession(config.effective_state_dir)
|
|
67
|
+
self.workflow_persistence = WorkflowPersistence(config.effective_state_dir)
|
|
68
|
+
self._startup_archive = self.workflow_persistence.archive_active_session()
|
|
65
69
|
self.workflow = WorkflowEngine(config.effective_state_dir)
|
|
66
70
|
self.tui.set_workflow_session(self.workflow.session)
|
|
67
71
|
|
|
@@ -97,6 +101,11 @@ class InteractiveSession:
|
|
|
97
101
|
def _show_welcome(self) -> None:
|
|
98
102
|
"""Show welcome message."""
|
|
99
103
|
self.console.print(self.tui.get_welcome_text())
|
|
104
|
+
if self._startup_archive:
|
|
105
|
+
self.console.print(
|
|
106
|
+
"[dim]Previous workflow saved to history. "
|
|
107
|
+
"Use /resume to restore it.[/dim]"
|
|
108
|
+
)
|
|
100
109
|
self.console.print(
|
|
101
110
|
"[dim]Type a question to start deliberation, or /help for commands.[/dim]\n"
|
|
102
111
|
)
|
|
@@ -156,6 +165,8 @@ class InteractiveSession:
|
|
|
156
165
|
self._cmd_packages()
|
|
157
166
|
elif cmd == "subtasks":
|
|
158
167
|
self._cmd_subtasks()
|
|
168
|
+
elif cmd == "resume":
|
|
169
|
+
self._cmd_resume(args)
|
|
159
170
|
else:
|
|
160
171
|
self.console.print(
|
|
161
172
|
f"[yellow]Unknown command: /{cmd}. "
|
|
@@ -542,6 +553,126 @@ class InteractiveSession:
|
|
|
542
553
|
|
|
543
554
|
self.console.print(table)
|
|
544
555
|
|
|
556
|
+
def _cmd_resume(self, args: list[str]) -> None:
|
|
557
|
+
"""Resume an archived workflow session.
|
|
558
|
+
|
|
559
|
+
Usage:
|
|
560
|
+
/resume
|
|
561
|
+
/resume latest
|
|
562
|
+
/resume <index|workflow-id>
|
|
563
|
+
"""
|
|
564
|
+
archives = self.workflow_persistence.list_archives()
|
|
565
|
+
if not archives:
|
|
566
|
+
self.console.print("[dim]No saved workflow sessions to resume.[/dim]")
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
selector = args[0] if args else ""
|
|
570
|
+
if not selector:
|
|
571
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
572
|
+
labels = [
|
|
573
|
+
self._archive_label(archive)
|
|
574
|
+
for archive in archives
|
|
575
|
+
]
|
|
576
|
+
selected = self._prompt_session.select_option(
|
|
577
|
+
title="Resume Workflow",
|
|
578
|
+
question="Select a saved workflow session.",
|
|
579
|
+
options=labels,
|
|
580
|
+
)
|
|
581
|
+
if selected is None:
|
|
582
|
+
self.console.print("[dim]Resume cancelled.[/dim]")
|
|
583
|
+
return
|
|
584
|
+
selector = selected
|
|
585
|
+
else:
|
|
586
|
+
self._print_resume_table(archives)
|
|
587
|
+
self.console.print("[dim]Usage: /resume <index|latest|workflow-id>[/dim]")
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
archive = self._resolve_archive(archives, selector)
|
|
591
|
+
if archive is None:
|
|
592
|
+
self.console.print(f"[yellow]No matching workflow session: {selector}[/yellow]")
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
archived_current = self.workflow_persistence.archive_active_session()
|
|
596
|
+
self.workflow_persistence.restore_archive(archive)
|
|
597
|
+
self.workflow = WorkflowEngine(self.config.effective_state_dir)
|
|
598
|
+
self.tui.set_workflow_session(self.workflow.session)
|
|
599
|
+
|
|
600
|
+
if archived_current:
|
|
601
|
+
self.console.print(
|
|
602
|
+
f"[dim]Current workflow saved as {archived_current.session.id}.[/dim]"
|
|
603
|
+
)
|
|
604
|
+
self.console.print(
|
|
605
|
+
f"[green]Resumed workflow {self.workflow.session.id}.[/green]"
|
|
606
|
+
)
|
|
607
|
+
self._cmd_workflow()
|
|
608
|
+
|
|
609
|
+
def _print_resume_table(self, archives) -> None:
|
|
610
|
+
"""Print archived workflow sessions for manual selection."""
|
|
611
|
+
from rich.table import Table
|
|
612
|
+
|
|
613
|
+
table = Table(title="Saved Workflow Sessions")
|
|
614
|
+
table.add_column("#", justify="right", style="cyan")
|
|
615
|
+
table.add_column("ID")
|
|
616
|
+
table.add_column("State")
|
|
617
|
+
table.add_column("Updated")
|
|
618
|
+
table.add_column("Goal")
|
|
619
|
+
|
|
620
|
+
for index, archive in enumerate(archives, 1):
|
|
621
|
+
session = archive.session
|
|
622
|
+
table.add_row(
|
|
623
|
+
str(index),
|
|
624
|
+
session.id,
|
|
625
|
+
session.state.value,
|
|
626
|
+
self._format_timestamp(session.updated_at),
|
|
627
|
+
self._short_goal(session.goal),
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
self.console.print(table)
|
|
631
|
+
|
|
632
|
+
def _resolve_archive(self, archives, selector: str):
|
|
633
|
+
"""Resolve a resume selector to one archive."""
|
|
634
|
+
normalized = selector.strip().lower()
|
|
635
|
+
if normalized in {"latest", "last", "newest"}:
|
|
636
|
+
return archives[0]
|
|
637
|
+
if normalized.isdigit():
|
|
638
|
+
index = int(normalized) - 1
|
|
639
|
+
if 0 <= index < len(archives):
|
|
640
|
+
return archives[index]
|
|
641
|
+
return None
|
|
642
|
+
return next(
|
|
643
|
+
(
|
|
644
|
+
archive
|
|
645
|
+
for archive in archives
|
|
646
|
+
if archive.session.id.lower() == normalized
|
|
647
|
+
),
|
|
648
|
+
None,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def _archive_label(self, archive) -> str:
|
|
652
|
+
"""Build a concise label for interactive resume selection."""
|
|
653
|
+
session = archive.session
|
|
654
|
+
return (
|
|
655
|
+
f"{session.id} · {session.state.value} · "
|
|
656
|
+
f"{self._format_timestamp(session.updated_at)} · "
|
|
657
|
+
f"{self._short_goal(session.goal)}"
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
@staticmethod
|
|
661
|
+
def _format_timestamp(timestamp: float) -> str:
|
|
662
|
+
"""Format a persisted Unix timestamp for the resume list."""
|
|
663
|
+
try:
|
|
664
|
+
return time.strftime("%Y-%m-%d %H:%M", time.localtime(timestamp))
|
|
665
|
+
except (OSError, ValueError):
|
|
666
|
+
return "unknown"
|
|
667
|
+
|
|
668
|
+
@staticmethod
|
|
669
|
+
def _short_goal(goal: str, limit: int = 60) -> str:
|
|
670
|
+
"""Return a compact single-line workflow goal."""
|
|
671
|
+
text = " ".join((goal or "(none)").split())
|
|
672
|
+
if len(text) <= limit:
|
|
673
|
+
return text
|
|
674
|
+
return text[: limit - 3] + "..."
|
|
675
|
+
|
|
545
676
|
# ─── Deliberation ──────────────────────────────────────────────────
|
|
546
677
|
|
|
547
678
|
def _handle_user_text(self, text: str) -> None:
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Workflow session persistence for Trinity v0.7.0."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from trinity.workflow.models import WorkflowSession
|
|
15
|
+
from trinity.workflow.models import WorkflowState
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class WorkflowArchive:
|
|
22
|
+
"""A saved workflow session available for explicit resume."""
|
|
23
|
+
|
|
24
|
+
session: WorkflowSession
|
|
25
|
+
path: Path
|
|
26
|
+
events_path: Path | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WorkflowPersistence:
|
|
30
|
+
"""Serialize workflow sessions and append-only event logs."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
state_dir: Path,
|
|
35
|
+
*,
|
|
36
|
+
state_file: Path | None = None,
|
|
37
|
+
events_file: Path | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
workflow_dir = state_dir / "workflow"
|
|
40
|
+
self.session_path = state_file or workflow_dir / "session.json"
|
|
41
|
+
self.events_path = events_file or workflow_dir / "events.jsonl"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def workflow_dir(self) -> Path:
|
|
45
|
+
"""Return the directory containing workflow persistence files."""
|
|
46
|
+
return self.session_path.parent
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def history_dir(self) -> Path:
|
|
50
|
+
"""Return the directory containing archived workflow sessions."""
|
|
51
|
+
return self.workflow_dir / "history"
|
|
52
|
+
|
|
53
|
+
def save(self, session: WorkflowSession) -> None:
|
|
54
|
+
"""Write the session to disk as JSON."""
|
|
55
|
+
self.session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
self.session_path.write_text(
|
|
57
|
+
json.dumps(session.to_dict(), indent=2, ensure_ascii=False),
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
)
|
|
60
|
+
logger.debug("Workflow session saved: %s", session.id)
|
|
61
|
+
|
|
62
|
+
def load(self) -> WorkflowSession | None:
|
|
63
|
+
"""Load a persisted session, returning None when unavailable or invalid."""
|
|
64
|
+
if not self.session_path.exists():
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(self.session_path.read_text(encoding="utf-8"))
|
|
68
|
+
if not isinstance(data, dict):
|
|
69
|
+
return None
|
|
70
|
+
return WorkflowSession.from_dict(data)
|
|
71
|
+
except (json.JSONDecodeError, OSError, ValueError):
|
|
72
|
+
logger.exception("Failed to load workflow session from %s", self.session_path)
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def append_event(self, event: Mapping[str, Any]) -> None:
|
|
76
|
+
"""Append one event dictionary to the JSONL event log."""
|
|
77
|
+
self.events_path.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
with self.events_path.open("a", encoding="utf-8") as fh:
|
|
79
|
+
fh.write(json.dumps(dict(event), ensure_ascii=False) + "\n")
|
|
80
|
+
|
|
81
|
+
def load_events(self) -> list[dict[str, Any]]:
|
|
82
|
+
"""Read event dictionaries from the JSONL event log."""
|
|
83
|
+
if not self.events_path.exists():
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
events: list[dict[str, Any]] = []
|
|
87
|
+
try:
|
|
88
|
+
with self.events_path.open("r", encoding="utf-8") as fh:
|
|
89
|
+
for line in fh:
|
|
90
|
+
stripped = line.strip()
|
|
91
|
+
if not stripped:
|
|
92
|
+
continue
|
|
93
|
+
event = json.loads(stripped)
|
|
94
|
+
if isinstance(event, dict):
|
|
95
|
+
events.append(event)
|
|
96
|
+
except (json.JSONDecodeError, OSError):
|
|
97
|
+
logger.exception("Failed to load workflow events from %s", self.events_path)
|
|
98
|
+
return events
|
|
99
|
+
|
|
100
|
+
def clear(self) -> None:
|
|
101
|
+
"""Remove persisted workflow files."""
|
|
102
|
+
for path in (self.session_path, self.events_path):
|
|
103
|
+
if path.exists():
|
|
104
|
+
path.unlink()
|
|
105
|
+
|
|
106
|
+
def archive_active_session(self, *, force: bool = False) -> WorkflowArchive | None:
|
|
107
|
+
"""Move the current active session into workflow history.
|
|
108
|
+
|
|
109
|
+
Returns the archive metadata, or None when there is no meaningful active
|
|
110
|
+
session to preserve.
|
|
111
|
+
"""
|
|
112
|
+
session = self.load()
|
|
113
|
+
if session is None:
|
|
114
|
+
self.clear()
|
|
115
|
+
return None
|
|
116
|
+
if not force and not self._has_meaningful_session(session):
|
|
117
|
+
self.clear()
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
self.history_dir.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
timestamp = int(time.time() * 1000)
|
|
122
|
+
session_id = self._safe_name(session.id or "workflow")
|
|
123
|
+
archive_path = self.history_dir / f"{timestamp}-{session_id}.json"
|
|
124
|
+
archive_events_path = self.history_dir / f"{timestamp}-{session_id}.events.jsonl"
|
|
125
|
+
|
|
126
|
+
archive_path.write_text(
|
|
127
|
+
json.dumps(session.to_dict(), indent=2, ensure_ascii=False),
|
|
128
|
+
encoding="utf-8",
|
|
129
|
+
)
|
|
130
|
+
events_path: Path | None = None
|
|
131
|
+
if self.events_path.exists():
|
|
132
|
+
shutil.copyfile(self.events_path, archive_events_path)
|
|
133
|
+
events_path = archive_events_path
|
|
134
|
+
|
|
135
|
+
self.clear()
|
|
136
|
+
return WorkflowArchive(session=session, path=archive_path, events_path=events_path)
|
|
137
|
+
|
|
138
|
+
def list_archives(self) -> list[WorkflowArchive]:
|
|
139
|
+
"""Return archived workflow sessions, newest first."""
|
|
140
|
+
if not self.history_dir.exists():
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
archives: list[WorkflowArchive] = []
|
|
144
|
+
for path in self.history_dir.glob("*.json"):
|
|
145
|
+
if path.name.endswith(".events.jsonl"):
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
149
|
+
if not isinstance(data, dict):
|
|
150
|
+
continue
|
|
151
|
+
session = WorkflowSession.from_dict(data)
|
|
152
|
+
except (json.JSONDecodeError, OSError, ValueError):
|
|
153
|
+
logger.exception("Failed to load workflow archive from %s", path)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
events_path = path.with_suffix(".events.jsonl")
|
|
157
|
+
archives.append(
|
|
158
|
+
WorkflowArchive(
|
|
159
|
+
session=session,
|
|
160
|
+
path=path,
|
|
161
|
+
events_path=events_path if events_path.exists() else None,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return sorted(
|
|
166
|
+
archives,
|
|
167
|
+
key=lambda archive: archive.session.updated_at,
|
|
168
|
+
reverse=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def restore_archive(self, archive: WorkflowArchive) -> WorkflowSession:
|
|
172
|
+
"""Restore an archive into the active workflow files."""
|
|
173
|
+
self.session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
shutil.copyfile(archive.path, self.session_path)
|
|
175
|
+
if archive.events_path and archive.events_path.exists():
|
|
176
|
+
shutil.copyfile(archive.events_path, self.events_path)
|
|
177
|
+
elif self.events_path.exists():
|
|
178
|
+
self.events_path.unlink()
|
|
179
|
+
return archive.session
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _has_meaningful_session(session: WorkflowSession) -> bool:
|
|
183
|
+
"""Return whether a session should be preserved in history."""
|
|
184
|
+
return any(
|
|
185
|
+
(
|
|
186
|
+
bool(session.goal.strip()),
|
|
187
|
+
session.state != WorkflowState.IDLE,
|
|
188
|
+
bool(session.active_agents),
|
|
189
|
+
bool(session.pending_questions),
|
|
190
|
+
bool(session.decisions),
|
|
191
|
+
bool(session.work_packages),
|
|
192
|
+
bool(session.execution_results),
|
|
193
|
+
bool(session.subtask_results),
|
|
194
|
+
bool(session.review_packages),
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _safe_name(value: str) -> str:
|
|
200
|
+
"""Return a filesystem-safe identifier fragment."""
|
|
201
|
+
cleaned = []
|
|
202
|
+
for char in value:
|
|
203
|
+
if char.isalnum() or char in {"-", "_"}:
|
|
204
|
+
cleaned.append(char)
|
|
205
|
+
else:
|
|
206
|
+
cleaned.append("-")
|
|
207
|
+
return "".join(cleaned).strip("-") or "workflow"
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
|
+
from prompt_toolkit.completion import CompleteEvent
|
|
6
|
+
from prompt_toolkit.document import Document
|
|
7
|
+
|
|
5
8
|
from trinity.tui.prompt import TRINITY_COMMANDS, TrinityPromptSession
|
|
6
9
|
|
|
7
10
|
|
|
@@ -46,6 +49,7 @@ class TestCommandCompletion:
|
|
|
46
49
|
"/decisions",
|
|
47
50
|
"/packages",
|
|
48
51
|
"/subtasks",
|
|
52
|
+
"/resume",
|
|
49
53
|
"/help",
|
|
50
54
|
"/quit",
|
|
51
55
|
}
|
|
@@ -56,6 +60,25 @@ class TestCommandCompletion:
|
|
|
56
60
|
session = TrinityPromptSession(tmp_path)
|
|
57
61
|
assert session.session.completer is not None
|
|
58
62
|
|
|
63
|
+
def test_completion_only_appears_for_slash_prefix(self, tmp_path):
|
|
64
|
+
session = TrinityPromptSession(tmp_path)
|
|
65
|
+
completer = session.session.completer
|
|
66
|
+
|
|
67
|
+
assert completer is not None
|
|
68
|
+
slash_matches = list(
|
|
69
|
+
completer.get_completions(Document("/sta"), CompleteEvent())
|
|
70
|
+
)
|
|
71
|
+
space_matches = list(
|
|
72
|
+
completer.get_completions(Document(" "), CompleteEvent())
|
|
73
|
+
)
|
|
74
|
+
text_matches = list(
|
|
75
|
+
completer.get_completions(Document("hello /sta"), CompleteEvent())
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert any(match.text == "/status" for match in slash_matches)
|
|
79
|
+
assert space_matches == []
|
|
80
|
+
assert text_matches == []
|
|
81
|
+
|
|
59
82
|
|
|
60
83
|
class TestGetInput:
|
|
61
84
|
"""get_input delegates to prompt_toolkit."""
|