rcrewai 0.7.1 → 0.8.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0a4a20865a08157cf9148d63439eeb653570c39f4f6e850bf6cc9f8e963a437
4
- data.tar.gz: 4abf734ac77a3bdb1c43887b077cd99e1aea4ff9e2f8ba08b5fcac72a27ac7b5
3
+ metadata.gz: cbd74067513b5593791b282baf20f18c8dd8f6cd9257b02f70423ee4cba3486d
4
+ data.tar.gz: 215cade06d04b032d254e792a2af9d36d7c00de073580dff31125d46e5de4b48
5
5
  SHA512:
6
- metadata.gz: 01bd0f68ca18dcee76ccca334821bd1412695922b1a84fba092b7bf4686179401c581844b36094b9cf765415e8ce2652cfb7300b496d4c57c9b78dbcd6b7ac22
7
- data.tar.gz: afdc22e50a5a61bc0e153c0e8a702e9e0aa465cbf268d06d71a82f372c626236a163ceab777f4eee5e8f6134740a8b54903fdb50cdebd38b6ae7b987d1394581
6
+ metadata.gz: 735222585596dc578801792d74a6f5435037c78352f10e80670b2e179e0cce11a6257b79624e8a1abd0a3aa6978d86ec852e02cbdd6dc22e00a1364eafbb1058
7
+ data.tar.gz: 3f5b7145864b603e292b9daced5ef1c39c2d3084847658265d2b67a58ef8e3575ba8383e9494e7a2bcfcd7d6de231daadaac43ae41245afea7ed2c4f817649b9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.1] - 2026-09-10
11
+
12
+ Dependency cleanup. No API or behavior change.
13
+
14
+ ### Changed
15
+ - Dropped three unused runtime dependencies: `anthropic`, `ruby-openai` and `faraday-multipart`. Every LLM client talks to Faraday directly and none of these gems was ever required, but they had been declared since `0.3.0`, so each install pulled them plus their transitive dependencies (`event_stream_parser`, `multipart-post`) — seven gems in total. No API change; the Anthropic and OpenAI clients are unaffected.
16
+
17
+ ## [0.8.0] - 2026-09-10
18
+
19
+ Closes the feature-parity gap against CrewAI's `1.x` line. Adds **LLM message
20
+ interceptors**, an **event hierarchy** with safe fan-out, **task-level
21
+ checkpointing** with resume and lineage, and **four new providers** (AWS
22
+ Bedrock, Snowflake Cortex, any OpenAI-compatible endpoint, and OpenAI's
23
+ Responses API).
24
+
25
+ Almost entirely additive. One behavior change — `Events.fan_out` now serializes
26
+ delivery, so stream subscribers no longer need their own mutex; sinks that
27
+ already lock remain correct. See **Changed** below.
28
+
29
+ Also repairs `bin/rcrewai`, which had never worked in any published version.
30
+
31
+ ### Added
32
+
33
+ #### Interceptors & observability
34
+ - LLM message interceptors: `before_request` / `after_response` hooks on every provider client (`LLMClients::Base`), registered via block, callable, or the `before_request:` / `after_response:` constructor kwargs. A `before_request` hook receives `(payload, context)` and may return a replacement payload; an `after_response` hook receives `(result, context)` and may return a replacement result. Returning `nil` keeps the original, so a pure-observer hook needs no return value. `context` carries `:provider` and `:model`, and `after_response` adds `:duration_ms`. Hooks run in registration order, threading the value through. A hook that raises is reported to stderr and skipped — instrumentation never breaks a call. Wired on both the plain and streaming paths of all five providers.
35
+ - Event hierarchy: every `Events::*` event now carries an auto-assigned `:id`, and `:parent_id` naming the enclosing span. `Events.with_parent(id) { ... }` opens a span for the current thread (nesting, restored on exit, and on raise); `Events.emit(sink, event)` stamps the enclosing parent before delivery. Both runners (`ToolRunner`, `LegacyReactRunner`) open a per-run span, so a subscriber can reassemble the flat stream into a tree — what tracing exporters need.
36
+
37
+ #### Checkpointing
38
+ - Checkpointing: `crew.execute(checkpoint: store)` records durable per-run state, and `crew.resume(run_id)` replays completed tasks instead of re-executing them. Granularity is task-level — a checkpoint is written after each task settles, so a crash loses at most the task in flight. Failed tasks are recorded as failed rather than omitted, so a resume retries them instead of treating them as never-attempted. Supported on the sequential, hierarchical, and consensual processes.
39
+ - Checkpoint stores follow the existing `Flow::StateStore` shape (`save`/`load`/`list`/`delete`): `Checkpoint::MemoryStore` (volatile) and `Checkpoint::FileStore` (one JSON file per run). `FileStore` rejects run ids containing path separators or traversal segments, since ids arrive both from callers and from stored records.
40
+ - Lineage: a resumed run gets its own run id linked to its parent via `parent_run_id`, leaving the original record intact. `Checkpoint.lineage(store, run_id)` walks the chain back to the root, truncating rather than raising if an ancestor has been pruned.
41
+ - CLI: `rcrewai checkpoint list` / `info RUN_ID` / `delete RUN_ID` inspect saved checkpoints (`--dir`, default `.rcrewai/checkpoints`).
42
+
43
+ #### Providers
44
+ - New providers: `:openai_compatible` (any endpoint speaking the OpenAI Chat Completions format — Together, Groq, Fireworks, vLLM, LiteLLM, OpenRouter, a self-hosted gateway; requires `base_url`), `:bedrock` (AWS Bedrock via the Converse API, giving every Bedrock model one request shape; requires `aws_region`), and `:snowflake` (Snowflake Cortex inference; requires `snowflake_account`). New configuration attributes `aws_region` and `snowflake_account`, also read from `AWS_REGION`/`AWS_DEFAULT_REGION` and `SNOWFLAKE_ACCOUNT`.
45
+ - `:openai_responses` — OpenAI's Responses API alongside the existing Chat Completions client. Messages go under `input` with the system prompt lifted to `instructions`, `max_tokens` becomes `max_output_tokens`, tools are sent flat rather than nested under `function`, and the `output` array is parsed back into the canonical `content` / `tool_calls` shape. An `incomplete` response capped by `max_output_tokens` is reported as `finish_reason: :length`. Non-streaming only — Responses streams a distinct set of semantic events that this client does not model.
46
+ - `LLMClient::PROVIDERS` — provider resolution is now a table rather than a `case`, so registering a client is a one-line change.
47
+
48
+
49
+ ### Changed
50
+ - **Behavior change:** subscribers passed to `crew.execute(stream:)` no longer need their own mutex. Existing sinks that lock are unaffected.
51
+
52
+ ### Fixed
53
+ - `Events.fan_out` now serializes delivery: sinks are invoked under a mutex, so a sink shared by concurrently executing agents is never entered from two threads at once. Previously it called sinks inline on the emitting thread with no serialization, which under `async: true` meant every subscriber had to do its own locking or race — the 0.7.1 notes documented this as a caveat, but for any aggregating subscriber it was a live defect. The lock is reentrant, so a sink that emits back through the same fan-out does not deadlock. Sinks that already lock internally remain correct.
54
+ - `LLMClient.for_provider` silently dropped interceptor hooks: it constructed each client with only the config, so `before_request` / `after_response` passed through the normal resolution path never reached the client. It now forwards them.
55
+ - `bin/rcrewai` never worked. `lib/rcrewai/cli.rb` defined `def run`, which Thor reserves, so the class raised `"run" is a Thor reserved word` on load; the file was consequently never required from `lib/rcrewai.rb`, which hid the breakage from the test suite while `bin/rcrewai` — shipped as a gem executable since the initial commit — crashed for every installed user. The command is now defined as `run_crew` and mapped back to `run`, so the user-facing invocation (`rcrewai run --crew NAME`) is unchanged, and the CLI is required and covered by specs.
56
+
10
57
  ## [0.7.1] - 2026-08-13
