fosm-rails-coding-agent 0.0.3 → 0.0.4

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: 1aae5e6770dc56f24572f7ddfcb604602386c1b165899f5414c02de8a649fb78
4
- data.tar.gz: 63b6a5639d9433d722864e7aa8a3c94f1623dd5d9457c595b84978ff07cfafb3
3
+ metadata.gz: f6aeb8d53bfaadc57317556a9e91f4c95fb633a8e3f365ca6ec3517e09b82af7
4
+ data.tar.gz: 062c37b29a701991b14c0f56782612f8c56b37bc2b16a171e70d434eb7078d43
5
5
  SHA512:
6
- metadata.gz: 2f41c95536622953a95e7ed07f6823e5974cbe6a154132513b63e4243aa924220be5e54727f4680b2aa8730c53f6006e7b82f68d0276b363d92c36b83235ef5e
7
- data.tar.gz: 77d342fb361156867f8ae7069c4474c02b6a48e628120e2fe670e00c28608b4c6c4e7e2908e0c3b281c6579a606d8aa63f61419f2d4b738a5aec03a753bdd37f
6
+ metadata.gz: 6cb0787f6e6937434f169847d67d7b0e650cab59e50b28f81b61a54047ec2975f7503a8630a6f4dcd3b25f83ed2cad825459093181a42d4bdacbfbe742beb364
7
+ data.tar.gz: f97879752c8e85594dea933577b0e20abccb4862a956af02ad87032913480b750b6e71d8dca1e3031503033defc82b7ac0b760afba73ab7081b749cc1cc23e6a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to fosm-rails-coding-agent will be documented in this file.
4
4
 
5
+ ## [0.0.4] - 2026-03-29
6
+
7
+ ### Changed
8
+
9
+ - **fosm-coding-agent is now a real coding agent** powered by Gemlings ToolCallingAgent.
10
+ Previously returned canned responses. Now uses an LLM (Claude Sonnet by default)
11
+ to read/write files, run bash commands, and reason step by step.
12
+ - ACP prompts are handled by Gemlings agent.run() with streaming step notifications.
13
+ - Added BashTool for running shell commands.
14
+ - Added gemlings >= 0.3 as a direct dependency.
15
+ - Agent model configurable via FOSM_AGENT_MODEL env var (default: anthropic/claude-sonnet-4-20250514).
16
+
5
17
  ## [0.0.1] - 2026-03-29
6
18
 
7
19
  ### Added
@@ -3,77 +3,198 @@
3
3
  require "agent_client_protocol"
4
4
  require "json"
5
5
  require "securerandom"
6
+ require "open3"
7
+ require "gemlings"
8
+ require "gemlings/tools/file_read"
9
+ require "gemlings/tools/file_write"
6
10
 
7
11
  module FosmRailsCodingAgent
8
- # ACP agent that implements the Agent Client Protocol.
9
- # Provides push-based FOSM lifecycle context to coding agents.
12
+ # ACP agent powered by Gemlings ToolCallingAgent.
10
13
  #
11
- # When a session starts, the agent proactively pushes:
12
- # - All FOSM lifecycle definitions in the project
13
- # - Available events for key records
14
- # - Recent transition activity
14
+ # Bridges the Agent Client Protocol (stdio JSON-RPC) to a real LLM-powered
15
+ # coding agent. Each prompt is handled by a Gemlings agent that can read/write
16
+ # files, call tools, and reason step by step — not canned responses.
15
17
  #
16
- # This is the José Valim insight: don't wait for the agent to ask,
17
- # PUSH context so the coding agent starts with full situational awareness.
18
+ # On session start, FOSM lifecycle context is pushed proactively.
18
19
  class AcpAgent
19
20
  include AgentClientProtocol::AgentInterface
20
21
  include AgentClientProtocol::Helpers
21
22
 
23
+ DEFAULT_MODEL = "anthropic/claude-sonnet-4-20250514"
24
+
22
25
  def on_connect(conn)
23
26
  @conn = conn
27
+ @sessions = {} # session_id => Gemlings::ToolCallingAgent
24
28
  end
25
29
 
26
- def initialize_agent(protocol_version:, **)
30
+ def initialize_agent(protocol_version:, **_)
27
31
  AgentClientProtocol::Schema::InitializeResponse.new(
28
32
  protocol_version: AgentClientProtocol::PROTOCOL_VERSION,
29
33
  agent_info: AgentClientProtocol::Schema::Implementation.new(
30
- name: "fosm-rails-coding-agent",
34
+ name: "fosm-coding-agent",
31
35
  version: FosmRailsCodingAgent::VERSION
32
36
  ),
33
37
  capabilities: AgentClientProtocol::Schema::AgentCapabilities.new
34
38
  )
35
39
  end
36
40
 
