rcrewai 0.8.1 → 0.10.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 +39 -0
- data/ROADMAP.md +76 -25
- data/lib/rcrewai/agent.rb +8 -2
- data/lib/rcrewai/cancel_token.rb +26 -0
- data/lib/rcrewai/crew.rb +3 -1
- data/lib/rcrewai/events.rb +2 -1
- data/lib/rcrewai/knowledge/embedder.rb +25 -7
- data/lib/rcrewai/llm_clients/anthropic.rb +27 -20
- data/lib/rcrewai/persistent_http.rb +87 -0
- data/lib/rcrewai/process.rb +34 -5
- data/lib/rcrewai/tool_runner.rb +77 -32
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +2 -0
- data/rcrewai.gemspec +1 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e33f9998ef9f32aa53d4c6df3e8e7b04314f7b5cbb9fe06e5c7d1936b466222
|
|
4
|
+
data.tar.gz: 294a6a4a5ed7d5f1b5fbb58c3edd0bb69a0cd5d9e61da07b9f8dabf6c4457414
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f64195cd3065e2c26dd65ddea08246bec905754192e8ba0fad1a23f7bcbfc998c2f8a4d3cad91f7491f5d8e0f2fc5c4cd53be60de47e737f9d41b2c898089ba7
|
|
7
|
+
data.tar.gz: 5107c4953f3ff80f86125fdbb5325bf582b1c624b07e97b739890fd21d0c83c892c7adff4fd982556141dd5a60b186814bcc3b1644056a89d589788810fee860
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Anthropic live streaming for real-time callers (voice agents):
|
|
14
|
+
- `RCrewAI::CancelToken`: pass `cancel:` to `chat`; `cancel!` aborts the
|
|
15
|
+
stream at the next chunk, closes the connection and raises
|
|
16
|
+
`RCrewAI::StreamCancelled`. Under an Async reactor, stopping the calling
|
|
17
|
+
task does the same.
|
|
18
|
+
- Streaming requests reuse one keep-alive connection per client
|
|
19
|
+
(`RCrewAI::PersistentHTTP`), so back-to-back turns skip TCP/TLS setup.
|
|
20
|
+
- `chat(model:)` overrides the configured model per call.
|
|
21
|
+
- Usage now carries `cached_tokens` (Anthropic `cache_read_input_tokens`)
|
|
22
|
+
in the result hash and on the `Events::Usage` stream event.
|
|
23
|
+
- The Anthropic client honours `config.base_url`.
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
- Verified fiber-safe on Ruby >= 3.2 / async 2.x: Anthropic streaming inside an
|
|
27
|
+
Async reactor never blocks other fibers (spec covers it, skipped < 3.2).
|
|
28
|
+
|
|
29
|
+
## [0.9.0] - 2026-09-10
|
|
30
|
+
|
|
31
|
+
Concurrency. Three points where independent, IO-bound work ran in sequence now
|
|
32
|
+
fan out across threads, each bounded and each preserving input order.
|
|
33
|
+
|
|
34
|
+
| | Before | After |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Tool calls in one turn (3 x 150ms) | 0.45s | 0.165s |
|
|
37
|
+
| Consensus with 3 agents | 1.22s | 0.22s |
|
|
38
|
+
|
|
39
|
+
Entirely additive: no API changes, and every fan-out can be disabled or
|
|
40
|
+
re-bounded. Concurrency is deliberately capped rather than unlimited — an
|
|
41
|
+
unbounded fan-out trades a latency problem for a rate-limit one.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Parallel tool execution: when a model requests several tools in one turn, `ToolRunner` now runs them concurrently instead of in sequence, so the turn costs the slowest call rather than their sum. Results are collected by index, so the message history stays aligned with the `tool_call` ids regardless of completion order. Bounded by `max_tool_concurrency` (default 8); a single tool call still runs inline with no thread. Disable per agent with `Agent.new(parallel_tools: false)`, or per runner with `ToolRunner.new(parallel_tools: false)`. The legacy `USE_TOOL[]` runner is unaffected — it parses one directive at a time.
|
|
45
|
+
- Tool events emitted from worker threads carry the enclosing run span, so the 0.8.0 event hierarchy stays intact under concurrency.
|
|
46
|
+
- Concurrent embedding: providers without a batch embeddings endpoint (`:google`, `:ollama`) issued one HTTP request per text, in sequence, so building a knowledge base cost the sum of every chunk's round-trip. Those requests now run concurrently, bounded by `max_concurrency` (default 8, configurable via `Knowledge::Embedder.new(max_concurrency:)`). Vectors are returned in input order, since `Knowledge::Base#build!` zips them back against their chunks positionally. An error in any request propagates rather than yielding a knowledge base with silently missing vectors. `:openai` and `:azure` already batch into a single request and are unchanged.
|
|
47
|
+
- Concurrent consensus: the `:consensual` process ran its proposal and scoring rounds one LLM call at a time — with the default three agents that is 3 proposals plus 9 scoring calls, twelve sequential round-trips per task. Proposals now run concurrently, and scoring fans out across the whole (candidate × voter) grid rather than per candidate (measured: 1.22s → 0.22s for three agents). Bounded by `consensus_max_concurrency` (default 8), since scoring is quadratic in participants — six agents would otherwise put 36 requests in flight at once. Results keep input order, so ties still break toward the task's assigned agent deterministically.
|
|
48
|
+
|
|
10
49
|
## [0.8.1] - 2026-09-10
|
|
11
50
|
|
|
12
51
|
Dependency cleanup. No API or behavior change.
|
data/ROADMAP.md
CHANGED
|
@@ -5,7 +5,7 @@ This roadmap tracks feature parity between **RCrewAI** (Ruby) and the upstream
|
|
|
5
5
|
|
|
6
6
|
## Current status
|
|
7
7
|
|
|
8
|
-
- **RCrewAI:** `0.
|
|
8
|
+
- **RCrewAI:** `0.8.1` released; 1.0.0 concurrency work merged to `main`, unreleased
|
|
9
9
|
- **Upstream crewai:** `1.15.21`
|
|
10
10
|
|
|
11
11
|
RCrewAI is a faithful port of CrewAI's **"Crews"** mental model (Agents / Tasks /
|
|
@@ -15,12 +15,12 @@ human-in-the-loop), and it carries CrewAI's second pillar (**Flows**) plus
|
|
|
15
15
|
**training/testing**. In one area — cognitive memory (semantic recall, SQLite
|
|
16
16
|
persistence, four memory types) — the gem went past what was originally ported.
|
|
17
17
|
|
|
18
|
-
**Status:
|
|
18
|
+
**Status: roadmap complete.** An earlier revision of this file declared
|
|
19
19
|
parity "complete" against a matrix that only covered CrewAI through roughly
|
|
20
20
|
`1.0` (October 2025) while quoting `1.15.x` in its header; everything upstream
|
|
21
|
-
added across `1.1`–`1.15` was unmeasured. That delta was re-derived, and
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
added across `1.1`–`1.15` was unmeasured. That delta was re-derived, and all four
|
|
22
|
+
scheduled milestones have since shipped. What remains are two additive items
|
|
23
|
+
(Bedrock/Responses streaming, SigV4) and one deferred concept (A2A).
|
|
24
24
|
|
|
25
25
|
## Parity matrix
|
|
26
26
|
|
|
@@ -57,7 +57,8 @@ is outstanding, and it is blocked on an open decision — see below.
|
|
|
57
57
|
|
|
58
58
|
| Concept | crewai | RCrewAI | Plan |
|
|
59
59
|
|---|---|---|---|
|
|
60
|
-
|
|
|
60
|
+
| Concurrent tool calls | ✅ (1.4–1.6) | ✅ (#47) | — |
|
|
61
|
+
| Concurrent embedding / consensus | ✅ (1.4–1.6) | ✅ (#49, #50) | — |
|
|
61
62
|
| OTel export for the event hierarchy | ✅ (1.10+) | ❌ | 1.0.x |
|
|
62
63
|
| Streaming for Bedrock / Responses | ✅ | ❌ | 1.0.x |
|
|
63
64
|
| Bedrock SigV4 signing | ✅ | ❌ (hook workaround) | 1.0.x |
|
|
@@ -115,24 +116,71 @@ Two deliberate limitations carried forward to 1.0.x: Bedrock does not implement
|
|
|
115
116
|
SigV4 (a hard `aws-sigv4` dependency for one provider is not worth it; sign via
|
|
116
117
|
a `before_request` hook), and Bedrock/Responses are non-streaming only.
|
|
117
118
|
|
|
118
|
-
### 1.0.0 —
|
|
119
|
+
### 1.0.0 — Concurrency ✅ shipped
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
async *through* the LLM and tool calls (1.4–1.6), covering flows, crews, tasks,
|
|
124
|
-
knowledge, and memory.
|
|
121
|
+
**Decision made: threads, not fibers.** The roadmap previously left this open.
|
|
122
|
+
Prototyping settled it, and not the way the fiber option's low apparent cost
|
|
123
|
+
suggested.
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
The attraction of fibers was that it looked cheap: keep Faraday, wrap calls in
|
|
126
|
+
`Async`, get non-blocking IO for free. That premise is false on this stack. On
|
|
127
|
+
Ruby 3.1.4 with `async` 2.24.0, a Faraday/`net_http` call inside a fiber
|
|
128
|
+
**never returns** — the task is silently abandoned and the process exits `0` as
|
|
129
|
+
though it succeeded. Raw `Net::HTTP` under `Async` raises `NoMethodError`
|
|
130
|
+
rather than yielding. A first benchmark appeared to show a 1.53s → 0.0s win;
|
|
131
|
+
it was measuring five failed requests that never reached the server.
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
Making fibers work would mean replacing Faraday with `async-http` across all
|
|
134
|
+
nine provider clients, adding a hard runtime dependency on a stack whose
|
|
135
|
+
observed failure mode is *silent abandonment* — the worst possible behavior in
|
|
136
|
+
an agent framework — and likely raising `required_ruby_version` from `3.0`,
|
|
137
|
+
since this fragility lives exactly in 3.0/3.1 scheduler support.
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
Threads cost none of that: `concurrent-ruby` is already a dependency, the Ruby
|
|
140
|
+
floor is unchanged, and failures are loud.
|
|
141
|
+
|
|
142
|
+
**Update (2026-10):** the fiber failure is specific to Ruby 3.0/3.1. On Ruby
|
|
143
|
+
3.3 with `async` 2.46, Net::HTTP streaming inside a fiber yields to the
|
|
144
|
+
reactor correctly (`spec/anthropic_live_streaming_spec.rb` asserts a 10 ms
|
|
145
|
+
ticker never stalls, and that stopping the task closes the stream). Threads
|
|
146
|
+
stay the internal concurrency model; fiber callers on 3.2+ are supported.
|
|
147
|
+
|
|
148
|
+
Upstream's async/await shape is a Python idiom; porting its *form* rather than
|
|
149
|
+
its *effect* would buy nothing here.
|
|
150
|
+
|
|
151
|
+
#### Shipped
|
|
152
|
+
|
|
153
|
+
Three fan-out points, all on threads, each bounded and each preserving input
|
|
154
|
+
order (every one of them feeds a positional zip or a deterministic tie-break
|
|
155
|
+
downstream):
|
|
156
|
+
|
|
157
|
+
- **Parallel tool calls** (#47) — a turn's tool calls run concurrently, so a
|
|
158
|
+
turn costs the slowest call rather than their sum. Measured 0.45s → 0.165s
|
|
159
|
+
for three 150ms tools. `Agent.new(parallel_tools: false)` opts out.
|
|
160
|
+
- **Concurrent embedding** (#49) — providers without a batch endpoint
|
|
161
|
+
(`:google`, `:ollama`) issued one request per text in sequence, so building a
|
|
162
|
+
knowledge base cost the sum of every chunk's round-trip. Bounded by
|
|
163
|
+
`Knowledge::Embedder.new(max_concurrency:)`.
|
|
164
|
+
- **Concurrent consensus** (#50) — the `:consensual` process made twelve
|
|
165
|
+
sequential LLM calls per task with three agents. Proposals and the whole
|
|
166
|
+
(candidate × voter) scoring grid now fan out. Measured 1.22s → 0.22s.
|
|
167
|
+
Bounded by `Crew.new(consensus_max_concurrency:)`.
|
|
168
|
+
|
|
169
|
+
Bounding was not optional: the first consensus implementation was unbounded and
|
|
170
|
+
put 36 concurrent LLM calls in flight with six agents — a thundering herd that
|
|
171
|
+
trips provider rate limits, which is a worse problem than the latency it
|
|
172
|
+
solves. Every fan-out here has a ceiling and a spec asserting it.
|
|
173
|
+
|
|
174
|
+
#### Remaining
|
|
175
|
+
|
|
176
|
+
An audit of `lib/` for per-item IO loops now turns up only CPU-bound work
|
|
177
|
+
(lexical similarity), so the task-boundary goal is met. What is left is not
|
|
178
|
+
concurrency work as such:
|
|
179
|
+
|
|
180
|
+
- Streaming for Bedrock and Responses (both non-streaming today).
|
|
181
|
+
- Bedrock SigV4 signing, currently a `before_request` hook workaround.
|
|
182
|
+
|
|
183
|
+
Neither blocks a 1.0.0 release; both are tracked in the gaps table.
|
|
136
184
|
|
|
137
185
|
### Deferred — A2A
|
|
138
186
|
|
|
@@ -147,12 +195,15 @@ no current RCrewAI user has asked for. Revisit once the items above land.
|
|
|
147
195
|
| 0.8.0 | Interceptors + observability | Low | ✅ merged (#39) |
|
|
148
196
|
| 0.9.0 | Checkpointing | Moderate | ✅ merged (#42) |
|
|
149
197
|
| 0.9.x | Providers, Responses API | Low | ✅ merged (#41) |
|
|
150
|
-
| 1.0.0 |
|
|
198
|
+
| 1.0.0 | Concurrency (threads) | Moderate | ✅ merged (#47, #49, #50) |
|
|
151
199
|
|
|
152
200
|
The three shipped milestones are on `main` and unreleased; they want a version
|
|
153
201
|
bump and a release before or alongside 1.0.0 work.
|
|
154
202
|
|
|
155
|
-
**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
203
|
+
**All scheduled milestones are merged.** The concurrency work landed on threads
|
|
204
|
+
across three fan-out points; see the milestone above for the prototype evidence
|
|
205
|
+
that ruled out fibers.
|
|
206
|
+
|
|
207
|
+
`main` carries the 1.0.0 concurrency work unreleased on top of `0.8.1`. The
|
|
208
|
+
remaining gaps — Bedrock/Responses streaming and SigV4 — are additive and do
|
|
209
|
+
not block cutting a release.
|
data/lib/rcrewai/agent.rb
CHANGED
|
@@ -37,6 +37,7 @@ module RCrewAI
|
|
|
37
37
|
@logger = Logger.new($stdout)
|
|
38
38
|
@logger.level = verbose ? Logger::DEBUG : Logger::INFO
|
|
39
39
|
@reasoning = options.fetch(:reasoning, false)
|
|
40
|
+
@parallel_tools = options.fetch(:parallel_tools, true)
|
|
40
41
|
@max_reasoning_attempts = options.fetch(:max_reasoning_attempts, 3)
|
|
41
42
|
@respect_context_window = options.fetch(:respect_context_window, false)
|
|
42
43
|
@memory = build_memory(options[:memory])
|
|
@@ -60,11 +61,16 @@ module RCrewAI
|
|
|
60
61
|
runner_class = pick_runner_class
|
|
61
62
|
@logger.info "[rcrewai] agent=#{name} runner=#{runner_class.name.split('::').last}"
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
runner_opts = {
|
|
64
65
|
agent: self, llm: @llm_client, tools: @tools,
|
|
65
66
|
max_iterations: opts.fetch(:max_iterations, max_iterations),
|
|
66
67
|
event_sink: sink
|
|
67
|
-
|
|
68
|
+
}
|
|
69
|
+
# Only the native-tool runner executes tool calls concurrently; the
|
|
70
|
+
# legacy runner parses one USE_TOOL[] directive at a time.
|
|
71
|
+
runner_opts[:parallel_tools] = @parallel_tools if runner_class == ToolRunner
|
|
72
|
+
|
|
73
|
+
runner = runner_class.new(**runner_opts)
|
|
68
74
|
|
|
69
75
|
runner_result = runner.run(messages: initial_messages)
|
|
70
76
|
execution_time = Time.now - start_time
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
# Raised inside a streaming call when its CancelToken is triggered.
|
|
5
|
+
class StreamCancelled < Error; end
|
|
6
|
+
|
|
7
|
+
# Cooperative cancellation for a streaming LLM call. Pass it as
|
|
8
|
+
# `cancel:` to a client's #chat; calling #cancel! (from the stream sink,
|
|
9
|
+
# another thread, or another fiber) aborts the stream at the next chunk,
|
|
10
|
+
# closes the connection and raises StreamCancelled in the caller.
|
|
11
|
+
#
|
|
12
|
+
# Under an Async reactor, stopping the calling task has the same effect.
|
|
13
|
+
class CancelToken
|
|
14
|
+
def initialize
|
|
15
|
+
@cancelled = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def cancel!
|
|
19
|
+
@cancelled = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cancelled?
|
|
23
|
+
@cancelled
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/rcrewai/crew.rb
CHANGED
|
@@ -23,6 +23,7 @@ module RCrewAI
|
|
|
23
23
|
@planning_llm = options[:planning_llm]
|
|
24
24
|
@planned = false
|
|
25
25
|
@consensus_agents = options.fetch(:consensus_agents, 3)
|
|
26
|
+
@consensus_max_concurrency = options[:consensus_max_concurrency]
|
|
26
27
|
@knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
|
|
27
28
|
@before_kickoff_hooks = []
|
|
28
29
|
@after_kickoff_hooks = []
|
|
@@ -35,7 +36,8 @@ module RCrewAI
|
|
|
35
36
|
validate_process_type!
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
attr_reader :knowledge, :stream_sink, :last_inputs, :consensus_agents, :run_id
|
|
39
|
+
attr_reader :knowledge, :stream_sink, :last_inputs, :consensus_agents, :run_id,
|
|
40
|
+
:consensus_max_concurrency
|
|
39
41
|
|
|
40
42
|
def planning?
|
|
41
43
|
@planning
|
data/lib/rcrewai/events.rb
CHANGED
|
@@ -17,7 +17,8 @@ module RCrewAI
|
|
|
17
17
|
ToolCallResult = Struct.new(*BASE_ATTRS, :tool, :call_id, :result, :duration_ms, keyword_init: true)
|
|
18
18
|
ToolCallError = Struct.new(*BASE_ATTRS, :tool, :call_id, :error, keyword_init: true)
|
|
19
19
|
Thinking = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
|
|
20
|
-
Usage = Struct.new(*BASE_ATTRS, :prompt_tokens, :completion_tokens, :total_tokens, :cost_usd,
|
|
20
|
+
Usage = Struct.new(*BASE_ATTRS, :prompt_tokens, :completion_tokens, :total_tokens, :cost_usd,
|
|
21
|
+
:cached_tokens, keyword_init: true)
|
|
21
22
|
IterationStart = Struct.new(*BASE_ATTRS, :iteration_index, keyword_init: true)
|
|
22
23
|
IterationEnd = Struct.new(*BASE_ATTRS, :finish_reason, keyword_init: true)
|
|
23
24
|
Error = Struct.new(*BASE_ATTRS, :error, keyword_init: true)
|
|
@@ -22,9 +22,16 @@ module RCrewAI
|
|
|
22
22
|
GOOGLE_BASE = 'https://generativelanguage.googleapis.com/v1beta'
|
|
23
23
|
OLLAMA_DEFAULT_URL = 'http://localhost:11434'
|
|
24
24
|
|
|
25
|
+
# Providers without a batch embeddings endpoint issue one request per
|
|
26
|
+
# text. Those are independent and IO-bound, so they run concurrently --
|
|
27
|
+
# bounded, since a large knowledge base would otherwise open a request
|
|
28
|
+
# per chunk at once.
|
|
29
|
+
DEFAULT_MAX_CONCURRENCY = 8
|
|
30
|
+
|
|
25
31
|
attr_reader :provider, :model
|
|
26
32
|
|
|
27
|
-
def initialize(provider: :openai, model: nil, api_key: nil, config: RCrewAI.configuration
|
|
33
|
+
def initialize(provider: :openai, model: nil, api_key: nil, config: RCrewAI.configuration,
|
|
34
|
+
max_concurrency: DEFAULT_MAX_CONCURRENCY)
|
|
28
35
|
@provider = provider.to_sym
|
|
29
36
|
if @provider == :anthropic
|
|
30
37
|
raise EmbeddingError,
|
|
@@ -34,6 +41,7 @@ module RCrewAI
|
|
|
34
41
|
@config = config
|
|
35
42
|
@model = model || DEFAULT_MODELS[@provider] || DEFAULT_MODELS[:openai]
|
|
36
43
|
@api_key = api_key
|
|
44
|
+
@max_concurrency = max_concurrency
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
def embed(texts)
|
|
@@ -62,19 +70,29 @@ module RCrewAI
|
|
|
62
70
|
|
|
63
71
|
def embed_google(texts)
|
|
64
72
|
key = api_key_for(:google)
|
|
65
|
-
texts
|
|
73
|
+
concurrent_map(texts) do |text|
|
|
66
74
|
url = "#{GOOGLE_BASE}/models/#{@model}:embedContent?key=#{key}"
|
|
67
75
|
payload = { model: "models/#{@model}", content: { parts: [{ text: text }] } }
|
|
68
|
-
|
|
69
|
-
body.dig('embedding', 'values')
|
|
76
|
+
post_json(url, payload).dig('embedding', 'values')
|
|
70
77
|
end
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
def embed_ollama(texts)
|
|
74
81
|
base = @config.base_url || OLLAMA_DEFAULT_URL
|
|
75
|
-
texts
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
concurrent_map(texts) do |text|
|
|
83
|
+
post_json("#{base}/api/embeddings", { model: @model, prompt: text })['embedding']
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Maps over texts in bounded parallel, preserving input order: the caller
|
|
88
|
+
# zips vectors back against their chunks, so completion order must not
|
|
89
|
+
# leak into the result. An error in any request propagates -- a knowledge
|
|
90
|
+
# base with silently missing vectors is worse than a failed build.
|
|
91
|
+
def concurrent_map(texts, &block)
|
|
92
|
+
return texts.map(&block) if texts.length < 2 || @max_concurrency < 2
|
|
93
|
+
|
|
94
|
+
texts.each_slice(@max_concurrency).flat_map do |slice|
|
|
95
|
+
slice.map { |text| Thread.new { block.call(text) } }.map(&:value)
|
|
78
96
|
end
|
|
79
97
|
end
|
|
80
98
|
|
|
@@ -7,6 +7,8 @@ require_relative '../events'
|
|
|
7
7
|
require_relative '../sse_parser'
|
|
8
8
|
require_relative '../provider_schema'
|
|
9
9
|
require_relative '../pricing'
|
|
10
|
+
require_relative '../cancel_token'
|
|
11
|
+
require_relative '../persistent_http'
|
|
10
12
|
|
|
11
13
|
module RCrewAI
|
|
12
14
|
module LLMClients
|
|
@@ -23,7 +25,7 @@ module RCrewAI
|
|
|
23
25
|
|
|
24
26
|
def initialize(config = RCrewAI.configuration, **hooks)
|
|
25
27
|
super(config, **hooks)
|
|
26
|
-
@base_url = BASE_URL
|
|
28
|
+
@base_url = config.base_url || BASE_URL
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options)
|
|
@@ -31,7 +33,7 @@ module RCrewAI
|
|
|
31
33
|
non_system = messages.reject { |m| m.is_a?(Hash) && m[:role] == 'system' }
|
|
32
34
|
|
|
33
35
|
payload = {
|
|
34
|
-
model: config.model,
|
|
36
|
+
model: options[:model] || config.model,
|
|
35
37
|
messages: format_messages(non_system),
|
|
36
38
|
max_tokens: options[:max_tokens] || config.max_tokens || 1000,
|
|
37
39
|
temperature: options[:temperature] || config.temperature
|
|
@@ -57,7 +59,7 @@ module RCrewAI
|
|
|
57
59
|
|
|
58
60
|
if stream
|
|
59
61
|
payload[:stream] = true
|
|
60
|
-
stream_chat(payload, stream)
|
|
62
|
+
stream_chat(payload, stream, cancel: options[:cancel])
|
|
61
63
|
else
|
|
62
64
|
plain_chat(payload)
|
|
63
65
|
end
|
|
@@ -91,7 +93,7 @@ module RCrewAI
|
|
|
91
93
|
apply_after_response(normalize_non_streaming(body), started_at)
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
def stream_chat(payload, sink) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
96
|
+
def stream_chat(payload, sink, cancel: nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
|
|
95
97
|
url = "#{@base_url}/messages"
|
|
96
98
|
payload = apply_before_request(payload)
|
|
97
99
|
started_at = Time.now
|
|
@@ -102,6 +104,7 @@ module RCrewAI
|
|
|
102
104
|
blocks = {}
|
|
103
105
|
finish_reason = nil
|
|
104
106
|
prompt_tokens = nil
|
|
107
|
+
cached_tokens = nil
|
|
105
108
|
completion_tokens = nil
|
|
106
109
|
|
|
107
110
|
parser = SSEParser.new do |sse|
|
|
@@ -109,6 +112,7 @@ module RCrewAI
|
|
|
109
112
|
case data['type']
|
|
110
113
|
when 'message_start'
|
|
111
114
|
prompt_tokens = data.dig('message', 'usage', 'input_tokens')
|
|
115
|
+
cached_tokens = data.dig('message', 'usage', 'cache_read_input_tokens')
|
|
112
116
|
when 'content_block_start'
|
|
113
117
|
cb = data['content_block'] || {}
|
|
114
118
|
blocks[data['index']] = { id: cb['id'], name: cb['name'], arguments: +'' } if cb['type'] == 'tool_use'
|
|
@@ -131,7 +135,7 @@ module RCrewAI
|
|
|
131
135
|
end
|
|
132
136
|
end
|
|
133
137
|
|
|
134
|
-
streaming_post(
|
|
138
|
+
streaming_post(payload, cancel: cancel) { |chunk| parser.feed(chunk) }
|
|
135
139
|
|
|
136
140
|
tool_calls = blocks.values.map do |b|
|
|
137
141
|
{
|
|
@@ -144,15 +148,16 @@ module RCrewAI
|
|
|
144
148
|
usage = {
|
|
145
149
|
prompt_tokens: prompt_tokens,
|
|
146
150
|
completion_tokens: completion_tokens,
|
|
147
|
-
total_tokens: (prompt_tokens || 0) + (completion_tokens || 0)
|
|
151
|
+
total_tokens: (prompt_tokens || 0) + (completion_tokens || 0),
|
|
152
|
+
cached_tokens: cached_tokens
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
if prompt_tokens || completion_tokens
|
|
151
156
|
sink.call(Events::Usage.new(
|
|
152
157
|
type: :usage, timestamp: Time.now, agent: nil, iteration: nil,
|
|
153
158
|
prompt_tokens: prompt_tokens, completion_tokens: completion_tokens,
|
|
154
|
-
total_tokens: usage[:total_tokens],
|
|
155
|
-
cost_usd: Pricing.cost_for(
|
|
159
|
+
total_tokens: usage[:total_tokens], cached_tokens: cached_tokens,
|
|
160
|
+
cost_usd: Pricing.cost_for(payload[:model],
|
|
156
161
|
prompt_tokens: prompt_tokens || 0,
|
|
157
162
|
completion_tokens: completion_tokens || 0)
|
|
158
163
|
))
|
|
@@ -163,23 +168,25 @@ module RCrewAI
|
|
|
163
168
|
tool_calls: tool_calls,
|
|
164
169
|
usage: usage,
|
|
165
170
|
finish_reason: finish_reason || :stop,
|
|
166
|
-
model:
|
|
171
|
+
model: payload[:model],
|
|
167
172
|
provider: :anthropic
|
|
168
173
|
}
|
|
169
174
|
apply_after_response(result, started_at)
|
|
170
175
|
end
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
# Streams over one kept-alive connection per client (see PersistentHTTP).
|
|
178
|
+
def streaming_post(payload, cancel: nil, &on_chunk)
|
|
179
|
+
uri = URI("#{@base_url}/messages")
|
|
180
|
+
response = streaming_connection.post_stream(
|
|
181
|
+
uri.request_uri, headers: build_headers.merge(auth_header), body: payload.to_json, cancel: cancel, &on_chunk
|
|
182
|
+
)
|
|
183
|
+
return response if response.is_a?(Net::HTTPSuccess)
|
|
184
|
+
|
|
185
|
+
handle_response(Struct.new(:status, :body).new(response.code.to_i, response.body))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def streaming_connection
|
|
189
|
+
@streaming_connection ||= PersistentHTTP.new(@base_url, timeout: config.timeout)
|
|
183
190
|
end
|
|
184
191
|
|
|
185
192
|
def normalize_non_streaming(body)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module RCrewAI
|
|
7
|
+
# One keep-alive HTTP connection per client for streaming POSTs, so
|
|
8
|
+
# back-to-back calls skip TCP/TLS setup. Requests on a connection are
|
|
9
|
+
# serialised. A stream that does not finish cleanly (cancel, task stop,
|
|
10
|
+
# error) discards the connection; the next call reconnects. Under Ruby's
|
|
11
|
+
# fiber scheduler Net::HTTP yields on IO, so this is reactor-friendly.
|
|
12
|
+
class PersistentHTTP
|
|
13
|
+
RETRYABLE = [EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(base_url, timeout:)
|
|
16
|
+
@uri = URI(base_url)
|
|
17
|
+
@timeout = timeout
|
|
18
|
+
@lock = Mutex.new
|
|
19
|
+
@http = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Yields body chunks as they arrive. Returns the Net::HTTPResponse.
|
|
23
|
+
# Non-2xx responses are returned with their body read (not yielded).
|
|
24
|
+
def post_stream(path, headers:, body:, cancel: nil, &on_chunk)
|
|
25
|
+
@lock.synchronize do
|
|
26
|
+
attempts = 0
|
|
27
|
+
begin
|
|
28
|
+
perform(path, headers, body, cancel, &on_chunk)
|
|
29
|
+
rescue *RETRYABLE
|
|
30
|
+
# A kept-alive connection the server already closed fails before any
|
|
31
|
+
# bytes arrive; retry once on a fresh connection.
|
|
32
|
+
raise if @received || (attempts += 1) > 1
|
|
33
|
+
|
|
34
|
+
retry
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
@http&.finish if @http&.started?
|
|
41
|
+
rescue IOError
|
|
42
|
+
nil
|
|
43
|
+
ensure
|
|
44
|
+
@http = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def perform(path, headers, body, cancel, &on_chunk)
|
|
50
|
+
@received = false
|
|
51
|
+
completed = false
|
|
52
|
+
request = Net::HTTP::Post.new(path, headers)
|
|
53
|
+
request.body = body
|
|
54
|
+
response = nil
|
|
55
|
+
connection.request(request) do |res|
|
|
56
|
+
response = res
|
|
57
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
58
|
+
res.read_body
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
res.read_body do |chunk|
|
|
63
|
+
@received = true
|
|
64
|
+
raise StreamCancelled, 'stream cancelled' if cancel&.cancelled?
|
|
65
|
+
|
|
66
|
+
on_chunk.call(chunk)
|
|
67
|
+
raise StreamCancelled, 'stream cancelled' if cancel&.cancelled?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
completed = true
|
|
71
|
+
response
|
|
72
|
+
ensure
|
|
73
|
+
close unless completed
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def connection
|
|
77
|
+
return @http if @http&.started?
|
|
78
|
+
|
|
79
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
80
|
+
@http.use_ssl = @uri.scheme == 'https'
|
|
81
|
+
@http.open_timeout = 10
|
|
82
|
+
@http.read_timeout = @timeout
|
|
83
|
+
@http.keep_alive_timeout = 30
|
|
84
|
+
@http.start
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/rcrewai/process.rb
CHANGED
|
@@ -410,6 +410,11 @@ module RCrewAI
|
|
|
410
410
|
class Consensual < Base
|
|
411
411
|
DEFAULT_CONSENSUS_AGENTS = 3
|
|
412
412
|
|
|
413
|
+
# Scoring is quadratic in participants (agents x candidates), so an
|
|
414
|
+
# unbounded fan-out would put a request per pair in flight at once --
|
|
415
|
+
# six agents is already 36 concurrent LLM calls. Cap it.
|
|
416
|
+
DEFAULT_MAX_CONCURRENCY = 8
|
|
417
|
+
|
|
413
418
|
def execute
|
|
414
419
|
log_execution_start
|
|
415
420
|
@logger.info 'Consensual execution - agents propose, vote, and pick'
|
|
@@ -458,23 +463,47 @@ module RCrewAI
|
|
|
458
463
|
chosen
|
|
459
464
|
end
|
|
460
465
|
|
|
466
|
+
# Proposals are independent LLM calls, so they run concurrently: with the
|
|
467
|
+
# default three agents this turns three sequential round-trips into one.
|
|
468
|
+
# filter_map order is preserved so ties still break toward the assigned
|
|
469
|
+
# agent deterministically.
|
|
461
470
|
def gather_proposals(task, participants)
|
|
462
|
-
participants.
|
|
471
|
+
bounded_parallel(participants.map { |a| [a] }) do |agent|
|
|
463
472
|
content = extract_content(agent.execute_task(task, stream: crew.stream_sink))
|
|
464
473
|
{ agent: agent, content: content }
|
|
465
474
|
rescue StandardError => e
|
|
466
475
|
@logger.warn "Agent #{agent.name} failed to propose: #{e.message}"
|
|
467
476
|
nil
|
|
468
|
-
end
|
|
477
|
+
end.compact
|
|
469
478
|
end
|
|
470
479
|
|
|
480
|
+
# Every (candidate, voter) pair is an independent LLM call. Scoring is the
|
|
481
|
+
# quadratic half of consensus -- three agents means nine calls -- so it
|
|
482
|
+
# fans out over the whole grid rather than per candidate.
|
|
471
483
|
def score_candidates(task, candidates, participants)
|
|
472
|
-
candidates.map
|
|
473
|
-
|
|
474
|
-
|
|
484
|
+
pairs = candidates.flat_map { |c| participants.map { |v| [c, v] } }
|
|
485
|
+
scores = bounded_parallel(pairs) { |c, v| score(v, task, c[:content]) }
|
|
486
|
+
|
|
487
|
+
candidates.each_with_index.map do |candidate, i|
|
|
488
|
+
row = scores[(i * participants.length), participants.length]
|
|
489
|
+
candidate.merge(score: row.sum)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Runs the block over items in bounded parallel, preserving input order.
|
|
494
|
+
def bounded_parallel(items)
|
|
495
|
+
span = Events.current_parent
|
|
496
|
+
items.each_slice(max_concurrency).flat_map do |slice|
|
|
497
|
+
slice.map { |item| Thread.new { Events.with_parent(span) { yield(*item) } } }
|
|
498
|
+
.map(&:value)
|
|
475
499
|
end
|
|
476
500
|
end
|
|
477
501
|
|
|
502
|
+
def max_concurrency
|
|
503
|
+
crew.respond_to?(:consensus_max_concurrency) && crew.consensus_max_concurrency ||
|
|
504
|
+
DEFAULT_MAX_CONCURRENCY
|
|
505
|
+
end
|
|
506
|
+
|
|
478
507
|
def score(voter, task, candidate_content)
|
|
479
508
|
prompt = <<~PROMPT
|
|
480
509
|
Score how well the following answer satisfies the task, from 0 to 10.
|
data/lib/rcrewai/tool_runner.rb
CHANGED
|
@@ -7,14 +7,18 @@ require_relative 'provider_schema'
|
|
|
7
7
|
module RCrewAI
|
|
8
8
|
class ToolRunner
|
|
9
9
|
DEFAULT_MAX_ITERATIONS = 10
|
|
10
|
+
DEFAULT_TOOL_CONCURRENCY = 8
|
|
10
11
|
|
|
11
|
-
def initialize(agent:, llm:, tools:,
|
|
12
|
+
def initialize(agent:, llm:, tools:, **opts)
|
|
12
13
|
@agent = agent
|
|
13
14
|
@llm = llm
|
|
14
15
|
@tools = tools
|
|
15
16
|
@tools_by_name = tools.each_with_object({}) { |t, h| h[t.name] = t }
|
|
16
|
-
@max_iterations = max_iterations
|
|
17
|
-
@sink = event_sink || ->(_) {}
|
|
17
|
+
@max_iterations = opts.fetch(:max_iterations, DEFAULT_MAX_ITERATIONS)
|
|
18
|
+
@sink = opts[:event_sink] || ->(_) {}
|
|
19
|
+
@parallel_tools = opts.fetch(:parallel_tools, true)
|
|
20
|
+
@max_tool_concurrency = opts.fetch(:max_tool_concurrency, DEFAULT_TOOL_CONCURRENCY)
|
|
21
|
+
@tool_usage_lock = Mutex.new
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def run(messages:)
|
|
@@ -23,7 +27,7 @@ module RCrewAI
|
|
|
23
27
|
|
|
24
28
|
private
|
|
25
29
|
|
|
26
|
-
def run_loop(messages:)
|
|
30
|
+
def run_loop(messages:)
|
|
27
31
|
msgs = messages.dup
|
|
28
32
|
history = []
|
|
29
33
|
iter = 0
|
|
@@ -48,34 +52,9 @@ module RCrewAI
|
|
|
48
52
|
|
|
49
53
|
msgs << { role: 'assistant', content: response[:content], tool_calls: response[:tool_calls] }
|
|
50
54
|
|
|
51
|
-
response[:tool_calls].each do |
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
tool: tc[:name], args: tc[:arguments], call_id: tc[:id])
|
|
55
|
-
|
|
56
|
-
if tool.nil?
|
|
57
|
-
err = "tool not found: #{tc[:name]}"
|
|
58
|
-
emit(Events::ToolCallError, iteration: iter,
|
|
59
|
-
tool: tc[:name], call_id: tc[:id], error: err)
|
|
60
|
-
msgs << tool_result_message(tc[:id], "ERROR: #{err}")
|
|
61
|
-
next
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
started = monotonic_ms
|
|
65
|
-
begin
|
|
66
|
-
result = tool.execute_with_validation(tc[:arguments] || {})
|
|
67
|
-
duration = monotonic_ms - started
|
|
68
|
-
@agent.memory.add_tool_usage(tc[:name], tc[:arguments], result) if @agent.respond_to?(:memory) && @agent.memory
|
|
69
|
-
emit(Events::ToolCallResult, iteration: iter,
|
|
70
|
-
tool: tc[:name], call_id: tc[:id], result: result,
|
|
71
|
-
duration_ms: duration)
|
|
72
|
-
history << { tool: tc[:name], args: tc[:arguments], result: result, duration_ms: duration }
|
|
73
|
-
msgs << tool_result_message(tc[:id], result.to_s)
|
|
74
|
-
rescue StandardError => e
|
|
75
|
-
emit(Events::ToolCallError, iteration: iter,
|
|
76
|
-
tool: tc[:name], call_id: tc[:id], error: e.message)
|
|
77
|
-
msgs << tool_result_message(tc[:id], "ERROR: #{e.message}")
|
|
78
|
-
end
|
|
55
|
+
run_tool_calls(response[:tool_calls], iter).each do |outcome|
|
|
56
|
+
history << outcome[:history] if outcome[:history]
|
|
57
|
+
msgs << outcome[:message]
|
|
79
58
|
end
|
|
80
59
|
|
|
81
60
|
emit(Events::IterationEnd, iteration: iter, finish_reason: :tool_calls)
|
|
@@ -95,6 +74,72 @@ module RCrewAI
|
|
|
95
74
|
{ role: 'tool', tool_call_id: call_id, content: content }
|
|
96
75
|
end
|
|
97
76
|
|
|
77
|
+
# Executes one turn's tool calls, concurrently when there is more than one.
|
|
78
|
+
#
|
|
79
|
+
# Models routinely request several independent tools in a single turn;
|
|
80
|
+
# running them in sequence makes the turn cost the sum of their latencies
|
|
81
|
+
# rather than the max. Results are collected by index, so the order the
|
|
82
|
+
# model asked for is preserved no matter which finishes first -- the
|
|
83
|
+
# message history must stay aligned with the tool_call ids.
|
|
84
|
+
def run_tool_calls(tool_calls, iter)
|
|
85
|
+
return tool_calls.map { |tc| execute_tool_call(tc, iter) } unless parallel?(tool_calls)
|
|
86
|
+
|
|
87
|
+
# Events are emitted from worker threads, so carry the run span across
|
|
88
|
+
# the boundary: Events.with_parent is thread-local by design.
|
|
89
|
+
span = Events.current_parent
|
|
90
|
+
tool_calls.each_slice(@max_tool_concurrency).flat_map do |slice|
|
|
91
|
+
slice.map { |tc| Thread.new { Events.with_parent(span) { execute_tool_call(tc, iter) } } }
|
|
92
|
+
.map(&:value)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parallel?(tool_calls)
|
|
97
|
+
@parallel_tools && tool_calls.length > 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Runs one tool call and returns what the caller should record. Never
|
|
101
|
+
# raises: a failing tool becomes an ERROR message fed back to the model,
|
|
102
|
+
# exactly as it did when this ran inline.
|
|
103
|
+
def execute_tool_call(call, iter)
|
|
104
|
+
tool = @tools_by_name[call[:name]]
|
|
105
|
+
emit(Events::ToolCallStart, iteration: iter,
|
|
106
|
+
tool: call[:name], args: call[:arguments], call_id: call[:id])
|
|
107
|
+
|
|
108
|
+
if tool.nil?
|
|
109
|
+
err = "tool not found: #{call[:name]}"
|
|
110
|
+
emit(Events::ToolCallError, iteration: iter,
|
|
111
|
+
tool: call[:name], call_id: call[:id], error: err)
|
|
112
|
+
return { message: tool_result_message(call[:id], "ERROR: #{err}") }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
started = monotonic_ms
|
|
116
|
+
begin
|
|
117
|
+
result = tool.execute_with_validation(call[:arguments] || {})
|
|
118
|
+
duration = monotonic_ms - started
|
|
119
|
+
record_tool_usage(call, result)
|
|
120
|
+
emit(Events::ToolCallResult, iteration: iter,
|
|
121
|
+
tool: call[:name], call_id: call[:id], result: result,
|
|
122
|
+
duration_ms: duration)
|
|
123
|
+
{
|
|
124
|
+
history: { tool: call[:name], args: call[:arguments], result: result, duration_ms: duration },
|
|
125
|
+
message: tool_result_message(call[:id], result.to_s)
|
|
126
|
+
}
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
emit(Events::ToolCallError, iteration: iter,
|
|
129
|
+
tool: call[:name], call_id: call[:id], error: e.message)
|
|
130
|
+
{ message: tool_result_message(call[:id], "ERROR: #{e.message}") }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Agent memory is shared across the worker threads of one turn.
|
|
135
|
+
def record_tool_usage(call, result)
|
|
136
|
+
return unless @agent.respond_to?(:memory) && @agent.memory
|
|
137
|
+
|
|
138
|
+
@tool_usage_lock.synchronize do
|
|
139
|
+
@agent.memory.add_tool_usage(call[:name], call[:arguments], result)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
98
143
|
def emit(klass, iteration:, **attrs)
|
|
99
144
|
type_sym = klass.name.split('::').last
|
|
100
145
|
.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
|
data/lib/rcrewai/version.rb
CHANGED
data/lib/rcrewai.rb
CHANGED
|
@@ -21,6 +21,8 @@ require_relative 'rcrewai/configuration'
|
|
|
21
21
|
require_relative 'rcrewai/events'
|
|
22
22
|
require_relative 'rcrewai/checkpoint'
|
|
23
23
|
require_relative 'rcrewai/sse_parser'
|
|
24
|
+
require_relative 'rcrewai/cancel_token'
|
|
25
|
+
require_relative 'rcrewai/persistent_http'
|
|
24
26
|
require_relative 'rcrewai/pricing'
|
|
25
27
|
require_relative 'rcrewai/llm_client'
|
|
26
28
|
require_relative 'rcrewai/similarity'
|
data/rcrewai.gemspec
CHANGED
|
@@ -60,6 +60,7 @@ Gem::Specification.new do |spec|
|
|
|
60
60
|
spec.add_dependency 'thor', '~> 1.3'
|
|
61
61
|
|
|
62
62
|
# Development dependencies
|
|
63
|
+
spec.add_development_dependency 'async', '~> 2.0'
|
|
63
64
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
64
65
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
65
66
|
spec.add_development_dependency 'rspec', '~> 3.12'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rcrewai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gkosmo
|
|
@@ -135,6 +135,20 @@ dependencies:
|
|
|
135
135
|
- - "~>"
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
137
|
version: '1.3'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: async
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '2.0'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '2.0'
|
|
138
152
|
- !ruby/object:Gem::Dependency
|
|
139
153
|
name: bundler
|
|
140
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -347,6 +361,7 @@ files:
|
|
|
347
361
|
- lib/rcrewai/agent.rb
|
|
348
362
|
- lib/rcrewai/agent_augmentations.rb
|
|
349
363
|
- lib/rcrewai/async_executor.rb
|
|
364
|
+
- lib/rcrewai/cancel_token.rb
|
|
350
365
|
- lib/rcrewai/checkpoint.rb
|
|
351
366
|
- lib/rcrewai/checkpoint/cli.rb
|
|
352
367
|
- lib/rcrewai/cli.rb
|
|
@@ -392,6 +407,7 @@ files:
|
|
|
392
407
|
- lib/rcrewai/memory/tool_memory.rb
|
|
393
408
|
- lib/rcrewai/multimodal.rb
|
|
394
409
|
- lib/rcrewai/output_schema.rb
|
|
410
|
+
- lib/rcrewai/persistent_http.rb
|
|
395
411
|
- lib/rcrewai/planning.rb
|
|
396
412
|
- lib/rcrewai/pricing.rb
|
|
397
413
|
- lib/rcrewai/process.rb
|