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, shared by closure into all four task tools so every mutation hits the same instance.
Not persisted: dropped when the Agent is collected, matching the
gem's "in-memory only" scope.
Sharing
P_one_agent — this is one conversation's plan, so two agents on one
list would read and complete each other's tasks. Unguarded, and it needs
no lock because its only caller is the agent whose tool calls are already
serialized. Another thread (a UI rendering the list) must not touch a
List directly: it consumes the ListChanged events Extension#bind
wires onto the listener stream, whose items payload is an immutable
snapshot safe to hand across threads.
Instance Attribute Summary collapse
-
#on_change ⇒ Proc?
Optional zero-argument hook invoked synchronously after every successful mutation (a raise means nothing changed, so no notify).
Instance Method Summary collapse
-
#add(content) ⇒ Item
Append a new item with status
pendingand the next id from a monotonic per-list counter. -
#clear ⇒ void
Drop every item and rewind the id counter to 1, then fire #on_change (always, even when already empty, so a UI listener sees the cleared state).
-
#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 every task tool returns as its observation, so the LLM sees the full state (ids included) each call without a separate read tool:.
-
#set_status(id:, status:) ⇒ Item
Update the status of the item whose
idmatches. - #size ⇒ Integer
Constructor Details
#initialize ⇒ List
45 46 47 48 49 |
# File 'lib/pikuri/tasks/list.rb', line 45 def initialize @items = [] @next_id = 1 @on_change = nil end |
Instance Attribute Details
#on_change ⇒ Proc?
Optional zero-argument hook invoked synchronously after every
successful mutation (a raise means nothing changed, so no
notify). Set by Extension#bind to emit a Pikuri::Tasks::ListChanged; nil
(default) disables notification.
57 58 59 |
# File 'lib/pikuri/tasks/list.rb', line 57 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.
85 86 87 88 89 90 91 92 93 |
# File 'lib/pikuri/tasks/list.rb', line 85 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 |
#clear ⇒ void
This method returns an undefined value.
Drop every item and rewind the id counter to 1, then fire
#on_change (always, even when already empty, so a UI listener
sees the cleared state). Rewinding @next_id is safe here — unlike
after #delete, a clear wipes the model's context too, so no stale
id survives to collide with a reused one.
138 139 140 141 142 143 |
# File 'lib/pikuri/tasks/list.rb', line 138 def clear @items = [] @next_id = 1 @on_change&.call nil end |
#delete(id) ⇒ Item
Remove the item whose id matches. The id is not reused for
later items (see #add).
122 123 124 125 126 127 128 129 |
# File 'lib/pikuri/tasks/list.rb', line 122 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
72 73 74 |
# File 'lib/pikuri/tasks/list.rb', line 72 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.
62 63 64 |
# File 'lib/pikuri/tasks/list.rb', line 62 def items @items.dup.freeze end |
#render ⇒ String
The canonical rendering every task tool returns as its observation, so the LLM sees the full state (ids included) each call without a separate read tool:
Empty renders as <tasks>(empty)</tasks> — an unambiguous "the
call worked, list is now empty" rather than a blank block.
159 160 161 162 163 164 |
# File 'lib/pikuri/tasks/list.rb', line 159 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.
103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/pikuri/tasks/list.rb', line 103 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
67 68 69 |
# File 'lib/pikuri/tasks/list.rb', line 67 def size @items.size end |