Class: Pikuri::Tasks::List

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initializeList



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_changeProc?

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.

Returns:

  • (Proc, nil)


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.

Parameters:

  • content (String)

    non-empty content; whitespace is the caller's responsibility.

Returns:

  • (Item)

    the newly added item

Raises:



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

#clearvoid

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).

Parameters:

  • id (Integer)

Returns:

  • (Item)

    the removed item.

Raises:



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

Returns:

  • (Boolean)


72
73
74
# File 'lib/pikuri/tasks/list.rb', line 72

def empty?
  @items.empty?
end

#itemsArray<Item>

Returns a frozen snapshot of the current items, in insertion order. Callers cannot mutate the internal storage through this accessor.

Returns:

  • (Array<Item>)

    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

#renderString

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:

- #1 [pending] Add dark mode toggle - #2 [in_progress] Write unit tests - #3 [completed] Update README

Empty renders as <tasks>(empty)</tasks> — an unambiguous "the call worked, list is now empty" rather than a blank block.

Returns:

  • (String)


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.

Parameters:

  • id (Integer)
  • status (String)

    one of STATUSES.

Returns:

  • (Item)

    the updated item (a fresh frozen Data instance — the old one is replaced in place).

Raises:



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

#sizeInteger

Returns:

  • (Integer)


67
68
69
# File 'lib/pikuri/tasks/list.rb', line 67

def size
  @items.size
end