ask-agent 0.40.17 → 0.40.19

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: 12c8a3be5e9171a9dea21e3a8107c7953d7772ddb2c6fd82d23805f54557f5dc
4
- data.tar.gz: cb751bf89f94b3206f1900d9d33f738a8f248dc9c61552867cf1349a3ffd8a6d
3
+ metadata.gz: 46ea77e0ac0feca360164604367729e05f15e22212f479c59ad1954da5e5e924
4
+ data.tar.gz: 5a1e0198d2e4d9faf7831bd1c7f8520a82a01c7fff46a9a94512a60a6d285409
5
5
  SHA512:
6
- metadata.gz: b046b9f9bef45c3ed959acf97a2b3c3afa6921f240ac4a3ac6d59204424122ef8fd3e6e8f6614ebe695918c043b3de8fb655a92783e52eed72f255791e20c1eb
7
- data.tar.gz: 3bf7b88e021f181cae2271ac9311976de44d73b5433aa4241347ad3c9c61d96056bc31e13ac73589a9e871e79d9210d5a3d1cd8577767c777a672558ad4b2f73
6
+ metadata.gz: d195e1e86df0f281de603cdd607d0b2d88bbbc3d07f4c46b5dbb81235fd102d7f4a19781e625b8ddd8c304babb47eaf60fa894c6513b5e944f493b0e4928c74f
7
+ data.tar.gz: c4289b1c74e4ee05de9ffe2c0c266e46599e49c546b0c22c23ea76258a23e291dd5804e3d5cdd0d514375540fc22d7732a920de1e6d303f7dfde0fcbc04ca883
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.40.17] — 2026-09-18
2
+
3
+ ### Added
4
+
5
+ - **`decision_provider` — a session can take a decision provider, and its
6
+ tool calls get judged.** Set it globally
7
+ (`Ask::Agent.configure { |c| c.decision_provider = :typesafe }`) or per
8
+ session. `Ask::Decisions::AgentAdapter` then wires a gate in front of every
9
+ tool call and an output judge behind it; ask-agent itself never requires
10
+ ask-decisions, and with the setting unset nothing changes at all.
11
+
12
+ ### Changed
13
+
14
+ - Requires `ask-core >= 0.12.0` for the decision vocabulary.
15
+
16
+
1
17
  ## [0.40.10] — 2026-09-05
2
18
 
3
19
  ### Fixed
data/lib/ask/agent/cli.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
3
5
  module Ask
4
6
  module Agent
5
7
  module CLI
6
8
  module_function
7
9
 
8
10
  def run(argv)
11
+ auto_sync_skills
9
12
  case argv.first
10
13
  when "run"
11
14
  cmd_run(argv[1..])
@@ -162,13 +165,21 @@ module Ask
162
165
  cmd_skills_show(args[1])
163
166
  when "search"
164
167
  cmd_skills_search(args[1])
168
+ when "install"
169
+ cmd_skills_install(args[1..])
170
+ when "uninstall"
171
+ cmd_skills_uninstall(args[1..])
165
172
  else
166
- puts "Usage: askr skills <list|show|search>"
173
+ puts "Usage: askr skills <list|show|search|install|uninstall>"
167
174
  puts ""
168
175
  puts "Commands:"
169
176
  puts " list List all discovered skills"
170
177
  puts " show <name> Show skill details and sibling files"
171
178
  puts " search <query> Search skills by name, description, or tags"
179
+ puts " install [--global] Install ask-agent skill to ~/.agents/skills/ (default)"
180
+ puts " install --local Install to .agents/skills/ in current directory"
181
+ puts " uninstall [--global] Remove installed skill"
182
+ puts " uninstall --local Remove locally installed skill"
172
183
  end
173
184
  end
174
185
 
@@ -259,6 +270,112 @@ module Ask
259
270
  end
260
271
  end
261
272
 
