Merge pull request #3 from ajaynomics/docs/readme-executable-quickstart
docs: make alpha7 Rails quickstart executable
This commit is contained in:
57
README.md
57
README.md
@@ -19,8 +19,13 @@ Production-grade [OpenCode](https://opencode.ai) integration for Rails apps. Lay
|
||||
|
||||
```ruby
|
||||
# Gemfile
|
||||
gem "opencode-ruby" # wire client + Reply state machine
|
||||
gem "opencode-rails" # AR-coupled session/turn/artifact stack
|
||||
gem "opencode-ruby", "= 0.0.1.alpha7" # wire client + Reply state machine
|
||||
|
||||
# v0.0.1.alpha7 is a GitHub source release, not a RubyGems release. Pin the
|
||||
# peeled release commit so a moved tag cannot change the code Bundler installs.
|
||||
gem "opencode-rails",
|
||||
git: "https://github.com/ajaynomics/opencode-rails.git",
|
||||
ref: "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb"
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -33,9 +38,12 @@ During the alpha series both gems are pinned in lockstep. Version 0.0.1.alpha7
|
||||
uses a subscribe-ready-before-prompt transport contract and reconnects an
|
||||
accepted turn without posting its prompt again.
|
||||
|
||||
Releases use RubyGems trusted publishing. Register `release.yml` as the gem's
|
||||
trusted publisher (using the `release` environment); after that, a `v*` tag
|
||||
publishes without a long-lived RubyGems API key.
|
||||
`opencode-rails` 0.0.1.alpha7 is available as a GitHub source release but has
|
||||
not been published to RubyGems. The `release.yml` workflow is prepared for
|
||||
RubyGems trusted publishing, but the gem's trusted publisher still has to be
|
||||
registered for that workflow and its `release` environment. Until then, pushing
|
||||
a `v*` tag does not make the gem installable from RubyGems; use the exact Git
|
||||
commit above. Trusted publishing does not require a long-lived RubyGems API key.
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -49,13 +57,29 @@ Opencode::ErrorReporter.adapter = ->(error, **opts) {
|
||||
}
|
||||
```
|
||||
|
||||
```ruby
|
||||
# app/services/noop_reply_observer.rb
|
||||
#
|
||||
# Turn requires an observer factory even when the app does not need live
|
||||
# partial rendering. For a streaming UI, replace this with an observer that
|
||||
# persists/broadcasts selected ReplyObserver callbacks (and throttle writes).
|
||||
class NoopReplyObserver
|
||||
include Opencode::ReplyObserver
|
||||
|
||||
def watch(reply)
|
||||
reply.add_observer(self)
|
||||
self
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# app/jobs/generate_response_job.rb
|
||||
class GenerateResponseJob < ApplicationJob
|
||||
def perform(assistant_message)
|
||||
conversation = assistant_message.conversation
|
||||
user_message = conversation.messages.where(role: :user).last
|
||||
client = Opencode::Client.new(base_url: ENV["OPENCODE_URL"])
|
||||
client = Opencode::Client.new(base_url: ENV.fetch("OPENCODE_URL"))
|
||||
|
||||
session = Opencode::Session.new(
|
||||
conversation,
|
||||
@@ -68,9 +92,15 @@ class GenerateResponseJob < ApplicationJob
|
||||
subject: conversation,
|
||||
query_text: user_message.content,
|
||||
client: client,
|
||||
session: session,
|
||||
session_for: session,
|
||||
observer_factory: ->(_message) { NoopReplyObserver.new },
|
||||
system_context: ->(record) { "You are assisting with #{record.title}." },
|
||||
agent_name: ->(_record) { ENV.fetch("OPENCODE_AGENT", "build") },
|
||||
tracer: ->(name, **payload) {
|
||||
ActiveSupport::Notifications.instrument("assistant.#{name}", payload)
|
||||
},
|
||||
on_turn_finished: ->(result) {
|
||||
# result.status #=> :completed | :error | :cancelled
|
||||
# result.status #=> :completed | :cancelled | :error | :failed
|
||||
# result.message #=> the AR row (reloaded)
|
||||
# result.duration_ms
|
||||
}
|
||||
@@ -87,7 +117,16 @@ class GenerateResponseJob < ApplicationJob
|
||||
end
|
||||
```
|
||||
|
||||
The host's record (here `conversation`) must respond to `#title`, `#opencode_session_id`, `#opencode_session_id=`, `#with_lock(&block)`, `#update!`, `#reload`, `#id`. The host's message record (here `assistant_message`) must respond to `#error!(content)`, `#update_columns(...)`, `#with_lock(&block)`, `#reload`, `#pending?`.
|
||||
The host record (here `conversation`) must respond to `#title`,
|
||||
`#opencode_session_id`, `#opencode_session_id=`, `#with_lock(&block)`,
|
||||
`#update!`, `#reload`, and `#id`. The assistant message must respond to `#id`,
|
||||
`#reload`, `#cancelled?`, `#finalize!(**attrs)`, `#update!(**attrs)`, and
|
||||
`#error!(content)`. A non-no-op observer may impose additional record methods
|
||||
for its own live snapshots.
|
||||
|
||||
`Opencode::Turn` is an internal, alpha-stage composition seam. Its keyword
|
||||
constructor is intentionally explicit and may change before 1.0, so keep this
|
||||
wiring in one host service/job and keep the gem source pinned exactly.
|
||||
|
||||
## What you get
|
||||
|
||||
|
||||
39
test/readme_test.rb
Normal file
39
test/readme_test.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "ripper"
|
||||
|
||||
class ReadmeTest < Minitest::Test
|
||||
README_PATH = File.expand_path("../README.md", __dir__)
|
||||
|
||||
def setup
|
||||
@readme = File.read(README_PATH)
|
||||
@quickstart = @readme[/^## Quickstart\n(?<body>.*?)(?=^## )/m, :body]
|
||||
refute_nil @quickstart, "README must retain a Quickstart section"
|
||||
end
|
||||
|
||||
def test_quickstart_turn_call_documents_every_required_keyword
|
||||
turn_call = @quickstart[/Opencode::Turn\.new\((?<args>.*?)^\s*\)\.call/m, :args]
|
||||
refute_nil turn_call, "Quickstart must contain an Opencode::Turn.new(...).call example"
|
||||
|
||||
documented = turn_call.scan(/^\s*([a-z_]+):/).flatten.map(&:to_sym)
|
||||
parameters = Opencode::Turn.instance_method(:initialize).parameters
|
||||
required = parameters.filter_map { |kind, name| name if kind == :keyreq }
|
||||
accepted = parameters.filter_map { |kind, name| name if %i[keyreq key].include?(kind) }
|
||||
|
||||
assert_empty required - documented,
|
||||
"Quickstart is missing required Turn keywords: #{(required - documented).join(", ")}"
|
||||
assert_empty documented - accepted,
|
||||
"Quickstart uses unsupported Turn keywords: #{(documented - accepted).join(", ")}"
|
||||
refute_includes documented, :session
|
||||
end
|
||||
|
||||
def test_readme_ruby_fences_parse
|
||||
ruby_fences = @readme.scan(/```ruby\n(.*?)```/m).flatten
|
||||
refute_empty ruby_fences
|
||||
|
||||
ruby_fences.each_with_index do |source, index|
|
||||
assert Ripper.sexp(source), "README Ruby fence #{index + 1} has invalid syntax"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user