Skip to content

Commit 4d66d57

Browse files
authored
Merge pull request #7 from i365dev/feature/execution-limits
Add Execution Limits and Statistics Tracking to AgentForge
2 parents 91b6247 + eb39724 commit 4d66d57

9 files changed

Lines changed: 1319 additions & 271 deletions

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,44 @@ Compose handlers into pipelines:
9393
workflow = [&validate/2, &process/2, &notify/2]
9494
```
9595

96+
## Execution Limits
97+
98+
AgentForge now supports execution limits for flows to prevent long-running processes:
99+
100+
```elixir
101+
# Create a handler
102+
handler = fn signal, state ->
103+
# Processing logic...
104+
{{:emit, Signal.new(:done, result)}, state}
105+
end
106+
107+
# Apply timeout limit
108+
{:ok, result, state} = AgentForge.process_with_limits(
109+
[handler],
110+
signal,
111+
%{},
112+
timeout_ms: 5000 # Execution limited to 5 seconds
113+
)
114+
115+
# Get execution statistics in the result
116+
{:ok, result, state, stats} = AgentForge.process_with_limits(
117+
[handler],
118+
signal,
119+
%{},
120+
return_stats: true
121+
)
122+
123+
# Or retrieve the last execution statistics afterwards
124+
stats = AgentForge.get_last_execution_stats()
125+
```
126+
127+
The execution limits feature supports the following options:
128+
- `timeout_ms`: Maximum execution time in milliseconds (default: `30000`)
129+
- `collect_stats`: Whether to collect execution statistics (default: `true`)
130+
- `return_stats`: Whether to include statistics in the return value (default: `false`)
131+
132+
See the documentation for more details.
133+
96134
## Documentation
97135

98136
- [Getting Started Guide](guides/getting_started.md)

examples/limited_workflow.exs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
defmodule Examples.LimitedWorkflow do
2+
@moduledoc """
3+
This example demonstrates how to use execution limits in AgentForge.
4+
5+
It shows:
6+
1. How to set timeout limits
7+
2. How to collect and analyze execution statistics
8+
3. How to handle timeouts gracefully
9+
"""
10+
11+
alias AgentForge.{Signal, Flow, ExecutionStats}
12+
13+
def run do
14+
IO.puts("=== Running Limited Workflow Example ===\n")
15+
16+
# Simple example with timeout
17+
run_with_timeout()
18+
19+
# Example collecting statistics
20+
run_with_statistics()
21+
22+
# Example with long-running handler that will timeout
23+
run_with_timeout_error()
24+
end
25+
26+
defp run_with_timeout do
27+
IO.puts("\n--- Basic Example with Timeout ---")
28+
29+
# Define a simple handler
30+
handler = fn signal, state ->
31+
IO.puts("Processing signal: #{signal.type} -> #{inspect(signal.data)}")
32+
Process.sleep(100) # Simulate some work
33+
{{:emit, Signal.new(:processed, signal.data)}, state}
34+
end
35+
36+
# Create signal and process with a generous timeout
37+
signal = Signal.new(:task, "Sample data")
38+
39+
{:ok, result, _state} = Flow.process_with_limits(
40+
[handler],
41+
signal,
42+
%{},
43+
timeout_ms: 5000 # 5 second timeout
44+
)
45+
46+
IO.puts("Result: #{result.type} -> #{inspect(result.data)}")
47+
end
48+
49+
defp run_with_statistics do
50+
IO.puts("\n--- Example with Statistics Collection ---")
51+
52+
# Define handlers that we'll track statistics for
53+
handlers = [
54+
# First handler - validate data
55+
fn signal, state ->
56+
IO.puts("Validating data...")
57+
Process.sleep(50) # Simulate validation
58+
{{:emit, Signal.new(:validated, signal.data)}, state}
59+
end,
60+
61+
# Second handler - transform data
62+
fn signal, state ->
63+
IO.puts("Transforming data...")
64+
Process.sleep(100) # Simulate transformation
65+
{{:emit, Signal.new(:transformed, "#{signal.data} (transformed)")}, state}
66+
end,
67+
68+
# Third handler - finalize
69+
fn signal, state ->
70+
IO.puts("Finalizing...")
71+
Process.sleep(75) # Simulate finalization
72+
{{:emit, Signal.new(:completed, signal.data)}, state}
73+
end
74+
]
75+
76+
# Create signal and process with statistics
77+
signal = Signal.new(:input, "Test data")
78+
79+
{:ok, result, _state, stats} = Flow.process_with_limits(
80+
handlers,
81+
signal,
82+
%{},
83+
timeout_ms: 5000,
84+
return_stats: true # Return stats in the result
85+
)
86+
87+
IO.puts("Result: #{result.type} -> #{inspect(result.data)}")
88+
IO.puts("\nExecution Statistics:")
89+
IO.puts("- Total steps: #{stats.steps}")
90+
IO.puts("- Elapsed time: #{stats.elapsed_ms}ms")
91+
IO.puts("- Completed: #{stats.complete}")
92+
end
93+
94+
defp run_with_timeout_error do
95+
IO.puts("\n--- Example with Timeout Error ---")
96+
97+
# Define a handler that will take too long
98+
slow_handler = fn signal, state ->
99+
IO.puts("Starting long process...")
100+
# This will exceed our timeout
101+
Process.sleep(2000)
102+
{{:emit, Signal.new(:done, signal.data)}, state}
103+
end
104+
105+
signal = Signal.new(:task, "Important data")
106+
107+
# Process with a short timeout - this should timeout
108+
result = Flow.process_with_limits(
109+
[slow_handler],
110+
signal,
111+
%{},
112+
timeout_ms: 500 # Only 500ms timeout
113+
)
114+
115+
case result do
116+
{:error, error_message, _state} ->
117+
IO.puts("Error handled gracefully: #{error_message}")
118+
119+
other ->
120+
IO.puts("Unexpected result: #{inspect(other)}")
121+
end
122+
123+
# We can still retrieve the execution stats afterwards
124+
stats = Flow.get_last_execution_stats()
125+
126+
if stats do
127+
IO.puts("\nTimeout Statistics:")
128+
IO.puts("- Elapsed time: #{stats.elapsed_ms}ms")
129+
IO.puts("- Completed: #{stats.complete}")
130+
else
131+
IO.puts("\nNo statistics available")
132+
end
133+
end
134+
end
135+
136+
# Run the example when this file is executed directly
137+
if Code.ensure_loaded?(IEx) && IEx.started?() do
138+
# Running in IEx, let the user decide when to run
139+
IO.puts("Run Examples.LimitedWorkflow.run() to execute the example")
140+
else
141+
# Running as a script, execute immediately
142+
Examples.LimitedWorkflow.run()
143+
end

0 commit comments

Comments
 (0)