feat: add safe cap call output modes

This commit is contained in:
lda
2026-06-09 16:44:29 +07:00 Verified
parent 3798d3c0cf
commit ae4c3b2aa8
7 changed files with 380 additions and 4 deletions
+109 -2
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
import json
from enum import StrEnum
from pathlib import Path
from typing import Annotated
from typing import Annotated, Any
import typer
@@ -10,6 +12,13 @@ from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import CliInputError, emit_json, parse_json_input
from wf_cli.remote_errors import run_cli_operation
class CapCallOutputFormat(StrEnum):
JSON = "json"
COMPACT = "compact"
TEXT = "text"
app = typer.Typer(
name="cap",
help="Inspect and call workflow capabilities.",
@@ -96,6 +105,25 @@ def call_capability(
help="Deployment id for saved wrappers with deployment-bound sources.",
),
] = None,
output_format: Annotated[
CapCallOutputFormat,
typer.Option("--format", help="Output format for rendered cap-call result."),
] = CapCallOutputFormat.JSON,
max_output_chars: Annotated[
int | None,
typer.Option(
"--max-output-chars",
min=1,
help="Maximum characters for compact/text output. JSON output is not truncated.",
),
] = None,
unwrap_text: Annotated[
bool,
typer.Option(
"--unwrap-text",
help="Only with --format text: unwrap one MCP text block.",
),
] = False,
) -> None:
"""Call one workflow capability once for authoring/runtime smoke tests."""
try:
@@ -112,4 +140,83 @@ def call_capability(
deployment_id=deployment_id,
),
)
emit_json(result)
try:
rendered = render_cap_call_output(
result,
output_format=output_format,
unwrap_text=unwrap_text,
max_output_chars=max_output_chars,
)
except ValueError as exc:
raise typer.BadParameter(str(exc)) from exc
print(rendered)
def render_cap_call_output(
result: dict[str, Any],
*,
output_format: CapCallOutputFormat,
unwrap_text: bool,
max_output_chars: int | None,
) -> str:
"""Render cap-call output without changing the API/RPC payload."""
if output_format is CapCallOutputFormat.JSON:
return json.dumps(result, indent=2, sort_keys=True)
if output_format is CapCallOutputFormat.TEXT:
if not unwrap_text:
raise ValueError("--format text requires --unwrap-text")
return _truncate_text(
_unwrap_single_mcp_text_block(result),
max_output_chars=max_output_chars,
)
summary = _compact_cap_call_summary(result)
return _truncate_text(summary, max_output_chars=max_output_chars)
def _compact_cap_call_summary(result: dict[str, Any]) -> str:
output = result.get("output")
output_summary = _summarize_output(output)
return "\t".join(
part
for part in (
str(result.get("qualified_name", "")),
f"source={result.get('source_id')}",
f"kind={result.get('kind')}",
f"outcome={result.get('outcome')}",
f"output={output_summary}",
)
if part
)
def _summarize_output(output: object) -> str:
if isinstance(output, dict):
content = output.get("content")
if isinstance(content, list):
return f"mcp_content_blocks[{len(content)}]"
return f"object keys={sorted(str(key) for key in output.keys())}"
if isinstance(output, list):
return f"array[{len(output)}]"
return type(output).__name__
def _unwrap_single_mcp_text_block(result: dict[str, Any]) -> str:
output = result.get("output")
if not isinstance(output, dict):
raise ValueError("--unwrap-text requires exactly one MCP text content block")
content = output.get("content")
if not isinstance(content, list) or len(content) != 1:
raise ValueError("--unwrap-text requires exactly one MCP text content block")
block = content[0]
if not isinstance(block, dict):
raise ValueError("--unwrap-text requires exactly one MCP text content block")
if block.get("type") != "text" or not isinstance(block.get("text"), str):
raise ValueError("--unwrap-text requires exactly one MCP text content block")
return block["text"]
def _truncate_text(text: str, *, max_output_chars: int | None) -> str:
if max_output_chars is None or len(text) <= max_output_chars:
return text
remaining = len(text) - max_output_chars
return f"{text[:max_output_chars]}...<truncated {remaining} chars>"