273
+ SKILL_NAME = "agent.build_agents"
274
+ SKILL_SOURCE_REL = "ask/skills/agent.build_agents/SKILL.md"
275
+
276
+ # Keep installed copies in sync after `gem update ask-agent`.
277
+ # Only touches copies created by `askr skills install` (identified
278
+ # by the marker file). Failures are swallowed.
279
+ def auto_sync_skills
280
+ skill_candidates.each do |dest|
281
+ next unless File.file?(dest)
282
+ next unless skill_managed?(dest)
283
+ skill_sync_copy(dest)
284
+ end
285
+ rescue StandardError
286
+ nil
287
+ end
288
+
289
+ def cmd_skills_install(args)
290
+ opts = parse_skill_flags(args)
291
+ dest = skill_target_dir(opts)
292
+ source = skill_bundled_path
293
+ unless File.file?(source)
294
+ puts "Bundled skill not found at #{source} — reinstall ask-agent"
295
+ exit 1
296
+ end
297
+
298
+ FileUtils.mkdir_p(File.dirname(dest))
299
+ FileUtils.cp(source, dest)
300
+ skill_stamp(dest)
301
+ puts "Installed #{SKILL_NAME} skill -> #{dest}"
302
+ 0
303
+ end
304
+
305
+ def cmd_skills_uninstall(args)
306
+ opts = parse_skill_flags(args)
307
+ dest = skill_target_dir(opts)
308
+ if File.file?(dest)
309
+ FileUtils.rm(dest)
310
+ FileUtils.rm_f(skill_managed_marker(dest))
311
+ puts "Removed #{dest}"
312
+ else
313
+ puts "Not installed at #{dest} (nothing to do)"
314
+ end
315
+ 0
316
+ end
317
+
318
+ def skill_target_dir(opts)
319
+ if opts[:dir]
320
+ File.expand_path(File.join(opts[:dir], SKILL_NAME, "SKILL.md"))
321
+ elsif opts[:local]
322
+ File.join(Dir.pwd, ".agents", "skills", SKILL_NAME, "SKILL.md")
323
+ else
324
+ File.expand_path("~/.agents/skills/#{SKILL_NAME}/SKILL.md")
325
+ end
326
+ end
327
+
328
+ def skill_bundled_path
329
+ File.expand_path("../../#{SKILL_SOURCE_REL}", __dir__)
330
+ end
331
+
332
+ def skill_managed_marker(dest)
333
+ File.join(File.dirname(dest), ".ask-agent-managed")
334
+ end
335
+
336
+ def skill_managed?(dest)
337
+ File.file?(skill_managed_marker(dest))
338
+ end
339
+
340
+ def skill_stamp(dest)
341
+ File.write(skill_managed_marker(dest), "managed by askr skills install; safe to auto-update\n")
342
+ rescue StandardError
343
+ nil
344
+ end
345
+
346
+ def skill_candidates
347
+ [
348
+ File.expand_path("~/.agents/skills/#{SKILL_NAME}/SKILL.md"),
349
+ File.join(Dir.pwd, ".agents", "skills", SKILL_NAME, "SKILL.md")
350
+ ]
351
+ end
352
+
353
+ def skill_sync_copy(dest)
354
+ source = skill_bundled_path
355
+ FileUtils.cp(source, dest) if File.exist?(source) && File.read(source) != File.read(dest)
356
+ skill_stamp(dest)
357
+ rescue StandardError
358
+ nil
359
+ end
360
+
361
+ def parse_skill_flags(args)
362
+ opts = {}
363
+ args.each do |arg|
364
+ case arg
365
+ when "--local" then opts[:local] = true
366
+ when "--global" then nil # default
367
+ when "--dir"
368
+ # next arg is the path; handled by shifting in the caller
369
+ when /\A--dir=(.+)/ then opts[:dir] = $1
370
+ when "--help", "-h"
371
+ puts "Usage: askr skills install [--global] [--local] [--dir <path>]"
372
+ puts " askr skills uninstall [--global] [--local] [--dir <path>]"
373
+ exit 0
374
+ end
375
+ end
376
+ opts
377
+ end
378
+
262
379
  def cmd_help
263
380
  puts <<~HELP
