10xgraph-api 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
  2. 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
  3. 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
  4. 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
  5. 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
  7. agentflow_cli/__init__.py +69 -0
  8. tenxgraph_api/__init__.py +23 -0
  9. tenxgraph_api/cli/__init__.py +6 -0
  10. tenxgraph_api/cli/capabilities.py +127 -0
  11. tenxgraph_api/cli/commands/__init__.py +49 -0
  12. tenxgraph_api/cli/commands/api.py +262 -0
  13. tenxgraph_api/cli/commands/audit.py +213 -0
  14. tenxgraph_api/cli/commands/build.py +305 -0
  15. tenxgraph_api/cli/commands/config.py +71 -0
  16. tenxgraph_api/cli/commands/demo.py +93 -0
  17. tenxgraph_api/cli/commands/eval.py +909 -0
  18. tenxgraph_api/cli/commands/init.py +531 -0
  19. tenxgraph_api/cli/commands/skills.py +532 -0
  20. tenxgraph_api/cli/commands/test.py +113 -0
  21. tenxgraph_api/cli/commands/version.py +47 -0
  22. tenxgraph_api/cli/config_editor/__init__.py +15 -0
  23. tenxgraph_api/cli/config_editor/schema.py +566 -0
  24. tenxgraph_api/cli/config_editor/server.py +240 -0
  25. tenxgraph_api/cli/config_editor/static/app.css +2 -0
  26. tenxgraph_api/cli/config_editor/static/app.js +3 -0
  27. tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
  28. tenxgraph_api/cli/config_editor/static/index.html +20 -0
  29. tenxgraph_api/cli/config_editor/store.py +120 -0
  30. tenxgraph_api/cli/config_editor/validation.py +377 -0
  31. tenxgraph_api/cli/constants.py +112 -0
  32. tenxgraph_api/cli/context.py +46 -0
  33. tenxgraph_api/cli/core/__init__.py +0 -0
  34. tenxgraph_api/cli/core/animations.py +550 -0
  35. tenxgraph_api/cli/core/config.py +279 -0
  36. tenxgraph_api/cli/core/output.py +625 -0
  37. tenxgraph_api/cli/core/prompts.py +220 -0
  38. tenxgraph_api/cli/core/screen.py +287 -0
  39. tenxgraph_api/cli/core/steps.py +621 -0
  40. tenxgraph_api/cli/core/theme.py +242 -0
  41. tenxgraph_api/cli/core/validation.py +251 -0
  42. tenxgraph_api/cli/exceptions.py +129 -0
  43. tenxgraph_api/cli/logger.py +84 -0
  44. tenxgraph_api/cli/main.py +936 -0
  45. tenxgraph_api/cli/templates/__init__.py +0 -0
  46. tenxgraph_api/cli/templates/defaults.py +844 -0
  47. tenxgraph_api/cli/templates/dev/.env.example +1 -0
  48. tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
  49. tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
  50. tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
  51. tenxgraph_api/cli/templates/prod/.env.example +75 -0
  52. tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
  53. tenxgraph_api/cli/templates/prod/.python-version +1 -0
  54. tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
  55. tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
  56. tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
  57. tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
  58. tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
  59. tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
  60. tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
  61. tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
  62. tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
  63. tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
  64. tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
  65. tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
  66. tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
  67. tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
  68. tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
  69. tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
  70. tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
  71. tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
  72. tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
  73. tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
  74. tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
  75. tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
  76. tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
  77. tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
  78. tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
  79. tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
  80. tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
  81. tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
  82. tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
  83. tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
  84. tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
  85. tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
  86. tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
  87. tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
  88. tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
  89. tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
  90. tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
  91. tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
  92. tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
  93. tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
  94. tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
  95. tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
  96. tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
  97. tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
  98. tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
  99. tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
  100. tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
  101. tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
  102. tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
  103. tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
  104. tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
  105. tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
  106. tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
  107. tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
  108. tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
  109. tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
  110. tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
  111. tenxgraph_api/py.typed +0 -0
  112. tenxgraph_api/src/__init__.py +0 -0
  113. tenxgraph_api/src/app/__init__.py +13 -0
  114. tenxgraph_api/src/app/core/__init__.py +15 -0
  115. tenxgraph_api/src/app/core/auth/__init__.py +0 -0
  116. tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
  117. tenxgraph_api/src/app/core/auth/authorization.py +379 -0
  118. tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
  119. tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
  120. tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
  121. tenxgraph_api/src/app/core/auth/permissions.py +354 -0
  122. tenxgraph_api/src/app/core/auth/request_config.py +130 -0
  123. tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
  124. tenxgraph_api/src/app/core/config/__init__.py +0 -0
  125. tenxgraph_api/src/app/core/config/graph_config.py +532 -0
  126. tenxgraph_api/src/app/core/config/media_settings.py +72 -0
  127. tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
  128. tenxgraph_api/src/app/core/config/settings.py +202 -0
  129. tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
  130. tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
  131. tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
  132. tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
  133. tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
  134. tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
  135. tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
  136. tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
  137. tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
  138. tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
  139. tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
  140. tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
  141. tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
  142. tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
  143. tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
  144. tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
  145. tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
  146. tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
  147. tenxgraph_api/src/app/core/utils/__init__.py +14 -0
  148. tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
  149. tenxgraph_api/src/app/loader.py +516 -0
  150. tenxgraph_api/src/app/main.py +180 -0
  151. tenxgraph_api/src/app/routers/__init__.py +4 -0
  152. tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
  153. tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
  154. tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
  155. tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
  156. tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
  157. tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
  158. tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
  159. tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
  160. tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
  161. tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
  162. tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
  163. tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
  164. tenxgraph_api/src/app/routers/evals/router.py +38 -0
  165. tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
  166. tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
  167. tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
  168. tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
  169. tenxgraph_api/src/app/routers/graph/router.py +821 -0
  170. tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
  171. tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
  172. tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
  173. tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
  174. tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
  175. tenxgraph_api/src/app/routers/media/__init__.py +506 -0
  176. tenxgraph_api/src/app/routers/media/router.py +242 -0
  177. tenxgraph_api/src/app/routers/media/schemas.py +39 -0
  178. tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
  179. tenxgraph_api/src/app/routers/ping/router.py +28 -0
  180. tenxgraph_api/src/app/routers/setup_router.py +60 -0
  181. tenxgraph_api/src/app/routers/store/__init__.py +4 -0
  182. tenxgraph_api/src/app/routers/store/router.py +200 -0
  183. tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
  184. tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
  185. tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
  186. tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
  187. tenxgraph_api/src/app/tasks/__init__.py +0 -0
  188. tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
  189. tenxgraph_api/src/app/utils/__init__.py +12 -0
  190. tenxgraph_api/src/app/utils/callable_helper.py +26 -0
  191. tenxgraph_api/src/app/utils/media/__init__.py +15 -0
  192. tenxgraph_api/src/app/utils/media/extractor.py +172 -0
  193. tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
  194. tenxgraph_api/src/app/utils/parse_output.py +17 -0
  195. tenxgraph_api/src/app/utils/response_helper.py +129 -0
  196. tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
  197. tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
  198. tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
  199. tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
  200. tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
  201. tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
  202. tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
  203. tenxgraph_api/src/app/worker.py +91 -0
@@ -0,0 +1,625 @@
1
+ """Adaptive terminal output utilities for the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ import time
8
+ from collections.abc import Iterator, Sequence
9
+ from contextlib import contextmanager
10
+ from typing import Any, TextIO
11
+
12
+ import click
13
+ from rich import box
14
+ from rich.console import Console
15
+ from rich.live import Live
16
+ from rich.panel import Panel
17
+ from rich.status import Status
18
+ from rich.table import Table
19
+ from rich.text import Text
20
+
21
+ from tenxgraph_api.cli.capabilities import (
22
+ ColorMode,
23
+ OutputFormat,
24
+ ProgressMode,
25
+ TerminalCapabilities,
26
+ )
27
+ from tenxgraph_api.cli.constants import CLI_VERSION
28
+ from tenxgraph_api.cli.core.animations import render_command_intro, render_session_header
29
+ from tenxgraph_api.cli.core.prompts import PromptService
30
+ from tenxgraph_api.cli.core.screen import AppFrame
31
+ from tenxgraph_api.cli.core.steps import (
32
+ LiveProgressRun,
33
+ LiveTimeline,
34
+ ProgressRun,
35
+ QuietProgressRun,
36
+ QuietTimeline,
37
+ StaticProgressRun,
38
+ StaticTimeline,
39
+ StructuredProgressRun,
40
+ StructuredTimeline,
41
+ Timeline,
42
+ )
43
+ from tenxgraph_api.cli.core.theme import (
44
+ ACCENT,
45
+ LINE,
46
+ TENXGRAPH_THEME,
47
+ Glyphs,
48
+ glyphs_for,
49
+ )
50
+
51
+
52
+ class OutputFormatter:
53
+ """Render semantic CLI output for terminals, pipes, and automation."""
54
+
55
+ def __init__(
56
+ self,
57
+ stream: TextIO | None = None,
58
+ *,
59
+ error_stream: TextIO | None = None,
60
+ output_format: OutputFormat = OutputFormat.HUMAN,
61
+ color_mode: ColorMode = ColorMode.AUTO,
62
+ progress_mode: ProgressMode = ProgressMode.AUTO,
63
+ quiet: bool = False,
64
+ ) -> None:
65
+ self._stream = stream
66
+ self._error_stream = error_stream
67
+ self.output_format = output_format
68
+ self.color_mode = color_mode
69
+ self.progress_mode = progress_mode
70
+ self.quiet = quiet
71
+ self._session_console: Console | None = None
72
+ self._frame: AppFrame | None = None
73
+ self._fullscreen_requested = False
74
+ self._consoles: dict[bool, Console] = {}
75
+ self.capabilities = TerminalCapabilities.detect(
76
+ stream=self.stream,
77
+ output_format=output_format,
78
+ color_mode=color_mode,
79
+ progress_mode=progress_mode,
80
+ )
81
+
82
+ @property
83
+ def stream(self) -> TextIO:
84
+ """Current stdout stream, remaining compatible with CliRunner capture."""
85
+ return self._stream or sys.stdout
86
+
87
+ @property
88
+ def error_stream(self) -> TextIO:
89
+ """Current stderr stream, or the explicit test stream when supplied."""
90
+ if self._error_stream is not None:
91
+ return self._error_stream
92
+ if self._stream is not None:
93
+ return self._stream
94
+ return sys.stderr
95
+
96
+ def configure(
97
+ self,
98
+ *,
99
+ output_format: OutputFormat | None = None,
100
+ color_mode: ColorMode | None = None,
101
+ progress_mode: ProgressMode | None = None,
102
+ quiet: bool | None = None,
103
+ ) -> None:
104
+ """Apply invocation-level rendering policy."""
105
+ if output_format is not None:
106
+ self.output_format = output_format
107
+ if color_mode is not None:
108
+ self.color_mode = color_mode
109
+ if progress_mode is not None:
110
+ self.progress_mode = progress_mode
111
+ if quiet is not None:
112
+ self.quiet = quiet
113
+ # Color and terminal policy are baked into a Console at construction.
114
+ self._consoles.clear()
115
+ self.capabilities = TerminalCapabilities.detect(
116
+ stream=self.stream,
117
+ output_format=self.output_format,
118
+ color_mode=self.color_mode,
119
+ progress_mode=self.progress_mode,
120
+ )
121
+
122
+ def _console(self, *, error: bool = False) -> Console:
123
+ """Return the one console bound to this stream.
124
+
125
+ Consoles are cached rather than rebuilt per call because a Rich live
126
+ display only knows to render ordinary prints *above* itself when those
127
+ prints go through the same console instance.
128
+ """
129
+ if self._session_console is not None:
130
+ return self._session_console
131
+ cached = self._consoles.get(error)
132
+ if cached is None:
133
+ cached = Console(
134
+ file=self.error_stream if error else self.stream,
135
+ theme=TENXGRAPH_THEME,
136
+ no_color=not self.capabilities.color,
137
+ force_terminal=self.capabilities.color,
138
+ highlight=False,
139
+ soft_wrap=False,
140
+ )
141
+ self._consoles[error] = cached
142
+ return cached
143
+
144
+ @property
145
+ def glyphs(self) -> Glyphs:
146
+ """Symbol set matching this terminal's encoding support."""
147
+ return glyphs_for(self.capabilities.unicode)
148
+
149
+ @property
150
+ def fullscreen_active(self) -> bool:
151
+ """Whether this invocation currently owns the alternate screen."""
152
+ return self._frame is not None and self._frame.active
153
+
154
+ def request_fullscreen(self, enabled: bool) -> None:
155
+ """Record whether this invocation may claim a full-screen surface.
156
+
157
+ The screen is not taken here. It is claimed lazily by the first
158
+ ``command_header`` call, so script-shaped commands — ``--version``,
159
+ ``config get`` — never take over the terminal or pause on exit.
160
+ """
161
+ self._fullscreen_requested = enabled
162
+
163
+ def start_fullscreen_session(self) -> bool:
164
+ """Claim the alternate screen for the whole command."""
165
+ if self.fullscreen_active or self.quiet or not self.capabilities.animation:
166
+ return False
167
+
168
+ console = self._console()
169
+ frame = AppFrame(console, glyphs=self.glyphs, color=self.capabilities.color)
170
+ if not frame.open():
171
+ return False
172
+
173
+ # Bind every later render to this console so chrome, live displays, and
174
+ # ordinary prints all agree on where the cursor is.
175
+ self._session_console = console
176
+ self._frame = frame
177
+ return True
178
+
179
+ def end_fullscreen_session(self) -> None:
180
+ """Restore the user's terminal, pausing on the closing hint after a normal finish.
181
+
182
+ Called from the CLI context's close callback, which runs while an exception is
183
+ still propagating. Ctrl+C (``KeyboardInterrupt``, or Click's ``Abort`` wrapping
184
+ it) releases the screen immediately instead of waiting for Enter; a failure
185
+ still pauses so its message can be read before the screen is discarded.
186
+ """
187
+ frame = self._frame
188
+ self._frame = None
189
+ self._session_console = None
190
+ if frame is not None:
191
+ in_flight = sys.exc_info()[1]
192
+ interrupted = isinstance(in_flight, KeyboardInterrupt | click.exceptions.Abort)
193
+ frame.close(hold=not interrupted)
194
+
195
+ def refresh_chrome(self) -> None:
196
+ """Repaint pinned chrome that something else may have drawn over."""
197
+ if self._frame is not None:
198
+ self._frame.redraw_chrome()
199
+
200
+ def prompts(self, *, interactive: bool | None = None) -> PromptService:
201
+ """Build a prompt service bound to this invocation's terminal state."""
202
+ return PromptService(
203
+ interactive=interactive,
204
+ on_answered=self.refresh_chrome,
205
+ )
206
+
207
+ @property
208
+ def _structured(self) -> bool:
209
+ return self.capabilities.output_format in {OutputFormat.JSON, OutputFormat.JSONL}
210
+
211
+ def _emit_event(self, event: str, message: str, **data: Any) -> None:
212
+ payload = {
213
+ "schema": "agentflow.cli/v1",
214
+ "type": event,
215
+ "message": message,
216
+ **data,
217
+ }
218
+ print(json.dumps(payload, ensure_ascii=False, default=str), file=self.stream, flush=True)
219
+
220
+ def print_banner(
221
+ self,
222
+ title: str,
223
+ subtitle: str | None = None,
224
+ color: str = LINE,
225
+ width: int = 50,
226
+ ) -> None:
227
+ """Render a compact section heading or a panel on an interactive terminal."""
228
+ if self.quiet:
229
+ return
230
+ if self._structured:
231
+ self._emit_event("section", title, subtitle=subtitle)
232
+ return
233
+ if self.capabilities.output_format == OutputFormat.PLAIN:
234
+ print(f"\n== {title} ==", file=self.stream)
235
+ if subtitle:
236
+ print(subtitle, file=self.stream)
237
+ print("", file=self.stream)
238
+ return
239
+
240
+ body = f"[tenxgraph.title]{title}[/tenxgraph.title]"
241
+ if subtitle:
242
+ body += f"\n[tenxgraph.muted]{subtitle}[/tenxgraph.muted]"
243
+ self._console().print(Panel.fit(body, border_style=color, padding=(0, 1), width=width))
244
+
245
+ def command_header(
246
+ self,
247
+ command: str,
248
+ subtitle: str | None = None,
249
+ *,
250
+ color: str = LINE,
251
+ hint: str | None = None,
252
+ ) -> None:
253
+ """Render an animated command identity when the terminal supports it.
254
+
255
+ ``hint`` is the closing line pinned in the full-screen footer; it should
256
+ say how to leave whatever the command is about to do.
257
+ """
258
+ if self.quiet:
259
+ return
260
+ if self._structured:
261
+ self._emit_event("section", command, subtitle=subtitle)
262
+ return
263
+ if self.capabilities.output_format == OutputFormat.PLAIN:
264
+ self.print_banner(command.title(), subtitle, color=color)
265
+ return
266
+ if not self.capabilities.animation:
267
+ # Still branded, just motionless — reduced motion should not mean
268
+ # a downgrade in the information the header carries.
269
+ render_session_header(
270
+ self._console(),
271
+ command=command,
272
+ subtitle=subtitle,
273
+ glyphs=self.glyphs,
274
+ version=CLI_VERSION,
275
+ )
276
+ return
277
+ if self._fullscreen_requested:
278
+ self.start_fullscreen_session()
279
+ # Without the opt-in frame the intro borrows a temporary screen, then prints a
280
+ # durable header into the normal scrollback: nothing to dismiss on exit.
281
+ render_command_intro(
282
+ self._console(),
283
+ command=command,
284
+ subtitle=subtitle,
285
+ unicode=self.capabilities.unicode,
286
+ persistent_screen=self.fullscreen_active,
287
+ )
288
+ if self._frame is not None:
289
+ # The intro owned the whole canvas; pin the chrome it collapses into
290
+ # and hand the body region over to the command's output.
291
+ self._frame.install_chrome(
292
+ command=command,
293
+ subtitle=subtitle,
294
+ version=CLI_VERSION,
295
+ hint=hint,
296
+ )
297
+
298
+ def timeline(
299
+ self,
300
+ title: str | None = None,
301
+ *,
302
+ steps: Sequence[tuple[str, str]] = (),
303
+ ) -> Timeline:
304
+ """Create a multi-stage progress timeline for the current output mode.
305
+
306
+ Declaring ``steps`` up front lets the user see the whole plan the moment
307
+ work begins, with pending stages dimmed and the running one animated.
308
+ """
309
+ if self.quiet:
310
+ return QuietTimeline(steps)
311
+ if self._structured:
312
+ return StructuredTimeline(emit=self._emit_event_data, title=title, steps=steps)
313
+ if not self.capabilities.animation:
314
+ return StaticTimeline(
315
+ glyphs=self.glyphs,
316
+ emit=self._write_line,
317
+ title=title,
318
+ steps=steps,
319
+ )
320
+ return LiveTimeline(self._console(), glyphs=self.glyphs, title=title, steps=steps)
321
+
322
+ def progress_run(self, title: str, *, total: int) -> ProgressRun:
323
+ """Create a determinate progress display with a running pass/fail tally."""
324
+ if self.quiet:
325
+ return QuietProgressRun(total=total, title=title)
326
+ if self._structured:
327
+ return StructuredProgressRun(total=total, title=title, emit=self._emit_event_data)
328
+ if not self.capabilities.animation:
329
+ return StaticProgressRun(
330
+ total=total,
331
+ title=title,
332
+ glyphs=self.glyphs,
333
+ emit=self._write_line,
334
+ )
335
+ return LiveProgressRun(self._console(), total=total, title=title, glyphs=self.glyphs)
336
+
337
+ def _emit_event_data(self, event: str, message: str, data: dict[str, Any]) -> None:
338
+ self._emit_event(event, message, **data)
339
+
340
+ def _write_line(self, message: str) -> None:
341
+ """Write one pre-formatted line without interpreting console markup."""
342
+ if self.capabilities.output_format == OutputFormat.PLAIN:
343
+ print(message, file=self.stream)
344
+ return
345
+ self._console().print(Text(message))
346
+
347
+ def success(self, message: str, emoji: bool = True) -> None:
348
+ if self.quiet:
349
+ return
350
+ symbol = "✓" if self.capabilities.unicode else "OK"
351
+ self._message("success", message, symbol if emoji else "", error=False)
352
+
353
+ def error(self, message: str, emoji: bool = True) -> None:
354
+ symbol = "✗" if self.capabilities.unicode else "X"
355
+ self._message("error", message, symbol if emoji else "", error=True)
356
+
357
+ def info(self, message: str, emoji: bool = True) -> None:
358
+ if self.quiet:
359
+ return
360
+ self._message("info", message, "i" if emoji else "", error=False)
361
+
362
+ def warning(self, message: str, emoji: bool = True) -> None:
363
+ if self.quiet:
364
+ return
365
+ self._message("warning", message, "!" if emoji else "", error=False)
366
+
367
+ def _message(
368
+ self,
369
+ level: str,
370
+ message: str,
371
+ symbol: str,
372
+ *,
373
+ error: bool,
374
+ ) -> None:
375
+ if self._structured:
376
+ self._emit_event(level, message)
377
+ return
378
+ prefix = f"{symbol} " if symbol else ""
379
+ if self.capabilities.output_format == OutputFormat.PLAIN:
380
+ print(f"{prefix}{message}", file=self.error_stream if error else self.stream)
381
+ return
382
+ self._console(error=error).print(
383
+ f"[tenxgraph.{level}]{prefix}{message}[/tenxgraph.{level}]"
384
+ )
385
+
386
+ def emphasize(self, message: str) -> None:
387
+ if self.quiet:
388
+ return
389
+ self._message(
390
+ "info",
391
+ message,
392
+ "•" if self.capabilities.unicode else "*",
393
+ error=False,
394
+ )
395
+
396
+ def print_list(
397
+ self,
398
+ items: list[str],
399
+ title: str | None = None,
400
+ bullet: str = "•",
401
+ ) -> None:
402
+ if self.quiet:
403
+ return
404
+ if self._structured:
405
+ self._emit_event("list", title or "", items=items)
406
+ return
407
+ console = self._console()
408
+ if title:
409
+ console.print(f"\n[bold]{title}[/bold]")
410
+ for item in items:
411
+ console.print(f" {bullet} {item}")
412
+
413
+ def print_key_value_pairs(
414
+ self,
415
+ pairs: dict[str, Any],
416
+ title: str | None = None,
417
+ indent: int = 2,
418
+ ) -> None:
419
+ if self.quiet:
420
+ return
421
+ if self._structured:
422
+ self._emit_event("data", title or "", data=pairs)
423
+ return
424
+ console = self._console()
425
+ if title:
426
+ console.print(f"\n[bold]{title}[/bold]")
427
+ pad = " " * indent
428
+ for key, value in pairs.items():
429
+ console.print(f"{pad}[tenxgraph.muted]{key}[/tenxgraph.muted] {value}")
430
+
431
+ def print_table(
432
+ self,
433
+ headers: list[str],
434
+ rows: list[list[str]],
435
+ title: str | None = None,
436
+ ) -> None:
437
+ if self.quiet:
438
+ return
439
+ if self._structured:
440
+ records = [
441
+ {
442
+ header: row[index] if index < len(row) else ""
443
+ for index, header in enumerate(headers)
444
+ }
445
+ for row in rows
446
+ ]
447
+ self._emit_event("table", title or "", columns=headers, rows=records)
448
+ return
449
+
450
+ table = Table(title=title, header_style="bold cyan", show_lines=False)
451
+ for header in headers:
452
+ table.add_column(str(header), overflow="fold")
453
+ for row in rows:
454
+ table.add_row(*(str(row[i]) if i < len(row) else "" for i in range(len(headers))))
455
+ self._console().print(table)
456
+
457
+ @contextmanager
458
+ def status(self, message: str, *, spinner: str = "dots12") -> Iterator[Status | None]:
459
+ """Show an indeterminate status when animation is safe."""
460
+ if self._structured:
461
+ self._emit_event("progress_start", message)
462
+ try:
463
+ yield None
464
+ except Exception:
465
+ self._emit_event("progress_end", message, status="failed")
466
+ raise
467
+ else:
468
+ self._emit_event("progress_end", message, status="completed")
469
+ return
470
+ if self.quiet or not self.capabilities.animation:
471
+ if not self.quiet and self.capabilities.progress_mode == ProgressMode.PLAIN:
472
+ self.info(message, emoji=False)
473
+ yield None
474
+ return
475
+ with self._console().status(
476
+ f"[tenxgraph.progress]{message}[/tenxgraph.progress]",
477
+ spinner=spinner,
478
+ spinner_style="tenxgraph.brand",
479
+ ) as status:
480
+ yield status
481
+
482
+ @contextmanager
483
+ def activity(
484
+ self,
485
+ message: str,
486
+ *,
487
+ done: str | None = None,
488
+ spinner: str = "dots12",
489
+ ) -> Iterator[Status | None]:
490
+ """Animate a bounded task and persist a timed completion state."""
491
+ started = time.monotonic()
492
+ try:
493
+ with self.status(message, spinner=spinner) as status:
494
+ yield status
495
+ except Exception:
496
+ elapsed = time.monotonic() - started
497
+ self.error(f"{message} failed after {elapsed:.1f}s")
498
+ raise
499
+ else:
500
+ elapsed = time.monotonic() - started
501
+ self.success(f"{done or message} ({elapsed:.1f}s)")
502
+
503
+ def completion_screen(
504
+ self,
505
+ title: str,
506
+ message: str,
507
+ *,
508
+ details: dict[str, Any] | None = None,
509
+ next_steps: list[str] | None = None,
510
+ ) -> None:
511
+ """Render a polished final state for a completed workflow."""
512
+ if self.quiet:
513
+ return
514
+ if self._structured:
515
+ self._emit_event(
516
+ "completion",
517
+ message,
518
+ title=title,
519
+ details=details or {},
520
+ next_steps=next_steps or [],
521
+ )
522
+ return
523
+ if self.capabilities.output_format == OutputFormat.PLAIN:
524
+ self.success(f"{title}: {message}")
525
+ if details:
526
+ self.print_key_value_pairs(details)
527
+ if next_steps:
528
+ self.print_list(next_steps, title="Next steps", bullet="->")
529
+ return
530
+
531
+ console = self._console()
532
+ rows = list((details or {}).items())
533
+ steps = list(next_steps or [])
534
+ total = len(rows) + len(steps)
535
+
536
+ if not self.capabilities.animation or not total:
537
+ console.print(self._completion_panel(title, message, rows, steps, total))
538
+ return
539
+
540
+ # Reveal one row at a time so the result reads as an outcome landing
541
+ # rather than a wall of text appearing at once.
542
+ with Live(
543
+ console=console,
544
+ auto_refresh=False,
545
+ transient=False,
546
+ redirect_stdout=False,
547
+ redirect_stderr=False,
548
+ ) as live:
549
+ for visible in range(total + 1):
550
+ live.update(
551
+ self._completion_panel(title, message, rows, steps, visible),
552
+ refresh=True,
553
+ )
554
+ time.sleep(0.05)
555
+
556
+ def _completion_panel(
557
+ self,
558
+ title: str,
559
+ message: str,
560
+ rows: list[tuple[str, Any]],
561
+ steps: list[str],
562
+ visible: int,
563
+ ) -> Panel:
564
+ """Build the completion panel with only its first ``visible`` rows shown."""
565
+ glyphs = self.glyphs
566
+ body = Text()
567
+ body.append(f"{glyphs.check} ", style="tenxgraph.success")
568
+ body.append(message, style="tenxgraph.command")
569
+
570
+ shown = 0
571
+ if rows:
572
+ label_width = max(len(str(key)) for key, _ in rows) + 2
573
+ body.append("\n")
574
+ for key, value in rows:
575
+ if shown >= visible:
576
+ break
577
+ body.append(f"\n {key!s:<{label_width}}", style="tenxgraph.muted")
578
+ body.append(str(value), style="tenxgraph.command")
579
+ shown += 1
580
+
581
+ if steps and visible > len(rows):
582
+ body.append("\n\n")
583
+ body.append("Next", style=f"bold {ACCENT}")
584
+ for step in steps:
585
+ if shown >= visible:
586
+ break
587
+ body.append(f"\n {glyphs.arrow} ", style="tenxgraph.accent")
588
+ body.append(step, style="tenxgraph.command")
589
+ shown += 1
590
+
591
+ return Panel(
592
+ body,
593
+ title=Text(f" {title} ", style="tenxgraph.title"),
594
+ title_align="left",
595
+ box=box.ROUNDED,
596
+ border_style=LINE,
597
+ padding=(1, 2),
598
+ )
599
+
600
+
601
+ output = OutputFormatter()
602
+
603
+
604
+ def print_banner(title: str, subtitle: str | None = None, color: str = LINE) -> None:
605
+ output.print_banner(title, subtitle, color)
606
+
607
+
608
+ def success(message: str, emoji: bool = True) -> None:
609
+ output.success(message, emoji)
610
+
611
+
612
+ def error(message: str, emoji: bool = True) -> None:
613
+ output.error(message, emoji)
614
+
615
+
616
+ def info(message: str, emoji: bool = True) -> None:
617
+ output.info(message, emoji)
618
+
619
+
620
+ def warning(message: str, emoji: bool = True) -> None:
621
+ output.warning(message, emoji)
622
+
623
+
624
+ def emphasize(message: str) -> None:
625
+ output.emphasize(message)