actionagent 1.9.2 → 1.9.3

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +23 -0
  3. data/app/assets/builds/action_agent.css +1 -1
  4. data/app/assets/builds/action_agent.js +61 -61
  5. data/app/controllers/action_agent/api/agents_controller.rb +23 -0
  6. data/app/controllers/action_agent/api/code_sessions_controller.rb +49 -10
  7. data/app/controllers/action_agent/api/evaluation_reports_controller.rb +66 -2
  8. data/app/controllers/action_agent/api/evaluations_controller.rb +41 -3
  9. data/app/controllers/action_agent/api/interactions_controller.rb +55 -1
  10. data/app/controllers/action_agent/api/sandboxes_controller.rb +13 -2
  11. data/app/controllers/action_agent/api/trace_reports_controller.rb +11 -6
  12. data/app/jobs/action_agent/code_session_job.rb +19 -2
  13. data/app/models/action_agent/agent.rb +54 -1
  14. data/app/models/action_agent/code_session.rb +80 -0
  15. data/app/models/action_agent/evaluation_run.rb +17 -2
  16. data/app/models/action_agent/provider_key.rb +5 -1
  17. data/app/queries/action_agent/trace_window_report.rb +60 -0
  18. data/app/serializers/action_agent/code_conversation_serializer.rb +86 -0
  19. data/app/serializers/action_agent/evaluation_serializer.rb +13 -2
  20. data/app/services/action_agent/agent_execution_service.rb +37 -0
  21. data/app/services/action_agent/agent_tool_roster.rb +53 -18
  22. data/app/services/action_agent/agent_toolbox.rb +156 -3
  23. data/app/services/action_agent/browser_client_token.rb +86 -0
  24. data/app/services/action_agent/checkout_tools.rb +154 -0
  25. data/app/services/action_agent/code_conversation.rb +133 -0
  26. data/app/services/action_agent/codex_auth.rb +81 -0
  27. data/app/services/action_agent/dashboard_assistant_service.rb +19 -1
  28. data/app/services/action_agent/diff_stat.rb +27 -0
  29. data/app/services/action_agent/evaluation_report_export.rb +175 -0
  30. data/app/services/action_agent/evaluation_report_import.rb +40 -17
  31. data/app/services/action_agent/evaluation_runner_service.rb +48 -12
  32. data/app/services/action_agent/local_sandbox_backend.rb +295 -30
  33. data/app/services/action_agent/mcp_catalog.rb +25 -0
  34. data/app/services/action_agent/mcp_client.rb +1 -0
  35. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -1
  36. data/app/services/action_agent/project_boot_fix.rb +10 -3
  37. data/app/services/action_agent/sandbox_orchestrator.rb +41 -1
  38. data/app/services/action_agent/scenario_evaluation_runner.rb +1 -0
  39. data/app/services/action_agent/slack_tools.rb +101 -0
  40. data/config/routes.rb +10 -1
  41. data/lib/action_agent/engine.rb +6 -0
  42. data/lib/action_agent/version.rb +1 -1
  43. data/lib/action_agent.rb +35 -2
  44. data/lib/generators/action_agent/templates/migrations/028_add_conversations_to_code_sessions.rb.erb +18 -0
  45. data/lib/tasks/evaluations.rake +78 -0
  46. metadata +13 -2
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module ActionAgent
6
+ # The slack capability: reading channels and posting as the workspace's bot,
7
+ # through the Slack Web API with the bot token the owner connected in
8
+ # Settings -> Integrations (a ProviderKey of provider "slack"). Results and
9
+ # errors are hashes for the model to read; the token never appears in them.
10
+ class SlackTools
11
+ FUNCTIONS = %w[slack_list_channels slack_read_channel slack_post_message].freeze
12
+ API = "https://slack.com/api/"
13
+ TIMEOUT_SECONDS = 10
14
+ MAX_CHANNELS = 200
15
+ MAX_MESSAGES = 50
16
+
17
+ NOT_CONNECTED = "Slack is not connected: add a bot token (xoxb-…) in Settings -> Integrations -> Slack"
18
+
19
+ def self.function?(name)
20
+ FUNCTIONS.include?(name.to_s)
21
+ end
22
+
23
+ # The bot token +owner+ connected, or nil.
24
+ def self.token_for(owner)
25
+ ProviderKey.for_owner(owner).find_by(provider: "slack")&.credential.presence
26
+ end
27
+
28
+ # @param token [String, nil]
29
+ def initialize(token)
30
+ @token = token
31
+ end
32
+
33
+ # @return [Hash]
34
+ def call(name, kwargs)
35
+ return { error: "Unknown tool: #{name}" } unless self.class.function?(name)
36
+ return { error: NOT_CONNECTED } if @token.blank?
37
+
38
+ args = kwargs.to_h.symbolize_keys
39
+ case name.to_s
40
+ when "slack_list_channels" then list_channels
41
+ when "slack_read_channel" then read_channel(**args.slice(:channel, :limit))
42
+ when "slack_post_message" then post_message(**args.slice(:channel, :text, :thread_ts))
43
+ end
44
+ end
45
+
46
+ def list_channels
47
+ body = request("conversations.list", { types: "public_channel,private_channel", exclude_archived: true, limit: MAX_CHANNELS })
48
+ return body if body[:error]
49
+
50
+ channels = Array(body["channels"]).map do |channel|
51
+ { id: channel["id"], name: channel["name"], is_member: channel["is_member"], topic: channel.dig("topic", "value").presence }.compact
52
+ end
53
+ { channels: channels }
54
+ end
55
+
56
+ def read_channel(channel:, limit: nil)
57
+ count = (limit || 20).to_i.clamp(1, MAX_MESSAGES)
58
+ body = request("conversations.history", { channel: channel.to_s, limit: count })
59
+ return body if body[:error]
60
+
61
+ messages = Array(body["messages"]).map do |message|
62
+ { ts: message["ts"], user: message["user"] || message["bot_id"], text: message["text"], thread_ts: message["thread_ts"] }.compact
63
+ end
64
+ { channel: channel, messages: messages }
65
+ end
66
+
67
+ def post_message(channel:, text:, thread_ts: nil)
68
+ return { error: "No text to post" } if text.to_s.strip.empty?
69
+
70
+ body = request("chat.postMessage", { channel: channel.to_s, text: text.to_s, thread_ts: thread_ts.presence }.compact, post: true)
71
+ return body if body[:error]
72
+
73
+ { posted: true, channel: body["channel"], ts: body["ts"] }
74
+ end
75
+
76
+ private
77
+
78
+ # Slack answers 200 with { ok: false, error: } for most failures.
79
+ def request(method, params, post: false)
80
+ uri = URI.join(API, method)
81
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: TIMEOUT_SECONDS, read_timeout: TIMEOUT_SECONDS) do |http|
82
+ headers = { "Authorization" => "Bearer #{@token}", "User-Agent" => "ActiveAgents-Toolbox/1.0" }
83
+ if post
84
+ http.post(uri.path, params.to_json, headers.merge("Content-Type" => "application/json; charset=utf-8"))
85
+ else
86
+ uri.query = URI.encode_www_form(params)
87
+ http.get(uri.request_uri, headers)
88
+ end
89
+ end
90
+
91
+ body = JSON.parse(response.body.to_s)
92
+ return { error: "Slack refused #{method}: #{body['error'] || "HTTP #{response.code}"}" } unless body["ok"]
93
+
94
+ body
95
+ rescue JSON::ParserError
96
+ { error: "Slack answered #{method} with something other than JSON (HTTP #{response&.code})" }
97
+ rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
98
+ { error: "Could not reach Slack: #{e.message}" }
99
+ end
100
+ end
101
+ end
data/config/routes.rb CHANGED
@@ -29,8 +29,13 @@ ActionAgent::Engine.routes.draw do
29
29
 
30
30
  # The collector for evaluation reports an application ran itself
31
31
  # (ActiveAgent::Evals::Publisher), at <mount>/api/evaluation_reports.
32
- # Authenticated like trace ingest, with a bearer token.
32
+ # Authenticated like trace ingest, with a bearer token. GET
33
+ # <mount>/api/evaluation_reports/:run_id exports a stored run in the
34
+ # envelope the collector takes. A run_id may hold dots, so the path has
35
+ # no format extension.
33
36
  resources :evaluation_reports, only: [ :create ]
37
+ get "evaluation_reports/:run_id", to: "evaluation_reports#show", as: :evaluation_report,
38
+ constraints: { run_id: %r{[^/]+} }, format: false
34
39
 
35
40
  post "feedback", to: "feedback_ingest#create"
36
41
  get "feedback", to: "feedbacks#index"
@@ -59,6 +64,8 @@ ActionAgent::Engine.routes.draw do
59
64
  end
60
65
  collection do
61
66
  get :presets
67
+ # The builder's roster for an agent not created yet.
68
+ get :tool_roster, action: :new_tool_roster
62
69
  end
63
70
  end
64
71
 
@@ -101,6 +108,8 @@ ActionAgent::Engine.routes.draw do
101
108
  resources :code_sessions, only: [ :index, :create, :show ] do
102
109
  member do
103
110
  post :cancel
111
+ # Another turn of the session's conversation.
112
+ post :messages
104
113
  end
105
114
  end
106
115
  resource :claude_login, only: [ :show, :create, :destroy ], controller: "claude_logins" do
@@ -80,6 +80,12 @@ module ActionAgent
80
80
  app.config.filter_parameters += [ :credential, :api_key, :access_token, :password, :claude_login_code, /\Arecording_events\z/, /(?:\A|\.)(?:answer|value)\z/i ]
81
81
  end
82
82
 
83
+ # Evaluation run reports are served as Markdown (`report.md`). Rails
84
+ # registers text/markdown as :md from 7.1; an older host gets it here.
85
+ initializer "action_agent.markdown_mime_type" do
86
+ Mime::Type.register "text/markdown", :md, [], %w[md markdown] unless Mime[:md]
87
+ end
88
+
83
89
  # The project setup assistant's request_secret answers go to its
84
90
  # handler. Registered on every reload, so the handler is always the
85
91
  # current class's.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.9.2"
4
+ VERSION = "1.9.3"
5
5
  end
data/lib/action_agent.rb CHANGED
@@ -111,6 +111,9 @@ module ActionAgent
111
111
  # What ActionAgent.claude_code_auth may be set to.
112
112
  CLAUDE_CODE_AUTH_MODES = %i[api_key local_login sandbox_login].freeze
113
113
 
114
+ # What ActionAgent.codex_auth may be set to.
115
+ CODEX_AUTH_MODES = %i[api_key local_login].freeze
116
+
114
117
  # What ActionAgent.provider_key_scope may be set to.
115
118
  PROVIDER_KEY_SCOPES = %i[organization personal_override].freeze
116
119
 
@@ -134,6 +137,9 @@ module ActionAgent
134
137
  # they are handed to, or delete a project that has
135
138
  # them
136
139
  # :take_over_browser drive a run's browser by hand
140
+ # :chat_code_session send a message to a Claude Code or Codex
141
+ # conversation someone may have started (the
142
+ # subject is its first session)
137
143
  # :manage_recordings delete a session recording
138
144
  # :replace_scenarios replace or merge an evaluation's scenarios
139
145
  PERMISSION_ACTIONS = %i[
@@ -144,6 +150,7 @@ module ActionAgent
144
150
  answer_input_request
145
151
  manage_project_secrets
146
152
  take_over_browser
153
+ chat_code_session
147
154
  manage_recordings
148
155
  replace_scenarios
149
156
  ].freeze
@@ -589,10 +596,35 @@ module ActionAgent
589
596
  # @return [Integer]
590
597
  attr_accessor :claude_code_timeout
591
598
 
592
- # Codex CLI sessions use the owner's OpenAI API key and workspace-write
593
- # sandboxing. They share checkout lifecycle and cancellation with Claude.
599
+ # Codex CLI sessions run with workspace-write sandboxing, and share the
600
+ # checkout lifecycle and cancellation with Claude Code.
594
601
  attr_accessor :codex_command, :codex_timeout
595
602
 
603
+ # How Codex sessions authenticate.
604
+ #
605
+ # :api_key (the default) runs them on the OpenAI API key the owner
606
+ # connected in Settings -> Integrations, handed to the session as
607
+ # CODEX_API_KEY with a Codex home of the sandbox's own. Usage is billed
608
+ # to that key's project.
609
+ #
610
+ # :local_login runs `codex` on whatever login this machine's user set up
611
+ # with `codex login` (a ChatGPT plan, or a key given to the CLI), which
612
+ # Codex keeps under ~/.codex. The dashboard never reads, copies or
613
+ # stores it; it only asks `codex login status` whether there is one.
614
+ # That login is the dashboard user's own, so this works with the :local
615
+ # sandbox backend only, and other backends refuse Codex sessions.
616
+ # @return [Symbol] :api_key or :local_login
617
+ attr_reader :codex_auth
618
+
619
+ def codex_auth=(value)
620
+ mode = value.to_s.to_sym
621
+ unless CODEX_AUTH_MODES.include?(mode)
622
+ raise ArgumentError, "ActionAgent.codex_auth must be :api_key or :local_login, not #{value.inspect}"
623
+ end
624
+
625
+ @codex_auth = mode
626
+ end
627
+
596
628
  # How Claude Code sessions authenticate.
597
629
  #
598
630
  # :api_key (the default) runs them on the Anthropic API key the owner
@@ -1248,6 +1280,7 @@ module ActionAgent
1248
1280
  @claude_code_timeout = 1800
1249
1281
  @codex_command = "codex"
1250
1282
  @codex_timeout = 1800
1283
+ @codex_auth = :api_key
1251
1284
  @claude_code_auth = :api_key
1252
1285
  @claude_code_hosted_login_enabled = false
1253
1286
  @claude_code_login_timeout = 300
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Makes Claude Code and Codex sessions conversations: each session is one
4
+ # turn, and a turn after the first names the conversation's first session as
5
+ # its root. A conversation resumes the same CLI session (resume_session_id),
6
+ # and a turn may carry a note from the dashboard ahead of the person's
7
+ # message (context_note), such as who drove the browser since the last turn.
8
+ class AddConversationsToCodeSessions < ActiveRecord::Migration<%= migration_version %>
9
+ def change
10
+ sessions = "#{ActionAgent.table_name_prefix}code_sessions"
11
+ add_column sessions, :conversation_root_id, :bigint
12
+ add_column sessions, :turn_index, :integer, default: 0, null: false
13
+ add_column sessions, :origin, :string
14
+ add_column sessions, :context_note, :text
15
+ add_column sessions, :resume_session_id, :string
16
+ add_index sessions, :conversation_root_id
17
+ end
18
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Evaluation run reports from the command line: a run out as the version-1
4
+ # envelope the evaluation report collector takes (or as its HTML report page,
5
+ # or as GitHub-flavored Markdown for a pull request), and an envelope in
6
+ # through the collector's own import. On a multi-tenant install name the
7
+ # tenant with ACCOUNT_ID=<id>.
8
+ #
9
+ # bin/rails action_agent:evaluations:export RUN=dashboard-run-12-20261021093000 FILE=tmp/run.json
10
+ # bin/rails action_agent:evaluations:export EVALUATION=3 RUN=12 FORMAT=html FILE=tmp/run.html
11
+ # bin/rails action_agent:evaluations:export EVALUATION=3 RUN=12 FORMAT=md > tmp/run.md
12
+ # bin/rails action_agent:evaluations:import FILE=tmp/run.json
13
+ namespace :action_agent do
14
+ namespace :evaluations do
15
+ desc "Write a run's report (RUN=run_id, or EVALUATION=id RUN=id; FORMAT=json|html|md; FILE=path, standard output without FILE)"
16
+ task export: :environment do
17
+ run = action_agent_evaluation_run!
18
+ export = ActionAgent::EvaluationReportExport.new(run)
19
+ format = ENV.fetch("FORMAT", "json").downcase
20
+ body =
21
+ case format
22
+ when "json" then export.to_json
23
+ when "html" then run.to_report.to_html
24
+ when "md", "markdown" then export.markdown
25
+ else abort "FORMAT is json, html or md"
26
+ end
27
+
28
+ if ENV["FILE"].present?
29
+ File.write(ENV["FILE"], body)
30
+ puts "Wrote run #{export.run_id} of #{run.evaluation.name} (#{format}) to #{ENV['FILE']}"
31
+ else
32
+ puts body
33
+ end
34
+ rescue ActionAgent::EvaluationReportExport::NotExportable => e
35
+ abort "Export refused: #{e.message}"
36
+ end
37
+
38
+ desc "Store a version-1 evaluation report envelope (FILE=path [ACCOUNT_ID=id]); importing it again changes nothing"
39
+ task import: :environment do
40
+ path = ENV.fetch("FILE") { abort "Name the file: FILE=tmp/run.json" }
41
+ abort "No file at #{path}" unless File.file?(path)
42
+ abort "Unavailable: #{ActionAgent::EvaluationReportImport.unavailable_reason}" if ActionAgent::EvaluationReportImport.unavailable_reason
43
+
44
+ account = action_agent_evaluation_account
45
+ run, duplicate = ActionAgent::EvaluationReportImport.call(account: account, payload: JSON.parse(File.read(path)))
46
+ ActionAgent.record_usage(account, :evaluation_report) unless duplicate
47
+ puts "#{duplicate ? 'Already stored (identical retry)' : 'Stored'}: run #{ActionAgent::EvaluationReportExport.run_id_for(run)} " \
48
+ "as run ##{run.id} of evaluation ##{run.evaluation_id} (#{run.evaluation.name}), #{run.status}"
49
+ rescue JSON::ParserError
50
+ abort "Import refused: #{path} is not JSON"
51
+ rescue ActionAgent::EvaluationReportImport::Error, ActiveRecord::RecordInvalid => e
52
+ abort "Import refused: #{e.message}"
53
+ end
54
+ end
55
+ end
56
+
57
+ # The tenant, from ACCOUNT_ID (ACCOUNT also works); nil on a single-tenant
58
+ # install.
59
+ def action_agent_evaluation_account
60
+ id = ENV["ACCOUNT_ID"].presence || ENV["ACCOUNT"].presence
61
+ return nil unless id
62
+
63
+ abort "ACCOUNT_ID names a tenant, which only a multi-tenant install has" unless ActionAgent.multi_tenant?
64
+ ActionAgent.account_class.to_s.constantize.find_by(id: id) || abort("No account #{id}")
65
+ end
66
+
67
+ # The run RUN names: with EVALUATION, a run id of that evaluation; without,
68
+ # a run_id as the export endpoint takes it, within the tenant.
69
+ def action_agent_evaluation_run!
70
+ run_id = ENV.fetch("RUN") { abort "Name the run: RUN=<run_id>, or EVALUATION=<id> RUN=<id>" }
71
+ if ENV["EVALUATION"].present?
72
+ evaluation = ActionAgent::Evaluation.find_by(id: ENV["EVALUATION"]) || abort("No evaluation #{ENV['EVALUATION']}")
73
+ evaluation.evaluation_runs.find_by(id: run_id) || abort("No run #{run_id} in evaluation #{evaluation.id}")
74
+ else
75
+ ActionAgent::EvaluationReportExport.find(run_id, account: action_agent_evaluation_account) ||
76
+ abort("No run with run_id #{run_id.inspect}#{' for that account' if ENV['ACCOUNT_ID'].present? || ENV['ACCOUNT'].present?}")
77
+ end
78
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.2
4
+ version: 1.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeagent
@@ -246,7 +246,9 @@ files:
246
246
  - app/models/concerns/action_agent/session_recordable.rb
247
247
  - app/queries/action_agent/agent_executions.rb
248
248
  - app/queries/action_agent/metrics_report.rb
249
+ - app/queries/action_agent/trace_window_report.rb
249
250
  - app/serializers/action_agent/agent_message_serializer.rb
251
+ - app/serializers/action_agent/code_conversation_serializer.rb
250
252
  - app/serializers/action_agent/evaluation_serializer.rb
251
253
  - app/serializers/action_agent/input_request_serializer.rb
252
254
  - app/serializers/action_agent/interaction_preview.rb
@@ -261,17 +263,23 @@ files:
261
263
  - app/services/action_agent/agent_sync.rb
262
264
  - app/services/action_agent/agent_tool_roster.rb
263
265
  - app/services/action_agent/agent_toolbox.rb
266
+ - app/services/action_agent/browser_client_token.rb
264
267
  - app/services/action_agent/browser_live_ticket.rb
265
268
  - app/services/action_agent/browser_sidecar.rb
266
269
  - app/services/action_agent/browser_sign_in.rb
267
270
  - app/services/action_agent/browser_tool_redaction.rb
271
+ - app/services/action_agent/checkout_tools.rb
268
272
  - app/services/action_agent/claude_code_auth.rb
273
+ - app/services/action_agent/code_conversation.rb
274
+ - app/services/action_agent/codex_auth.rb
269
275
  - app/services/action_agent/codex_session_events.rb
270
276
  - app/services/action_agent/dashboard_assistant_service.rb
277
+ - app/services/action_agent/diff_stat.rb
271
278
  - app/services/action_agent/draft_pull_request_publisher.rb
272
279
  - app/services/action_agent/evaluation_evidence.rb
273
280
  - app/services/action_agent/evaluation_fix.rb
274
281
  - app/services/action_agent/evaluation_fix_comparison.rb
282
+ - app/services/action_agent/evaluation_report_export.rb
275
283
  - app/services/action_agent/evaluation_report_import.rb
276
284
  - app/services/action_agent/evaluation_run_cost.rb
277
285
  - app/services/action_agent/evaluation_runner_service.rb
@@ -316,6 +324,7 @@ files:
316
324
  - app/services/action_agent/session_index.rb
317
325
  - app/services/action_agent/session_recording_service.rb
318
326
  - app/services/action_agent/session_timeline.rb
327
+ - app/services/action_agent/slack_tools.rb
319
328
  - app/services/action_agent/tool_discovery.rb
320
329
  - app/views/action_agent/dashboard/index.html.erb
321
330
  - app/views/action_agent/github_app_manifests/created.html.erb
@@ -360,9 +369,11 @@ files:
360
369
  - lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb
361
370
  - lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb
362
371
  - lib/generators/action_agent/templates/migrations/027_create_active_agent_interaction_feedbacks.rb.erb
372
+ - lib/generators/action_agent/templates/migrations/028_add_conversations_to_code_sessions.rb.erb
363
373
  - lib/tasks/action_agent.rake
364
374
  - lib/tasks/browser.rake
365
375
  - lib/tasks/claude_code.rake
376
+ - lib/tasks/evaluations.rake
366
377
  - lib/tasks/sandbox.rake
367
378
  - lib/tasks/scenario_catalogs.rake
368
379
  homepage: https://activeagents.ai