11
58
 
12
59
  ### Fixed
@@ -14,7 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
61
  - Tasks retained a reference to the caller's sink after `execute` returned, keeping request-scoped subscribers reachable for the lifetime of the task object. The sink is now cleared in an `ensure`.
15
62
 
16
63
  ### Note
17
- - `Events.fan_out` invokes sinks inline on the emitting thread with no serialization, so under `async: true` a sink may be called concurrently from multiple worker threads. Subscribers must do their own locking.
64
+ - `Events.fan_out` invokes sinks inline on the emitting thread with no serialization, so under `async: true` a sink may be called concurrently from multiple worker threads. Subscribers must do their own locking. **Superseded in `0.8.0`:** fan-out now serializes delivery.
18
65
 
19
66
  ## [0.7.0] - 2026-07-07
20
67
 
data/ROADMAP.md CHANGED
@@ -5,85 +5,154 @@ This roadmap tracks feature parity between **RCrewAI** (Ruby) and the upstream
5
5
 
6
6
  ## Current status
7
7
 
8
- - **RCrewAI:** `0.3.0` (2026-05-12)
9
- - **Upstream crewai:** `1.15.x` (mid-2026)
8
+ - **RCrewAI:** `0.7.1` released; `0.8.0`, `0.9.0` and `0.9.x` merged to `main`, unreleased
9
+ - **Upstream crewai:** `1.15.21`
10
10
 