37
- def new_session(cwd:, **)
38
- session_id = "fosm-agent-#{SecureRandom.uuid}"
41
+ def new_session(cwd:, **_)
42
+ session_id = "fosm-#{SecureRandom.hex(8)}"
43
+
44
+ # Build a Gemlings agent with coding tools + FOSM context
45
+ agent = build_agent(cwd)
46
+ @sessions[session_id] = agent
39
47
 
40
- # Proactively push FOSM context if available
41
- push_fosm_context(session_id) if FosmRailsCodingAgent.fosm_available?
48
+ # Push FOSM context proactively
49
+ push_fosm_context(session_id)
42
50
 
43
51
  AgentClientProtocol::Schema::NewSessionResponse.new(
44
52
  session_id: session_id
45
53
  )
46
54
  end
47
55
 
48
- def prompt(prompt:, session_id:, **)
49
- prompt.each do |block|
50
- text = case block
51
- when AgentClientProtocol::Schema::TextContent then block.text
52
- when Hash then block["text"] || block.inspect
53
- else block.inspect
54
- end
56
+ def prompt(prompt:, session_id:, **_)
57
+ agent = @sessions[session_id]
58
+ unless agent
59
+ send_error_message(session_id, "Session not found. Create a new session first.")
60
+ return end_turn
61
+ end
62
+
63
+ # Extract text from ACP prompt content blocks
64
+ text = prompt.map { |block|
65
+ case block
66
+ when AgentClientProtocol::Schema::TextContent then block.text
67
+ when Hash then block["text"] || block.inspect
68
+ else block.inspect
69
+ end
70
+ }.join("\n")
71
+
72
+ # Run the Gemlings agent in streaming mode — forward steps as ACP notifications
73
+ run_agent(agent, text, session_id)
74
+
75
+ end_turn
76
+ end
77
+
78
+ def cancel(session_id:, **_)
79
+ agent = @sessions[session_id]
80
+ agent&.interrupt
81
+ end
82
+
83
+ def authenticate(method_id:, **_)
84
+ AgentClientProtocol::Schema::AuthenticateResponse.new
85
+ end
86
+
87
+ private
88
+
89
+ def end_turn
90
+ AgentClientProtocol::Schema::PromptResponse.new(
91
+ stop_reason: AgentClientProtocol::Schema::StopReason::END_TURN
92
+ )
93
+ end
94
+
95
+ # Build a Gemlings ToolCallingAgent with file tools and FOSM instructions.
96
+ def build_agent(cwd)
97
+ tools = [Gemlings::FileRead, Gemlings::FileWrite, BashTool]
98
+
99
+ model = ENV.fetch("FOSM_AGENT_MODEL", DEFAULT_MODEL)
100
+ instructions = build_instructions(cwd)
101
+
102
+ Gemlings::ToolCallingAgent.new(
103
+ model: model,
104
+ tools: tools,
105
+ max_steps: 20,
106
+ instructions: instructions,
107
+ name: "fosm-coding-agent"
108
+ )
109
+ end
110
+
111
+ def build_instructions(cwd)
112
+ instructions = <<~INST
113
+ You are a coding agent working in a Rails application at: #{cwd}
114
+
115
+ You can read files, write files, and run bash commands to help the developer.
116
+ When asked about the app, explore the codebase. When asked to make changes, do so.
117
+ Always explain what you're doing before making changes.
118
+ INST
119
+
120
+ # Append FOSM context if available
121
+ fosm_context = build_fosm_context
122
+ instructions += "\n\n#{fosm_context}" unless fosm_context.empty?
123
+
124
+ instructions
125
+ end
126
+
127
+ # Run the agent and stream results back as ACP notifications.
128
+ def run_agent(agent, task, session_id)
129
+ # Use step-by-step execution so we can stream each step
130
+ agent.reset!
131
+ step = agent.step(task)
132
+ stream_step(step, session_id)
133
+
134
+ until agent.done?
135
+ step = agent.step
136
+ stream_step(step, session_id)
137
+ end
138
+
139
+ # Send the final answer
140
+ if agent.final_answer_value
141
+ send_agent_message(session_id, agent.final_answer_value.to_s)
142
+ end
143
+ rescue Gemlings::MaxStepsError
144
+ send_agent_message(session_id, "Reached maximum steps. Please provide more guidance.")
145
+ rescue Gemlings::InterruptError
146
+ send_agent_message(session_id, "Cancelled.")
147
+ rescue => e
148
+ send_error_message(session_id, "Agent error: #{e.message}")
149
+ end
55
150
 
56
- response = handle_prompt_text(text, session_id)
151
+ # Convert a Gemlings step into ACP session/update notifications.
152
+ def stream_step(step, session_id)
153
+ return unless step
57
154
 
