Class: Pikuri::Tasks::List
- Inherits:
-
Object
- Object
- Pikuri::Tasks::List
- Defined in:
- lib/pikuri/tasks/list.rb
Overview
An in-memory ordered list of Items, scoped to a single Agent. Held inside Extension and captured by closure into the execute block of each of the four task tool classes, so every mutation hits the same instance.
Concurrency
The list is confined to the agent’s thread: the agent loop is single-threaded with respect to tool calls (ruby_llm dispatches them sequentially), so no locking. Other threads (e.g. a web UI rendering the list) must not touch a List directly — they consume the ListChanged events Extension#bind wires onto the listener stream, whose items payload is an immutable snapshot safe to hand across threads. A future parallel-tool-execution feature would need a Mutex here.
Persistence
None. The list is dropped when the Agent is garbage-collected. That matches the gem’s stated scope: “in-memory only, no session-state-on-disk.”
Instance Attribute Summary collapse
-
#on_change ⇒ Proc?
Optional zero-argument hook invoked after every successful mutation (#add / #set_status / #delete) — not on failed ones (a raise means nothing changed).
Instance Method Summary collapse
-
#add(content) ⇒ Item
Append a new item with status
pendingand the next id from a monotonic per-list counter. -
#delete(id) ⇒ Item
Remove the item whose
idmatches. - #empty? ⇒ Boolean
- #initialize ⇒ List constructor
-
#items ⇒ Array<Item>
A frozen snapshot of the current items, in insertion order.
-
#render ⇒ String
The canonical rendering returned as the observation by every task tool, so the LLM sees the latest full state — including each item’s id — on each call without needing a separate read tool.
-
#set_status(id:, status:) ⇒ Item
Update the status of the item whose
idmatches. - #size ⇒ Integer
Constructor Details
#initialize ⇒ List
56 57 58 59 60 |
# File 'lib/pikuri/tasks/list.rb', line 56 def initialize @items = [] @next_id = 1 @on_change = nil end |
Instance Attribute Details
#on_change ⇒ Proc?
Optional zero-argument hook invoked after every successful mutation (#add / #set_status / #delete) — not on failed ones (a raise means nothing changed). Set by Extension#bind to emit a Pikuri::Tasks::ListChanged onto the agent’s listener stream; nil (the default) disables notification. Runs on the mutating (agent) thread, synchronously inside the mutation call.
71 72 73 |
# File 'lib/pikuri/tasks/list.rb', line 71 def on_change @on_change end |
Instance Method Details
#add(content) ⇒ Item
Append a new item with status pending and the next id from a monotonic per-list counter. Ids are never reused: after a delete, the freed id stays dead, so a stale id held by the LLM errors loudly instead of silently resolving to a newer task.
100 101 102 103 104 105 106 107 108 |
# File 'lib/pikuri/tasks/list.rb', line 100 def add(content) raise DuplicateItem, content if @items.any? { |i| i.content == content } item = Item.new(id: @next_id, content: content, status: 'pending') @next_id += 1 @items << item @on_change&.call item end |
#delete(id) ⇒ Item
Remove the item whose id matches. The id is not reused for later items (see #add).
137 138 139 140 141 142 143 144 |
# File 'lib/pikuri/tasks/list.rb', line 137 def delete(id) idx = @items.index { |i| i.id == id } raise ItemNotFound, id.to_s if idx.nil? removed = @items.delete_at(idx) @on_change&.call removed end |
#empty? ⇒ Boolean
86 87 88 |
# File 'lib/pikuri/tasks/list.rb', line 86 def empty? @items.empty? end |
#items ⇒ Array<Item>
Returns a frozen snapshot of the current items, in insertion order. Callers cannot mutate the internal storage through this accessor.
76 77 78 |
# File 'lib/pikuri/tasks/list.rb', line 76 def items @items.dup.freeze end |
#render ⇒ String
The canonical rendering returned as the observation by every task tool, so the LLM sees the latest full state — including each item’s id — on each call without needing a separate read tool. Format:
<tasks>
- #1 [pending] Add dark mode toggle
- #2 [in_progress] Write unit tests
- #3 [completed] Update README
</tasks>
Empty list renders as <tasks>(empty)</tasks> so the LLM gets an unambiguous “yes, the call worked and the list is now empty” signal rather than an ambiguous blank block.
162 163 164 165 166 167 |
# File 'lib/pikuri/tasks/list.rb', line 162 def render return '<tasks>(empty)</tasks>' if @items.empty? lines = @items.map { |i| "- ##{i.id} [#{i.status}] #{i.content}" } "<tasks>\n#{lines.join("\n")}\n</tasks>" end |
#set_status(id:, status:) ⇒ Item
Update the status of the item whose id matches.
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/pikuri/tasks/list.rb', line 118 def set_status(id:, status:) unless STATUSES.include?(status) raise ArgumentError, "invalid status: #{status.inspect} (allowed: #{STATUSES.join(', ')})" end idx = @items.index { |i| i.id == id } raise ItemNotFound, id.to_s if idx.nil? @items[idx] = @items[idx].with(status: status) @on_change&.call @items[idx] end |
#size ⇒ Integer
81 82 83 |
# File 'lib/pikuri/tasks/list.rb', line 81 def size @items.size end |