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 +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/ask/agent/cli.rb +118 -1
- data/lib/ask/agent/session.rb +80 -0
- data/lib/ask/agent/sub_agent.rb +1 -1
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +18 -73
- data/lib/ask/skills/agent.build_agents/SKILL.md +261 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46ea77e0ac0feca360164604367729e05f15e22212f479c59ad1954da5e5e924
|
|
4
|
+
data.tar.gz: 5a1e0198d2e4d9faf7831bd1c7f8520a82a01c7fff46a9a94512a60a6d285409
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -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
|
data/lib/ask/agent/sub_agent.rb
CHANGED
|
@@ -161,7 +161,7 @@ module Ask
|
|
|
161
161
|
@provider = config[:provider]
|
|
162
162
|
|
|
163
163
|
# Resolve tools from definition
|
|
164
|
-
resolved_tools = Ask::Agent.
|
|
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
|
data/lib/ask/agent/version.rb
CHANGED
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
|
-
|
|
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 =
|
|
342
|
-
|
|
343
|
-
|
|
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.
|
|
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
|