Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c5885f6
fix: repair test suite truncation and latent failures it was hiding
claude Jul 5, 2026
5540067
docs: ten Rubyist perspectives review with field-note index
claude Jul 5, 2026
11c9407
refactor: make Zeitwerk the single code loader for the gem
claude Jul 5, 2026
cb2e1ae
docs: add the three-line haiku agent example (Matz persona)
claude Jul 5, 2026
2662932
feat: add Agentic.run - plan and execute a goal in one call
claude Jul 5, 2026
59251e8
feat: add boot benchmark and make agent assembly init thread-safe
claude Jul 5, 2026
17a3695
fix: compose PlanOrchestrator with running reactors; honor retry backoff
claude Jul 5, 2026
c1c2f93
feat: fail-fast LLM credential validation; quiet default logger
claude Jul 5, 2026
6811147
feat: enforce capability contracts with dry-schema
claude Jul 5, 2026
076298c
feat: add ExecutionJournal for crash-surviving plan state
claude Jul 5, 2026
32c28ff
feat: honest execute_with_schema, inheritable factory DSL, named errors
claude Jul 5, 2026
1fcde58
feat: real pluggable web_search capability
claude Jul 5, 2026
b9eff75
fix: Agentic::Logger was silently discarding its level argument
claude Jul 5, 2026
0e38ee0
docs: renga circle example - dependency graphs as poetic form (Matz r…
claude Jul 6, 2026
8e1c02d
docs: HEY-style ticket screener example (DHH round 2)
claude Jul 6, 2026
f502e3f
docs: Prism-powered performance detective example (tenderlove round 2)
claude Jul 6, 2026
796a0d8
docs: namespace cartographer example (fxn round 2)
claude Jul 6, 2026
29bd445
docs: latency lab example (ioquatix round 2)
claude Jul 6, 2026
ba73c55
docs: schema advisor example (Jeremy Evans round 2)
claude Jul 6, 2026
7e4d226
docs: typed ETL pipeline example (solnic round 2)
claude Jul 6, 2026
77aef71
docs: durable batch crash-and-resume example (mperham round 2)
claude Jul 6, 2026
5784f7d
docs: refactoring dojo example (Sandi Metz round 2)
claude Jul 6, 2026
e288389
docs: gem scout example and round-2 index (ankane round 2)
claude Jul 6, 2026
f1b0a84
feat: deliver the round-2 consumer roadmap
claude Jul 6, 2026
137f8af
docs: telephone game example (Matz round 3)
claude Jul 6, 2026
2dcf878
docs: standup digest example (DHH round 3)
claude Jul 6, 2026
340697a
fix: scheduler deadlock at tight concurrency limits; add plan Gantt e…
claude Jul 6, 2026
fd09a91
docs: documentation coverage surveyor example (fxn round 3)
claude Jul 6, 2026
223ed02
docs: live dashboard example (ioquatix round 3)
claude Jul 6, 2026
6063b85
docs: contract fuzzer example (Jeremy Evans round 3)
claude Jul 6, 2026
ddd7cd5
docs: typed command bus example (solnic round 3)
claude Jul 6, 2026
1ac4072
docs: flaky API drill example (mperham round 3)
claude Jul 6, 2026
d7b5713
docs: collaboration tracer example (Sandi Metz round 3)
claude Jul 6, 2026
6c8e3d6
docs: changelog scout example and round-3 index (ankane round 3)
claude Jul 6, 2026
71c9b40
feat: deliver the round-3 asks
claude Jul 6, 2026
a18af56
docs: exquisite corpse example (Matz round 4)
claude Jul 6, 2026
1f3cf71
docs: setup doctor example (DHH round 4)
claude Jul 6, 2026
08fdeba
docs: concurrency knee finder example (tenderlove round 4)
claude Jul 6, 2026
b0d140f
docs: coupling cartographer example (fxn round 4)
claude Jul 6, 2026
4b66c0d
docs: shared rate limit example (ioquatix round 4)
claude Jul 6, 2026
294aa54
fix: canceled plans no longer report :completed; add invariant sentin…
claude Jul 6, 2026
27824f3
docs: contract state machine example (solnic round 4)
claude Jul 6, 2026
608f2a4
docs: error taxonomy drill example (mperham round 4)
claude Jul 6, 2026
c68fc40
docs: graph critic example (Sandi Metz round 4)
claude Jul 6, 2026
b4afde4
docs: README verifier example (ankane round 4); fix broken README sni…
claude Jul 6, 2026
9432bbd
docs: round-4 index and findings
claude Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
.rspec_status
# environment variables
.env
result-*.json
80 changes: 79 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,84 @@ else
end
```

### Passing work to the orchestrator directly

You don't need an agent provider to run a plan. Tasks carry an arbitrary
`payload`, accept their agent (or a bare callable) directly, and receive the
outputs of the tasks they depend on:

```ruby
orchestrator = Agentic::PlanOrchestrator.new

fetch = Agentic::Task.new(
description: "fetch the order",
agent_spec: {"name" => "Fetcher", "instructions" => "fetch"},
payload: order_id # any domain object, opaque to the framework
)
notify = Agentic::Task.new(
description: "notify the customer",
agent_spec: {"name" => "Notifier", "instructions" => "notify"}
)

# A callable receives the Task itself; its return value becomes the output
orchestrator.add_task(fetch, agent: ->(task) { OrderApi.fetch(task.payload) })

# Dependencies pipe their outputs into dependents
orchestrator.add_task(notify, [fetch], agent: ->(task) {
Mailer.deliver(task.output_of(fetch)) # or task.dependency_outputs
})

result = orchestrator.execute_plan # no provider needed
```

A plan-wide block acts as an agent factory when you do want one place that
builds agents: `orchestrator.execute_plan { |task| build_agent_for(task) }`.

Dependencies can be **named**, so they're declared and consumed under one
word, and single-dependency chains have a shorthand:

```ruby
orchestrator.add_task(digest, needs: {shipped: commits, owed: debt}, agent: ->(task) {
"#{task.needs.shipped} commits shipped, #{task.needs.owed} TODOs owed"
})

orchestrator.add_task(next_verse, [previous_verse], agent: ->(task) {
answer(task.previous_output) # the sole dependency's output
})
```

Capability contracts can constrain values beyond type and presence —
`enum: %w[standard express]`, `min:`/`max:` bounds, and `non_empty: true`
for strings and arrays. The `task_slot_acquired` lifecycle hook fires when
a task obtains a concurrency slot (with `waited:` time), separating queue
wait from run time; and retry policies consult an error's own
`retryable?` verdict before falling back to the `retryable_errors` list.

### The concurrency contract

Tasks run as fibers inside an [async](https://github.com/socketry/async)
reactor. That means:

- **IO-bound tasks scale nearly perfectly.** LLM calls, HTTP requests,
`sleep`, database queries — anything that waits on IO yields to other
tasks. Twenty 200ms API calls at `concurrency_limit: 20` take ~200ms of
wall clock, not 4 seconds (see `examples/latency_lab.rb` for the measured
curve).
- **CPU-bound tasks do not parallelize.** Fibers share one thread; parsing,
hashing, and number crunching gain nothing from a higher concurrency
limit. If your tasks are compute, the limit is a queue, not a speedup.
- `execute_plan` composes with a running reactor: called inside `Async`
(e.g. under Falcon), it joins the current event loop instead of nesting
a new one; standalone, it creates its own and blocks until done.

### When to reach for which layer

Start with **capabilities** — lambdas with declared contracts — and call
them directly. Add the **orchestrator** when you have an actual queue:
many independent items, or tasks with dependencies. Add the **planner**
(`Agentic.run` / `TaskPlanner`) when the task list itself should come from
an LLM. Each layer is optional and composes with the ones below it.

## Extension System

Agentic includes a powerful Extension System to help integrate with external systems and customize the framework's behavior. The Extension System consists of three main components:
Expand Down Expand Up @@ -454,7 +532,7 @@ registry.compose(

# Use the composed capability
agent.add_capability("comprehensive_report")
result = agent.execute_capability("comprehensive_report", { data: { ... } })
result = agent.execute_capability("comprehensive_report", { data: { sales: [120, 90, 143] } })
```

## Learning System
Expand Down
40 changes: 40 additions & 0 deletions benchmark/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

# Boot-time benchmark: what does it cost to require this gem?
#
# bundle exec ruby benchmark/boot.rb
#
# Each scenario runs in a fresh subprocess so measurements don't
# contaminate each other. Wall time, object allocations, and the number
# of loaded files tell three different stories:
# - wall time is what your app's boot feels like
# - allocations approximate the parse/define work done
# - $LOADED_FEATURES is how much of the world you dragged in

require "rbconfig"

LIB = File.expand_path("../lib", __dir__)

SCENARIOS = {
"baseline (empty ruby)" => "",
"require \"agentic\"" => 'require "agentic"',
"... + Agentic::CLI (thor, tty-*)" => 'require "agentic"; Agentic::CLI',
"... + agent assembly init" => 'require "agentic"; Agentic.logger.level = :error; Agentic.initialize_agent_assembly'
}.freeze

def measure(label, code)
script = <<~RUBY
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
allocated_before = GC.stat(:total_allocated_objects)
#{code}
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000
allocated = GC.stat(:total_allocated_objects) - allocated_before
puts format("%-36s %9.1f ms %12d objects %6d features",
#{label.inspect}, elapsed_ms, allocated, $LOADED_FEATURES.size)
RUBY
system(RbConfig.ruby, "-I", LIB, "-e", script) || abort("scenario failed: #{label}")
end

puts format("%-36s %12s %20s %15s", "scenario", "wall", "allocations", "loaded files")
puts "-" * 88
SCENARIOS.each { |label, code| measure(label, code) }
Loading
Loading