pikuri-tasks 0.0.6 → 0.0.7
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/README.md +16 -14
- data/lib/pikuri/tasks/completed.rb +19 -19
- data/lib/pikuri/tasks/create.rb +30 -23
- data/lib/pikuri/tasks/delete.rb +21 -19
- data/lib/pikuri/tasks/extension.rb +30 -11
- data/lib/pikuri/tasks/in_progress.rb +25 -21
- data/lib/pikuri/tasks/list.rb +65 -43
- data/lib/pikuri/tasks/list_changed.rb +26 -0
- metadata +6 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 113302bc63d0e9bd10c56550592d8e0aee68212fd1ee2cfe4032b8d1f2836b2c
|
|
4
|
+
data.tar.gz: 7d9e4351247953a5b7d7f5e54acbd68ace82404966bdeca47389b81804f2714e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b287b65b9b99bd77966901c622aa503942cd02b09bbc8b10a2a43451dac01754136666bb7df53473990d31eed5ff2a5bd41213bdff08635bfe9657d92bce1412
|
|
7
|
+
data.tar.gz: 760eed62da11196fd09c3bcbb5bd8036e3697683bedd6d7c3eeb86ed5ce1aa2baeaf325b1780bb9ab285ef38ee12d8ec3b65773b794945cbf6ff76898b17b64f
|
data/README.md
CHANGED
|
@@ -5,19 +5,19 @@ In-memory task list + four LLM-facing tools for the
|
|
|
5
5
|
|
|
6
6
|
Provides:
|
|
7
7
|
- `Pikuri::Tasks::List` — per-Agent in-memory list of
|
|
8
|
-
`(content, status)` items. Status is one of `pending`,
|
|
8
|
+
`(id, content, status)` items. Status is one of `pending`,
|
|
9
9
|
`in_progress`, `completed`. Nothing is written to disk.
|
|
10
10
|
- Four tool classes, all sharing one `List` instance:
|
|
11
11
|
- `Pikuri::Tasks::Create` (`task_create`) — mass-create pending
|
|
12
|
-
items from a
|
|
13
|
-
|
|
14
|
-
nothing is added.
|
|
12
|
+
items from a JSON array of strings. Atomic: if any element is
|
|
13
|
+
blank or a duplicate (within the batch or already on the
|
|
14
|
+
list), nothing is added.
|
|
15
15
|
- `Pikuri::Tasks::InProgress` (`task_in_progress`) — mark an item
|
|
16
|
-
as `in_progress` by
|
|
16
|
+
as `in_progress` by numeric id.
|
|
17
17
|
- `Pikuri::Tasks::Completed` (`task_completed`) — mark an item as
|
|
18
|
-
`completed` by
|
|
18
|
+
`completed` by numeric id.
|
|
19
19
|
- `Pikuri::Tasks::Delete` (`task_delete`) — remove an item by
|
|
20
|
-
|
|
20
|
+
numeric id.
|
|
21
21
|
- `Pikuri::Tasks::Extension` — wires the four tools + a brief
|
|
22
22
|
`<tasks_usage>` workflow snippet into a `Pikuri::Agent` via the
|
|
23
23
|
`c.add_extension(...)` block API.
|
|
@@ -25,10 +25,12 @@ Provides:
|
|
|
25
25
|
Two shape choices worth flagging:
|
|
26
26
|
- **Status is baked into the tool name** (no `status:` parameter
|
|
27
27
|
with an enum). Removes the `"in-progress"` vs `"in_progress"`
|
|
28
|
-
vs `"inprogress"` typo failure mode
|
|
29
|
-
- **
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
vs `"inprogress"` typo failure mode.
|
|
29
|
+
- **Items are addressed by numeric id** — assigned on create,
|
|
30
|
+
shown in every rendered list, never reused after a delete. A
|
|
31
|
+
near-miss when re-typing the task's content cannot lock the
|
|
32
|
+
model out of its own list; duplicates are still rejected on
|
|
33
|
+
`task_create` because they are almost always a mistake.
|
|
32
34
|
|
|
33
35
|
## Install
|
|
34
36
|
|
|
@@ -59,9 +61,9 @@ so the LLM always sees fresh state without a separate read tool:
|
|
|
59
61
|
|
|
60
62
|
```
|
|
61
63
|
<tasks>
|
|
62
|
-
- [pending] Add dark mode toggle
|
|
63
|
-
- [in_progress] Write unit tests
|
|
64
|
-
- [completed] Update README
|
|
64
|
+
- #1 [pending] Add dark mode toggle
|
|
65
|
+
- #2 [in_progress] Write unit tests
|
|
66
|
+
- #3 [completed] Update README
|
|
65
67
|
</tasks>
|
|
66
68
|
```
|
|
67
69
|
|
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# The +task_completed+ tool: mark the item
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# The +task_completed+ tool: mark the item with the given +id+ as
|
|
6
|
+
# +completed+. Same shape and rationale as {InProgress} — the
|
|
7
|
+
# status is baked into the tool name to remove the enum-typo
|
|
8
|
+
# failure mode (+"completed"+ / +"complete"+ / +"done"+), and the
|
|
9
|
+
# item is addressed by the numeric +#id+ from the rendered list.
|
|
10
10
|
#
|
|
11
|
-
# Returns the rendered current list via {List#render} on success
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Returns the rendered current list via {List#render} on success.
|
|
12
|
+
# On a bad id returns +"Error: no such task id: <id>"+ plus the
|
|
13
|
+
# current list, so the LLM can pick the right id in one turn.
|
|
14
14
|
class Completed < Pikuri::Tool
|
|
15
15
|
# @return [String]
|
|
16
16
|
DESCRIPTION = <<~DESC
|
|
17
17
|
Mark a task as `completed` once the work — including any required verification — is actually done.
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
20
|
-
- Pass the
|
|
20
|
+
- Pass the task's numeric `id` as shown in the rendered list (`- #3 [in_progress] ...` → id 3).
|
|
21
21
|
- Do NOT mark `completed` based on intent; mark it only after the underlying work is verified.
|
|
22
22
|
- If the work is partially done or blocked, leave the task `in_progress` and add a follow-up via `task_create`.
|
|
23
|
-
- On `Error: no such task: ...` the call did nothing —
|
|
23
|
+
- On `Error: no such task id: ...` the call did nothing — the error includes the current list; pick the right id from it.
|
|
24
24
|
- On success the full current list is returned for you to read back.
|
|
25
25
|
DESC
|
|
26
26
|
|
|
@@ -31,24 +31,24 @@ module Pikuri
|
|
|
31
31
|
name: 'task_completed',
|
|
32
32
|
description: DESCRIPTION,
|
|
33
33
|
parameters: Pikuri::Tool::Parameters.build { |p|
|
|
34
|
-
p.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
p.required_integer :id,
|
|
35
|
+
'Numeric id of the existing task to mark as ' \
|
|
36
|
+
'completed, as shown in the rendered list, e.g. 3.'
|
|
37
37
|
},
|
|
38
|
-
execute: lambda { |
|
|
39
|
-
Completed.execute(list: list,
|
|
38
|
+
execute: lambda { |id:|
|
|
39
|
+
Completed.execute(list: list, id: id)
|
|
40
40
|
}
|
|
41
41
|
)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# @param list [List]
|
|
45
|
-
# @param
|
|
45
|
+
# @param id [Integer]
|
|
46
46
|
# @return [String]
|
|
47
|
-
def self.execute(list:,
|
|
48
|
-
list.set_status(
|
|
47
|
+
def self.execute(list:, id:)
|
|
48
|
+
list.set_status(id: id, status: 'completed')
|
|
49
49
|
list.render
|
|
50
50
|
rescue ItemNotFound
|
|
51
|
-
"Error: no such task:
|
|
51
|
+
"Error: no such task id: #{id}. Current list:\n#{list.render}"
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
data/lib/pikuri/tasks/create.rb
CHANGED
|
@@ -3,22 +3,27 @@
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
5
|
# The +task_create+ tool: mass-create pending items in a single
|
|
6
|
-
# call from a
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
6
|
+
# call from a JSON array of strings — a native +items+ array
|
|
7
|
+
# parameter, the shape every mainstream harness's task tool uses
|
|
8
|
+
# and therefore the shape the model's training prior produces
|
|
9
|
+
# unprompted. (An earlier newline-separated-string design tried to
|
|
10
|
+
# spare small models the bracket-balancing; in practice models
|
|
11
|
+
# sent JSON arrays anyway — the prior beats the parameter
|
|
12
|
+
# description — and the splitter turned `[`, `"foo",`, `]` into
|
|
13
|
+
# garbage tasks. Match the prior instead of fighting it.)
|
|
12
14
|
#
|
|
13
|
-
# Each
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# Each element is whitespace-stripped; a blank element aborts the
|
|
16
|
+
# call (it is always an LLM mistake, never intent). If any input
|
|
17
|
+
# is a duplicate (within the batch, or already in the list), the
|
|
18
|
+
# whole call likewise aborts with an +"Error: ..."+ string and
|
|
19
|
+
# nothing is added — the LLM resends a corrected batch on the
|
|
17
20
|
# next turn. Atomic semantics keep the list in a coherent state
|
|
18
21
|
# the LLM doesn't have to reconcile.
|
|
19
22
|
#
|
|
20
|
-
# On success returns the rendered current list via {List#render}
|
|
21
|
-
#
|
|
23
|
+
# On success returns the rendered current list via {List#render} —
|
|
24
|
+
# including each new item's +#id+, which the three mutation tools
|
|
25
|
+
# address items by — so the LLM always sees fresh state without a
|
|
26
|
+
# separate read tool.
|
|
22
27
|
class Create < Pikuri::Tool
|
|
23
28
|
# @return [String] static description shown to the LLM,
|
|
24
29
|
# opencode-shape (summary + +Usage:+ bullets).
|
|
@@ -27,10 +32,9 @@ module Pikuri
|
|
|
27
32
|
|
|
28
33
|
Usage:
|
|
29
34
|
- Use at the start of a multi-step task to capture the plan.
|
|
30
|
-
- `items` is a
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- On success the full current list is returned for you to read back.
|
|
35
|
+
- `items` is a JSON array of strings — one task per element.
|
|
36
|
+
- 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
|
+
- On success the full current list is returned, with each task's `#id` — use that id with `task_in_progress` / `task_completed` / `task_delete`.
|
|
34
38
|
DESC
|
|
35
39
|
|
|
36
40
|
# @param list [List] the shared per-Agent list, captured by
|
|
@@ -42,10 +46,9 @@ module Pikuri
|
|
|
42
46
|
name: 'task_create',
|
|
43
47
|
description: DESCRIPTION,
|
|
44
48
|
parameters: Pikuri::Tool::Parameters.build { |p|
|
|
45
|
-
p.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'Blank lines are ignored.'
|
|
49
|
+
p.required_string_array :items,
|
|
50
|
+
'Task contents, one task per array element, e.g. ' \
|
|
51
|
+
'["Add dark mode toggle", "Write unit tests", "Update README"].'
|
|
49
52
|
},
|
|
50
53
|
execute: lambda { |items:|
|
|
51
54
|
Create.execute(list: list, items: items)
|
|
@@ -57,12 +60,16 @@ module Pikuri
|
|
|
57
60
|
# without constructing a tool instance.
|
|
58
61
|
#
|
|
59
62
|
# @param list [List]
|
|
60
|
-
# @param items [String] raw +items+ argument from the LLM
|
|
63
|
+
# @param items [Array<String>] raw +items+ argument from the LLM
|
|
64
|
+
# (already type-validated by {Pikuri::Tool::Parameters}).
|
|
61
65
|
# @return [String] either {List#render} on success or an
|
|
62
66
|
# +"Error: ..."+ string the LLM can react to.
|
|
63
67
|
def self.execute(list:, items:)
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
return 'Error: task_create requires at least one item' if items.empty?
|
|
69
|
+
|
|
70
|
+
cleaned = items.map(&:strip)
|
|
71
|
+
blank = cleaned.index('')
|
|
72
|
+
return "Error: blank item at index #{blank} — every element must be non-blank task text" if blank
|
|
66
73
|
|
|
67
74
|
seen_in_batch = {}
|
|
68
75
|
cleaned.each do |c|
|
data/lib/pikuri/tasks/delete.rb
CHANGED
|
@@ -2,29 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# The +task_delete+ tool: remove the item
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# The +task_delete+ tool: remove the item with the given +id+ from
|
|
6
|
+
# the list. Used to drop items that turned out to be unnecessary,
|
|
7
|
+
# were created in error, or have been superseded — rather than
|
|
8
|
+
# leaving them sitting in +pending+ as visual noise.
|
|
9
9
|
#
|
|
10
10
|
# Distinct from {Completed}: +completed+ means the work was
|
|
11
11
|
# actually done; +delete+ means the task should never have been
|
|
12
12
|
# there. The list itself draws no such distinction once an item
|
|
13
13
|
# is gone, but the LLM picks the right verb because the tool
|
|
14
|
-
# names make the intent clear.
|
|
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.
|
|
15
17
|
#
|
|
16
|
-
# Returns the rendered current list via {List#render} on success
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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.
|
|
19
21
|
class Delete < Pikuri::Tool
|
|
20
22
|
# @return [String]
|
|
21
23
|
DESCRIPTION = <<~DESC
|
|
22
24
|
Remove a task from the list. Use this for items that turn out not to be needed, were created in error, or have been superseded.
|
|
23
25
|
|
|
24
26
|
Usage:
|
|
25
|
-
- Pass the
|
|
27
|
+
- Pass the task's numeric `id` as shown in the rendered list (`- #3 [pending] ...` → id 3).
|
|
26
28
|
- Use `task_completed` (not this) when the work was actually done.
|
|
27
|
-
- On `Error: no such task: ...` the call did nothing —
|
|
29
|
+
- On `Error: no such task id: ...` the call did nothing — the error includes the current list; pick the right id from it.
|
|
28
30
|
- On success the full current list is returned for you to read back.
|
|
29
31
|
DESC
|
|
30
32
|
|
|
@@ -35,24 +37,24 @@ module Pikuri
|
|
|
35
37
|
name: 'task_delete',
|
|
36
38
|
description: DESCRIPTION,
|
|
37
39
|
parameters: Pikuri::Tool::Parameters.build { |p|
|
|
38
|
-
p.
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
p.required_integer :id,
|
|
41
|
+
'Numeric id of the existing task to remove, ' \
|
|
42
|
+
'as shown in the rendered list, e.g. 3.'
|
|
41
43
|
},
|
|
42
|
-
execute: lambda { |
|
|
43
|
-
Delete.execute(list: list,
|
|
44
|
+
execute: lambda { |id:|
|
|
45
|
+
Delete.execute(list: list, id: id)
|
|
44
46
|
}
|
|
45
47
|
)
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
# @param list [List]
|
|
49
|
-
# @param
|
|
51
|
+
# @param id [Integer]
|
|
50
52
|
# @return [String]
|
|
51
|
-
def self.execute(list:,
|
|
52
|
-
list.delete(
|
|
53
|
+
def self.execute(list:, id:)
|
|
54
|
+
list.delete(id)
|
|
53
55
|
list.render
|
|
54
56
|
rescue ItemNotFound
|
|
55
|
-
"Error: no such task:
|
|
57
|
+
"Error: no such task id: #{id}. Current list:\n#{list.render}"
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
end
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
# Namespace for the in-memory task-list feature. Holds the
|
|
5
5
|
# {List} value type, the four task tool classes ({Create},
|
|
6
|
-
# {InProgress}, {Completed}, {Delete}),
|
|
7
|
-
# wires them into an
|
|
6
|
+
# {InProgress}, {Completed}, {Delete}), the {ListChanged} domain
|
|
7
|
+
# event, and the {Extension} that wires them into an
|
|
8
|
+
# {Pikuri::Agent}.
|
|
8
9
|
module Tasks
|
|
9
10
|
# An {Pikuri::Agent::Extension} that auto-wires an in-memory
|
|
10
11
|
# task list onto an agent: constructs a fresh {List}, registers
|
|
11
|
-
# the four task tool classes against it,
|
|
12
|
-
# workflow snippet to the system prompt
|
|
12
|
+
# the four task tool classes against it, appends a brief
|
|
13
|
+
# workflow snippet to the system prompt, and (in {#bind}) arms
|
|
14
|
+
# {ListChanged} emission so listeners can observe every list
|
|
15
|
+
# mutation.
|
|
13
16
|
#
|
|
14
17
|
# == Usage
|
|
15
18
|
#
|
|
@@ -51,14 +54,14 @@ module Pikuri
|
|
|
51
54
|
You have an in-memory task list. Use it to plan and track multi-step work.
|
|
52
55
|
|
|
53
56
|
Workflow:
|
|
54
|
-
- When a task has 3+ steps, call `task_create` once with the full plan (
|
|
55
|
-
- Before starting an item, call `task_in_progress` with its
|
|
56
|
-
- When an item is fully done (including any required verification), call `task_completed` with its
|
|
57
|
+
- When a task has 3+ steps, call `task_create` once with the full plan (a JSON array of strings, all start as `pending`).
|
|
58
|
+
- Before starting an item, call `task_in_progress` with its numeric id. Keep exactly one item `in_progress` at a time.
|
|
59
|
+
- When an item is fully done (including any required verification), call `task_completed` with its numeric id.
|
|
57
60
|
- Use `task_delete` to remove items that turn out not to be needed.
|
|
58
61
|
|
|
59
62
|
Skip task tracking entirely for single-step or purely informational requests — it adds noise, not value.
|
|
60
63
|
|
|
61
|
-
Every mutation returns the full current list, so you do not need a separate read tool.
|
|
64
|
+
Every mutation returns the full current list, with each task's id (`- #3 [pending] ...` → id 3), so you do not need a separate read tool. Ids never change and are never reused.
|
|
62
65
|
</tasks_usage>
|
|
63
66
|
PROMPT
|
|
64
67
|
|
|
@@ -71,9 +74,11 @@ module Pikuri
|
|
|
71
74
|
@list = List.new
|
|
72
75
|
end
|
|
73
76
|
|
|
74
|
-
# @return [List] the per-agent list, exposed for tests
|
|
75
|
-
# hosts
|
|
76
|
-
#
|
|
77
|
+
# @return [List] the per-agent list, exposed for tests. UI
|
|
78
|
+
# hosts should NOT read it from another thread — the list is
|
|
79
|
+
# agent-thread-confined; consume the {ListChanged} events
|
|
80
|
+
# wired by {#bind} instead (each carries an immutable
|
|
81
|
+
# snapshot safe to render from anywhere).
|
|
77
82
|
attr_reader :list
|
|
78
83
|
|
|
79
84
|
# Construct the four tools (each sharing +@list+) and register
|
|
@@ -98,6 +103,20 @@ module Pikuri
|
|
|
98
103
|
c.append_system_prompt(PROMPT_SNIPPET)
|
|
99
104
|
nil
|
|
100
105
|
end
|
|
106
|
+
|
|
107
|
+
# Arm {List#on_change} to emit a {ListChanged} (carrying a
|
|
108
|
+
# fresh {List#items} snapshot) onto the agent's listener
|
|
109
|
+
# stream after every mutation. This is what lets a UI listener
|
|
110
|
+
# observe the task list without ever touching the
|
|
111
|
+
# agent-thread-confined {List} — see the Concurrency note on
|
|
112
|
+
# {List}.
|
|
113
|
+
#
|
|
114
|
+
# @param ctx [Pikuri::Agent::ExtensionContext]
|
|
115
|
+
# @return [void]
|
|
116
|
+
def bind(ctx)
|
|
117
|
+
@list.on_change = -> { ctx.emit_event(ListChanged.new(items: @list.items)) }
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
101
120
|
end
|
|
102
121
|
end
|
|
103
122
|
end
|
|
@@ -2,26 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
|
-
# The +task_in_progress+ tool: mark the item
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# extra tool class.
|
|
5
|
+
# The +task_in_progress+ tool: mark the item with the given +id+
|
|
6
|
+
# as +in_progress+. The status name is baked into the tool name
|
|
7
|
+
# rather than passed as a parameter — that takes one degree of
|
|
8
|
+
# freedom away from the LLM (no +"in-progress"+ vs +"in_progress"+
|
|
9
|
+
# vs +"inprogress"+ typos) at the cost of one extra tool class.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# Items are addressed by the numeric +#id+ shown in every rendered
|
|
12
|
+
# list (an earlier design used the content string as identifier;
|
|
13
|
+
# one near-miss in reproducing the exact bytes — a stray quote, a
|
|
14
|
+
# trailing comma — and the LLM is locked out of its own list. An
|
|
15
|
+
# id is two digits it just read back; nothing to mis-transcribe.)
|
|
16
|
+
#
|
|
17
|
+
# Returns the rendered current list via {List#render} on success.
|
|
18
|
+
# On a bad id returns +"Error: no such task id: <id>"+ plus the
|
|
19
|
+
# current list, so the LLM can pick the right id in one turn.
|
|
16
20
|
class InProgress < Pikuri::Tool
|
|
17
21
|
# @return [String]
|
|
18
22
|
DESCRIPTION = <<~DESC
|
|
19
23
|
Mark a task as `in_progress` immediately before you start working on it.
|
|
20
24
|
|
|
21
25
|
Usage:
|
|
22
|
-
- Pass the
|
|
26
|
+
- Pass the task's numeric `id` as shown in the rendered list (`- #3 [pending] ...` → id 3).
|
|
23
27
|
- Keep exactly one task `in_progress` at a time. Finish (or revert) the current one before starting another.
|
|
24
|
-
- On `Error: no such task: ...` the call did nothing —
|
|
28
|
+
- On `Error: no such task id: ...` the call did nothing — the error includes the current list; pick the right id from it.
|
|
25
29
|
- On success the full current list is returned for you to read back.
|
|
26
30
|
DESC
|
|
27
31
|
|
|
@@ -32,24 +36,24 @@ module Pikuri
|
|
|
32
36
|
name: 'task_in_progress',
|
|
33
37
|
description: DESCRIPTION,
|
|
34
38
|
parameters: Pikuri::Tool::Parameters.build { |p|
|
|
35
|
-
p.
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
p.required_integer :id,
|
|
40
|
+
'Numeric id of the existing task to mark as ' \
|
|
41
|
+
'in_progress, as shown in the rendered list, e.g. 3.'
|
|
38
42
|
},
|
|
39
|
-
execute: lambda { |
|
|
40
|
-
InProgress.execute(list: list,
|
|
43
|
+
execute: lambda { |id:|
|
|
44
|
+
InProgress.execute(list: list, id: id)
|
|
41
45
|
}
|
|
42
46
|
)
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# @param list [List]
|
|
46
|
-
# @param
|
|
50
|
+
# @param id [Integer]
|
|
47
51
|
# @return [String]
|
|
48
|
-
def self.execute(list:,
|
|
49
|
-
list.set_status(
|
|
52
|
+
def self.execute(list:, id:)
|
|
53
|
+
list.set_status(id: id, status: 'in_progress')
|
|
50
54
|
list.render
|
|
51
55
|
rescue ItemNotFound
|
|
52
|
-
"Error: no such task:
|
|
56
|
+
"Error: no such task id: #{id}. Current list:\n#{list.render}"
|
|
53
57
|
end
|
|
54
58
|
end
|
|
55
59
|
end
|
data/lib/pikuri/tasks/list.rb
CHANGED
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Tasks
|
|
5
5
|
# Raised by {List#add} when a new item's +content+ already appears
|
|
6
|
-
# in the list.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# in the list. Items are addressed by {Item#id}, so duplicates
|
|
7
|
+
# would not be ambiguous — they are rejected because a duplicate
|
|
8
|
+
# is almost always an LLM mistake (re-sending an already-captured
|
|
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.
|
|
11
12
|
class DuplicateItem < StandardError; end
|
|
12
13
|
|
|
13
14
|
# Raised by {List#set_status} and {List#delete} when no item with
|
|
14
|
-
# the given +
|
|
15
|
-
# report +"Error: no such task: <
|
|
16
|
-
# the
|
|
15
|
+
# the given +id+ exists. The three mutation tools rescue this and
|
|
16
|
+
# report +"Error: no such task id: <id>"+ plus the current list
|
|
17
|
+
# render, so the LLM can pick the right id on the next turn.
|
|
17
18
|
class ItemNotFound < StandardError; end
|
|
18
19
|
|
|
19
20
|
# Allowed values for {Item#status}. Kept deliberately small — three
|
|
@@ -23,10 +24,11 @@ module Pikuri
|
|
|
23
24
|
# fourth status.
|
|
24
25
|
STATUSES = %w[pending in_progress completed].freeze
|
|
25
26
|
|
|
26
|
-
# One row in a {List}. +
|
|
27
|
-
#
|
|
27
|
+
# 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
|
+
# see there); +content+ is the LLM-visible task text; +status+ is
|
|
28
30
|
# one of {STATUSES}.
|
|
29
|
-
Item = Data.define(:content, :status)
|
|
31
|
+
Item = Data.define(:id, :content, :status)
|
|
30
32
|
|
|
31
33
|
# An in-memory ordered list of {Item}s, scoped to a single
|
|
32
34
|
# {Pikuri::Agent}. Held inside {Extension} and captured by closure
|
|
@@ -35,8 +37,13 @@ module Pikuri
|
|
|
35
37
|
#
|
|
36
38
|
# == Concurrency
|
|
37
39
|
#
|
|
38
|
-
# The
|
|
39
|
-
#
|
|
40
|
+
# The list is confined to the agent's thread: the agent loop is
|
|
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
|
|
40
47
|
# parallel-tool-execution feature would need a +Mutex+ here.
|
|
41
48
|
#
|
|
42
49
|
# == Persistence
|
|
@@ -48,8 +55,21 @@ module Pikuri
|
|
|
48
55
|
# @return [List]
|
|
49
56
|
def initialize
|
|
50
57
|
@items = []
|
|
58
|
+
@next_id = 1
|
|
59
|
+
@on_change = nil
|
|
51
60
|
end
|
|
52
61
|
|
|
62
|
+
# Optional zero-argument hook invoked after every successful
|
|
63
|
+
# mutation ({#add} / {#set_status} / {#delete}) — not on failed
|
|
64
|
+
# ones (a raise means nothing changed). Set by {Extension#bind}
|
|
65
|
+
# to emit a {ListChanged} onto the agent's listener stream;
|
|
66
|
+
# +nil+ (the default) disables notification. Runs on the
|
|
67
|
+
# mutating (agent) thread, synchronously inside the mutation
|
|
68
|
+
# call.
|
|
69
|
+
#
|
|
70
|
+
# @return [Proc, nil]
|
|
71
|
+
attr_accessor :on_change
|
|
72
|
+
|
|
53
73
|
# @return [Array<Item>] a frozen snapshot of the current items,
|
|
54
74
|
# in insertion order. Callers cannot mutate the internal
|
|
55
75
|
# storage through this accessor.
|
|
@@ -67,63 +87,71 @@ module Pikuri
|
|
|
67
87
|
@items.empty?
|
|
68
88
|
end
|
|
69
89
|
|
|
70
|
-
# Append a new item with status +pending
|
|
71
|
-
#
|
|
72
|
-
# the
|
|
90
|
+
# Append a new item with status +pending+ and the next id from a
|
|
91
|
+
# monotonic per-list counter. Ids are never reused: after a
|
|
92
|
+
# delete, the freed id stays dead, so a stale id held by the LLM
|
|
93
|
+
# errors loudly instead of silently resolving to a newer task.
|
|
73
94
|
#
|
|
74
95
|
# @param content [String] non-empty content; whitespace is the
|
|
75
96
|
# caller's responsibility.
|
|
76
97
|
# @return [Item] the newly added item
|
|
77
98
|
# @raise [DuplicateItem] if an item with the same +content+
|
|
78
|
-
# already exists.
|
|
99
|
+
# already exists (a duplicate is almost always an LLM mistake).
|
|
79
100
|
def add(content)
|
|
80
|
-
raise DuplicateItem, content if
|
|
101
|
+
raise DuplicateItem, content if @items.any? { |i| i.content == content }
|
|
81
102
|
|
|
82
|
-
item = Item.new(content: content, status: 'pending')
|
|
103
|
+
item = Item.new(id: @next_id, content: content, status: 'pending')
|
|
104
|
+
@next_id += 1
|
|
83
105
|
@items << item
|
|
106
|
+
@on_change&.call
|
|
84
107
|
item
|
|
85
108
|
end
|
|
86
109
|
|
|
87
|
-
# Update the status of the item whose +
|
|
110
|
+
# Update the status of the item whose +id+ matches.
|
|
88
111
|
#
|
|
89
|
-
# @param
|
|
112
|
+
# @param id [Integer]
|
|
90
113
|
# @param status [String] one of {STATUSES}.
|
|
91
114
|
# @return [Item] the updated item (a fresh frozen +Data+
|
|
92
115
|
# instance — the old one is replaced in place).
|
|
93
116
|
# @raise [ItemNotFound] if no matching item exists.
|
|
94
117
|
# @raise [ArgumentError] if +status+ is not in {STATUSES}.
|
|
95
|
-
def set_status(
|
|
118
|
+
def set_status(id:, status:)
|
|
96
119
|
unless STATUSES.include?(status)
|
|
97
120
|
raise ArgumentError, "invalid status: #{status.inspect} (allowed: #{STATUSES.join(', ')})"
|
|
98
121
|
end
|
|
99
122
|
|
|
100
|
-
idx = @items.index { |i| i.
|
|
101
|
-
raise ItemNotFound,
|
|
123
|
+
idx = @items.index { |i| i.id == id }
|
|
124
|
+
raise ItemNotFound, id.to_s if idx.nil?
|
|
102
125
|
|
|
103
|
-
@items[idx] =
|
|
126
|
+
@items[idx] = @items[idx].with(status: status)
|
|
127
|
+
@on_change&.call
|
|
104
128
|
@items[idx]
|
|
105
129
|
end
|
|
106
130
|
|
|
107
|
-
# Remove the item whose +
|
|
131
|
+
# Remove the item whose +id+ matches. The id is not reused for
|
|
132
|
+
# later items (see {#add}).
|
|
108
133
|
#
|
|
109
|
-
# @param
|
|
134
|
+
# @param id [Integer]
|
|
110
135
|
# @return [Item] the removed item.
|
|
111
136
|
# @raise [ItemNotFound] if no matching item exists.
|
|
112
|
-
def delete(
|
|
113
|
-
idx = @items.index { |i| i.
|
|
114
|
-
raise ItemNotFound,
|
|
137
|
+
def delete(id)
|
|
138
|
+
idx = @items.index { |i| i.id == id }
|
|
139
|
+
raise ItemNotFound, id.to_s if idx.nil?
|
|
115
140
|
|
|
116
|
-
@items.delete_at(idx)
|
|
141
|
+
removed = @items.delete_at(idx)
|
|
142
|
+
@on_change&.call
|
|
143
|
+
removed
|
|
117
144
|
end
|
|
118
145
|
|
|
119
146
|
# The canonical rendering returned as the observation by every
|
|
120
|
-
# task tool, so the LLM sees the latest full state
|
|
121
|
-
# without needing a separate read
|
|
147
|
+
# task tool, so the LLM sees the latest full state — including
|
|
148
|
+
# each item's id — on each call without needing a separate read
|
|
149
|
+
# tool. Format:
|
|
122
150
|
#
|
|
123
151
|
# <tasks>
|
|
124
|
-
# - [pending] Add dark mode toggle
|
|
125
|
-
# - [in_progress] Write unit tests
|
|
126
|
-
# - [completed] Update README
|
|
152
|
+
# - #1 [pending] Add dark mode toggle
|
|
153
|
+
# - #2 [in_progress] Write unit tests
|
|
154
|
+
# - #3 [completed] Update README
|
|
127
155
|
# </tasks>
|
|
128
156
|
#
|
|
129
157
|
# Empty list renders as +<tasks>(empty)</tasks>+ so the LLM gets
|
|
@@ -134,15 +162,9 @@ module Pikuri
|
|
|
134
162
|
def render
|
|
135
163
|
return '<tasks>(empty)</tasks>' if @items.empty?
|
|
136
164
|
|
|
137
|
-
lines = @items.map { |i| "- [#{i.status}] #{i.content}" }
|
|
165
|
+
lines = @items.map { |i| "- ##{i.id} [#{i.status}] #{i.content}" }
|
|
138
166
|
"<tasks>\n#{lines.join("\n")}\n</tasks>"
|
|
139
167
|
end
|
|
140
|
-
|
|
141
|
-
private
|
|
142
|
-
|
|
143
|
-
def find(content)
|
|
144
|
-
@items.find { |i| i.content == content }
|
|
145
|
-
end
|
|
146
168
|
end
|
|
147
169
|
end
|
|
148
170
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pikuri
|
|
4
|
+
module Tasks
|
|
5
|
+
# Domain event emitted onto the agent's listener stream after
|
|
6
|
+
# every {List} mutation (add / status change / delete), wired by
|
|
7
|
+
# {Extension#bind} via
|
|
8
|
+
# {Pikuri::Agent::ExtensionContext#emit_event}. Carries a frozen
|
|
9
|
+
# point-in-time snapshot of the whole list ({List#items}'s
|
|
10
|
+
# shape), so a consumer needs no +List+ reference — render or
|
|
11
|
+
# serialize the payload as-is.
|
|
12
|
+
#
|
|
13
|
+
# Lands between the mutating tool's {Pikuri::Agent::Event::ToolCall}
|
|
14
|
+
# and {Pikuri::Agent::Event::ToolResult} in the stream (the
|
|
15
|
+
# +on_change+ hook fires inside the tool's +execute+). A batch
|
|
16
|
+
# +task_create+ emits one event per added item — consumers that
|
|
17
|
+
# render should treat the latest snapshot as authoritative
|
|
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}.
|
|
24
|
+
ListChanged = Data.define(:items)
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Vysny
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: pikuri-core
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - '='
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.7
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - '='
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.7
|
|
27
26
|
description: |
|
|
28
27
|
pikuri-tasks gives a pikuri-core agent an in-memory task list it
|
|
29
28
|
can use to plan and track multi-step work. A +Pikuri::Tasks::List+
|
|
@@ -51,6 +50,7 @@ files:
|
|
|
51
50
|
- lib/pikuri/tasks/extension.rb
|
|
52
51
|
- lib/pikuri/tasks/in_progress.rb
|
|
53
52
|
- lib/pikuri/tasks/list.rb
|
|
53
|
+
- lib/pikuri/tasks/list_changed.rb
|
|
54
54
|
homepage: https://codeberg.org/mvysny/pikuri
|
|
55
55
|
licenses:
|
|
56
56
|
- MIT
|
|
@@ -59,7 +59,6 @@ metadata:
|
|
|
59
59
|
changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
|
|
60
60
|
bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
|
|
61
61
|
rubygems_mfa_required: 'true'
|
|
62
|
-
post_install_message:
|
|
63
62
|
rdoc_options: []
|
|
64
63
|
require_paths:
|
|
65
64
|
- lib
|
|
@@ -74,8 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
73
|
- !ruby/object:Gem::Version
|
|
75
74
|
version: '0'
|
|
76
75
|
requirements: []
|
|
77
|
-
rubygems_version: 3.
|
|
78
|
-
signing_key:
|
|
76
|
+
rubygems_version: 3.6.7
|
|
79
77
|
specification_version: 4
|
|
80
78
|
summary: Per-session in-memory task list + tools for pikuri.
|
|
81
79
|
test_files: []
|