boundflow 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {boundflow-0.2.0 → boundflow-0.4.0}/PKG-INFO +3 -1
  2. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/__init__.py +11 -3
  3. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/_transport.py +11 -5
  4. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/_client.py +3 -2
  5. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/_output.py +14 -0
  6. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/policies.py +32 -3
  7. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/workflows.py +64 -2
  8. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/control_plane.py +250 -13
  9. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/errors.py +6 -6
  10. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/lifecycle.py +2 -1
  11. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/llm.py +18 -5
  12. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/policies.py +32 -5
  13. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/trace.py +7 -0
  14. boundflow-0.4.0/boundflow/v1/agent_policy_pb2.py +53 -0
  15. boundflow-0.4.0/boundflow/v1/lifecycle_pb2.py +183 -0
  16. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/lifecycle_pb2_grpc.py +304 -0
  17. boundflow-0.4.0/boundflow/v1/operation_pb2.py +70 -0
  18. boundflow-0.4.0/boundflow/v1/workflow_pb2.py +49 -0
  19. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/worker.py +126 -26
  20. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/PKG-INFO +3 -1
  21. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/SOURCES.txt +13 -0
  22. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/requires.txt +2 -0
  23. {boundflow-0.2.0 → boundflow-0.4.0}/pyproject.toml +3 -1
  24. boundflow-0.4.0/tests/test_agent_call_timeout.py +80 -0
  25. boundflow-0.4.0/tests/test_agent_metrics_latency.py +27 -0
  26. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_approval_gate.py +4 -0
  27. boundflow-0.4.0/tests/test_await_input.py +202 -0
  28. boundflow-0.4.0/tests/test_complete_result.py +154 -0
  29. boundflow-0.4.0/tests/test_delete_workflow.py +84 -0
  30. boundflow-0.4.0/tests/test_get_workflow_metrics.py +49 -0
  31. boundflow-0.4.0/tests/test_invoke_context.py +119 -0
  32. boundflow-0.4.0/tests/test_invoke_mode.py +142 -0
  33. boundflow-0.4.0/tests/test_metrics_epoch_isolation.py +72 -0
  34. boundflow-0.4.0/tests/test_pending_approval.py +87 -0
  35. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_periodic_workflow.py +56 -0
  36. boundflow-0.4.0/tests/test_policy_getters.py +58 -0
  37. boundflow-0.4.0/tests/test_request_info.py +52 -0
  38. boundflow-0.4.0/tests/test_set_workflow_config.py +51 -0
  39. boundflow-0.2.0/boundflow/v1/agent_policy_pb2.py +0 -53
  40. boundflow-0.2.0/boundflow/v1/lifecycle_pb2.py +0 -147
  41. boundflow-0.2.0/boundflow/v1/operation_pb2.py +0 -68
  42. boundflow-0.2.0/boundflow/v1/workflow_pb2.py +0 -42
  43. {boundflow-0.2.0 → boundflow-0.4.0}/LICENSE +0 -0
  44. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/anthropic_client.py +0 -0
  45. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/__init__.py +0 -0
  46. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/__main__.py +0 -0
  47. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/__init__.py +0 -0
  48. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/audit.py +0 -0
  49. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/pricing.py +0 -0
  50. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/tenants.py +0 -0
  51. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/__init__.py +0 -0
  52. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/approval_gate.py +0 -0
  53. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/hello.py +0 -0
  54. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/langchain_adapter.py +0 -0
  55. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/langgraph_workflow.py +0 -0
  56. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/model_switching.py +0 -0
  57. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/runtime_caps.py +0 -0
  58. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/self_healing.py +0 -0
  59. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/langchain_client.py +0 -0
  60. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/__init__.py +0 -0
  61. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/agent_policy_pb2_grpc.py +0 -0
  62. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/operation_pb2_grpc.py +0 -0
  63. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/policy_pb2.py +0 -0
  64. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/policy_pb2_grpc.py +0 -0
  65. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/pricing_pb2.py +0 -0
  66. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/pricing_pb2_grpc.py +0 -0
  67. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/registration_pb2.py +0 -0
  68. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/registration_pb2_grpc.py +0 -0
  69. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_group_pb2.py +0 -0
  70. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_group_pb2_grpc.py +0 -0
  71. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_pb2.py +0 -0
  72. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_pb2_grpc.py +0 -0
  73. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/worker_pb2.py +0 -0
  74. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/worker_pb2_grpc.py +0 -0
  75. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/workflow_pb2_grpc.py +0 -0
  76. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/dependency_links.txt +0 -0
  77. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/entry_points.txt +0 -0
  78. {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/top_level.txt +0 -0
  79. {boundflow-0.2.0 → boundflow-0.4.0}/setup.cfg +0 -0
  80. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_agent_lifecycle_policy.py +0 -0
  81. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_agent_policy_audit.py +0 -0
  82. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_approval_audit.py +0 -0
  83. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_capability_routing.py +0 -0
  84. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_cooldown_and_resume.py +0 -0
  85. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_get_workflow.py +0 -0
  86. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_langchain_client.py +0 -0
  87. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_lifecycle_states.py +0 -0
  88. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_list_tenants.py +0 -0
  89. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_list_workflows.py +0 -0
  90. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_max_tokens_per_call.py +0 -0
  91. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_mock_llm.py +0 -0
  92. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_model_pricing.py +0 -0
  93. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_otel_sink.py +0 -0
  94. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_policy_audit.py +0 -0
  95. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_resolve_interrupted_workflow.py +0 -0
  96. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_tool_call_limits.py +0 -0
  97. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_trace.py +0 -0
  98. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_workflow_lifecycle_policy.py +0 -0
  99. {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_workflow_runs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boundflow
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: BoundFlow Python SDK — build governed fleets of agents against a self-hostable control plane.
5
5
  License-Expression: MIT
6
6
  Keywords: agents,llm,orchestration,control-plane,governance
@@ -18,6 +18,8 @@ Requires-Dist: pytest; extra == "dev"
18
18
  Requires-Dist: pytest-asyncio; extra == "dev"
19
19
  Requires-Dist: langchain-core>=0.3; extra == "dev"
20
20
  Requires-Dist: langchain-anthropic>=0.3; extra == "dev"
21
+ Requires-Dist: opentelemetry-sdk; extra == "dev"
22
+ Requires-Dist: opentelemetry-exporter-otlp; extra == "dev"
21
23
  Provides-Extra: langchain
22
24
  Requires-Dist: langchain-core>=0.3; extra == "langchain"
23
25
  Provides-Extra: otel
@@ -5,10 +5,15 @@ from .control_plane import (
5
5
  ControlPlaneClient,
6
6
  ApprovalAuditRecord,
7
7
  ApprovalDecision,
8
+ InputAuditRecord,
9
+ InputDecision,
8
10
  PolicyActionRecord,
9
11
  WorkflowPolicyAction,
10
12
  AgentPolicyActionRecord,
13
+ InvokeMode,
11
14
  LifecycleState,
15
+ PendingApproval,
16
+ PendingInput,
12
17
  RequestInfo,
13
18
  Run,
14
19
  RunOutcome,
@@ -18,6 +23,7 @@ from .control_plane import (
18
23
  Workflow,
19
24
  WorkflowConfig,
20
25
  WorkflowInfo,
26
+ WorkflowMetrics,
21
27
  WorkflowState,
22
28
  )
23
29
  from .errors import (
@@ -32,7 +38,7 @@ from .errors import (
32
38
  UnauthenticatedError,
33
39
  UnavailableError,
34
40
  )
35
- from .llm import MockLlmClient, MockContext, Turn, turn, submit
41
+ from .llm import AgentCallTimeout, MockLlmClient, MockContext, Turn, turn, submit
36
42
  from .trace import (
37
43
  AgentRunTrace,
38
44
  JsonlFileTraceSink,
@@ -62,8 +68,10 @@ from .worker import (
62
68
  AgentDefinition,
63
69
  ApprovalRequest,
64
70
  AwaitApproval,
71
+ AwaitInput,
65
72
  BoundFlowWorker,
66
73
  Complete,
74
+ InputRequest,
67
75
  Next,
68
76
  OperationContext,
69
77
  OperationResult,
@@ -75,11 +83,11 @@ __all__ = [
75
83
  "AnthropicLlmClient",
76
84
  "ControlPlaneClient", "LifecycleState", "RunStatus", "RunOutcome", "Run", "RequestInfo",
77
85
  "Tenant", "TenantGroup", "Workflow",
78
- "WorkflowConfig", "WorkflowState", "WorkflowInfo", "ApprovalAuditRecord", "ApprovalDecision", "PolicyActionRecord", "WorkflowPolicyAction", "AgentPolicyActionRecord", "MockLlmClient", "MockContext", "Turn",
86
+ "WorkflowConfig", "WorkflowState", "WorkflowInfo", "WorkflowMetrics", "PendingApproval", "PendingInput", "InvokeMode", "ApprovalAuditRecord", "ApprovalDecision", "InputAuditRecord", "InputDecision", "PolicyActionRecord", "WorkflowPolicyAction", "AgentPolicyActionRecord", "AgentCallTimeout", "MockLlmClient", "MockContext", "Turn",
79
87
  "turn", "submit", "AgentMetric", "AgentRule", "Cooldown", "Op", "Pause",
80
88
  "RuntimePolicy", "SetMaxCostUsd", "SetMaxLlmCalls", "SetMaxTokensPerCall",
81
89
  "SetModel", "SetVersion", "ToolCallLimit", "WorkflowMetric", "WorkflowRule",
82
- "AgentDefinition", "ApprovalRequest", "AwaitApproval", "BoundFlowWorker",
90
+ "AgentDefinition", "ApprovalRequest", "AwaitApproval", "AwaitInput", "InputRequest", "BoundFlowWorker",
83
91
  "Complete", "Next", "OperationContext", "OperationResult", "Tool", "tool",
84
92
  "AgentRunTrace", "OperationTrace", "Span", "TraceSink", "LoggingTraceSink",
85
93
  "JsonlFileTraceSink", "OTelTraceSink",
@@ -1,9 +1,9 @@
1
1
  """gRPC transport — worker bidi stream + proto⇄domain conversion.
2
2
 
3
- Port of BoundFlow.SDK WorkerClient (the stream loop) and the MapToProto helpers
4
- in BoundFlowWorker, on top of grpc.aio. The server drives the session with
5
- Launch/Cancel commands; the worker acks IN_PROGRESS, runs the operation off the
6
- receive loop, then reports the result and re-arms with ReadyForWork.
3
+ The worker stream loop and proto mapping helpers, on top of grpc.aio. The server
4
+ drives the session with Launch/Cancel commands; the worker acks IN_PROGRESS, runs
5
+ the operation off the receive loop, then reports the result and re-arms with
6
+ ReadyForWork.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -34,7 +34,7 @@ def _strip_scheme(addr: str) -> str:
34
34
 
35
35
  def context_to_dict(op: op_pb.AtomicOperation) -> dict:
36
36
  """AtomicOperation.context (Struct) → plain dict, camelCase top-level keys
37
- (matching the .NET JsonFormatter), opaque policy contents preserved."""
37
+ (proto JSON convention), opaque policy contents preserved."""
38
38
  if not op.HasField("context"):
39
39
  return {}
40
40
  return json_format.MessageToDict(op.context)
@@ -50,11 +50,16 @@ def new_approval_id() -> str:
50
50
  return str(uuid.uuid4())
51
51
 
52
52
 
53
+ def new_input_id() -> str:
54
+ return str(uuid.uuid4())
55
+
56
+
53
57
  def metrics_to_proto(snapshot: dict) -> op_pb.AgentInvocationMetrics:
54
58
  m = op_pb.AgentInvocationMetrics(
55
59
  cost_usd=snapshot.get("cost_usd", 0.0),
56
60
  llm_calls=snapshot.get("llm_calls", 0),
57
61
  tokens_used=snapshot.get("tokens_used", 0),
62
+ latency_seconds=snapshot.get("latency_seconds", 0.0),
58
63
  ran_at=snapshot.get("ran_at", 0),
59
64
  )
60
65
  for tool, count in (snapshot.get("calls_per_tool") or {}).items():
@@ -89,6 +94,7 @@ def _runtime_policy_to_proto(p) -> ap_pb.AgentRuntimePolicy:
89
94
  max_llm_calls=p.max_llm_calls,
90
95
  max_cost_usd=p.max_cost_usd,
91
96
  max_tokens_per_call=p.max_tokens_per_call,
97
+ max_call_seconds=p.max_call_seconds,
92
98
  tool_call_limits=[ap_pb.ToolCallLimit(tool=l.tool, max_calls=l.max_calls) for l in p.tool_call_limits],
93
99
  )
94
100
 
@@ -5,6 +5,7 @@ import os
5
5
 
6
6
  import typer
7
7
 
8
+ from boundflow.cli._output import error as report_error
8
9
  from boundflow.control_plane import ControlPlaneClient, DEFAULT_SERVER_ADDRESS
9
10
 
10
11
  _server: str = DEFAULT_SERVER_ADDRESS
@@ -26,7 +27,7 @@ def cp_call(fn):
26
27
  Converts any exception into a user-facing error message + Exit(1).
27
28
  """
28
29
  if not _api_key:
29
- typer.echo("Error: no API key. Set BOUNDFLOW_API_KEY or pass --api-key.", err=True)
30
+ report_error("no API key. Set BOUNDFLOW_API_KEY or pass --api-key.")
30
31
  raise typer.Exit(1)
31
32
 
32
33
  async def _run():
@@ -38,5 +39,5 @@ def cp_call(fn):
38
39
  except typer.Exit:
39
40
  raise
40
41
  except Exception as exc:
41
- typer.echo(f"Error: {exc}", err=True)
42
+ report_error(str(exc))
42
43
  raise typer.Exit(1)
@@ -18,6 +18,10 @@ def set_json(value: bool) -> None:
18
18
  _json_mode = value
19
19
 
20
20
 
21
+ def is_json() -> bool:
22
+ return _json_mode
23
+
24
+
21
25
  def _normalize(value):
22
26
  """Recursively convert enum instances to their .value, so table display
23
27
  and JSON output both show plain strings rather than 'EnumClass.member'."""
@@ -88,3 +92,13 @@ def success(msg: str) -> None:
88
92
  typer.echo(json.dumps({"status": "ok", "message": msg}))
89
93
  else:
90
94
  console.print(f"[green]{msg}[/green]")
95
+
96
+
97
+ def error(msg: str) -> None:
98
+ """Report a failure. In --json mode this goes to stdout as structured JSON,
99
+ so scripted/--json callers get a machine-readable error instead of having
100
+ to parse stderr text."""
101
+ if _json_mode:
102
+ typer.echo(json.dumps({"status": "error", "message": msg}))
103
+ else:
104
+ typer.echo(f"Error: {msg}", err=True)
@@ -7,7 +7,7 @@ from typing import List, Optional
7
7
  import typer
8
8
 
9
9
  from boundflow.cli._client import cp_call
10
- from boundflow.cli._output import success
10
+ from boundflow.cli._output import error, output, success
11
11
  from boundflow.policies import AgentRule, RuntimePolicy, ToolCallLimit, WorkflowRule
12
12
 
13
13
  app = typer.Typer(help="Manage agent and workflow policies.")
@@ -28,13 +28,13 @@ def _parse_tool_limit(value: str) -> ToolCallLimit:
28
28
 
29
29
  def _load_json(file: Optional[Path], inline: Optional[str]) -> list:
30
30
  if file and inline:
31
- typer.echo("Error: provide --file or --rules, not both.", err=True)
31
+ error("provide --file or --rules, not both.")
32
32
  raise typer.Exit(1)
33
33
  if file:
34
34
  return json.loads(file.read_text(encoding="utf-8-sig"))
35
35
  if inline:
36
36
  return json.loads(inline)
37
- typer.echo("Error: provide --file or --rules.", err=True)
37
+ error("provide --file or --rules.")
38
38
  raise typer.Exit(1)
39
39
 
40
40
 
@@ -65,6 +65,16 @@ def runtime_set(
65
65
  success(f"Runtime policy set on {workflow_id} / {agent_name}.")
66
66
 
67
67
 
68
+ @app.command("get-runtime")
69
+ def runtime_get(
70
+ workflow_id: str = typer.Argument(..., help="Workflow ID"),
71
+ agent_name: str = typer.Argument(..., help="Agent name"),
72
+ ):
73
+ """Show the armed runtime (hard cap) policy for an agent (empty if none is set)."""
74
+ policy = cp_call(lambda cp: cp.get_agent_runtime_policy(workflow_id, agent_name))
75
+ output(policy or {})
76
+
77
+
68
78
  @lifecycle_app.command("set-agent")
69
79
  def lifecycle_set_agent(
70
80
  workflow_id: str = typer.Argument(..., help="Workflow ID"),
@@ -98,3 +108,22 @@ def lifecycle_set_workflow(
98
108
  wf_rules = [WorkflowRule.model_validate(r) for r in raw]
99
109
  cp_call(lambda cp: cp.set_workflow_lifecycle_policy(workflow_id, wf_rules))
100
110
  success(f"Workflow lifecycle policy set on {workflow_id}.")
111
+
112
+
113
+ @lifecycle_app.command("get-agent")
114
+ def lifecycle_get_agent(
115
+ workflow_id: str = typer.Argument(..., help="Workflow ID"),
116
+ agent_name: str = typer.Argument(..., help="Agent name"),
117
+ ):
118
+ """Show the armed lifecycle rules for an agent (empty if none is set)."""
119
+ policy = cp_call(lambda cp: cp.get_agent_lifecycle_policy(workflow_id, agent_name))
120
+ output(policy or {})
121
+
122
+
123
+ @lifecycle_app.command("get-workflow")
124
+ def lifecycle_get_workflow(
125
+ workflow_id: str = typer.Argument(..., help="Workflow ID"),
126
+ ):
127
+ """Show the armed lifecycle rules for a workflow (empty if none is set)."""
128
+ rules = cp_call(lambda cp: cp.get_workflow_lifecycle_policy(workflow_id))
129
+ output([r.model_dump(mode="json") for r in rules])
@@ -1,12 +1,13 @@
1
1
  """boundflow workflow — workflow lifecycle commands."""
2
2
 
3
+ import json
3
4
  from typing import Optional
4
5
 
5
6
  import typer
6
7
 
7
8
  from boundflow.cli._client import cp_call
8
- from boundflow.cli._output import output, success
9
- from boundflow.control_plane import WorkflowConfig
9
+ from boundflow.cli._output import error, output, success
10
+ from boundflow.control_plane import InvokeMode, WorkflowConfig
10
11
 
11
12
  app = typer.Typer(help="Manage workflows.")
12
13
 
@@ -19,6 +20,12 @@ def create(
19
20
  timeout: int = typer.Option(60, "--timeout", help="Invoke timeout in seconds"),
20
21
  repeat: int = typer.Option(0, "--repeat", help="Repeat every N seconds (0 = no repeat)"),
21
22
  no_triggerable: bool = typer.Option(False, "--no-triggerable", help="Disable manual triggering"),
23
+ invoke_mode: InvokeMode = typer.Option(
24
+ InvokeMode.COALESCE, "--invoke-mode",
25
+ help="How piled-up invokes are handled: coalesce (latest-wins) or queue (run each, FIFO)"),
26
+ max_queue_depth: int = typer.Option(
27
+ 0, "--max-queue-depth",
28
+ help="Queue-mode backlog cap; invokes past it are rejected (0 = server default)"),
22
29
  ):
23
30
  """Create a new workflow."""
24
31
  config = WorkflowConfig(
@@ -26,6 +33,8 @@ def create(
26
33
  invoke_timeout_seconds=timeout,
27
34
  repeat_every_seconds=repeat,
28
35
  triggerable=not no_triggerable,
36
+ invoke_mode=invoke_mode,
37
+ max_queue_depth=max_queue_depth,
29
38
  )
30
39
  result = cp_call(lambda cp: cp.create_workflow(workflow_type, tenant_id, config=config))
31
40
  output(result)
@@ -59,6 +68,42 @@ def get(
59
68
  output(result)
60
69
 
61
70
 
71
+ @app.command("set-config")
72
+ def set_config(
73
+ workflow_id: str = typer.Argument(..., help="Workflow ID"),
74
+ version: int = typer.Option(..., "--version", "-v", help="Workflow version to switch to"),
75
+ timeout: int = typer.Option(60, "--timeout", help="Invoke timeout in seconds"),
76
+ repeat: int = typer.Option(0, "--repeat", help="Repeat every N seconds (0 = no repeat)"),
77
+ no_triggerable: bool = typer.Option(False, "--no-triggerable", help="Disable manual triggering"),
78
+ invoke_mode: InvokeMode = typer.Option(
79
+ InvokeMode.COALESCE, "--invoke-mode",
80
+ help="How piled-up invokes are handled: coalesce (latest-wins) or queue (run each, FIFO)"),
81
+ max_queue_depth: int = typer.Option(
82
+ 0, "--max-queue-depth",
83
+ help="Queue-mode backlog cap; invokes past it are rejected (0 = server default)"),
84
+ ):
85
+ """Update a workflow's config settings."""
86
+ config = WorkflowConfig(
87
+ version=version,
88
+ invoke_timeout_seconds=timeout,
89
+ repeat_every_seconds=repeat,
90
+ triggerable=not no_triggerable,
91
+ invoke_mode=invoke_mode,
92
+ max_queue_depth=max_queue_depth,
93
+ )
94
+ result = cp_call(lambda cp: cp.set_workflow_config(workflow_id, config))
95
+ output(result)
96
+
97
+
98
+ @app.command("metrics")
99
+ def metrics(
100
+ workflow_id: str = typer.Argument(..., help="Workflow ID"),
101
+ ):
102
+ """Cumulative totals (cost, run count, failures, ...) for a workflow's current version."""
103
+ result = cp_call(lambda cp: cp.get_workflow_metrics(workflow_id))
104
+ output(result)
105
+
106
+
62
107
  @app.command("list")
63
108
  def list_workflows():
64
109
  """List all workflows in the tenant group."""
@@ -116,6 +161,23 @@ def reject(
116
161
  success(f"Approval {approval_id} rejected.")
117
162
 
118
163
 
164
+ @app.command("submit-input")
165
+ def submit_input(
166
+ workflow_id: str = typer.Argument(..., help="Workflow ID"),
167
+ input_id: str = typer.Argument(..., help="Input ID (from the input request)"),
168
+ answer: str = typer.Option(..., "--answer", help="Answer as a JSON object, e.g. '{\"choice\": \"refund\"}'"),
169
+ actor: str = typer.Option("", "--actor", help="Who supplied the answer (e.g. email or user ID)"),
170
+ ):
171
+ """Answer a pending workflow input gate."""
172
+ try:
173
+ parsed = json.loads(answer)
174
+ except json.JSONDecodeError as e:
175
+ error(f"--answer must be valid JSON: {e}")
176
+ raise typer.Exit(1)
177
+ cp_call(lambda cp: cp.submit_input(workflow_id, input_id, parsed, actor=actor))
178
+ success(f"Input {input_id} submitted.")
179
+
180
+
119
181
  @app.command("delete")
120
182
  def delete(
121
183
  workflow_id: str = typer.Argument(..., help="Workflow ID"),