264
381
  Usage: askr <command> [options]
@@ -23,6 +23,58 @@ module Ask
23
23
  # @return [Ask::Decisions::AgentAdapter, nil] decision adapter (when decision_provider is set)
24
24
  attr_reader :decision_adapter
25
25
 
26
+ # Build a Session from a discovered Definition class and directory.
27
+ #
28
+ # This is the shared implementation used by both Ask::Agent.new(name)
29
+ # and Session.new(name:). It resolves the definition's config, tools,
30
+ # and instructions, then applies any caller-supplied overrides.
31
+ #
32
+ # @param klass [Class] the Definition subclass
33
+ # @param dir [String] the agent directory path
34
+ # @param opts [Hash] runtime overrides (model, provider, tools, system_prompt, etc.)
35
+ # @return [Session]
36
+ def self.build_from_definition(klass, dir, opts = {})
37
+ config = klass._config
38
+ session_opts = { model: config[:model] || Ask::Agent.configuration.default_model }
39
+
40
+ # Pass optional config
41
+ session_opts[:provider] = config[:provider] if config[:provider]
42
+ session_opts[:max_turns] = config[:max_turns] if config[:max_turns]
43
+ session_opts[:parallel_tools] = config[:parallel_tools] if config.key?(:parallel_tools)
44
+ session_opts[:skills_disclosure] = config[:skills_disclosure] if config.key?(:skills_disclosure)
45
+
46
+ # Pass arbitrary session options
47
+ if config[:options]
48
+ session_opts.merge!(config[:options])
49
+ end
50
+
51
+ # Pass agent directory for per-agent skills discovery
52
+ session_opts[:agent_dir] = dir
53
+
54
+ # Resolve tools
55
+ tools = resolve_definition_tools(config[:tools], dir)
56
+ session_opts[:tools] = tools if tools.any?
57
+
58
+ # Load instructions
59
+ prompt = klass.instructions_content
60
+ session_opts[:system_prompt] = prompt if prompt
61
+
62
+ # Apply schedule if defined
63
+ schedule = config[:schedule]
64
+ if schedule
65
+ task_block = ->(_sess = nil) {
66
+ agent = build_from_definition(klass, dir)
67
+ agent.run("")
68
+ }
69
+ Ask::Agent.configuration.scheduler.every(schedule, name: File.basename(dir), &task_block)
70
+ end
71
+
72
+ # Caller-supplied runtime options win over the definition's config.
73
+ session_opts.merge!(opts) unless opts.empty?
74
+
75
+ new(**session_opts)
76
+ end
77
+
26
78
  def initialize(model:, tools: [], max_turns: 25, max_tool_retries: 3,
27
79
  compactor: nil, hooks: {}, state: nil, persistence: nil,
28
80
  id: nil, system_prompt: nil, parallel_tools: true,
@@ -1148,6 +1200,34 @@ end
1148
1200
 
1149
1201
  # Recursively convert string keys to symbol keys in hashes.
1150
1202
  # Needed when loading session data that was serialized through JSON.
1203
+ # Resolve tool specs (symbols, strings, or classes) from a Definition
1204
+ # into instantiated tool objects. Symbols are looked up in the agent's
1205
+ # per-agent tools/ directory first, then shared tools, then the global
1206
+ # Ask::Tools registry.
1207
+ def self.resolve_definition_tools(tool_specs, dir)
1208
+ tools = []
1209
+ tool_specs.each do |spec|
1210
+ case spec
1211
+ when Symbol, String
1212
+ name = spec.to_s
1213
+ # Try per-agent tools directory
1214
+ agent_tool_path = File.join(dir, "tools", "#{name}.rb")
1215
+ if File.exist?(agent_tool_path)
1216
+ require agent_tool_path
1217
+ end
1218
+
1219
+ resolved = Ask::Agent.resolve_tool_symbol(name)
1220
+ if resolved
1221
+ tool_class = resolved.is_a?(Class) ? resolved : Ask::Tools[name]
1222
+ tools << tool_class if tool_class
1223
+ end
1224
+ when Class
1225
+ tools << spec
1226
+ end
1227
+ end
1228
+ tools
1229
+ end
1230
+
1151
1231
  def self.deep_symbolize_keys(obj)
1152
1232
  case obj
1153
1233
  when Hash
@@ -161,7 +161,7 @@ module Ask
161
161
  @provider = config[:provider]
162
162
 
163
163
  # Resolve tools from definition
164
- resolved_tools = Ask::Agent.__send__(:resolve_definition_tools, config[:tools] || [], dir)
164
+ resolved_tools = Ask::Agent::Session.resolve_definition_tools(config[:tools] || [], dir)
165
165
  @tools = resolved_tools.map { |t| t.is_a?(Class) ? t.new : t }
166
166
 
167
167
  # Load instructions from definition
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.40.17"
5
+ VERSION = "0.40.19"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -94,7 +94,7 @@ module Ask
94
94
  raise UnknownAgent, "Unknown agent: #{name.inspect}. Searched agents/ and app/agents/." unless entry
95
95
 
96
96
  klass, dir = entry
97
- build_session_from_definition(klass, dir, opts)
97
+ Session.build_from_definition(klass, dir, opts)
98
98
  end
99
99
 
100
100
  # Force re-discovery of agent definitions.
@@ -188,73 +188,6 @@ module Ask
188
188
  @shared_tools[tool_name] = tool_name
189
189
  end
190
190
  end
191
-
192
- def build_session_from_definition(klass, dir, opts = {})
193
- config = klass._config
194
- session_opts = { model: config[:model] || Ask::Agent.configuration.default_model }
195
-
196
- # Pass optional config
197
- session_opts[:provider] = config[:provider] if config[:provider]
198
- session_opts[:max_turns] = config[:max_turns] if config[:max_turns]
199
- session_opts[:parallel_tools] = config[:parallel_tools] if config.key?(:parallel_tools)
200
- session_opts[:skills_disclosure] = config[:skills_disclosure] if config.key?(:skills_disclosure)
201
-
202
- # Pass arbitrary session options
203
- if config[:options]
204
- session_opts.merge!(config[:options])
205
- end
206
-
207
- # Pass agent directory for per-agent skills discovery
208
- session_opts[:agent_dir] = dir
209
-
210
- # Resolve tools
211
- tools = resolve_definition_tools(config[:tools], dir)
212
- session_opts[:tools] = tools if tools.any?
213
-
214
- # Load instructions
215
- prompt = klass.instructions_content
216
- session_opts[:system_prompt] = prompt if prompt
217
-
218
- # Apply schedule if defined
219
- schedule = config[:schedule]
220
- if schedule
221
- task_block = ->(sess = nil) {
222
- agent = build_session_from_definition(klass, dir)
223
- agent.run("")
224
- }
225
- Ask::Agent.configuration.scheduler.every(schedule, name: File.basename(dir), &task_block)
226
- end
227
-
228
- # Caller-supplied runtime options (model/provider/api_key/api_base/...)
229
- # win over the definition's config.
230
- session_opts.merge!(opts) unless opts.empty?
231
-
232
- Session.new(**session_opts)
233
- end
234
-
235
- def resolve_definition_tools(tool_specs, dir)
236
- tools = []
237
- tool_specs.each do |spec|
238
- case spec
239
- when Symbol, String
240
- name = spec.to_s
241
- # Try per-agent tools directory
242
- agent_tool_path = File.join(dir, "tools", "#{name}.rb")
243
- if File.exist?(agent_tool_path)
244
- require agent_tool_path
245
- end
246
-
247
- resolved = resolve_tool_symbol(name)
248
- if resolved
249
- tool_class = resolved.is_a?(Class) ? resolved : Ask::Tools[name]
250
- tools << tool_class if tool_class
251
- end
252
- when Class
253
- tools << spec
254
- end
255
- end
256
- tools
257
- end
258
191
  end
259
192
 
260
193
  # Register the global config
@@ -336,12 +269,24 @@ end
336
269
  # Ask.chat("Tell me about X", model: "gpt-4o")
337
270
  # Ask.chat("Stream this") { |chunk| puts chunk.content }
338
271
  #
272
+ # With a named definition:
273
+ #
274
+ # Ask.chat("Check health", name: "health_check")
275
+ # Ask.chat("Check health", name: "health_check", model: "claude-sonnet-4")
276
+ #
339
277
  module Ask
340
- def self.chat(message, model: nil, system_prompt: nil, &block)
341
- session = Agent::Session.new(
342
- model: model || Agent.configuration.default_model,
343
- system_prompt: system_prompt
344
- )
278
+ def self.chat(message, name: nil, model: nil, system_prompt: nil, &block)
279
+ session = if name
280
+ overrides = {}
281
+ overrides[:model] = model if model
282
+ overrides[:system_prompt] = system_prompt if system_prompt
283
+ Agent.new(name, **overrides)
284
+ else
285
+ Agent::Session.new(
286
+ model: model || Agent.configuration.default_model,
287
+ system_prompt: system_prompt
288
+ )
289
+ end
345
290
  if block
346
291
  session.run(message, &block)
347
292
  else
@@ -0,0 +1,261 @@
1
+ ---
2
+ name: agent.build_agents
3
+ description: Build AI agents with ask-rb — define agents, register tools, configure sessions, and wire up the agent loop. Use when creating new agents, adding tools to existing agents, debugging agent behavior, or setting up agent definitions.
4
+ tags: agents, tools, sessions, ask-rb, development
5
+ ---
6
+
7
+ # Building Agents with ask-rb
8
+
9
+ Step-by-step methodology for creating AI agents using the ask-rb ecosystem.
10
+
11
+ ## Agent Definition (Convention-Based)
12
+
13
+ Every agent lives in a directory under `agents/` or `app/agents/`. The directory name is the agent name.
14
+
15
+ ### Directory Structure
16
+
17
+ ```
18
+ agents/
19
+ ├── health_check/
20
+ │ ├── agent.rb → Definition subclass (required)
21
+ │ ├── instructions.md → System prompt (auto-loaded)
22
+ │ ├── tools/ → Per-agent tools (optional)
23
+ │ │ └── disk_check.rb
24
+ │ └── skills/ → Per-agent skills (optional)
25
+ │ └── nginx_debug/SKILL.md
26
+ ├── shared/
27
+ │ ├── tools/ → Shared across all agents
28
+ │ │ └── notify.rb
29
+ │ └── skills/ → Shared skills
30
+ └── daily_report/
31
+ ├── agent.rb
32
+ └── instructions.md
33
+ ```
34
+
35
+ ### Definition DSL
36
+
37
+ ```ruby
38
+ # agents/health_check/agent.rb
39
+ module HealthCheck
40
+ class Agent < Ask::Agent::Definition
41
+ model "gpt-4o" # required: which LLM to use
42
+ provider :anthropic # optional: override provider
43
+ tools :bash, :read, :grep # tool symbols or classes
44
+ max_turns 30 # optional: conversation limit
45
+ parallel_tools true # optional: parallel execution (default: true)
46
+ skills_disclosure true # optional: load_skill tool (default: true)
47
+ schedule "every 5 minutes" # optional: cron/interval
48
+ option :temperature, 0.7 # optional: arbitrary Session option
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### Tool Symbol Resolution
54
+
55
+ Symbols (`:bash`, `:read`) are resolved in order:
56
+ 1. Per-agent tools: `agents/<name>/tools/<name>.rb`
57
+ 2. Shared tools: `agents/shared/tools/<name>.rb`
58
+ 3. Global registry: `Ask::Tools[name]` (built-in tools from ask-tools)
59
+
60
+ ## Session Creation (Unified API)
61
+
62
+ `Session.build_from_definition` is the single source of truth. Three entry points converge on it:
63
+
64
+ ### From a Definition (Recommended)
65
+
66
+ ```ruby
67
+ # Via Agent.new — convenience shorthand
68
+ agent = Ask::Agent.new("health_check")
69
+ agent.run("Check server health")
70
+
71
+ # Explicit — when you have the class and directory
72
+ session = Ask::Agent::Session.build_from_definition(
73
+ HealthCheck::Agent, "agents/health_check"
74
+ )
75
+ ```
76
+
77
+ ### Direct (No Definition)
78
+
79
+ ```ruby
80
+ session = Ask::Agent::Session.new(
81
+ model: "gpt-4o",
82
+ tools: [Ask::Tools::Bash, Ask::Tools::Read],
83
+ system_prompt: "You are a helpful assistant."
84
+ )
85
+ session.run("Hello")
86
+ ```
87
+
88
+ ### One-Shot Chat
89
+
90
+ ```ruby
91
+ Ask.chat("Check health")
92
+ Ask.chat("Check health", model: "claude-sonnet-4")
93
+ Ask.chat("Check health", name: "health_check") # from definition
94
+ ```
95
+
96
+ ### Overriding Definition Config
97
+
98
+ Any explicit option overrides the definition's value:
99
+
100
+ ```ruby
101
+ agent = Ask::Agent.new("health_check", model: "claude-sonnet-4")
102
+ agent = Ask::Agent.new("health_check", system_prompt: "Custom prompt")
103
+ ```
104
+
105
+ ## Writing Tools
106
+
107
+ ### Tool Class
108
+
109
+ ```ruby
110
+ # agents/health_check/tools/disk_check.rb
111
+ class DiskCheck < Ask::Tool
112
+ description "Check disk usage on a path"
113
+ param :path, type: :string, desc: "Path to check", required: true
114
+ param :threshold, type: :integer, desc: "Warning threshold in %", default: 80
115
+
116
+ def execute(path:, threshold: 80)
117
+ usage = `df -h #{path} | tail -1 | awk '{print $5}'`.strip.to_i
118
+ if usage > threshold
119
+ Ask::Result.error(data: "Disk usage #{usage}% exceeds threshold #{threshold}%")
120
+ else
121
+ Ask::Result.ok(data: "Disk usage: #{usage}%")
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### Tool Registration
128
+
129
+ ```ruby
130
+ # In agent.rb — symbols resolve via convention
131
+ tools :bash, :read, :disk_check
132
+
133
+ # Or pass classes directly (no file convention needed)
134
+ tools Ask::Tools::Bash, DiskCheck
135
+ ```
136
+
137
+ ### Async Tools (Background Execution)
138
+
139
+ ```ruby
140
+ class LongTask < Ask::Tool
141
+ description "Run a long-running task in the background"
142
+
143
+ def execute(task:)
144
+ # Return pending — the loop continues while this runs
145
+ Ask::Result.pending(
146
+ tool_call_id: current_tool_call_id,
147
+ message: "Task started in background"
148
+ )
149
+ end
150
+ end
151
+ ```
152
+
153
+ Complete with `session.complete_pending_tool(tool_call_id:, result:)` from a background thread.
154
+
155
+ ## Session Options
156
+
157
+ | Option | Default | Purpose |
158
+ |--------|---------|---------|
159
+ | `model:` | (required) | LLM model identifier |
160
+ | `tools:` | `[]` | Tool classes or instances |
161
+ | `system_prompt:` | `nil` | System instructions |
162
+ | `max_turns:` | `25` | Conversation turn limit |
163
+ | `max_tool_retries:` | `3` | Retries per failed tool call |
164
+ | `parallel_tools:` | `true` | Execute tools concurrently |
165
+ | `skills_disclosure:` | `true` | Include load_skill tool |
166
+ | `state:` | `nil` | Persistence adapter |
167
+ | `checkpoints:` | `false` | Enable fork/rollback |
168
+ | `todos:` | `false` | Enable task list tool |
169
+ | `plan_mode:` | `false` | Read-only research phase |
170
+ | `memory:` | `nil` | Durable memory adapter |
171
+ | `memory_learning:` | `false` | Auto-extract facts |
172
+ | `evaluator:` | `nil` | Independent evaluation |
173
+ | `reflector:` | `nil` | Self-reflection |
174
+ | `approval:` | `nil` | Human-in-the-loop |
175
+ | `compactor:` | `nil` | Context window management |
176
+ | `hooks:` | `{}` | Before/after tool callbacks |
177
+ | `telemetry:` | `true` | Error tracking |
178
+
179
+ ## Event System
180
+
181
+ ```ruby
182
+ session.on_event do |event|
183
+ case event
184
+ when Ask::Agent::Events::TextDelta
185
+ print event.content
186
+ when Ask::Agent::Events::ToolExecutionStart
187
+ puts "Running #{event.name}..."
188
+ when Ask::Agent::Events::SessionEnd
189
+ puts "Done: #{event.tool_calls_made} tools, $#{event.cost}"
190
+ end
191
+ end
192
+ ```
193
+
194
+ ## Common Patterns
195
+
196
+ ### Multi-Agent Coordination
197
+
198
+ ```ruby
199
+ search = Ask::Agent::SubAgent.new("web_search")
200
+ review = Ask::Agent::SubAgent.new("code_review")
201
+
202
+ coordinator = Ask::Agent::Session.new(
203
+ model: "gpt-4o",
204
+ tools: [search, review, Ask::Tools::Bash]
205
+ )
206
+ coordinator.run("Find the latest Rails release and check our Gemfile")
207
+ ```
208
+
209
+ ### Persistent Sessions
210
+
211
+ ```ruby
212
+ store = Ask::State::Providers::SQLite.new
213
+ session = Ask::Agent::Session.new(
214
+ model: "gpt-4o",
215
+ tools: tools,
216
+ state: store,
217
+ checkpoints: true
218
+ )
219
+
220
+ session.run("Investigate the error")
221
+ restored = Ask::Agent::Session.load(session.id, adapter: store)
222
+ restored.run("What else should I check?")
223
+ ```
224
+
225
+ ### Scheduled Agents
226
+
227
+ ```ruby
228
+ # In agent.rb:
229
+ class HealthCheck < Ask::Agent::Definition
230
+ model "gpt-4o"
231
+ tools :bash, :read
232
+ schedule "every 5 minutes"
233
+ end
234
+
235
+ # Or globally:
236
+ Ask::Agent.configure do |c|
237
+ c.scheduler.every "5 minutes", name: "health-check" do
238
+ Ask::Agent.new("health_check").run("Check server health")
239
+ end
240
+ end
241
+ Ask::Agent::Scheduler.start
242
+ ```
243
+
244
+ ## CLI
245
+
246
+ ```bash
247
+ askr list # List all discovered agents
248
+ askr run health_check # Run an agent
249
+ askr run health_check "..." # Run with a prompt
250
+ askr new deploy_bot # Scaffold a new agent
251
+ askr skills list # List all discovered skills
252
+ askr skills install # Install ask-agent skill to ~/.agents/skills/
253
+ askr skills uninstall # Remove installed skill
254
+ ```
255
+
256
+ ## Troubleshooting
257
+
258
+ - **Agent not found**: Check `agents/<name>/agent.rb` exists and the class subclasses `Ask::Agent::Definition`
259
+ - **Tool not resolving**: Verify the tool file is in `agents/<name>/tools/` or registered in `Ask::Tools`
260
+ - **No system prompt**: Ensure `instructions.md` exists next to `agent.rb`
261
+ - **Skills not loading**: Check `skills_disclosure: true` (default) and the skill file follows `SKILL.md` convention
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.17
4
+ version: 0.40.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -219,6 +219,7 @@ files:
219
219
  - lib/ask/agent/tool_executor.rb
220
220
  - lib/ask/agent/tool_output_store.rb
221
221
  - lib/ask/agent/version.rb
222
+ - lib/ask/skills/agent.build_agents/SKILL.md
222
223
  homepage: https://github.com/ask-rb/ask-agent
223
224
  licenses:
224
225
  - MIT