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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14e351edbbd119bd5ae5bf8701fd38dd1370b496a415d6be9bc386cb3c5bb04b
4
- data.tar.gz: 4a156a06a909b8725685ec7cb5bff121e46b2ebcb2976b6411cb4c29c75ab364
3
+ metadata.gz: bb1a00c9b6f32029b7d27271482fe82dfa7333601776537a38160635bf4fde0b
4
+ data.tar.gz: 80d666d9485e6e058c9a81d20fdbfa48b4b196a33d1675655fbbd7f6ffea9240
5
5
  SHA512:
6
- metadata.gz: 852a51b092bbe1d4e3a2b57c87001474261d413428ab83bb4a71fae4858a229e0c9a9d337c24a89177773c1b2ee82a8aac8069455f2f1191350d0d66c19a477f
7
- data.tar.gz: ca6761470ac7c95191bf7e2bd49c47bf10a71fd0ba6796468ddc113692f5f685f6a73d57f9fe46cf7fc6295f6b3c7aa11806212b62d8c18f60562888bcd7d4c0
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
@@ -48,24 +48,10 @@ module Ask
48
48
  @compactor = compactor ? build_compactor(compactor) : nil
49
49
  @hooks = Hooks.new(hooks)
50
50
 
51
- # Auto-discover skills (shared + per-agent if agent_dir is given)
52
- @skills_registry = Ask::Skills.discover(agent_dir: @agent_dir) rescue nil
53
- if @skills_registry && !@skills_registry.names.empty?
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.11.0"
5
+ VERSION = "0.12.0"
6
6
  end
7
7
  end
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.11.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