pikuri-tasks 0.0.7 → 0.1.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 +4 -4
- data/lib/pikuri/tasks/completed.rb +8 -8
- data/lib/pikuri/tasks/create.rb +20 -27
- data/lib/pikuri/tasks/delete.rb +11 -16
- data/lib/pikuri/tasks/extension.rb +38 -52
- data/lib/pikuri/tasks/in_progress.rb +14 -14
- data/lib/pikuri/tasks/list.rb +49 -52
- data/lib/pikuri/tasks/list_changed.rb +9 -17
- data/lib/pikuri-tasks.rb +2 -10
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bef39f17faad60151665971aac68c2090ca08d02e343ce209ca00cdc81e4e7c5
|
|
4
|
+
data.tar.gz: bc0e54db37e868a58015c8ff18f0a9aba2a7e1232310faa608fcd4e525e2aea8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6583cca82b6b2b8ac13169ce045420cb960a06157a2cc2f4da7f6ebbbbb232066ae58d9b484c4736bc509b4c78625265a5b84838cd44e9cbe2dbd94c5575fbb0
|
|
7
|
+
data.tar.gz: b778189760eb74d9ee7d882da3b308ed4bd8a020d21c2f480e6f2cc76b1868b64ea907e24f5ce132b688b14c09fcd38e8237e8825bf0a02398c31d99e077ad0e
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
5
|
# The +task_completed+ tool: mark the item with the given +id+ as
|
|
6
|
-
# +completed+. Same shape
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# item is addressed by the numeric +#id+ from the rendered list.
|
|
6
|
+
# +completed+. Same shape as {InProgress} (status in the tool name,
|
|
7
|
+
# item addressed by numeric +#id+). Returns {List#render}; a bad id
|
|
8
|
+
# returns +"Error: no such task id"+ plus the current list.
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# current list, so the LLM can pick the right id in one turn.
|
|
10
|
+
# Sharing: +P_one_agent+ — closes over one agent's {List}; see its
|
|
11
|
+
# +== Sharing+.
|
|
14
12
|
class Completed < Pikuri::Tool
|
|
15
13
|
# @return [String]
|
|
16
14
|
DESCRIPTION = <<~DESC
|
|
@@ -37,7 +35,9 @@ module Pikuri
|
|
|
37
35
|
},
|
|
38
36
|
execute: lambda { |id:|
|
|
39
37
|
Completed.execute(list: list, id: id)
|
|
40
|
-
}
|
|
38
|
+
},
|
|
39
|
+
# No legs; see {Create}.
|
|
40
|
+
trifecta_legs: Pikuri::Tool::TrifectaLegs::NONE
|
|
41
41
|
)
|
|
42
42
|
end
|
|
43
43
|
|
data/lib/pikuri/tasks/create.rb
CHANGED
|
@@ -2,28 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# The +task_create+ tool: mass-create pending items
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# description — and the splitter turned `[`, `"foo",`, `]` into
|
|
13
|
-
# garbage tasks. Match the prior instead of fighting it.)
|
|
5
|
+
# The +task_create+ tool: mass-create pending items from a JSON array
|
|
6
|
+
# of strings — a native +items+ array parameter, the shape the model's
|
|
7
|
+
# training prior produces unprompted. (Regression guard: don't revert
|
|
8
|
+
# to a newline-separated string. An earlier such design meant to spare
|
|
9
|
+
# small models the bracket-balancing, but models sent JSON arrays
|
|
10
|
+
# anyway and the splitter turned `[`, `"foo",`, `]` into garbage tasks
|
|
11
|
+
# — match the prior, don't fight it.)
|
|
14
12
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# next turn. Atomic semantics keep the list in a coherent state
|
|
21
|
-
# the LLM doesn't have to reconcile.
|
|
13
|
+
# Atomic: each element is stripped, and a blank or duplicate element
|
|
14
|
+
# (within the batch or already in the list) aborts the *whole* call
|
|
15
|
+
# with an +"Error: ..."+ string and adds nothing — the LLM resends a
|
|
16
|
+
# corrected batch, and never has to reconcile a half-applied list. On
|
|
17
|
+
# success returns {List#render}.
|
|
22
18
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# address items by — so the LLM always sees fresh state without a
|
|
26
|
-
# separate read tool.
|
|
19
|
+
# Sharing: +P_one_agent+ — closes over one agent's {List}; see its
|
|
20
|
+
# +== Sharing+.
|
|
27
21
|
class Create < Pikuri::Tool
|
|
28
22
|
# @return [String] static description shown to the LLM,
|
|
29
23
|
# opencode-shape (summary + +Usage:+ bullets).
|
|
@@ -32,14 +26,12 @@ module Pikuri
|
|
|
32
26
|
|
|
33
27
|
Usage:
|
|
34
28
|
- Use at the start of a multi-step task to capture the plan.
|
|
35
|
-
- `items` is a JSON array of strings — one task per element.
|
|
36
29
|
- A blank element, a duplicate (within the batch or already on the list), or an empty array aborts the whole call with `Error: ...` and adds nothing — resend a corrected batch.
|
|
37
30
|
- On success the full current list is returned, with each task's `#id` — use that id with `task_in_progress` / `task_completed` / `task_delete`.
|
|
38
31
|
DESC
|
|
39
32
|
|
|
40
|
-
# @param list [List] the shared per-Agent list, captured by
|
|
41
|
-
#
|
|
42
|
-
# same instance.
|
|
33
|
+
# @param list [List] the shared per-Agent list, captured by closure
|
|
34
|
+
# so every task tool mutates the same instance.
|
|
43
35
|
# @return [Create]
|
|
44
36
|
def initialize(list:)
|
|
45
37
|
super(
|
|
@@ -52,12 +44,13 @@ module Pikuri
|
|
|
52
44
|
},
|
|
53
45
|
execute: lambda { |items:|
|
|
54
46
|
Create.execute(list: list, items: items)
|
|
55
|
-
}
|
|
47
|
+
},
|
|
48
|
+
# No legs: an in-memory, per-agent list. Nothing read from disk, nothing sent.
|
|
49
|
+
trifecta_legs: Pikuri::Tool::TrifectaLegs::NONE
|
|
56
50
|
)
|
|
57
51
|
end
|
|
58
52
|
|
|
59
|
-
# Validate and apply the batch.
|
|
60
|
-
# without constructing a tool instance.
|
|
53
|
+
# Validate and apply the batch.
|
|
61
54
|
#
|
|
62
55
|
# @param list [List]
|
|
63
56
|
# @param items [Array<String>] raw +items+ argument from the LLM
|
data/lib/pikuri/tasks/delete.rb
CHANGED
|
@@ -2,22 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# The +task_delete+ tool: remove the item with the given +id
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# The +task_delete+ tool: remove the item with the given +id+.
|
|
6
|
+
# Distinct from {Completed} in intent — +completed+ means the work was
|
|
7
|
+
# done, +delete+ means the task shouldn't have been there — a
|
|
8
|
+
# distinction the tool names carry, not the list. A deleted id is never
|
|
9
|
+
# reused ({List#add}). Returns {List#render}; a bad id returns
|
|
10
|
+
# +"Error: no such task id"+ plus the current list.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# there. The list itself draws no such distinction once an item
|
|
13
|
-
# is gone, but the LLM picks the right verb because the tool
|
|
14
|
-
# names make the intent clear. A deleted item's id is never
|
|
15
|
-
# reused (see {List#add}), so a stale id errors loudly instead of
|
|
16
|
-
# silently hitting a newer task.
|
|
17
|
-
#
|
|
18
|
-
# Returns the rendered current list via {List#render} on success.
|
|
19
|
-
# On a bad id returns +"Error: no such task id: <id>"+ plus the
|
|
20
|
-
# current list, so the LLM can pick the right id in one turn.
|
|
12
|
+
# Sharing: +P_one_agent+ — closes over one agent's {List}; see its
|
|
13
|
+
# +== Sharing+.
|
|
21
14
|
class Delete < Pikuri::Tool
|
|
22
15
|
# @return [String]
|
|
23
16
|
DESCRIPTION = <<~DESC
|
|
@@ -43,7 +36,9 @@ module Pikuri
|
|
|
43
36
|
},
|
|
44
37
|
execute: lambda { |id:|
|
|
45
38
|
Delete.execute(list: list, id: id)
|
|
46
|
-
}
|
|
39
|
+
},
|
|
40
|
+
# No legs; see {Create}.
|
|
41
|
+
trifecta_legs: Pikuri::Tool::TrifectaLegs::NONE
|
|
47
42
|
)
|
|
48
43
|
end
|
|
49
44
|
|
|
@@ -7,46 +7,24 @@ module Pikuri
|
|
|
7
7
|
# event, and the {Extension} that wires them into an
|
|
8
8
|
# {Pikuri::Agent}.
|
|
9
9
|
module Tasks
|
|
10
|
-
# An {Pikuri::Agent::Extension} that auto-wires an in-memory
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# {ListChanged} emission so listeners can observe every list
|
|
15
|
-
# mutation.
|
|
16
|
-
#
|
|
17
|
-
# == Usage
|
|
10
|
+
# An {Pikuri::Agent::Extension} that auto-wires an in-memory task list
|
|
11
|
+
# onto an agent: constructs a fresh {List}, registers the four task
|
|
12
|
+
# tools against it, contributes a workflow snippet, and (in {#bind})
|
|
13
|
+
# arms {ListChanged} emission.
|
|
18
14
|
#
|
|
19
15
|
# Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
|
|
20
16
|
# c.add_extension Pikuri::Tasks::Extension.new
|
|
21
17
|
# end
|
|
22
18
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# Sub-agents do not inherit extensions (see CLAUDE.md's "Seams").
|
|
29
|
-
# Concretely: a sub-agent spawned by the +agent+ tool gets a
|
|
30
|
-
# fresh persona, fresh toolset, no task list. That keeps the
|
|
31
|
-
# parent's plan private to the parent and avoids the
|
|
32
|
-
# who-owns-which-task confusion a shared list would produce.
|
|
33
|
-
# If a host wants the sub-agent to also have a task list, it
|
|
34
|
-
# adds the extension to the sub-agent's own configurator —
|
|
35
|
-
# cleanly opt-in, no implicit sharing.
|
|
36
|
-
#
|
|
37
|
-
# == Empty default
|
|
38
|
-
#
|
|
39
|
-
# No +catalog+-style empty state: registering the extension
|
|
40
|
-
# always installs the four tools and the snippet. A host that
|
|
41
|
-
# doesn't want tasks simply omits the extension.
|
|
19
|
+
# Sub-agents don't inherit extensions, so a spawned sub-agent gets no
|
|
20
|
+
# task list — keeping the parent's plan private and avoiding
|
|
21
|
+
# who-owns-which-task confusion. A host wanting one adds the extension
|
|
22
|
+
# to the sub-agent's own configurator.
|
|
42
23
|
class Extension
|
|
43
24
|
include Pikuri::Agent::Extension
|
|
44
25
|
|
|
45
|
-
# System-prompt snippet appended once per agent.
|
|
46
|
-
#
|
|
47
|
-
# descriptions cover their own usage). Mirrors the shape of
|
|
48
|
-
# opencode's +todowrite.txt+ but condensed to fit pikuri's
|
|
49
|
-
# "short prose over abstract framing" docs convention.
|
|
26
|
+
# System-prompt snippet appended once per agent. Rules-of-thumb
|
|
27
|
+
# only, no inventory (the tool descriptions cover their own usage).
|
|
50
28
|
#
|
|
51
29
|
# @return [String]
|
|
52
30
|
PROMPT_SNIPPET = <<~PROMPT
|
|
@@ -65,8 +43,7 @@ module Pikuri
|
|
|
65
43
|
</tasks_usage>
|
|
66
44
|
PROMPT
|
|
67
45
|
|
|
68
|
-
# Tool classes the extension auto-registers.
|
|
69
|
-
# construction and for the duplicate-registration guard below.
|
|
46
|
+
# Tool classes the extension auto-registers.
|
|
70
47
|
TOOL_CLASSES = [Create, InProgress, Completed, Delete].freeze
|
|
71
48
|
|
|
72
49
|
# @return [Extension]
|
|
@@ -74,19 +51,16 @@ module Pikuri
|
|
|
74
51
|
@list = List.new
|
|
75
52
|
end
|
|
76
53
|
|
|
77
|
-
# @return [List] the per-agent list, exposed for tests.
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# wired by {#bind} instead (each carries an immutable
|
|
81
|
-
# snapshot safe to render from anywhere).
|
|
54
|
+
# @return [List] the per-agent list, exposed for tests. Do NOT read
|
|
55
|
+
# it from another thread — it's agent-thread-confined; consume the
|
|
56
|
+
# {ListChanged} events wired by {#bind} instead.
|
|
82
57
|
attr_reader :list
|
|
83
58
|
|
|
84
|
-
# Construct the four tools (each sharing +@list+)
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
# a different list.
|
|
59
|
+
# Construct the four tools (each sharing +@list+), register them,
|
|
60
|
+
# and append {PROMPT_SNIPPET}. Raises if any of the four were
|
|
61
|
+
# pre-registered via +c.add_tool+ — the extension is the single
|
|
62
|
+
# owner of the shared list, and a pre-registered tool would bind to
|
|
63
|
+
# a different one.
|
|
90
64
|
#
|
|
91
65
|
# @param c [Pikuri::Agent::Configurator]
|
|
92
66
|
# @return [void]
|
|
@@ -100,16 +74,15 @@ module Pikuri
|
|
|
100
74
|
end
|
|
101
75
|
|
|
102
76
|
TOOL_CLASSES.each { |cls| c.add_tool(cls.new(list: @list)) }
|
|
103
|
-
c.append_system_prompt(PROMPT_SNIPPET)
|
|
104
77
|
nil
|
|
105
78
|
end
|
|
106
79
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# {List}.
|
|
80
|
+
# @return [Array<String>] the static task-workflow snippet.
|
|
81
|
+
def system_prompt_snippets = [PROMPT_SNIPPET]
|
|
82
|
+
|
|
83
|
+
# Arm {List#on_change} to emit a {ListChanged} (carrying a fresh
|
|
84
|
+
# {List#items} snapshot) after every mutation — how a UI listener
|
|
85
|
+
# observes the list without touching the agent-thread-confined {List}.
|
|
113
86
|
#
|
|
114
87
|
# @param ctx [Pikuri::Agent::ExtensionContext]
|
|
115
88
|
# @return [void]
|
|
@@ -117,6 +90,19 @@ module Pikuri
|
|
|
117
90
|
@list.on_change = -> { ctx.emit_event(ListChanged.new(items: @list.items)) }
|
|
118
91
|
nil
|
|
119
92
|
end
|
|
93
|
+
|
|
94
|
+
# Clear the task list on conversation reset — a plan is meaningless
|
|
95
|
+
# without the conversation that produced it. {List#clear} fires the
|
|
96
|
+
# armed +on_change+, so an empty {ListChanged} reaches the listener
|
|
97
|
+
# stream like any other mutation.
|
|
98
|
+
#
|
|
99
|
+
# @param ctx [Pikuri::Agent::ExtensionContext] unused; part of the
|
|
100
|
+
# protocol signature.
|
|
101
|
+
# @return [void]
|
|
102
|
+
def on_conversation_reset(_ctx)
|
|
103
|
+
@list.clear
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
120
106
|
end
|
|
121
107
|
end
|
|
122
108
|
end
|
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# The +task_in_progress+ tool: mark the item with the given +id+
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# vs +"inprogress"+ typos) at the cost of one extra tool class.
|
|
5
|
+
# The +task_in_progress+ tool: mark the item with the given +id+ as
|
|
6
|
+
# +in_progress+. The status is baked into the tool name, not a
|
|
7
|
+
# parameter — one fewer degree of freedom for the LLM (no
|
|
8
|
+
# +"in-progress"+/+"inprogress"+ typos) at the cost of one tool class.
|
|
10
9
|
#
|
|
11
|
-
# Items are addressed by
|
|
12
|
-
#
|
|
13
|
-
# one near-miss
|
|
14
|
-
#
|
|
15
|
-
#
|
|
10
|
+
# Items are addressed by numeric +#id+, not content string (regression
|
|
11
|
+
# guard: an earlier content-as-id design locked the LLM out of its own
|
|
12
|
+
# list on one near-miss byte — a stray quote or trailing comma; an id
|
|
13
|
+
# is two digits it just read back). Returns {List#render}; a bad id
|
|
14
|
+
# returns +"Error: no such task id"+ plus the current list.
|
|
16
15
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# current list, so the LLM can pick the right id in one turn.
|
|
16
|
+
# Sharing: +P_one_agent+ — closes over one agent's {List}; see its
|
|
17
|
+
# +== Sharing+.
|
|
20
18
|
class InProgress < Pikuri::Tool
|
|
21
19
|
# @return [String]
|
|
22
20
|
DESCRIPTION = <<~DESC
|
|
@@ -42,7 +40,9 @@ module Pikuri
|
|
|
42
40
|
},
|
|
43
41
|
execute: lambda { |id:|
|
|
44
42
|
InProgress.execute(list: list, id: id)
|
|
45
|
-
}
|
|
43
|
+
},
|
|
44
|
+
# No legs; see {Create}.
|
|
45
|
+
trifecta_legs: Pikuri::Tool::TrifectaLegs::NONE
|
|
46
46
|
)
|
|
47
47
|
end
|
|
48
48
|
|
data/lib/pikuri/tasks/list.rb
CHANGED
|
@@ -2,55 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# Raised by {List#add} when a new item's +content+ already appears
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# plan), and catching it early keeps the list coherent. The
|
|
10
|
-
# exception carries the offending content string so the tool layer
|
|
11
|
-
# can surface it verbatim in the +"Error: ..."+ observation.
|
|
5
|
+
# Raised by {List#add} when a new item's +content+ already appears in
|
|
6
|
+
# the list — rejected because a duplicate is almost always an LLM
|
|
7
|
+
# mistake (re-sending an already-captured plan). Carries the offending
|
|
8
|
+
# content for the +"Error: ..."+ observation.
|
|
12
9
|
class DuplicateItem < StandardError; end
|
|
13
10
|
|
|
14
|
-
# Raised by {List#set_status} and {List#delete} when no item with
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# render, so the LLM can pick the right id on the next turn.
|
|
11
|
+
# Raised by {List#set_status} and {List#delete} when no item with the
|
|
12
|
+
# given +id+ exists; the mutation tools rescue it into an +"Error: no
|
|
13
|
+
# such task id"+ observation.
|
|
18
14
|
class ItemNotFound < StandardError; end
|
|
19
15
|
|
|
20
|
-
# Allowed values for {Item#status}.
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# +
|
|
24
|
-
# fourth status.
|
|
16
|
+
# Allowed values for {Item#status}. Deliberately three — the workflow
|
|
17
|
+
# the prompt advertises (pending → in_progress → completed) — with
|
|
18
|
+
# +task_delete+ removing items outright rather than adding a fourth
|
|
19
|
+
# +cancelled+/+deleted+ status.
|
|
25
20
|
STATUSES = %w[pending in_progress completed].freeze
|
|
26
21
|
|
|
27
22
|
# One row in a {List}. +id+ is the numeric identifier the mutation
|
|
28
|
-
# tools address the item by (assigned by {List#add}, never reused
|
|
29
|
-
#
|
|
30
|
-
# one of {STATUSES}.
|
|
23
|
+
# tools address the item by (assigned by {List#add}, never reused);
|
|
24
|
+
# +content+ is the LLM-visible task text; +status+ is one of {STATUSES}.
|
|
31
25
|
Item = Data.define(:id, :content, :status)
|
|
32
26
|
|
|
33
27
|
# An in-memory ordered list of {Item}s, scoped to a single
|
|
34
|
-
# {Pikuri::Agent}
|
|
35
|
-
#
|
|
36
|
-
# so every mutation hits the same instance.
|
|
28
|
+
# {Pikuri::Agent}, shared by closure into all four task tools so every
|
|
29
|
+
# mutation hits the same instance.
|
|
37
30
|
#
|
|
38
|
-
#
|
|
31
|
+
# Not persisted: dropped when the +Agent+ is collected, matching the
|
|
32
|
+
# gem's "in-memory only" scope.
|
|
39
33
|
#
|
|
40
|
-
#
|
|
41
|
-
# single-threaded with respect to tool calls (ruby_llm dispatches
|
|
42
|
-
# them sequentially), so no locking. Other threads (e.g. a web UI
|
|
43
|
-
# rendering the list) must not touch a +List+ directly — they
|
|
44
|
-
# consume the {ListChanged} events {Extension#bind} wires onto
|
|
45
|
-
# the listener stream, whose +items+ payload is an immutable
|
|
46
|
-
# snapshot safe to hand across threads. A future
|
|
47
|
-
# parallel-tool-execution feature would need a +Mutex+ here.
|
|
34
|
+
# == Sharing
|
|
48
35
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
36
|
+
# +P_one_agent+ — this is one conversation's plan, so two agents on one
|
|
37
|
+
# list would read and complete each other's tasks. Unguarded, and it needs
|
|
38
|
+
# no lock because its only caller is the agent whose tool calls are already
|
|
39
|
+
# serialized. Another thread (a UI rendering the list) must not touch a
|
|
40
|
+
# +List+ directly: it consumes the {ListChanged} events {Extension#bind}
|
|
41
|
+
# wires onto the listener stream, whose +items+ payload is an immutable
|
|
42
|
+
# snapshot safe to hand across threads.
|
|
54
43
|
class List
|
|
55
44
|
# @return [List]
|
|
56
45
|
def initialize
|
|
@@ -59,13 +48,10 @@ module Pikuri
|
|
|
59
48
|
@on_change = nil
|
|
60
49
|
end
|
|
61
50
|
|
|
62
|
-
# Optional zero-argument hook invoked after every
|
|
63
|
-
# mutation (
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# +nil+ (the default) disables notification. Runs on the
|
|
67
|
-
# mutating (agent) thread, synchronously inside the mutation
|
|
68
|
-
# call.
|
|
51
|
+
# Optional zero-argument hook invoked synchronously after every
|
|
52
|
+
# *successful* mutation (a raise means nothing changed, so no
|
|
53
|
+
# notify). Set by {Extension#bind} to emit a {ListChanged}; +nil+
|
|
54
|
+
# (default) disables notification.
|
|
69
55
|
#
|
|
70
56
|
# @return [Proc, nil]
|
|
71
57
|
attr_accessor :on_change
|
|
@@ -95,8 +81,7 @@ module Pikuri
|
|
|
95
81
|
# @param content [String] non-empty content; whitespace is the
|
|
96
82
|
# caller's responsibility.
|
|
97
83
|
# @return [Item] the newly added item
|
|
98
|
-
# @raise [DuplicateItem] if an item with the same +content+
|
|
99
|
-
# already exists (a duplicate is almost always an LLM mistake).
|
|
84
|
+
# @raise [DuplicateItem] if an item with the same +content+ exists.
|
|
100
85
|
def add(content)
|
|
101
86
|
raise DuplicateItem, content if @items.any? { |i| i.content == content }
|
|
102
87
|
|
|
@@ -143,10 +128,23 @@ module Pikuri
|
|
|
143
128
|
removed
|
|
144
129
|
end
|
|
145
130
|
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
131
|
+
# Drop every item and rewind the id counter to 1, then fire
|
|
132
|
+
# {#on_change} (always, even when already empty, so a UI listener
|
|
133
|
+
# sees the cleared state). Rewinding +@next_id+ is safe here — unlike
|
|
134
|
+
# after {#delete}, a clear wipes the model's context too, so no stale
|
|
135
|
+
# id survives to collide with a reused one.
|
|
136
|
+
#
|
|
137
|
+
# @return [void]
|
|
138
|
+
def clear
|
|
139
|
+
@items = []
|
|
140
|
+
@next_id = 1
|
|
141
|
+
@on_change&.call
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The canonical rendering every task tool returns as its
|
|
146
|
+
# observation, so the LLM sees the full state (ids included) each
|
|
147
|
+
# call without a separate read tool:
|
|
150
148
|
#
|
|
151
149
|
# <tasks>
|
|
152
150
|
# - #1 [pending] Add dark mode toggle
|
|
@@ -154,9 +152,8 @@ module Pikuri
|
|
|
154
152
|
# - #3 [completed] Update README
|
|
155
153
|
# </tasks>
|
|
156
154
|
#
|
|
157
|
-
# Empty
|
|
158
|
-
#
|
|
159
|
-
# signal rather than an ambiguous blank block.
|
|
155
|
+
# Empty renders as +<tasks>(empty)</tasks>+ — an unambiguous "the
|
|
156
|
+
# call worked, list is now empty" rather than a blank block.
|
|
160
157
|
#
|
|
161
158
|
# @return [String]
|
|
162
159
|
def render
|
|
@@ -2,25 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# Domain event emitted
|
|
6
|
-
#
|
|
7
|
-
# {Extension#bind} via
|
|
5
|
+
# Domain event emitted after every {List} mutation (add / status
|
|
6
|
+
# change / delete), wired by {Extension#bind} via
|
|
8
7
|
# {Pikuri::Agent::ExtensionContext#emit_event}. Carries a frozen
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# serialize the payload as-is.
|
|
8
|
+
# whole-list snapshot ({List#items}), so a consumer needs no +List+
|
|
9
|
+
# reference — render or serialize the payload as-is.
|
|
12
10
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# (last-wins) rather than diffing event-by-event.
|
|
19
|
-
#
|
|
20
|
-
# Fired on the agent's thread. A listener feeding another thread
|
|
21
|
-
# (e.g. a web UI pushing over SSE) should serialize inside
|
|
22
|
-
# +on_event+ and hand off only the immutable result — see the
|
|
23
|
-
# Concurrency note on {List}.
|
|
11
|
+
# A batch +task_create+ emits one event per added item, so a renderer
|
|
12
|
+
# should treat the latest snapshot as authoritative (last-wins), not
|
|
13
|
+
# diff event-by-event. Fired on the agent's thread — a listener
|
|
14
|
+
# feeding another thread serializes inside +on_event+ and hands off
|
|
15
|
+
# only the immutable result (see {List}'s thread-confinement note).
|
|
24
16
|
ListChanged = Data.define(:items)
|
|
25
17
|
end
|
|
26
18
|
end
|
data/lib/pikuri-tasks.rb
CHANGED
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pikuri-core'
|
|
4
4
|
|
|
5
|
-
# Entry file for the pikuri-tasks gem
|
|
6
|
-
# loader
|
|
7
|
-
# +Pikuri::+ namespace alongside pikuri-core. After +require
|
|
8
|
-
# 'pikuri-tasks'+, +Pikuri::Tasks::List+, the four task tool classes
|
|
9
|
-
# (+Create+, +InProgress+, +Completed+, +Delete+), and
|
|
10
|
-
# +Pikuri::Tasks::Extension+ are all defined.
|
|
11
|
-
#
|
|
12
|
-
# The loader is per-gem (not shared with pikuri-core's loader) so each
|
|
13
|
-
# gem owns its own +lib/+ tree and the cooperation between gems is via
|
|
14
|
-
# the Pikuri namespace alone.
|
|
5
|
+
# Entry file for the pikuri-tasks gem: sets up the per-gem Zeitwerk
|
|
6
|
+
# loader. See {Pikuri::Tasks} for the namespace overview.
|
|
15
7
|
module Pikuri
|
|
16
8
|
module Tasks
|
|
17
9
|
LOADER = Zeitwerk::Loader.new
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pikuri-tasks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Vysny
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0
|
|
18
|
+
version: 0.1.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0
|
|
25
|
+
version: 0.1.0
|
|
26
26
|
description: |
|
|
27
27
|
pikuri-tasks gives a pikuri-core agent an in-memory task list it
|
|
28
28
|
can use to plan and track multi-step work. A +Pikuri::Tasks::List+
|