docs: make Rails quickstart match Turn API
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
|
```ruby
|
||||||
# Gemfile
|
# Gemfile
|
||||||
gem "opencode-ruby" # wire client + Reply state machine
|
gem "opencode-ruby", "= 0.0.1.alpha7" # wire client + Reply state machine
|
||||||
gem "opencode-rails" # AR-coupled session/turn/artifact stack
|
|
||||||
|
# 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
|
```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
|
uses a subscribe-ready-before-prompt transport contract and reconnects an
|
||||||
accepted turn without posting its prompt again.
|
accepted turn without posting its prompt again.
|
||||||
|
|
||||||
Releases use RubyGems trusted publishing. Register `release.yml` as the gem's
|
`opencode-rails` 0.0.1.alpha7 is available as a GitHub source release but has
|
||||||
trusted publisher (using the `release` environment); after that, a `v*` tag
|
not been published to RubyGems. The `release.yml` workflow is prepared for
|
||||||
publishes without a long-lived RubyGems API key.
|
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
|
## 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
|
```ruby
|
||||||
# app/jobs/generate_response_job.rb
|
# app/jobs/generate_response_job.rb
|
||||||
class GenerateResponseJob < ApplicationJob
|
class GenerateResponseJob < ApplicationJob
|
||||||
def perform(assistant_message)
|
def perform(assistant_message)
|
||||||
conversation = assistant_message.conversation
|
conversation = assistant_message.conversation
|
||||||
user_message = conversation.messages.where(role: :user).last
|
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(
|
session = Opencode::Session.new(
|
||||||
conversation,
|
conversation,
|
||||||
@@ -68,9 +92,15 @@ class GenerateResponseJob < ApplicationJob
|
|||||||
subject: conversation,
|
subject: conversation,
|
||||||
query_text: user_message.content,
|
query_text: user_message.content,
|
||||||
client: client,
|
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) {
|
on_turn_finished: ->(result) {
|
||||||
# result.status #=> :completed | :error | :cancelled
|
# result.status #=> :completed | :cancelled | :error | :failed
|
||||||
# result.message #=> the AR row (reloaded)
|
# result.message #=> the AR row (reloaded)
|
||||||
# result.duration_ms
|
# result.duration_ms
|
||||||
}
|
}
|
||||||
@@ -87,7 +117,16 @@ class GenerateResponseJob < ApplicationJob
|
|||||||
end
|
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
|
## 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