ask-tokens 0.2.1 → 0.3.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: ae96192b20e70ac88fb98af984b5a13cf749c7d7bf73fc786d59fc89e27002c1
4
- data.tar.gz: e48974d2d1998878c63579e788a6882757bc39aaa524b71c99c1fde426e939d9
3
+ metadata.gz: ceb47fceedf25098c5bc27e2c544feaad5226839b337a87d1cf1503fcc5331d9
4
+ data.tar.gz: 928470bc83407814f4a8aa8f78b24170a23789cde42ea21b63b9c8b22c160118
5
5
  SHA512:
6
- metadata.gz: 52b0688ccb3e4ef1c0983b6a642937981c7ffd8fad2380fc8385781c371c773e33387dbb11ef953627338aedcefeb11e60a8a88245e8ef00fccd6be46602eeaf
7
- data.tar.gz: e5244ed51b9567605caa139c29b200372f3327fbd1950cf3b051e4b081a9c23656067dbbac0195a3e95942400edd1bfcf5982e9dd4861b93eb25b1f1db4c23d3
6
+ metadata.gz: e073422cdd0e0ef386a2532382644abcb77d4161b8d3e49b03983c9d13185a7dfac3743ad485acf12aa5cd3748f298f6d73d5a8c7425456e288c9596a6dc657c
7
+ data.tar.gz: 4acccaf24b0afe5597978ef93ad8f75117d77980be674b441b6764df95f5ab182ee1d225035e4e861d8d58af4e8ca6e631c1404ab3b51c22ead52cd58682a776
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-10-20
4
+
5
+ ### Added
6
+ - Activity namespacing: `Activity#namespace` and `#qualified_name` so multiple projects sharing one registry never collide on common names like `:compute_hour`. `Ask::Tokens.activity(name, namespace:, ...)` and `Ask::Tokens.estimate(name, namespace:)` resolve scoped activities.
7
+ - `ActivityRegistry#fetch(name, namespace:)`, `#names(namespace:)`, `#each(namespace:)`, `#clear(namespace:)`, and `#registered?(name, namespace:)` for scoped access.
8
+ - `Ask::Tokens#wallet_for(owner, store:)` override, so a billing service can route different owners to different backends (useful for tests and multi-tenant routing).
9
+ - `Wallet#estimate`, `#enough_for?`, and `#spend!` accept `namespace:` plus keyword params that are merged into the params hash.
10
+
11
+ ### Changed
12
+ - `Ask::Tokens#estimate` and `Wallet#estimate/#spend!` accept keyword arguments in addition to a positional params Hash.
13
+
14
+
3
15
  ## [0.1.0] - 2026-08-19
4
16
 
5
17
  ### Added
@@ -10,16 +10,25 @@ module Ask
10
10
  # end
11
11
  #
12
12
  class Activity
13
- attr_reader :name
13
+ attr_reader :name, :namespace
14
14
 
15
- def initialize(name, cost: nil, &block)
15
+ def initialize(name, cost: nil, namespace: nil, &block)
16
16
  raise ArgumentError, "activity #{name.inspect} needs a cost or a block" if cost.nil? && block.nil?
17
17
 
18
18
  @name = name.to_sym
19
+ @namespace = namespace&.to_sym
19
20
  @cost = cost
20
21
  @block = block
21
22
  end
22
23
 
24
+ # The fully-qualified name, including namespace when present.
25
+ # Used as the registry key so scoped and unscoped activities never
26
+ # collide. A billing service serving multiple projects can namespace
27
+ # each project's activities (e.g. :ship) without collisions.
28
+ def qualified_name
29
+ @namespace ? :"#{@namespace}:#{@name}" : @name
30
+ end
31
+
23
32
  # The token cost of running this activity with +params+. Always returns
24
33
  # an Integer, rounded per configuration. A fixed cost ignores params; a
25
34
  # block receives them (zero-arity blocks are called with no arguments).
@@ -36,7 +45,7 @@ module Ask
36
45
  end
37
46
 
38
47
  def to_s
39
- "Activity(#{@name})"
48
+ @namespace ? "Activity(#{@namespace}:#{@name})" : "Activity(#{@name})"
40
49
  end
41
50
 
42
51
  def inspect
@@ -3,32 +3,63 @@
3
3
  module Ask
4
4
  module Tokens
5
5
  # Stores declared activities by name.
6
+ #
7
+ # Activities can be scoped to a namespace (e.g. a project or service
8
+ # name) so that multiple projects sharing one registry never collide
9
+ # on a common name like :compute_hour. The registry key is the
10
+ # activity's qualified name (namespace:name when namespaced).
6
11
  class ActivityRegistry
7
12
  def initialize
8
13
  @activities = {}
9
14
  end
10
15
 
11
- def register(name, cost: nil, &block)
12
- activity = Activity.new(name, cost: cost, &block)
13
- @activities[activity.name] = activity
16
+ def register(name, cost: nil, namespace: nil, &block)
17
+ activity = Activity.new(name, cost: cost, namespace: namespace, &block)
18
+ @activities[activity.qualified_name] = activity
14
19
  end
15
20
 
16
- def fetch(name)
17
- @activities.fetch(name.to_sym) do
18
- raise KeyError, "Unknown activity: #{name.inspect}. Registered: #{@activities.keys.inspect}"
21
+ def fetch(name, namespace: nil)
22
+ key = namespace ? :"#{namespace}:#{name}" : name.to_sym
23
+ @activities.fetch(key) do
24
+ raise KeyError, "Unknown activity: #{name.inspect} (namespace: #{namespace.inspect}). Registered: #{@activities.keys.inspect}"
19
25
  end
