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.
- {boundflow-0.2.0 → boundflow-0.4.0}/PKG-INFO +3 -1
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/__init__.py +11 -3
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/_transport.py +11 -5
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/_client.py +3 -2
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/_output.py +14 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/policies.py +32 -3
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/workflows.py +64 -2
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/control_plane.py +250 -13
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/errors.py +6 -6
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/lifecycle.py +2 -1
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/llm.py +18 -5
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/policies.py +32 -5
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/trace.py +7 -0
- boundflow-0.4.0/boundflow/v1/agent_policy_pb2.py +53 -0
- boundflow-0.4.0/boundflow/v1/lifecycle_pb2.py +183 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/lifecycle_pb2_grpc.py +304 -0
- boundflow-0.4.0/boundflow/v1/operation_pb2.py +70 -0
- boundflow-0.4.0/boundflow/v1/workflow_pb2.py +49 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/worker.py +126 -26
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/PKG-INFO +3 -1
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/SOURCES.txt +13 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/requires.txt +2 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/pyproject.toml +3 -1
- boundflow-0.4.0/tests/test_agent_call_timeout.py +80 -0
- boundflow-0.4.0/tests/test_agent_metrics_latency.py +27 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_approval_gate.py +4 -0
- boundflow-0.4.0/tests/test_await_input.py +202 -0
- boundflow-0.4.0/tests/test_complete_result.py +154 -0
- boundflow-0.4.0/tests/test_delete_workflow.py +84 -0
- boundflow-0.4.0/tests/test_get_workflow_metrics.py +49 -0
- boundflow-0.4.0/tests/test_invoke_context.py +119 -0
- boundflow-0.4.0/tests/test_invoke_mode.py +142 -0
- boundflow-0.4.0/tests/test_metrics_epoch_isolation.py +72 -0
- boundflow-0.4.0/tests/test_pending_approval.py +87 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_periodic_workflow.py +56 -0
- boundflow-0.4.0/tests/test_policy_getters.py +58 -0
- boundflow-0.4.0/tests/test_request_info.py +52 -0
- boundflow-0.4.0/tests/test_set_workflow_config.py +51 -0
- boundflow-0.2.0/boundflow/v1/agent_policy_pb2.py +0 -53
- boundflow-0.2.0/boundflow/v1/lifecycle_pb2.py +0 -147
- boundflow-0.2.0/boundflow/v1/operation_pb2.py +0 -68
- boundflow-0.2.0/boundflow/v1/workflow_pb2.py +0 -42
- {boundflow-0.2.0 → boundflow-0.4.0}/LICENSE +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/anthropic_client.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/__init__.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/__main__.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/__init__.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/audit.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/pricing.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/cli/commands/tenants.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/__init__.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/approval_gate.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/hello.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/langchain_adapter.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/langgraph_workflow.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/model_switching.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/runtime_caps.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/examples/self_healing.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/langchain_client.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/__init__.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/agent_policy_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/operation_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/policy_pb2.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/policy_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/pricing_pb2.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/pricing_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/registration_pb2.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/registration_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_group_pb2.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_group_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_pb2.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/tenant_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/worker_pb2.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/worker_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow/v1/workflow_pb2_grpc.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/dependency_links.txt +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/entry_points.txt +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/boundflow.egg-info/top_level.txt +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/setup.cfg +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_agent_lifecycle_policy.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_agent_policy_audit.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_approval_audit.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_capability_routing.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_cooldown_and_resume.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_get_workflow.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_langchain_client.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_lifecycle_states.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_list_tenants.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_list_workflows.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_max_tokens_per_call.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_mock_llm.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_model_pricing.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_otel_sink.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_policy_audit.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_resolve_interrupted_workflow.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_tool_call_limits.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_trace.py +0 -0
- {boundflow-0.2.0 → boundflow-0.4.0}/tests/test_workflow_lifecycle_policy.py +0 -0
- {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.
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"),
|