11
11
  RCrewAI is a faithful port of CrewAI's **"Crews"** mental model (Agents / Tasks /
12
- Crew, sequential + hierarchical processes, tools, memory, human-in-the-loop). As
13
- of `0.3.0` the LLM plumbing is modern: native function calling across all five
14
- providers, a tool-schema DSL, typed streaming events, MCP client, and per-model
15
- pricing.
12
+ Crew, sequential + hierarchical + consensual processes, tools, memory,
13
+ human-in-the-loop), and it carries CrewAI's second pillar (**Flows**) plus
14
+ **Knowledge (RAG)**, **guardrails**, **structured output**, **planning**, and
15
+ **training/testing**. In one area — cognitive memory (semantic recall, SQLite
16
+ persistence, four memory types) — the gem went past what was originally ported.
17
+
18
+ **Status: one milestone remaining.** An earlier revision of this file declared
19
+ parity "complete" against a matrix that only covered CrewAI through roughly
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 three
22
+ of the four scheduled milestones have since shipped. Only native async (1.0.0)
23
+ is outstanding, and it is blocked on an open decision — see below.
16
24
 
17
- Since CrewAI's `1.0`, the framework grew a second pillar (**Flows**) plus
18
- **Knowledge (RAG)**, **Guardrails**, **structured output**, **Planning**, and
19
- **Training/Testing**. RCrewAI now implements all of these — see the matrix below.
25
+ ## Parity matrix
20
26
 
21
- **Status: complete.** All milestone issues (#5–#12) shipped in v0.4.0, and all
22
- backlog items (#15–#20) shipped in v0.5.0. There is no outstanding roadmap work.
27
+ ### Shipped
28
+
29
+ | Concept | crewai | RCrewAI |
30
+ |---|---|---|
31
+ | Agents / Tasks / Crew | ✅ | ✅ |
32
+ | Sequential / hierarchical process | ✅ | ✅ |
33
+ | Consensual process (propose → vote → pick) | ✅ | ✅ (0.7.0) |
34
+ | Native function calling + tool DSL | ✅ | ✅ (0.3.0) |
35
+ | Streaming events | ✅ | ✅ (0.3.0) |
36
+ | MCP client | ✅ | ✅ (0.3.0) |
37
+ | Per-model pricing / cost | ✅ | ✅ (0.3.0) |
38
+ | Per-agent LLM override | ✅ | ✅ (0.4.0) |
39
+ | Structured output (schema) | ✅ | ✅ (0.4.0) |
40
+ | Task guardrails | ✅ | ✅ (0.4.0) |
41
+ | `output_file` / markdown | ✅ | ✅ (0.4.0) |
42
+ | Knowledge / RAG | ✅ | ✅ (0.4.0) |
43
+ | Planning | ✅ | ✅ (0.4.0) |
44
+ | Flows (`start`/`listen`/`router`) | ✅ | ✅ (0.4.0) |
45
+ | Flow state + persistence | ✅ | ✅ (0.4.0) |
46
+ | Training / testing | ✅ | ✅ (0.4.0) |
47
+ | Lifecycle hooks, batch kickoff, rate limiting | ✅ | ✅ (0.5.0) |
48
+ | Reasoning, context window, multimodal | ✅ | ✅ (0.5.0) |
49
+ | Cognitive memory (semantic, persistent, typed) | ✅ | ✅ (0.6.x) |
50
+ | LLM message interceptor hooks | ✅ | ✅ (0.8.0) |
51
+ | Event hierarchy + safe fan-out | ✅ | ✅ (0.8.0) |
52
+ | Checkpointing (save / resume / lineage) | ✅ | ✅ (0.9.0) |
53
+ | Newer providers (Bedrock, Cortex, OpenAI-compatible) | ✅ | ✅ (0.9.x) |
54
+ | OpenAI Responses API | ✅ | ✅ (0.9.x) |
55
+
56
+ ### Gaps
57
+
58
+ | Concept | crewai | RCrewAI | Plan |
59
+ |---|---|---|---|
60
+ | Native async (LLM + tool level) | ✅ (1.4–1.6) | ⚠️ partial | 1.0.0 |
61
+ | OTel export for the event hierarchy | ✅ (1.10+) | ❌ | 1.0.x |
62
+ | Streaming for Bedrock / Responses | ✅ | ❌ | 1.0.x |
63
+ | Bedrock SigV4 signing | ✅ | ❌ (hook workaround) | 1.0.x |
64
+ | A2A (agent-to-agent) | ✅ (1.7–1.9) | ❌ | deferred |
23
65
 
24
- ## Parity matrix
66
+ ### Out of scope
67
+
68
+ Three upstream areas are deliberately **not** targets. They are CrewAI's
69
+ commercial platform surface rather than framework capability, and porting them
70
+ means tracking someone else's product roadmap with no Ruby-side consumer:
71
+
72
+ - **JSON-first project format** (`agents/*.jsonc`, `crew.jsonc`, declarative and
73
+ conversational flows in the CLI TUI) — a config format whose shape is set by
74
+ upstream's tooling.
75
+ - **Skills Repository** — a hosted registry plus authentication.
76
+ - **Policies** — CrewAI enforces these at the infrastructure level via the NVIDIA
77
+ OpenShell runtime. Reimplementing them in-process would provide the appearance
78
+ of enforcement without the property that makes enforcement worth having.
79
+
80
+ If a Ruby-side need for any of these appears, revisit — but not speculatively.
81
+
82
+ ## Milestones
83
+
84
+ ### 0.8.0 — Interceptors & observability ✅ shipped (#39)
85
+
86
+ `before_request` / `after_response` hooks on `LLMClients::Base`, inherited by
87
+ every provider and wired on both the plain and streaming paths. Events gained
88
+ `:id` / `:parent_id` with `Events.with_parent` spans opened per agent run.
89
+
90
+ `Events.fan_out` now serializes delivery under a reentrant mutex, fixing a live
91
+ race: it previously called sinks inline on the emitting thread, so under
92
+ `async: true` an aggregating subscriber was entered from several pool workers at
93
+ once. **Behavior change:** subscribers no longer need their own mutex.
94
+
95
+ ### 0.9.0 — Checkpointing ✅ shipped (#42)
96
+
97
+ Task-level `crew.execute(checkpoint: store)` / `crew.resume(run_id)` across the
98
+ sequential, hierarchical and consensual processes, with `MemoryStore` and
99
+ `FileStore` following the `Flow::StateStore` shape. Resumed runs link to their
100
+ parent via `parent_run_id`; `Checkpoint.lineage` walks the chain to the root.
101
+ CLI: `rcrewai checkpoint list|info|delete`.
102
+
103
+ Also repaired `bin/rcrewai`, which had never worked: `lib/rcrewai/cli.rb`
104
+ defined `def run`, a Thor reserved word, so the class raised on load and the
105
+ file was never required — hiding the breakage from the suite while the shipped
106
+ gem executable crashed for every installed user.
107
+
108
+ ### 0.9.x — Providers & Responses API ✅ shipped (#41)
109
+
110
+ `:openai_compatible`, `:bedrock` (Converse v4), `:snowflake` (Cortex) and
111
+ `:openai_responses`. Provider resolution moved to a `LLMClient::PROVIDERS`
112
+ table, which also fixed `for_provider` silently dropping interceptor hooks.
113
+
114
+ Two deliberate limitations carried forward to 1.0.x: Bedrock does not implement
115
+ SigV4 (a hard `aws-sigv4` dependency for one provider is not worth it; sign via
116
+ a `before_request` hook), and Bedrock/Responses are non-streaming only.
117
+
118
+ ### 1.0.0 — Native async
119
+
120
+ The largest item, and the one with a genuine architecture decision attached.
121
+ Today `AsyncExecutor` fans tasks out across a `Concurrent::ThreadPoolExecutor` in
122
+ dependency-ordered phases; concurrency stops at the task boundary. CrewAI went
123
+ async *through* the LLM and tool calls (1.4–1.6), covering flows, crews, tasks,
124
+ knowledge, and memory.
125
+
126
+ Ruby has no direct port of that. Two candidate models:
127
+
128
+ 1. **Fibers** via the `async` gem — closer to upstream's shape, new runtime
129
+ dependency, and every provider client's HTTP layer has to cooperate.
130
+ 2. **Stay on threads** and make the client layer non-blocking — smaller
131
+ conceptual change, keeps `concurrent-ruby`, less faithful to upstream.
132
+
133
+ **This decision is open and blocks the milestone.** It touches all five LLM
134
+ clients either way. The 8.2k lines of `lib/` are backed by 4.9k lines of spec,
135
+ which is what makes a refactor at this depth tractable.
136
+
137
+ ### Deferred — A2A
138
+
139
+ Agent-to-agent task execution utilities and server configuration (upstream
140
+ 1.7–1.9). Real framework capability, but it presumes a deployment topology that
141
+ no current RCrewAI user has asked for. Revisit once the items above land.
142
+
143
+ ## Sequencing
25
144
 
26
- | Concept | crewai | RCrewAI 0.3.0 | Target |
145
+ | Milestone | Contents | Risk | Status |
27
146
  |---|---|---|---|
28
- | Agents / Tasks / Crew | | ✅ | |
29
- | Sequential / hierarchical process | | ✅ | |
30
- | Native function calling + tool DSL | | ✅ (0.3.0) | — |
31
- | Streaming events | | (0.3.0) | |
32
- | MCP client | ✅ | ✅ (0.3.0) | — |
33
- | Per-model pricing / cost | | (0.3.0) | |
34
- | Per-agent LLM override | | (#5) | ✅ done |
35
- | Structured output (schema) | ✅ | ✅ (#6) | ✅ done |
36
- | Task guardrails | | (#7) | done |
37
- | `output_file` / markdown | | (#8) | done |
38
- | Knowledge / RAG | | (#9) | done |
39
- | Planning | | ✅ (#10) | ✅ done |
40
- | Flows (`start`/`listen`/`router`) | ✅ | ✅ (#11) | ✅ done |
41
- | Flow state + persistence | ✅ | ✅ (#11) | ✅ done |
42
- | Training / Testing | ✅ | ✅ (#12) | ✅ done |
43
- | Reasoning, rate-limiting, batch kickoff, hooks, context window, multimodal | ✅ | ✅ (#15–#20) | ✅ done |
44
-
45
- ## Milestones (highest leverage first)
46
-
47
- ### 0.3.1 — Per-agent LLM override
48
- Let `Agent.new(llm:)` accept a provider/model, instead of only the global
49
- `RCrewAI.configure`. Unblocks mixed-model crews (cheap model for workers, strong
50
- model for the manager).
51
-
52
- ### 0.4.0 — Structured output & guardrails
53
- Builds directly on the 0.3.0 tool-schema/JSON-schema plumbing.
54
- - `Task.new(output_schema:)` → validated, coerced structured result.
55
- - `Task.new(guardrail:)` → proc/object that validates & transforms output, with
56
- bounded retries (`guardrail_max_retries`).
57
- - `output_file:` + `markdown:` output formatting.
58
-
59
- ### 0.5.0 — Knowledge (RAG) & Planning
60
- - Knowledge sources: string, `.txt`, PDF (have `pdf-reader`), CSV, JSON, URL
61
- (have `nokogiri`). Embeddings client + a pluggable vector store (start with an
62
- in-memory / SQLite cosine store; no hard Chroma dependency).
63
- - Attach at agent **and** crew level.
64
- - `Crew.new(planning: true)` → a planner pass that drafts a step plan before
65
- execution.
66
-
67
- ### 0.6.0 — Flows
68
- The flagship. A Ruby DSL mirroring CrewAI Flows:
69
- - `start`, `listen`, `router` decorators/class-methods.
70
- - `and_` / `or_` trigger combinators.
71
- - Structured flow **state** (a plain struct/`Data` or dry-struct) with a UUID.
72
- - `@persist`-equivalent state persistence across restarts.
73
- - `human_feedback` pause/resume point.
74
-
75
- ### 0.7.0 — Training & Testing
76
- - `crew.train(n_iterations:, filename:)` capturing human feedback.
77
- - `crew.test(n_iterations:, model:)` scoring runs.
78
-
79
- ### Backlog — ✅ all complete
80
-
81
- Formerly polish items with no set version; all shipped in the `[Unreleased]`
82
- changes (see CHANGELOG):
83
-
84
- - [#15](https://github.com/gkosmo/rcrewAI/issues/15) — `before_kickoff` / `after_kickoff` lifecycle hooks ✅
85
- - [#16](https://github.com/gkosmo/rcrewAI/issues/16) — `kickoff_for_each` batch execution ✅
86
- - [#17](https://github.com/gkosmo/rcrewAI/issues/17) — `max_rpm` rate limiting ✅
87
- - [#18](https://github.com/gkosmo/rcrewAI/issues/18) — per-agent reasoning (`reasoning:`, `max_reasoning_attempts:`) ✅
88
- - [#19](https://github.com/gkosmo/rcrewAI/issues/19) — `respect_context_window` history trimming ✅
89
- - [#20](https://github.com/gkosmo/rcrewAI/issues/20) — multimodal agents (image/file inputs) ✅
147
+ | 0.8.0 | Interceptors + observability | Low | ✅ merged (#39) |
148
+ | 0.9.0 | Checkpointing | Moderate | ✅ merged (#42) |
149
+ | 0.9.x | Providers, Responses API | Low | ✅ merged (#41) |
150
+ | 1.0.0 | Native async | High decision open | blocked |
151
+
152
+ The three shipped milestones are on `main` and unreleased; they want a version
153
+ bump and a release before or alongside 1.0.0 work.
154
+
155
+ **1.0.0 is blocked on the fibers-vs-threads decision above.** It is the only
156
+ remaining scheduled work, and the largest single change in this roadmap: it
157
+ touches all nine provider clients and the executor. Nothing else should start
158
+ before that call is made.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module RCrewAI
6
+ module Checkpoint
7
+ # Inspects checkpoints written by a crew run.
8
+ class CLI < Thor
9
+ DEFAULT_DIR = '.rcrewai/checkpoints'
10
+
11
+ class_option :dir, type: :string, default: DEFAULT_DIR,
12
+ desc: 'Directory holding checkpoint files'
13
+
14
+ desc 'list', 'List saved checkpoint runs'
15
+ def list
16
+ ids = store.list
17
+ if ids.empty?
18
+ puts 'No checkpoints found.'
19
+ return
20
+ end
21
+
22
+ puts "Checkpoints in #{options[:dir]}:"
23
+ ids.sort.each do |id|
24
+ record = store.load(id)
25
+ next unless record
26
+
27
+ puts " #{id} #{summarize(record)}"
28
+ end
29
+ end
30
+
31
+ desc 'info RUN_ID', 'Show a checkpoint in detail'
32
+ def info(run_id)
33
+ record = store.load(run_id)
34
+ unless record
35
+ puts "No checkpoint for run id #{run_id}."
36
+ return
37
+ end
38
+
39
+ puts "run: #{record['run_id']}"
40
+ puts "crew: #{record['crew']}"
41
+ puts "updated: #{record['updated_at']}"
42
+ print_lineage(run_id, record)
43
+ print_tasks(record)
44
+ end
45
+
46
+ desc 'delete RUN_ID', 'Delete a checkpoint'
47
+ def delete(run_id)
48
+ unless store.load(run_id)
49
+ puts "No checkpoint for run id #{run_id}."
50
+ return
51
+ end
52
+
53
+ store.delete(run_id)
54
+ puts "Deleted checkpoint #{run_id}."
55
+ end
56
+
57
+ # Thor exits non-zero on an unhandled error rather than swallowing it.
58
+ def self.exit_on_failure?
59
+ true
60
+ end
61
+
62
+ private
63
+
64
+ def store
65
+ @store ||= FileStore.new(options[:dir])
66
+ end
67
+
68
+ def summarize(record)
69
+ tasks = record['tasks'] || {}
70
+ done = tasks.count { |_n, t| t['status'] == 'completed' }
71
+ "#{record['crew']} #{done}/#{tasks.size} tasks"
72
+ end
73
+
74
+ def print_lineage(run_id, record)
75
+ return unless record['parent_run_id']
76
+
77
+ chain = Checkpoint.lineage(store, run_id)
78
+ puts "lineage: #{chain.join(' -> ')}"
79
+ end
80
+
81
+ def print_tasks(record)
82
+ tasks = record['tasks'] || {}
83
+ return puts 'tasks: (none)' if tasks.empty?
84
+
85
+ puts 'tasks:'
86
+ tasks.each do |name, entry|
87
+ secs = entry['execution_time']
88
+ timing = secs ? format(' (%.2fs)', secs) : ''
89
+ puts " #{status_mark(entry['status'])} #{name} #{entry['status']}#{timing}"
90
+ end
91
+ end
92
+
93
+ def status_mark(status)
94
+ status == 'completed' ? '+' : '!'
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'securerandom'
6
+ require 'time'
7
+
8
+ module RCrewAI
9
+ # Durable execution state for a crew run.
10
+ #
11
+ # A checkpoint records, per task, whether it completed and what it produced.
12
+ # Resuming a run replays those results instead of re-executing the tasks --
13
+ # the expensive part of a crew run is the LLM calls, so skipping a completed
14
+ # task is the whole point.
15
+ #
16
+ # Granularity is task-level: a checkpoint is written after each task settles,
17
+ # so a crash loses at most the task in flight. Stores are pluggable; anything
18
+ # responding to #save(id, record), #load(id), #list and #delete works.
19
+ module Checkpoint
20
+ class CheckpointError < RCrewAI::Error; end
21
+
22
+ # Volatile; for tests and single-process runs.
23
+ class MemoryStore
24
+ def initialize
25
+ @data = {}
26
+ end
27
+
28
+ def save(id, record)
29
+ @data[id] = deep_dup(record)
30
+ end
31
+
32
+ def load(id)
33
+ record = @data[id]
34
+ record && deep_dup(record)
35
+ end
36
+
37
+ def list
38
+ @data.keys
39
+ end
40
+
41
+ def delete(id)
42
+ @data.delete(id)
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ # Records are plain JSON-shaped data, so a round-trip is a sufficient
49
+ # deep copy and keeps a caller's later mutation from reaching the store.
50
+ def deep_dup(record)
51
+ JSON.parse(JSON.generate(record))
52
+ end
53
+ end
54
+
55
+ # One JSON file per run under a directory.
56
+ class FileStore
57
+ def initialize(dir)
58
+ @dir = dir
59
+ FileUtils.mkdir_p(@dir)
60
+ end
61
+
62
+ def save(id, record)
63
+ File.write(path_for(id), JSON.pretty_generate(record))
64
+ end
65
+
66
+ def load(id)
67
+ path = path_for(id)
68
+ return nil unless File.exist?(path)
69
+
70
+ JSON.parse(File.read(path))
71
+ end
72
+
73
+ def list
74
+ Dir.glob(File.join(@dir, '*.json')).map { |p| File.basename(p, '.json') }
75
+ end
76
+
77
+ def delete(id)
78
+ path = path_for(id)
79
+ File.delete(path) if File.exist?(path)
80
+ nil
81
+ end
82
+
83
+ private
84
+
85
+ # Run ids reach here from callers and from stored records, so a path
86
+ # separator or traversal segment must not be able to steer the write
87
+ # outside the checkpoint directory.
88
+ def path_for(id)
89
+ s = id.to_s
90
+ raise CheckpointError, "invalid run id: #{id.inspect}" if s.empty? ||
91
+ s.include?('/') ||
92
+ s.include?('\\') ||
93
+ s == '.' || s == '..'
94
+
95
+ File.join(@dir, "#{s}.json")
96
+ end
97
+ end
98
+
99
+ module_function
100
+
101
+ def new_run_id
102
+ SecureRandom.uuid
103
+ end
104
+
105
+ # Builds the record persisted for a run.
106
+ def record_for(run_id:, crew_name:, tasks:, parent_run_id: nil)
107
+ {
108
+ 'run_id' => run_id,
109
+ 'parent_run_id' => parent_run_id,
110
+ 'crew' => crew_name,
111
+ 'updated_at' => Time.now.utc.iso8601,
112
+ 'tasks' => tasks
113
+ }
114
+ end
115
+
116
+ # Serializes one task's settled state.
117
+ def task_entry(task, status)
118
+ {
119
+ 'status' => status.to_s,
120
+ 'result' => task.result,
121
+ 'execution_time' => task.execution_time
122
+ }
123
+ end
124
+
125
+ # Walks parent_run_id links from +run_id+ back to the root, returning the
126
+ # chain oldest-first. Stops on a missing record rather than raising, so a
127
+ # pruned ancestor truncates the chain instead of breaking it.
128
+ def lineage(store, run_id)
129
+ chain = []
130
+ seen = {}
131
+ current = run_id
132
+
133
+ while current && !seen[current]
134
+ seen[current] = true
135
+ record = store.load(current)
136
+ break unless record
137
+
138
+ chain.unshift(current)
139
+ current = record['parent_run_id']
140
+ end
141
+
142
+ chain
143
+ end
144
+ end
145
+ end
data/lib/rcrewai/cli.rb CHANGED
@@ -8,9 +8,13 @@ module RCrewAI
8
8
  Crew.create(crew_name)
9
9
  end
10
10
 
11
+ # Thor reserves #run, so the command is defined under another name and
12
+ # mapped back. Without this the whole class raises on load, which is why
13
+ # cli.rb went unrequired -- and why bin/rcrewai never worked.
11
14
  desc 'run', 'Run the AI crew'
12
15
  option :crew, type: :string, required: true, desc: 'Name of the crew to run'
13
- def run
16
+ map 'run' => :run_crew
17
+ def run_crew
14
18
  crew_name = options[:crew]
15
19
  puts "Running crew: #{crew_name}"
16
20
  crew = Crew.load(crew_name)
@@ -31,9 +35,16 @@ module RCrewAI
31
35
  desc 'task SUBCOMMAND ...ARGS', 'Manage tasks'
32
36
  subcommand 'task', Task::CLI
33
37
 
38
+ desc 'checkpoint SUBCOMMAND ...ARGS', 'Inspect run checkpoints'
39
+ subcommand 'checkpoint', Checkpoint::CLI
40
+
34
41
  desc 'version', 'Show version'
35
42
  def version
36
43
  puts "rcrewai version #{RCrewAI::VERSION}"
37
44
  end
45
+
46
+ def self.exit_on_failure?
47
+ true
48
+ end
38
49
  end
39
50
  end
@@ -6,6 +6,7 @@ module RCrewAI
6
6
  :openai_api_key, :anthropic_api_key, :google_api_key, :azure_api_key,
7
7
  :openai_model, :anthropic_model, :google_model, :azure_model,
8
8
  :base_url, :api_version, :deployment_name,
9
+ :aws_region, :snowflake_account,
9
10
  :pricing, :ollama_native_tools, :log_level
10
11
 
11
12
  def initialize
@@ -21,6 +22,9 @@ module RCrewAI
21
22
  @google_model = 'gemini-pro'
22
23
  @azure_model = 'gpt-4'
23
24
 
25
+ @aws_region = nil
26
+ @snowflake_account = nil
27
+
24
28
  @pricing = nil
25
29
  @ollama_native_tools = nil
26
30
  @log_level = :info
@@ -101,6 +105,9 @@ module RCrewAI
101
105
  @google_api_key = ENV['GOOGLE_API_KEY'] || ENV['GEMINI_API_KEY']
102
106
  @azure_api_key = ENV['AZURE_OPENAI_API_KEY']
103
107
 
108
+ @aws_region ||= ENV.fetch('AWS_REGION', nil) || ENV.fetch('AWS_DEFAULT_REGION', nil)
109
+ @snowflake_account ||= ENV.fetch('SNOWFLAKE_ACCOUNT', nil)
110
+
104
111
  @api_key = ENV['LLM_API_KEY'] if @api_key.nil?
105
112
  @base_url = ENV['LLM_BASE_URL'] if @base_url.nil?
106
113
  @api_version = ENV['AZURE_API_VERSION'] if @api_version.nil?