20
26
  end
21
27
 
22
- def names
23
- @activities.keys
28
+ # All registered activity names. Pass +namespace:+ to filter to one
29
+ # namespace; omit for every activity.
30
+ def names(namespace: nil)
31
+ if namespace
32
+ ns = namespace.to_sym
33
+ @activities.keys.select { |k| k.to_s.start_with?("#{ns}:") }
34
+ else
35
+ @activities.keys
36
+ end
24
37
  end
25
38
 
26
- def each(&block)
27
- @activities.each_value(&block)
39
+ # Iterate activities. Pass +namespace:+ to scope.
40
+ def each(namespace: nil, &block)
41
+ if namespace
42
+ ns = namespace.to_sym
43
+ @activities.each_value { |a| block.call(a) if a.namespace == ns }
44
+ else
45
+ @activities.each_value(&block)
46
+ end
47
+ end
48
+
49
+ # Remove all activities, or only those in a namespace.
50
+ def clear(namespace: nil)
51
+ if namespace
52
+ ns = namespace.to_sym
53
+ @activities.delete_if { |_, a| a.namespace == ns }
54
+ else
55
+ @activities.clear
56
+ end
28
57
  end
29
58
 
30
- def clear
31
- @activities.clear
59
+ # True when an activity (optionally in a namespace) is registered.
60
+ def registered?(name, namespace: nil)
61
+ key = namespace ? :"#{namespace}:#{name}" : name.to_sym
62
+ @activities.key?(key)
32
63
  end
33
64
  end
34
65
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Tokens
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -38,13 +38,17 @@ module Ask
38
38
  end
39
39
 
40
40
  # Estimated token cost of running +activity+ with +params+.
41
- def estimate(activity_name, params = {})
42
- Ask::Tokens.estimate(activity_name, params)
41
+ # Pass +namespace:+ to resolve a namespaced activity.
42
+ # Params may be passed positionally as a Hash or as keyword arguments.
43
+ def estimate(activity_name, params = {}, namespace: nil, **extra)
44
+ params = params.merge(extra) unless extra.empty?
45
+ Ask::Tokens.estimate(activity_name, params, namespace: namespace)
43
46
  end
44
47
 
45
48
  # True when the balance covers the estimated cost of an activity.
46
- def enough_for?(activity_name, params = {})
47
- has?(estimate(activity_name, params))
49
+ def enough_for?(activity_name, params = {}, namespace: nil, **extra)
50
+ params = params.merge(extra) unless extra.empty?
51
+ has?(estimate(activity_name, params, namespace: namespace))
48
52
  end
49
53
 
50
54
  # Total tokens debited since +time+ (absolute value). Useful for
@@ -84,8 +88,11 @@ module Ask
84
88
  # Spend the estimated token cost of +activity+ given +params+. With a
85
89
  # block, charge only if the block succeeds — the standard wrapper for
86
90
  # LLM calls and render jobs.
87
- def spend!(activity_name, params = {}, &block)
88
- amount = estimate(activity_name, params)
91
+ #
92
+ # Pass +namespace:+ to resolve a namespaced activity.
93
+ def spend!(activity_name, params = {}, namespace: nil, **extra, &block)
94
+ params = params.merge(extra) unless extra.empty?
95
+ amount = estimate(activity_name, params, namespace: namespace)
89
96
  deduct!(amount, reason: activity_name.to_s, metadata: params.dup, &block)
90
97
  end
91
98
 
data/lib/ask/tokens.rb CHANGED
@@ -85,14 +85,23 @@ module Ask
85
85
 
86
86
  # Register an activity. +cost:+ is a fixed integer; without it the block
87
87
  # receives params and must return an integer token count.
88
- def activity(name, cost: nil, &block)
89
- activities.register(name, cost: cost, &block)
88
+ #
89
+ # Pass +namespace:+ to scope the activity to a project or service so
90
+ # that multiple projects sharing one registry never collide on a
91
+ # common name like :compute_hour:
92
+ #
93
+ # Ask::Tokens.activity(:compute_hour, namespace: :ship, cost: 34)
94
+ #
95
+ def activity(name, cost: nil, namespace: nil, &block)
96
+ activities.register(name, cost: cost, namespace: namespace, &block)
90
97
  end
91
98
 
92
99
  # The estimated token cost of running +activity+ with +params+, without
93
- # spending anything.
94
- def estimate(activity_name, params = {})
95
- activities.fetch(activity_name).cost(params)
100
+ # spending anything. Pass +namespace:+ to resolve a scoped activity.
101
+ # Params may be passed positionally as a Hash or as keyword arguments.
102
+ def estimate(activity_name, params = {}, namespace: nil, **extra)
103
+ params = params.merge(extra) unless extra.empty?
104
+ activities.fetch(activity_name, namespace: namespace).cost(params)
96
105
  end
97
106
 
98
107
  def activities
@@ -100,9 +109,11 @@ module Ask
100
109
  end
101
110
 
102
111
  # Build a wallet for +owner+. +owner+ is an opaque identifier the store
103
- # keys on.
104
- def wallet_for(owner)
105
- Wallet.new(owner: owner, store: config.store, time: config.time)
112
+ # keys on. Pass +store:+ to override the configured store — useful for
113
+ # tests and for a billing service that routes different owners to
114
+ # different backends.
115
+ def wallet_for(owner, store: nil)
116
+ Wallet.new(owner: owner, store: (store || config.store), time: config.time)
106
117
  end
107
118
 
108
119
  # Reset configuration and activity registry (mainly for tests).
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-tokens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto