phronomy 0.13.0 → 0.15.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/README.md +266 -38
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/docs/mcp-client.md +75 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/gemfiles/mcp_1_0.gemfile +9 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +509 -420
- data/lib/phronomy/agent/context/capability/base.rb +57 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +58 -53
- data/lib/phronomy/diagnostics.rb +1 -1
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
- data/lib/phronomy/engine/event_loop.rb +507 -303
- data/lib/phronomy/engine/fsm_session.rb +181 -140
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
- data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
- data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
- data/lib/phronomy/engine/runtime.rb +247 -57
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +6 -3
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +410 -81
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +55 -104
- data/lib/phronomy/workflow_runner.rb +239 -291
- data/lib/phronomy.rb +30 -23
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +63 -11
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -346
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
|
@@ -2,74 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Superseded by [ADR-011](011-delegate-transport-policy-to-adapters.md).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Historical decision
|
|
8
8
|
|
|
9
|
-
`Agent::Base
|
|
10
|
-
|
|
9
|
+
Phronomy previously exposed `Agent::Base.invoke_timeout` as a class-level wait
|
|
10
|
+
boundary. The initial implementation did not stop background work; a later
|
|
11
|
+
implementation attached a cancellation scope to the complete Agent invocation.
|
|
11
12
|
|
|
12
|
-
The
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
same hazards because it can interrupt anywhere inside a `rescue`/`ensure` block.
|
|
18
|
-
|
|
19
|
-
Cooperative cancellation (checking a shared flag periodically) is safe but
|
|
20
|
-
requires every tool, every LLM call, and every framework-internal loop to
|
|
21
|
-
participate — a significant API surface change.
|
|
22
|
-
|
|
23
|
-
## Decision
|
|
24
|
-
|
|
25
|
-
`invoke_timeout` is a **wait timeout only**. When the deadline is reached:
|
|
26
|
-
|
|
27
|
-
- `TimeoutError` is raised in the calling thread.
|
|
28
|
-
- The agent's background thread continues running until it either completes
|
|
29
|
-
normally or is garbage-collected when the process ends.
|
|
30
|
-
- No cancellation signal is sent to the agent.
|
|
31
|
-
|
|
32
|
-
This is explicitly documented in the README and in the DSL source.
|
|
33
|
-
|
|
34
|
-
A proper cooperative cancellation mechanism is tracked in Issue #216
|
|
35
|
-
(`CancellationToken`), which is a separate feature requiring agent, tool, and
|
|
36
|
-
transport layer participation.
|
|
37
|
-
|
|
38
|
-
## Consequences
|
|
39
|
-
|
|
40
|
-
**Positive:**
|
|
41
|
-
- No risk of leaving shared resources (DB connections, mutexes, sockets) in a
|
|
42
|
-
broken state due to forced thread interruption.
|
|
43
|
-
- Implementation is simple: `Timeout.timeout` on the calling side only.
|
|
44
|
-
- The contract is explicit and predictable.
|
|
45
|
-
|
|
46
|
-
**Negative / Tradeoffs:**
|
|
47
|
-
- Background threads may continue consuming resources (LLM API quota, etc.)
|
|
48
|
-
after the caller has given up.
|
|
49
|
-
- Users who expect "cancel" semantics from a timeout will be surprised.
|
|
50
|
-
- Proper cancellation requires the `CancellationToken` feature (#216), which
|
|
51
|
-
has not yet been implemented.
|
|
52
|
-
|
|
53
|
-
## Extension: PendingOperation#await cooperative cancellation semantics
|
|
54
|
-
|
|
55
|
-
`BlockingAdapterPool::PendingOperation#await` also supports both `timeout:` and
|
|
56
|
-
`cancellation_token:` parameters. The same non-preemptive rule applies here,
|
|
57
|
-
consistent with ADR-010 (cooperative-first, non-preemptive concurrency model):
|
|
58
|
-
|
|
59
|
-
1. **No forcible thread termination.** When a `cancellation_token` is cancelled,
|
|
60
|
-
`CancellationError` is raised to the `await` caller; when the timeout fires,
|
|
61
|
-
`TimeoutError` is raised instead. In both cases, the underlying worker thread
|
|
62
|
-
is **not** killed. The worker runs its block to natural completion.
|
|
63
|
-
2. **Cooperative, not preemptive.** Cancellation takes effect only at `await`
|
|
64
|
-
call sites or at explicit `token.check!` checkpoints inside the submitted
|
|
65
|
-
block. Code that ignores the token will not be interrupted.
|
|
66
|
-
3. **Timeout scope.** `timeout:` at `await` time is measured from the moment
|
|
67
|
-
`await` is called. If both submit-time and await-time timeouts are provided,
|
|
68
|
-
the earlier deadline wins.
|
|
69
|
-
4. **Error propagation.** `CancellationError` (or `TimeoutError`) is raised to
|
|
70
|
-
the `await` caller; the submitter is responsible for handling it.
|
|
71
|
-
|
|
72
|
-
These semantics are identical in spirit to the `invoke_timeout` decision above:
|
|
73
|
-
the framework exposes a *wait* boundary, not a hard-kill boundary. Safe resource
|
|
74
|
-
cleanup is the caller's responsibility.
|
|
13
|
+
The API has been removed. An Agent class no longer owns a default invocation
|
|
14
|
+
timeout. Applications that need a deadline for a particular root operation pass
|
|
15
|
+
an `InvocationContext` with `deadline:` or `cancellation_token:`. Those values are
|
|
16
|
+
coordination context supplied by the caller, not an implicit Agent execution
|
|
17
|
+
policy.
|
|
75
18
|
|
|
19
|
+
LLM transport timeout belongs to RubyLLM or another configured LLM adapter.
|
|
20
|
+
Tool transport timeout belongs to the Tool implementation or its underlying
|
|
21
|
+
client. Phronomy retains generic deadline and cancellation primitives for the
|
|
22
|
+
execution tree it coordinates.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR-011: Delegate Transport Timeout and Retry to Adapters
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Phronomy accumulated execution-policy settings at several layers:
|
|
10
|
+
|
|
11
|
+
- `Agent::Base.retry_policy`, which replayed the complete Agent invocation;
|
|
12
|
+
- `Agent::Base.invoke_timeout`, which imposed an Agent-class deadline;
|
|
13
|
+
- `config[:llm_timeout]`, applied outside RubyLLM;
|
|
14
|
+
- Tool `retry_on` and `config[:tool_timeout]`;
|
|
15
|
+
- `max_parallel_tools` and the unused `InvocationContext#provider_limits`.
|
|
16
|
+
|
|
17
|
+
The policies did not share a reliable resource model. In particular, replaying
|
|
18
|
+
an Agent or Tool can repeat external side effects, and a pool-level timeout does
|
|
19
|
+
not terminate a blocking provider call that is already running. RubyLLM already
|
|
20
|
+
owns LLM request timeout, transient-error retry, backoff, and jitter. Tool
|
|
21
|
+
implementations commonly use clients that own equivalent transport behavior.
|
|
22
|
+
|
|
23
|
+
## Decision
|
|
24
|
+
|
|
25
|
+
1. RubyLLM, or another configured LLM adapter, owns LLM transport timeout,
|
|
26
|
+
retry, backoff, jitter, and provider rate-limit handling.
|
|
27
|
+
2. Phronomy translates final provider errors but does not replay the complete
|
|
28
|
+
Agent invocation.
|
|
29
|
+
3. Tool implementations or their underlying clients own Tool-specific timeout
|
|
30
|
+
and retry. Phronomy does not provide a generic Tool replay policy.
|
|
31
|
+
4. Agent classes do not own a default invocation timeout. Callers may pass an
|
|
32
|
+
`InvocationContext` containing a `deadline` or `cancellation_token` when a
|
|
33
|
+
specific root operation needs a boundary.
|
|
34
|
+
5. Phronomy retains the timeout mechanisms for boundaries it owns: Workflow
|
|
35
|
+
actions, authorization evaluation, Orchestrator aggregate waits, Runtime
|
|
36
|
+
shutdown/drain, and generic concurrency primitives.
|
|
37
|
+
6. Parallel Tool execution is a boolean mode. When enabled, all authorized
|
|
38
|
+
calls in the intercepted batch are dispatched. Runtime's bounded workers and
|
|
39
|
+
queues remain the coarse process-protection mechanism.
|
|
40
|
+
7. No resource manager, provider limiter, priority scheduler, or compatibility
|
|
41
|
+
shim is introduced by this change.
|
|
42
|
+
|
|
43
|
+
## Consequences
|
|
44
|
+
|
|
45
|
+
### Positive
|
|
46
|
+
|
|
47
|
+
- LLM transport behavior has one configuration authority.
|
|
48
|
+
- Agent and Tool side effects are not implicitly replayed by the framework.
|
|
49
|
+
- Timeout behavior is controlled at the layer capable of interrupting or
|
|
50
|
+
cancelling the underlying I/O safely.
|
|
51
|
+
- Invocation context remains small and contains only values consumed by the
|
|
52
|
+
execution path.
|
|
53
|
+
- The Agent FSM has one invocation attempt and one error propagation path.
|
|
54
|
+
|
|
55
|
+
### Tradeoffs
|
|
56
|
+
|
|
57
|
+
- Applications must configure RubyLLM explicitly when its defaults are not
|
|
58
|
+
appropriate for production.
|
|
59
|
+
- Custom LLM adapters must document and implement their own transport policy.
|
|
60
|
+
- Tool authors are responsible for idempotency when they choose to retry in a
|
|
61
|
+
Tool or client.
|
|
62
|
+
- Enabling parallel Tool execution dispatches the complete authorized batch;
|
|
63
|
+
applications should leave it disabled when unbounded batch fan-out is not
|
|
64
|
+
acceptable.
|
|
65
|
+
|
|
66
|
+
## Example
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
RubyLLM.configure do |config|
|
|
70
|
+
config.request_timeout = 120
|
|
71
|
+
config.max_retries = 3
|
|
72
|
+
config.retry_interval = 0.1
|
|
73
|
+
config.retry_backoff_factor = 2
|
|
74
|
+
config.retry_interval_randomness = 0.5
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context = Phronomy::InvocationContext.new(
|
|
78
|
+
deadline: Phronomy::Concurrency::Deadline.in(30)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
result = MyAgent.new.invoke("Hello", invocation_context: context)
|
|
82
|
+
```
|
data/docs/mcp-client.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# MCP client support
|
|
2
|
+
|
|
3
|
+
Phronomy supports the official `mcp` Ruby SDK **1.x**. MCP SDK 0.x is not supported.
|
|
4
|
+
|
|
5
|
+
## Transports
|
|
6
|
+
|
|
7
|
+
`Phronomy::Tools::Mcp.from_server` supports:
|
|
8
|
+
|
|
9
|
+
- `stdio://...`
|
|
10
|
+
- `http://...`
|
|
11
|
+
- `https://...`
|
|
12
|
+
|
|
13
|
+
HTTP and SSE support are standard dependencies of Phronomy through `faraday` and
|
|
14
|
+
`event_stream_parser`.
|
|
15
|
+
|
|
16
|
+
## Input-schema subset
|
|
17
|
+
|
|
18
|
+
Phronomy intentionally supports a strict subset of JSON Schema 2020-12 for MCP
|
|
19
|
+
Tool inputs.
|
|
20
|
+
|
|
21
|
+
Supported root keywords:
|
|
22
|
+
|
|
23
|
+
- `$schema`
|
|
24
|
+
- `type` (`object` only)
|
|
25
|
+
- `properties`
|
|
26
|
+
- `required`
|
|
27
|
+
- `title`
|
|
28
|
+
- `description`
|
|
29
|
+
- `additionalProperties` when omitted or `false`
|
|
30
|
+
|
|
31
|
+
Supported property types:
|
|
32
|
+
|
|
33
|
+
- `string`
|
|
34
|
+
- `integer`
|
|
35
|
+
- `number`
|
|
36
|
+
- `boolean`
|
|
37
|
+
|
|
38
|
+
Supported property keywords:
|
|
39
|
+
|
|
40
|
+
- `type`
|
|
41
|
+
- `description`
|
|
42
|
+
- `enum`
|
|
43
|
+
- `title`
|
|
44
|
+
|
|
45
|
+
The following validation keywords are accepted but ignored with a warning:
|
|
46
|
+
|
|
47
|
+
- `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`
|
|
48
|
+
- `minLength`, `maxLength`, `pattern`, `format`, `default`, `examples`
|
|
49
|
+
|
|
50
|
+
Unknown keywords, structural composition (`oneOf`, `anyOf`, `allOf`, `$ref`,
|
|
51
|
+
conditionals), arrays, nested objects, and nullable type arrays fail fast with
|
|
52
|
+
`Phronomy::ToolError` rather than silently changing the Tool contract.
|
|
53
|
+
|
|
54
|
+
When `additionalProperties` is omitted, the remote schema is accepted, but
|
|
55
|
+
Phronomy still exposes and accepts only names declared in `properties`.
|
|
56
|
+
|
|
57
|
+
`outputSchema` is detected and reported with a warning, but is not validated in
|
|
58
|
+
this phase.
|
|
59
|
+
|
|
60
|
+
## Errors and cancellation
|
|
61
|
+
|
|
62
|
+
- JSON-RPC errors raised by the SDK are converted to `Phronomy::ToolError`.
|
|
63
|
+
- MCP Tool results with `isError: true` are returned to the model as Tool error
|
|
64
|
+
text so the model can correct its arguments.
|
|
65
|
+
- An expired HTTP session is reconnected for future calls. The failed Tool call
|
|
66
|
+
is not replayed because Tool side effects are not assumed to be idempotent.
|
|
67
|
+
- Cancellation invalidates the current client and transport. The next call creates
|
|
68
|
+
a new connection, preventing an abandoned SDK worker from sharing the old stdio
|
|
69
|
+
stream with a new request.
|
|
70
|
+
|
|
71
|
+
Calls, reconnects, and explicit `close` operations on a Tool instance are
|
|
72
|
+
serialized. `close` synchronously closes the current client and permits a later
|
|
73
|
+
call to reconnect. A transport detached by cancellation is owned by the bounded
|
|
74
|
+
MCP cleanup pool and is drained during Runtime shutdown; `close` does not wait
|
|
75
|
+
for an older detached transport.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "phronomy"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
class GenerationContext
|
|
7
|
+
include Phronomy::WorkflowContext
|
|
8
|
+
|
|
9
|
+
field :prompt
|
|
10
|
+
field :generation_request_id
|
|
11
|
+
field :answer
|
|
12
|
+
field :error_message
|
|
13
|
+
|
|
14
|
+
# This is application logic. Phronomy transports the event and payload but
|
|
15
|
+
# does not decide which Agent invocation is current or where the result lives.
|
|
16
|
+
def handle_fsm_event(event)
|
|
17
|
+
request_id = event.payload[:generation_request_id]
|
|
18
|
+
return :consume unless request_id == generation_request_id
|
|
19
|
+
|
|
20
|
+
case event.type
|
|
21
|
+
when :generation_completed
|
|
22
|
+
self.answer = event.payload[:agent_result][:output]
|
|
23
|
+
when :generation_failed
|
|
24
|
+
self.error_message = event.payload[:error].message
|
|
25
|
+
end
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class AnswerAgent < Phronomy::Agent::Base
|
|
31
|
+
model "gpt-4o-mini"
|
|
32
|
+
instructions "Answer clearly and briefly."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
agent = AnswerAgent.new
|
|
36
|
+
workflow = nil
|
|
37
|
+
|
|
38
|
+
workflow = Phronomy::Workflow.define(GenerationContext) do
|
|
39
|
+
initial :generating
|
|
40
|
+
|
|
41
|
+
state :generating, action: ->(context) {
|
|
42
|
+
request_id = context.generation_request_id
|
|
43
|
+
|
|
44
|
+
# The Agent Task is intentionally not returned from the entry action.
|
|
45
|
+
# on_event is the application-level integration channel.
|
|
46
|
+
agent.invoke_async(
|
|
47
|
+
context.prompt,
|
|
48
|
+
on_event: ->(agent_event) {
|
|
49
|
+
workflow_event =
|
|
50
|
+
case agent_event.type
|
|
51
|
+
when :done
|
|
52
|
+
:generation_completed
|
|
53
|
+
when :error, :timeout, :cancelled, :approval_required
|
|
54
|
+
:generation_failed
|
|
55
|
+
end
|
|
56
|
+
next unless workflow_event
|
|
57
|
+
|
|
58
|
+
workflow.signal(
|
|
59
|
+
thread_id: context.thread_id,
|
|
60
|
+
event: workflow_event,
|
|
61
|
+
payload: {
|
|
62
|
+
generation_request_id: request_id,
|
|
63
|
+
agent_result: (
|
|
64
|
+
agent_event.payload if agent_event.type == :done
|
|
65
|
+
),
|
|
66
|
+
error:
|
|
67
|
+
agent_event.payload[:error] ||
|
|
68
|
+
Phronomy::Error.new(
|
|
69
|
+
"Agent requested Tool approval"
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
context
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
state :succeeded
|
|
80
|
+
state :failed
|
|
81
|
+
|
|
82
|
+
transition(
|
|
83
|
+
from: :generating,
|
|
84
|
+
on: :generation_completed,
|
|
85
|
+
to: :succeeded
|
|
86
|
+
)
|
|
87
|
+
transition(
|
|
88
|
+
from: :generating,
|
|
89
|
+
on: :generation_failed,
|
|
90
|
+
to: :failed
|
|
91
|
+
)
|
|
92
|
+
transition from: :succeeded, to: :__finish__
|
|
93
|
+
transition from: :failed, to: :__finish__
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result = workflow.invoke(
|
|
97
|
+
{
|
|
98
|
+
prompt: "What is Run-to-Completion?",
|
|
99
|
+
generation_request_id: SecureRandom.uuid
|
|
100
|
+
},
|
|
101
|
+
config: {thread_id: SecureRandom.uuid}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
puts(result.answer || result.error_message)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "phronomy"
|
|
4
|
+
|
|
5
|
+
class ImportContext
|
|
6
|
+
include Phronomy::WorkflowContext
|
|
7
|
+
|
|
8
|
+
field :record_count, default: 0
|
|
9
|
+
field :error_message
|
|
10
|
+
|
|
11
|
+
def handle_fsm_event(event)
|
|
12
|
+
case event.type
|
|
13
|
+
when :import_completed
|
|
14
|
+
self.record_count = event.payload[:record_count]
|
|
15
|
+
when :import_failed
|
|
16
|
+
self.error_message = event.payload[:error].message
|
|
17
|
+
end
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
workflow = nil
|
|
23
|
+
|
|
24
|
+
workflow = Phronomy::Workflow.define(ImportContext) do
|
|
25
|
+
initial :importing
|
|
26
|
+
|
|
27
|
+
state :importing, action: ->(context) {
|
|
28
|
+
task = Phronomy::Runtime.instance.spawn do
|
|
29
|
+
# Replace with application-owned asynchronous work.
|
|
30
|
+
100
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
task.on_complete do |record_count, error|
|
|
34
|
+
workflow.signal(
|
|
35
|
+
thread_id: context.thread_id,
|
|
36
|
+
event: error ? :import_failed : :import_completed,
|
|
37
|
+
payload: {
|
|
38
|
+
record_count: record_count,
|
|
39
|
+
error: error
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Do not return task. The state is active after this synchronous entry ends.
|
|
45
|
+
context
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
state :completed
|
|
49
|
+
state :failed
|
|
50
|
+
|
|
51
|
+
transition from: :importing, on: :import_completed, to: :completed
|
|
52
|
+
transition from: :importing, on: :import_failed, to: :failed
|
|
53
|
+
transition from: :completed, to: :__finish__
|
|
54
|
+
transition from: :failed, to: :__finish__
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
result = workflow.invoke({})
|
|
58
|
+
puts "Imported #{result.record_count} records"
|