155
+ # Stream thought
156
+ if step.respond_to?(:thought) && step.thought
58
157
  @conn.session_update(
59
158
  session_id: session_id,
60
- update: update_agent_message_text(response)
159
+ update: update_agent_thought_text(step.thought)
61
160
  )
62
161
  end
63
162
 
64
- AgentClientProtocol::Schema::PromptResponse.new(
65
- stop_reason: AgentClientProtocol::Schema::StopReason::END_TURN
66
- )
163
+ # Stream tool calls
164
+ if step.respond_to?(:tool_calls) && step.tool_calls&.any?
165
+ step.tool_calls.each do |tc|
166
+ @conn.session_update(
167
+ session_id: session_id,
168
+ update: start_tool_call(
169
+ tc.id || SecureRandom.hex(8),
170
+ "#{tc.function.name}(#{tc.function.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")})",
171
+ status: AgentClientProtocol::Schema::ToolCallStatus::COMPLETED
172
+ )
173
+ )
174
+ end
175
+ end
176
+
177
+ # Stream observation/output
178
+ if step.respond_to?(:observation) && step.observation
179
+ send_agent_message(session_id, step.observation)
180
+ end
67
181
  end
68
182
 
69
- def authenticate(method_id:, **)
70
- AgentClientProtocol::Schema::AuthenticateResponse.new
183
+ def send_agent_message(session_id, text)
184
+ @conn.session_update(
185
+ session_id: session_id,
186
+ update: update_agent_message_text(text)
187
+ )
71
188
  end
72
189
 
73
- private
190
+ def send_error_message(session_id, text)
191
+ @conn.session_update(
192
+ session_id: session_id,
193
+ update: update_agent_message_text("Error: #{text}")
194
+ )
195
+ end
74
196
 
75
- # Push FOSM lifecycle context at session start so the coding agent
76
- # has full awareness of the state machines in the project.
197
+ # Push FOSM lifecycle context at session start.
77
198
  def push_fosm_context(session_id)
78
199
  context = build_fosm_context
79
200
  return if context.empty?
@@ -82,8 +203,7 @@ module FosmRailsCodingAgent
82
203
  session_id: session_id,
83
204
  update: update_agent_message_text(
84
205
  "## FOSM Lifecycle Context\n\n" \
85
- "This Rails application uses FOSM (Finite Observable State Machine) " \
86
- "lifecycles. Here are the registered state machines:\n\n#{context}"
206
+ "This Rails application uses FOSM lifecycles:\n\n#{context}"
87
207
  )
88
208
  )
89
209
  end
@@ -109,24 +229,27 @@ module FosmRailsCodingAgent
109
229
  " #{e.name}: #{e.from_states.join(", ")} → #{e.to_state}"
110
230
  end
111
231
 
112
- "### #{klass.name}\n" \
113
- "States: #{states.join(", ")}\n" \
114
- "Events:\n#{events.join("\n")}\n"
232
+ "### #{klass.name}\nStates: #{states.join(", ")}\nEvents:\n#{events.join("\n")}\n"
115
233
  end.join("\n")
116
234
  end
235
+ end
117
236
 
118
- def handle_prompt_text(text, session_id)
119
- case text.downcase.strip
120
- when /lifecycles?/
121
- build_fosm_context.presence || "No FOSM lifecycles found in this project."
122
- when /help/
123
- "FosmRailsCodingAgent ACP agent provides FOSM-aware runtime intelligence.\n" \
124
- "FOSM tools are available via the MCP server at /fosm-agent/mcp.\n" \
125
- "Ask about lifecycles, states, events, or transitions."
126
- else
127
- "FosmRailsCodingAgent is listening. Use the MCP tools at /fosm-agent/mcp for " \
128
- "SQL queries, logs, code evaluation, and FOSM introspection."
129
- end
237
+ # Simple bash tool for the Gemlings agent.
238
+ class BashTool < Gemlings::Tool
239
+ tool_name "bash"
240
+ description "Execute a bash command and return stdout/stderr. Use for running tests, git, rake, etc."
241
+ input :command, type: :string, description: "The bash command to execute"
242
+ output_type :string
243
+
244
+ def call(command:)
245
+ stdout, stderr, status = Open3.capture3(command)
246
+ output = ""
247
+ output += stdout unless stdout.empty?
248
+ output += "\nSTDERR: #{stderr}" unless stderr.empty?
249
+ output += "\nExit: #{status.exitstatus}" unless status.success?
250
+ output.empty? ? "(no output)" : output.strip
251
+ rescue => e
252
+ "Error: #{e.message}"
130
253
  end
131
254
  end
132
255
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FosmRailsCodingAgent
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fosm-rails-coding-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhishek Parolkar
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: gemlings
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
69
83
  description: |
70
84
  Embeds a FOSM-aware MCP server and ACP agent into your Rails development
71
85
  environment, giving coding agents (Claude Code, Codex, Copilot) runtime intelligence: