ask-agent 0.1.1 → 0.1.2
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/lib/ask/agent/session.rb +34 -0
- data/lib/ask/agent/session_backup.rb +237 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc1ea1b84a7139944acf9787cfdd3e5a9fad739b759fc4501f59f68bdd3c0b77
|
|
4
|
+
data.tar.gz: 37733f0982e0d991e284d46c9202fe5846fff67a433acae0385a703912c7f898
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e4ff593f7856c2fe0da46dfe8091ad38217e29b27aa343e95f6f939ab741b8c249217dbaa397ad609c21b9b4f39e09a781738a556368e69307fa01329fb00d9
|
|
7
|
+
data.tar.gz: fc45d3ae62bc2e325a22dedab88625f312d3e9b07188d9614c5cb2ddfa9683ed31e2bb8473f10344d73ac18f7cef202fcf53cd68272affbab54c208770017dde
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -14,6 +14,8 @@ module Ask
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
attr_reader :meta_agent_results
|
|
17
|
+
# @return [Ask::Skills::Registry, nil] auto-discovered skills registry
|
|
18
|
+
attr_reader :skills_registry
|
|
17
19
|
|
|
18
20
|
def initialize(model:, tools: [], max_turns: 25, max_tool_retries: 3,
|
|
19
21
|
compactor: nil, hooks: {}, persistence: nil,
|
|
@@ -39,6 +41,16 @@ module Ask
|
|
|
39
41
|
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
40
42
|
@compactor = compactor ? build_compactor(compactor) : nil
|
|
41
43
|
@hooks = Hooks.new(hooks)
|
|
44
|
+
|
|
45
|
+
# Auto-discover skills and inject into system prompt
|
|
46
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
47
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
48
|
+
skill_text = @skills_registry.format_for_prompt
|
|
49
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
50
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
51
|
+
@chat.with_instructions(current + skill_text)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
42
54
|
@persistence = persistence
|
|
43
55
|
|
|
44
56
|
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
@@ -202,6 +214,28 @@ module Ask
|
|
|
202
214
|
|
|
203
215
|
def abort_requested? = @abort_requested
|
|
204
216
|
|
|
217
|
+
# Load a skill by name or file path.
|
|
218
|
+
# Injects the skill's full instructions into the conversation as a system message.
|
|
219
|
+
#
|
|
220
|
+
# @param name [String] skill name (e.g. "rails.db_debug") or path to a .md file
|
|
221
|
+
# @raise [Ask::Skills::Error] if the skill is not found
|
|
222
|
+
def skill(name)
|
|
223
|
+
if @skills_registry && (s = @skills_registry[name])
|
|
224
|
+
@chat.add_message(
|
|
225
|
+
role: :system,
|
|
226
|
+
content: "## Skill: #{s.name}\n\n#{s.description}\n\n---\n\n#{s.instructions}"
|
|
227
|
+
)
|
|
228
|
+
elsif File.exist?(name.to_s)
|
|
229
|
+
content = File.read(name.to_s)
|
|
230
|
+
@chat.add_message(
|
|
231
|
+
role: :system,
|
|
232
|
+
content: "## Skill: #{name}\n\n---\n\n#{content}"
|
|
233
|
+
)
|
|
234
|
+
else
|
|
235
|
+
raise Ask::Skills::Error, "Skill not found: #{name.inspect}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
205
239
|
def reset_messages!
|
|
206
240
|
@chat.reset_messages!
|
|
207
241
|
@messages = []
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Ask
|
|
7
|
+
module Agent
|
|
8
|
+
class Session
|
|
9
|
+
attr_reader :id, :chat, :tools, :turn_count, :created_at, :messages
|
|
10
|
+
attr_reader :tool_calls_made
|
|
11
|
+
|
|
12
|
+
def reflection_count
|
|
13
|
+
@reflector&.reflection_count || 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# @return [Ask::Skills::Registry, nil] auto-discovered skills registry
|
|
18
|
+
attr_reader :skills_registry
|
|
19
|
+
attr_reader :meta_agent_results
|
|
20
|
+
# @return [Ask::Skills::Registry, nil] auto-discovered skills registry
|
|
21
|
+
attr_reader :skills_registry
|
|
22
|
+
|
|
23
|
+
def initialize(model:, tools: [], max_turns: 25, max_tool_retries: 3,
|
|
24
|
+
compactor: nil, hooks: {}, persistence: nil,
|
|
25
|
+
id: nil, system_prompt: nil, parallel_tools: true,
|
|
26
|
+
reflector: nil, telemetry: true, meta_agent: nil, **chat_options)
|
|
27
|
+
@id = id || SecureRandom.uuid
|
|
28
|
+
@max_turns = max_turns
|
|
29
|
+
@max_tool_retries = max_tool_retries
|
|
30
|
+
@parallel_tools = parallel_tools
|
|
31
|
+
@event_handlers = { all: [] }
|
|
32
|
+
@running = false
|
|
33
|
+
@deleted = false
|
|
34
|
+
@abort_requested = false
|
|
35
|
+
@turn_count = 0
|
|
36
|
+
@created_at = Time.now
|
|
37
|
+
@_no_tools_instructed = false
|
|
38
|
+
|
|
39
|
+
@telemetry = telemetry.is_a?(Telemetry) ? telemetry : Telemetry.new(enabled: !!telemetry)
|
|
40
|
+
|
|
41
|
+
@chat = build_chat(model, system_prompt, tools, **chat_options)
|
|
42
|
+
@tools = resolve_tools(tools)
|
|
43
|
+
@loop = Loop.new(max_turns: max_turns)
|
|
44
|
+
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
45
|
+
|
|
46
|
+
# Auto-discover skills and inject into system prompt
|
|
47
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
48
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
49
|
+
skill_text = @skills_registry.format_for_prompt
|
|
50
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
51
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
52
|
+
@chat.with_instructions(current + skill_text)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
@compactor = compactor ? build_compactor(compactor) : nil
|
|
56
|
+
@hooks = Hooks.new(hooks)
|
|
57
|
+
|
|
58
|
+
# Auto-discover skills and inject into system prompt
|
|
59
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
60
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
61
|
+
skill_text = @skills_registry.format_for_prompt
|
|
62
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
63
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
64
|
+
@chat.with_instructions(current + skill_text)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
@persistence = persistence
|
|
68
|
+
|
|
69
|
+
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
70
|
+
@reflector = if reflector
|
|
71
|
+
Reflector.new(
|
|
72
|
+
model: @chat,
|
|
73
|
+
max_reflections: reflector_opts[:max_reflections] || 1
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@meta_agent_config = meta_agent
|
|
78
|
+
@meta_agent_results = nil
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Auto-discover skills and inject into system prompt
|
|
82
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
83
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
84
|
+
skill_text = @skills_registry.format_for_prompt
|
|
85
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
86
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
87
|
+
@chat.with_instructions(current + skill_text)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
@compactor&.chat = @chat
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run(message, tools: nil)
|
|
94
|
+
raise "Session deleted" if @deleted
|
|
95
|
+
raise "Session already running" if @running
|
|
96
|
+
|
|
97
|
+
@running = true
|
|
98
|
+
@abort_requested = false
|
|
99
|
+
@turn_count = 0
|
|
100
|
+
@loop.reset!
|
|
101
|
+
|
|
102
|
+
emit(Events::SessionStart.new)
|
|
103
|
+
|
|
104
|
+
active_tools = resolve_tools(tools || [])
|
|
105
|
+
active_tools = @tools if active_tools.empty?
|
|
106
|
+
|
|
107
|
+
if active_tools.empty? && !@_no_tools_instructed
|
|
108
|
+
@chat.add_message(role: :system, content: "You have no tools available. Do not claim you can look up information or use tools of any kind. Just respond based on your existing knowledge.")
|
|
109
|
+
@_no_tools_instructed = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
begin
|
|
113
|
+
@tool_executor.telemetry = @telemetry
|
|
114
|
+
|
|
115
|
+
response = @loop.run_turn(
|
|
116
|
+
chat: @chat,
|
|
117
|
+
message: message,
|
|
118
|
+
tools: active_tools,
|
|
119
|
+
tool_executor: @tool_executor,
|
|
120
|
+
compactor: @compactor,
|
|
121
|
+
hooks: @hooks,
|
|
122
|
+
event_emitter: self,
|
|
123
|
+
session_id: @id
|
|
124
|
+
)
|
|
125
|
+
rescue MaxTurnsExceeded => e
|
|
126
|
+
emit(Events::MaxTurnsExceeded.new(max_turns: @max_turns))
|
|
127
|
+
@telemetry.log(:max_turns_exceeded, session_id: @id, max_turns: @max_turns)
|
|
128
|
+
response = last_content
|
|
129
|
+
rescue LoopDetected => e
|
|
130
|
+
emit(Events::LoopDetected.new(tool_name: e.message, repeated_count: 3))
|
|
131
|
+
@telemetry.log(:loop_detected, session_id: @id, tool_name: e.message, repeated_count: 3)
|
|
132
|
+
response = last_content
|
|
133
|
+
rescue Ask::ContextLengthExceeded
|
|
134
|
+
if @compactor && !@compactor.overflow_recovered?
|
|
135
|
+
@compactor.recover_from_overflow
|
|
136
|
+
retry
|
|
137
|
+
end
|
|
138
|
+
response = "I'm sorry, the conversation has grown too long. Please start a new session."
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
emit(Events::Error.new(error: e.message, recoverable: true))
|
|
141
|
+
raise
|
|
142
|
+
ensure
|
|
143
|
+
@running = false
|
|
144
|
+
persist! if @persistence
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@tool_calls_made = @tool_executor.total_executions
|
|
148
|
+
|
|
149
|
+
if @reflector && @reflector.reflect?(@tool_calls_made) && !@abort_requested
|
|
150
|
+
eval_result = @reflector.evaluate(response: response, event_emitter: self)
|
|
151
|
+
@telemetry.log(:reflection_end, session_id: @id, decision: eval_result[:decision], feedback: eval_result[:feedback])
|
|
152
|
+
|
|
153
|
+
if eval_result[:decision] == :improve && !@abort_requested
|
|
154
|
+
@chat.add_message(
|
|
155
|
+
role: :system,
|
|
156
|
+
content: "Improve your last response: #{eval_result[:feedback]}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
response = @loop.run_turn(
|
|
160
|
+
chat: @chat,
|
|
161
|
+
message: "",
|
|
162
|
+
tools: active_tools,
|
|
163
|
+
tool_executor: @tool_executor,
|
|
164
|
+
compactor: @compactor,
|
|
165
|
+
hooks: @hooks,
|
|
166
|
+
event_emitter: self,
|
|
167
|
+
session_id: @id
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if @meta_agent_config
|
|
173
|
+
@telemetry.increment_session_count!
|
|
174
|
+
try_auto_meta_agent
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
emit(Events::SessionEnd.new(result: response, turn_count: @turn_count, tool_calls_made: @tool_calls_made))
|
|
178
|
+
@messages = @chat.messages.dup
|
|
179
|
+
|
|
180
|
+
response
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def on_event(&block)
|
|
184
|
+
@event_handlers[:all] << block
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def on(type, &block)
|
|
189
|
+
@event_handlers[type] ||= []
|
|
190
|
+
@event_handlers[type] << block
|
|
191
|
+
self
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def emit(event)
|
|
195
|
+
@event_handlers[:all].each { |h| h.call(event) }
|
|
196
|
+
handlers = @event_handlers[event.class]
|
|
197
|
+
handlers&.each { |h| h.call(event) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def running? = @running
|
|
201
|
+
def deleted? = @deleted
|
|
202
|
+
|
|
203
|
+
def save
|
|
204
|
+
persist! if @persistence
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.load(id, adapter:)
|
|
208
|
+
data = adapter.load(id)
|
|
209
|
+
return nil unless data
|
|
210
|
+
|
|
211
|
+
session = new(
|
|
212
|
+
id: data[:id],
|
|
213
|
+
model: data.dig(:metadata, :model),
|
|
214
|
+
tools: data.dig(:metadata, :tools)&.map(&:constantize) || [],
|
|
215
|
+
persistence: adapter
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
data[:messages].each do |msg|
|
|
219
|
+
session.chat.add_message(
|
|
220
|
+
role: msg[:role].to_sym,
|
|
221
|
+
content: msg[:content],
|
|
222
|
+
tool_call_id: msg[:tool_call_id]
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
session
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def delete
|
|
230
|
+
@deleted = true
|
|
231
|
+
@persistence&.delete(@id)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def abort
|
|
235
|
+
@abort_requested = true
|
|
236
|
+
end
|
|
237
|
+
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
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.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ask-skills
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.1'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: ask-tools-shell
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -133,6 +147,7 @@ files:
|
|
|
133
147
|
- lib/ask/agent/persistence/in_memory.rb
|
|
134
148
|
- lib/ask/agent/reflector.rb
|
|
135
149
|
- lib/ask/agent/session.rb
|
|
150
|
+
- lib/ask/agent/session_backup.rb
|
|
136
151
|
- lib/ask/agent/telemetry.rb
|
|
137
152
|
- lib/ask/agent/tool_abort_controller.rb
|
|
138
153
|
- lib/ask/agent/tool_executor.rb
|