ask-agent 0.11.0 → 0.12.0
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 +55 -0
- data/lib/ask/agent/context_source.rb +79 -0
- data/lib/ask/agent/context_sources.rb +85 -0
- data/lib/ask/agent/session.rb +29 -17
- data/lib/ask/agent/system_context.rb +77 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb1a00c9b6f32029b7d27271482fe82dfa7333601776537a38160635bf4fde0b
|
|
4
|
+
data.tar.gz: 80d666d9485e6e058c9a81d20fdbfa48b4b196a33d1675655fbbd7f6ffea9240
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7ef76f83baba0df18a1f60a6a00aae8cefb0b191ccf6eb0d5b680ed68e0c305dde891591673805b28bec5705ae632da2bd6acc4bd5810602a3b04878483f890
|
|
7
|
+
data.tar.gz: 639f577894d439805d0f06d988c4b528241f82b31656dea7b9a28ba54eca173c4262e3a48b865705bfb0ae153653c9a9c4e4ac6fc3cd7cb8fe02c92efe4495f9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
## [0.12.0] — 2026-07-22
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **System Context algebra** — typed, independently-observable context sources
|
|
6
|
+
that compose into the system prompt. Each source has a unique key, a `load`
|
|
7
|
+
function, a `baseline` render for initialization, and an `update` render for
|
|
8
|
+
mid-conversation changes.
|
|
9
|
+
|
|
10
|
+
- **Built-in context sources:**
|
|
11
|
+
- `Instructions` — agent's core system prompt / instructions.md
|
|
12
|
+
- `SkillsList` — "## Available Skills" listing from the skills registry
|
|
13
|
+
- `AlwaysActiveSkills` — full instructions for skills with `always: true`
|
|
14
|
+
- `Date` — today's date in ISO 8601 format
|
|
15
|
+
|
|
16
|
+
- **`SystemContext#changes`** — detects which sources have changed since the
|
|
17
|
+
last snapshot and returns update texts. Enables mid-conversation updates
|
|
18
|
+
(e.g., date rollover, skill registry changes) without rebuilding the entire
|
|
19
|
+
prompt.
|
|
20
|
+
|
|
21
|
+
- **`Ask::Agent::ContextSource` base class** — DSL for defining typed context
|
|
22
|
+
sources with `key`, `load`, `baseline`, and optional `update`.
|
|
23
|
+
|
|
24
|
+
- **`Session` uses SystemContext** — the system prompt is now assembled from
|
|
25
|
+
typed sources instead of string concatenation, with 17 tests covering
|
|
26
|
+
rendering, change detection, and source composition.
|
|
27
|
+
|
|
28
|
+
## [0.11.0] — 2026-07-22
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`Definition#parallel_tools` DSL** — set parallel tool execution per agent:
|
|
33
|
+
```ruby
|
|
34
|
+
class MyAgent < Ask::Agent::Definition
|
|
35
|
+
model "gpt-4o"
|
|
36
|
+
parallel_tools false
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
Defaults to `true`.
|
|
40
|
+
|
|
41
|
+
- **`Definition#option` DSL** — pass arbitrary Session options:
|
|
42
|
+
```ruby
|
|
43
|
+
class MyAgent < Ask::Agent::Definition
|
|
44
|
+
model "gpt-4o"
|
|
45
|
+
option :temperature, 0.7
|
|
46
|
+
option :reflector, true
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **17 new tests** for Definition DSL — model, provider, max_turns, parallel_tools, tools, schedule, option, instructions_path, instructions_content, subclass tracking.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **`build_session_from_definition` passes `parallel_tools` and custom options** to `Session.new`. Previously only `model`, `provider`, and `max_turns` were forwarded.
|
|
55
|
+
|
|
1
56
|
## [0.10.0] — 2026-07-21
|
|
2
57
|
|
|
3
58
|
### Added
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Agent
|
|
5
|
+
# A typed, independently observable piece of the system context.
|
|
6
|
+
#
|
|
7
|
+
# Each source has a unique +key+, a +load+ function that returns its
|
|
8
|
+
# current value, a +baseline+ render for initial prompt inclusion,
|
|
9
|
+
# and an +update+ render for mid-conversation change notifications.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class DateSource < Ask::Agent::ContextSource
|
|
13
|
+
# key "core/date"
|
|
14
|
+
#
|
|
15
|
+
# def load
|
|
16
|
+
# Date.today.iso8601
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def baseline(date)
|
|
20
|
+
# "Today is #{date}."
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def update(prev, curr)
|
|
24
|
+
# "Earlier I said the date was #{prev}, but it is now #{curr}."
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class ContextSource
|
|
28
|
+
class << self
|
|
29
|
+
# Unique namespaced key for this source (e.g., "core/date").
|
|
30
|
+
# Used for change detection and deterministic ordering.
|
|
31
|
+
def key(value = :__no_value__)
|
|
32
|
+
if value == :__no_value__
|
|
33
|
+
@key
|
|
34
|
+
else
|
|
35
|
+
@key = value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inherited(subclass)
|
|
40
|
+
super
|
|
41
|
+
# Register source type for discovery
|
|
42
|
+
@registered_sources ||= []
|
|
43
|
+
@registered_sources << subclass
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def registered_sources
|
|
47
|
+
@registered_sources || []
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String] unique key for this source
|
|
52
|
+
def key
|
|
53
|
+
self.class.key
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Load the current value of this source.
|
|
57
|
+
# @return [Object] any value that will be passed to +baseline+ and +update+
|
|
58
|
+
def load
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Render the initial text for this source.
|
|
63
|
+
# @param value [Object] the value returned by +load+
|
|
64
|
+
# @return [String]
|
|
65
|
+
def baseline(value)
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Render a mid-conversation update message.
|
|
70
|
+
# Called when +load+ returns a value different from the previous one.
|
|
71
|
+
# @param previous [Object] the value from the last load
|
|
72
|
+
# @param current [Object] the current value
|
|
73
|
+
# @return [String, nil] nil if no update is needed
|
|
74
|
+
def update(previous, current)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Agent
|
|
5
|
+
module ContextSources
|
|
6
|
+
# The agent's core instructions (system prompt / instructions.md).
|
|
7
|
+
class Instructions < Ask::Agent::ContextSource
|
|
8
|
+
key "core/instructions"
|
|
9
|
+
|
|
10
|
+
def initialize(content)
|
|
11
|
+
@content = content.to_s
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load
|
|
15
|
+
@content
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def baseline(content)
|
|
19
|
+
content
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The "## Available Skills" listing from the skills registry.
|
|
24
|
+
# Only renders when the registry has skills to list.
|
|
25
|
+
class SkillsList < Ask::Agent::ContextSource
|
|
26
|
+
key "core/skills"
|
|
27
|
+
|
|
28
|
+
def initialize(registry)
|
|
29
|
+
@registry = registry
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def load
|
|
33
|
+
@registry&.format_for_prompt.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def baseline(listing)
|
|
37
|
+
listing.empty? ? nil : listing
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update(prev, curr)
|
|
41
|
+
return nil if prev == curr
|
|
42
|
+
return "Skills have been updated:\n#{curr}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Full instructions for always-active skills (those with +always: true+).
|
|
47
|
+
class AlwaysActiveSkills < Ask::Agent::ContextSource
|
|
48
|
+
key "core/skills.always_active"
|
|
49
|
+
|
|
50
|
+
def initialize(registry)
|
|
51
|
+
@registry = registry
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def load
|
|
55
|
+
return "" unless @registry
|
|
56
|
+
|
|
57
|
+
@registry.always_active_skills.map { |s|
|
|
58
|
+
"## Skill: #{s.name}\n#{s.description}\n\n#{s.instructions}"
|
|
59
|
+
}.join("\n\n")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def baseline(instructions)
|
|
63
|
+
instructions.empty? ? nil : instructions
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Today's date in ISO 8601 format.
|
|
68
|
+
class Date < Ask::Agent::ContextSource
|
|
69
|
+
key "core/date"
|
|
70
|
+
|
|
71
|
+
def load
|
|
72
|
+
::Date.today.iso8601
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def baseline(date)
|
|
76
|
+
"Today's date is #{date}."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update(prev, curr)
|
|
80
|
+
"I previously said the date was #{prev}, but it is now #{curr}."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -48,24 +48,10 @@ module Ask
|
|
|
48
48
|
@compactor = compactor ? build_compactor(compactor) : nil
|
|
49
49
|
@hooks = Hooks.new(hooks)
|
|
50
50
|
|
|
51
|
-
#
|
|
52
|
-
@
|
|
53
|
-
|
|
54
|
-
skill_text = @skills_registry.format_for_prompt
|
|
55
|
-
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
56
|
-
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
57
|
-
|
|
58
|
-
# Append skill listing
|
|
59
|
-
augmented = current + skill_text
|
|
60
|
-
|
|
61
|
-
# Auto-inject full instructions for always-active skills
|
|
62
|
-
@skills_registry.always_active_skills.each do |skill|
|
|
63
|
-
augmented += "\n\n## Skill: #{skill.name}\n#{skill.description}\n\n#{skill.instructions}"
|
|
64
|
-
end
|
|
51
|
+
# Build system context from typed sources
|
|
52
|
+
@system_context = build_system_context(system_prompt)
|
|
53
|
+
apply_system_context
|
|
65
54
|
|
|
66
|
-
@chat.with_instructions(augmented)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
55
|
@persistence = persistence
|
|
70
56
|
|
|
71
57
|
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
@@ -355,6 +341,32 @@ module Ask
|
|
|
355
341
|
.select { |m| m.role == :assistant && m.content.to_s.strip.length > 0 }
|
|
356
342
|
.first&.content.to_s
|
|
357
343
|
end
|
|
344
|
+
|
|
345
|
+
# Build the system context from typed sources.
|
|
346
|
+
def build_system_context(prompt)
|
|
347
|
+
sources = []
|
|
348
|
+
sources << Ask::Agent::ContextSources::Instructions.new(prompt) if prompt
|
|
349
|
+
|
|
350
|
+
# Auto-discover skills (shared + per-agent if agent_dir is given)
|
|
351
|
+
@skills_registry = Ask::Skills.discover(agent_dir: @agent_dir) rescue nil
|
|
352
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
353
|
+
sources << Ask::Agent::ContextSources::SkillsList.new(@skills_registry)
|
|
354
|
+
if @skills_registry.always_active_skills.any?
|
|
355
|
+
sources << Ask::Agent::ContextSources::AlwaysActiveSkills.new(@skills_registry)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
SystemContext.new(sources)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Render the system context and apply it to the chat.
|
|
363
|
+
def apply_system_context
|
|
364
|
+
rendered = @system_context.render
|
|
365
|
+
return if rendered.empty?
|
|
366
|
+
return unless @chat.messages.any? { |m| m.role == :system }
|
|
367
|
+
|
|
368
|
+
@chat.with_instructions(rendered)
|
|
369
|
+
end
|
|
358
370
|
end
|
|
359
371
|
end
|
|
360
372
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Agent
|
|
5
|
+
# Composes typed context sources into a coherent system prompt.
|
|
6
|
+
#
|
|
7
|
+
# At initialization, each source renders its +baseline+ text. Sources
|
|
8
|
+
# are ordered by their +key+ for deterministic output.
|
|
9
|
+
#
|
|
10
|
+
# At any point, +changes+ returns a list of update texts for sources
|
|
11
|
+
# whose values have changed since the last snapshot. This enables
|
|
12
|
+
# mid-conversation updates without rebuilding the entire prompt.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# ctx = SystemContext.new([
|
|
16
|
+
# InstructionsSource.new("You are a helper."),
|
|
17
|
+
# SkillsListSource.new(registry),
|
|
18
|
+
# DateSource.new,
|
|
19
|
+
# ])
|
|
20
|
+
#
|
|
21
|
+
# prompt = ctx.render # full system prompt
|
|
22
|
+
# updates = ctx.changes # nil if nothing changed
|
|
23
|
+
class SystemContext
|
|
24
|
+
Snapshot = Data.define(:key, :value)
|
|
25
|
+
|
|
26
|
+
def initialize(sources)
|
|
27
|
+
@sources = sources
|
|
28
|
+
@snapshots = {}
|
|
29
|
+
take_snapshot
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Render the full baseline system prompt.
|
|
33
|
+
# @return [String]
|
|
34
|
+
def render
|
|
35
|
+
parts = @sources.map do |source|
|
|
36
|
+
value = @snapshots[source.key]&.value
|
|
37
|
+
text = source.baseline(value) rescue nil
|
|
38
|
+
text
|
|
39
|
+
end
|
|
40
|
+
parts.compact.join("\n\n")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Detect changes since the last snapshot.
|
|
44
|
+
# Returns update texts for sources whose value changed, or nil.
|
|
45
|
+
# @return [Array<String>, nil]
|
|
46
|
+
def changes
|
|
47
|
+
updates = []
|
|
48
|
+
@sources.each do |source|
|
|
49
|
+
prev_value = @snapshots[source.key]&.value
|
|
50
|
+
current_value = source.load
|
|
51
|
+
next if prev_value == current_value
|
|
52
|
+
|
|
53
|
+
update_text = source.update(prev_value, current_value)
|
|
54
|
+
updates << update_text if update_text
|
|
55
|
+
@snapshots[source.key] = Snapshot.new(key: source.key, value: current_value)
|
|
56
|
+
end
|
|
57
|
+
updates.any? ? updates : nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Access a source by key.
|
|
61
|
+
# @param key [String]
|
|
62
|
+
# @return [ContextSource, nil]
|
|
63
|
+
def [](key)
|
|
64
|
+
@sources.find { |s| s.key == key }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def take_snapshot
|
|
70
|
+
@sources.each do |source|
|
|
71
|
+
value = source.load
|
|
72
|
+
@snapshots[source.key] = Snapshot.new(key: source.key, value: value)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -222,6 +222,9 @@ end
|
|
|
222
222
|
|
|
223
223
|
require_relative "agent/version"
|
|
224
224
|
require_relative "agent/events"
|
|
225
|
+
require_relative "agent/system_context"
|
|
226
|
+
require_relative "agent/context_source"
|
|
227
|
+
require_relative "agent/context_sources"
|
|
225
228
|
require_relative "agent/chat"
|
|
226
229
|
require_relative "agent/telemetry"
|
|
227
230
|
require_relative "agent/tool_abort_controller"
|
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.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -154,6 +154,8 @@ files:
|
|
|
154
154
|
- lib/ask/agent/cli.rb
|
|
155
155
|
- lib/ask/agent/compactor.rb
|
|
156
156
|
- lib/ask/agent/configuration.rb
|
|
157
|
+
- lib/ask/agent/context_source.rb
|
|
158
|
+
- lib/ask/agent/context_sources.rb
|
|
157
159
|
- lib/ask/agent/definition.rb
|
|
158
160
|
- lib/ask/agent/events.rb
|
|
159
161
|
- lib/ask/agent/extensions/audit_log.rb
|
|
@@ -178,6 +180,7 @@ files:
|
|
|
178
180
|
- lib/ask/agent/stream_transforms/pipeline.rb
|
|
179
181
|
- lib/ask/agent/stream_transforms/text_buffer.rb
|
|
180
182
|
- lib/ask/agent/stream_transforms/thinking_separator.rb
|
|
183
|
+
- lib/ask/agent/system_context.rb
|
|
181
184
|
- lib/ask/agent/telemetry.rb
|
|
182
185
|
- lib/ask/agent/test.rb
|
|
183
186
|
- lib/ask/agent/tool_abort_controller.rb
|