@xl0/pi-lovely-agents 0.1.1 → 0.1.2
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.
- package/CHANGELOG.md +27 -0
- package/README.md +44 -5
- package/extensions/lovely-agents/agent.ts +50 -20
- package/extensions/lovely-agents/bash.ts +5 -2
- package/extensions/lovely-agents/child-session.ts +43 -2
- package/extensions/lovely-agents/coordinator.ts +2 -0
- package/extensions/lovely-agents/definitions.ts +2 -1
- package/extensions/lovely-agents/index.ts +1 -1
- package/extensions/lovely-agents/lifecycle.ts +70 -39
- package/extensions/lovely-agents/management.ts +49 -10
- package/extensions/lovely-agents/notifications.ts +18 -5
- package/extensions/lovely-agents/state.ts +188 -56
- package/extensions/lovely-agents/storage.ts +204 -0
- package/extensions/lovely-agents/task-panel.ts +78 -29
- package/extensions/lovely-agents/tools.ts +38 -8
- package/package.json +3 -1
- package/scripts/prune-tasks.ts +244 -0
- package/skills/agent/SKILL.md +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.2] - 2026-09-10
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Keep discarded task files at their original paths and return `taskDirectory` with `state: "discarded"` from `task_discard`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Retrieve retained results by run index with `task_output(run: N)`, including after discard, and identify runs in results and completion notices.
|
|
14
|
+
- Browse non-discarded tasks through an `active/` index and safely prune discarded tasks with the dry-run-first `scripts/prune-tasks.ts` command.
|
|
15
|
+
- Report child progress in task inspection and the panel with `task_update` without notifying the parent.
|
|
16
|
+
- Scroll live output with keyboard navigation, Bash tail-following, and visible exit codes or termination signals.
|
|
17
|
+
- Read shorter Bash output tails with `task_output(lines: N)`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Group Agents and Bash separately and list active tasks first.
|
|
22
|
+
- Show the latest Bash output in UTF-8-safe completion previews.
|
|
23
|
+
- Clarify Steer acceptance and Follow-up conversion, and expose process-wide capacity and Bash queue reasons.
|
|
24
|
+
- Guide agents to delegate independent implementation work and keep coupled changes in the parent.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Keep output waits attached to the selected run across Follow-ups.
|
|
29
|
+
- Deliver pending completion notifications for discarded tasks.
|
|
30
|
+
- Prevent another Pi process from claiming tasks whose cleanup failed.
|
|
31
|
+
|
|
5
32
|
## [0.1.1] - 2026-09-06
|
|
6
33
|
|
|
7
34
|
- Publish through GitHub Actions with npm provenance.
|
package/README.md
CHANGED
|
@@ -83,14 +83,19 @@ agents get the normal tools and extensions; a role description is not a sandbox.
|
|
|
83
83
|
|
|
84
84
|
Open **`/lovely-agents` → Tasks**, or press **Down with an empty editor** to focus
|
|
85
85
|
the task list below it.
|
|
86
|
+
Agents and Bash appear in separate groups, active tasks first within each group.
|
|
86
87
|
|
|
87
88
|
- **Arrow keys** select a task; **Enter** opens its actions.
|
|
88
|
-
- **Live output**
|
|
89
|
+
- **Live output** scrolls with arrows, PgUp/PgDn, Home/End. Bash opens at the
|
|
90
|
+
bottom and follows new output; scrolling up pauses following, End resumes it.
|
|
91
|
+
Its fixed header includes the exit code and termination signal.
|
|
89
92
|
- **Inputs / history** shows earlier requests and results.
|
|
90
93
|
- **Follow-up** adds another request after the agent's current work.
|
|
91
|
-
- **Steer**
|
|
94
|
+
- **Steer** queues input for a live streaming run. Without a live target it becomes
|
|
95
|
+
a Follow-up; foreground busy tasks reject it. Queued input can be lost on stop.
|
|
92
96
|
- **Stop** cancels the work but keeps its files. An agent can take a new request later.
|
|
93
|
-
- **Discard** stops and
|
|
97
|
+
- **Discard** stops it and removes it from active work. Files stay in place and
|
|
98
|
+
results remain readable, but it cannot receive new input.
|
|
94
99
|
- **Esc** returns to the editor.
|
|
95
100
|
|
|
96
101
|
You can also ask Pi directly:
|
|
@@ -102,6 +107,14 @@ You can also ask Pi directly:
|
|
|
102
107
|
Tool calls and notifications are compact by default. **Ctrl+O** expands their
|
|
103
108
|
full contents.
|
|
104
109
|
|
|
110
|
+
Children can call `task_update({ progress: "Root cause found; testing the fix" })`
|
|
111
|
+
to report meaningful milestones or blockers in up to 240 characters. Progress
|
|
112
|
+
replaces the initial-input preview in the panel and appears in task inspection.
|
|
113
|
+
It does not rename the task, change its lifecycle state, notify or wake the parent.
|
|
114
|
+
Each new run clears the report; earlier reports remain with their run's result.
|
|
115
|
+
Default-tool children get this tool automatically. If a Definition has an explicit
|
|
116
|
+
`tools` list, include `task_update` to allow progress reporting.
|
|
117
|
+
|
|
105
118
|
## Background Bash
|
|
106
119
|
|
|
107
120
|
Ask Pi to run a long command in the background:
|
|
@@ -110,6 +123,8 @@ Ask Pi to run a long command in the background:
|
|
|
110
123
|
|
|
111
124
|
The command appears alongside agent tasks. You can inspect its output or stop
|
|
112
125
|
it from the same menu. Normal, short Bash commands still work as before.
|
|
126
|
+
Detached completion automatically notifies Pi and wakes an idle parent.
|
|
127
|
+
Synchronous completion and explicit stops do not send a completion notice.
|
|
113
128
|
|
|
114
129
|
For a command that needs input, **Write stdin** sends exactly what you type;
|
|
115
130
|
include a newline if the command expects one. **Close stdin** sends EOF: “there
|
|
@@ -131,6 +146,8 @@ work running. Turning either switch off does not stop tasks already accepted.
|
|
|
131
146
|
Agent work and Bash jobs have separate concurrency limits, both initially 4.
|
|
132
147
|
Extra work queues until a slot is free. Pi initially waits up to 30 seconds for
|
|
133
148
|
an agent result before leaving it in the background.
|
|
149
|
+
These limits are shared process-wide, not per conversation. The roster reports
|
|
150
|
+
held/max execution permits; task lists show only the current parent's tasks.
|
|
134
151
|
|
|
135
152
|
Under **Models**, choose additional models Pi may use for agents. You can also
|
|
136
153
|
configure these optional shortcuts:
|
|
@@ -152,10 +169,16 @@ Long results aren't lost. Pi's output-reading tool returns at most **2,000 lines
|
|
|
152
169
|
or 50 KiB** at a time, with a truncation notice and a file path when capped.
|
|
153
170
|
These are snapshots, not pages to assemble by repeatedly reading. Full agent
|
|
154
171
|
replies are in `history.md`; full Bash output is in `output.log`.
|
|
172
|
+
Each agent assignment has a 1-based run index, shown in tool results and notices.
|
|
173
|
+
`task_output(id, run: 2)` retrieves that run even after later Follow-ups start.
|
|
174
|
+
Omit `run` for the current snapshot; `lines: 20` requests a shorter Bash tail.
|
|
175
|
+
Older runs completed before this feature may require reading `history.md`.
|
|
155
176
|
|
|
156
177
|
If Pi asks to wait for a result, the wait ends when the run finishes, pauses on
|
|
157
178
|
a provider limit, or reaches the requested timeout. A timeout returns the
|
|
158
179
|
latest output—it does not stop the task.
|
|
180
|
+
The wait stays attached to the selected run; a later Follow-up cannot replace
|
|
181
|
+
its answer. Prefer completion notices or a meaningful wait over short polling.
|
|
159
182
|
|
|
160
183
|
Tasks belong to the Pi conversation that started them. **`/reload` keeps work
|
|
161
184
|
running; quitting or switching conversations stops it.** After a crash, lost
|
|
@@ -168,8 +191,24 @@ an error or was aborted, **`/continue`** resumes it and eligible paused agents.
|
|
|
168
191
|
|
|
169
192
|
Task files live in `.pi/lovely-agents/` under your working directory and are
|
|
170
193
|
ignored by Git. They include conversation history and command output, so treat
|
|
171
|
-
them as potentially sensitive.
|
|
172
|
-
|
|
194
|
+
them as potentially sensitive. Each task keeps its original
|
|
195
|
+
`<parent-session>/<task-id>/` path. The parent's `active/` directory links to all
|
|
196
|
+
non-discarded tasks, including idle specialists. It is a browsing index, rebuilt
|
|
197
|
+
when that parent is reopened—not the authority for execution or deletion.
|
|
198
|
+
Keep tasks until dependent work is integrated, then discard what is no longer
|
|
199
|
+
needed. Existing old `archive/` contents are left untouched.
|
|
200
|
+
|
|
201
|
+
There is no automatic deletion. From the package checkout/install directory:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
bun scripts/prune-tasks.ts /path/to/workspace # dry-run
|
|
205
|
+
bun scripts/prune-tasks.ts /path/to/workspace --apply # permanently delete eligible tasks
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Pruning requires closed parent sessions and retains non-discarded tasks, pending
|
|
209
|
+
notifications, unknown/corrupt records, and unsafe or still-needed descendants.
|
|
210
|
+
Missing `active/` links never authorize deletion. Crash leftovers stay until
|
|
211
|
+
their parent is reopened; abandoned sessions are not automatically collected.
|
|
173
212
|
|
|
174
213
|
## Development
|
|
175
214
|
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type AgentTaskMetadata,
|
|
30
30
|
acquireParentLease,
|
|
31
31
|
appendHistoryLog,
|
|
32
|
-
archivedTaskStoragePaths,
|
|
33
32
|
displayWorkspacePath,
|
|
34
33
|
ensureParentStorage,
|
|
35
34
|
initializeRetainedLogs,
|
|
@@ -82,6 +81,7 @@ export function recoverProviderTuple(tuple: ModelTuple): number {
|
|
|
82
81
|
|
|
83
82
|
export type AgentCreationResult = {
|
|
84
83
|
id: string
|
|
84
|
+
run: number
|
|
85
85
|
label: string
|
|
86
86
|
definition: string
|
|
87
87
|
state: TaskMetadata["state"]
|
|
@@ -98,8 +98,10 @@ export type AgentCreationResult = {
|
|
|
98
98
|
|
|
99
99
|
export type TaskInputResult = {
|
|
100
100
|
id: string
|
|
101
|
+
run: number
|
|
101
102
|
requestedDelivery: "followup" | "steer" | undefined
|
|
102
103
|
effectiveDelivery: "followup" | "steer" | "stdin"
|
|
104
|
+
conversionReason?: string
|
|
103
105
|
queuePosition: number | null
|
|
104
106
|
state: TaskMetadata["state"]
|
|
105
107
|
latestOutcome: TaskMetadata["latestOutcome"]
|
|
@@ -304,7 +306,7 @@ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions):
|
|
|
304
306
|
const detached = waitMs === 0 ? await wait() : await coordinator.withLentPermit(wait, signal)
|
|
305
307
|
const loaded = await readTaskMetadata(paths)
|
|
306
308
|
if (loaded.status !== "ok") throw new Error(`Could not read accepted task ${paths.taskRef}`)
|
|
307
|
-
const output = await readRetainedOutput(paths)
|
|
309
|
+
const output = await readRetainedOutput(paths, { run: 1 })
|
|
308
310
|
const tasks = (await loadTaskList(ctx.cwd, parentSessionId)).details
|
|
309
311
|
return buildAgentCreationToolResult(loaded.metadata, detached, output, tasks)
|
|
310
312
|
} finally {
|
|
@@ -340,7 +342,11 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
340
342
|
? "Write literal stdin to a running Bash task"
|
|
341
343
|
: "Send input to an owned task",
|
|
342
344
|
promptGuidelines: [
|
|
343
|
-
...(agentInput
|
|
345
|
+
...(agentInput
|
|
346
|
+
? [
|
|
347
|
+
"Use task_input Follow-up for a new run; Steer queues input for a live streaming run. Without a live steering target, Steer becomes a Follow-up (foreground busy tasks reject it). Acceptance does not guarantee observation before stop."
|
|
348
|
+
]
|
|
349
|
+
: []),
|
|
344
350
|
...(bashInput ? ["For Bash, omit delivery and write literal stdin; eof closes stdin without restarting the command."] : [])
|
|
345
351
|
],
|
|
346
352
|
parameters: Type.Object(
|
|
@@ -383,12 +389,12 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
383
389
|
type: "text",
|
|
384
390
|
text:
|
|
385
391
|
result.effectiveDelivery === "stdin"
|
|
386
|
-
? `${result.id}: stdin delivered${params.eof ? " (EOF)" : ""}`
|
|
392
|
+
? `${result.id} run=${result.run}: stdin delivered${params.eof ? " (EOF)" : ""}`
|
|
387
393
|
: result.effectiveDelivery === "steer"
|
|
388
|
-
? `${result.id}
|
|
394
|
+
? `${result.id} run=${result.run}: steer queued for current run`
|
|
389
395
|
: result.output
|
|
390
|
-
? `${result.id}: followup ${result.latestOutcome}\n${result.output.text}`
|
|
391
|
-
: `${result.id}: followup accepted (position ${result.queuePosition};
|
|
396
|
+
? `${result.id} run=${result.run}: ${result.conversionReason ? `requested steer -> followup (${result.conversionReason}); ` : ""}followup ${result.latestOutcome}\n${result.output.text}`
|
|
397
|
+
: `${result.id} run=${result.run}: ${result.conversionReason ? `requested steer -> ` : ""}followup accepted${result.conversionReason ? `; ${result.conversionReason}` : ""} (position ${result.queuePosition}; task=${result.state}; ${result.queuedFollowUps} queued)`
|
|
392
398
|
}
|
|
393
399
|
],
|
|
394
400
|
details: result
|
|
@@ -403,12 +409,12 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
403
409
|
description:
|
|
404
410
|
action === "stop"
|
|
405
411
|
? "Stop active or queued work for an owned Agent or Bash task while preserving its files."
|
|
406
|
-
: "Stop and permanently discard an owned task subtree
|
|
412
|
+
: "Stop and permanently discard an owned task subtree from active work. Files stay at their original paths for read-only inspection. Unsupported metadata versions can also be discarded.",
|
|
407
413
|
promptSnippet: action === "stop" ? "Stop work for a durable task" : "Discard a durable task",
|
|
408
414
|
promptGuidelines:
|
|
409
415
|
action === "discard"
|
|
410
416
|
? [
|
|
411
|
-
"
|
|
417
|
+
"Use task_discard after dependent work is integrated and no Follow-up is expected. Keep reusable specialists and tasks whose results are still needed; idle agents already unload."
|
|
412
418
|
]
|
|
413
419
|
: [],
|
|
414
420
|
parameters: Type.Object(
|
|
@@ -426,7 +432,7 @@ export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOption
|
|
|
426
432
|
type: "text",
|
|
427
433
|
text:
|
|
428
434
|
action === "discard"
|
|
429
|
-
? `${details.id}: discarded (files
|
|
435
|
+
? `${details.id}: discarded (files retained at ${details.taskDirectory})`
|
|
430
436
|
: `${details.id}: stop complete (${details.state}; outcome ${details.latestOutcome ?? "none"}; ${details.queuedFollowUps} queued)`
|
|
431
437
|
}
|
|
432
438
|
],
|
|
@@ -472,6 +478,7 @@ export async function sendTaskInput(
|
|
|
472
478
|
if (current.status !== "ok") throw new Error(`Could not read accepted task ${id}`)
|
|
473
479
|
return {
|
|
474
480
|
id,
|
|
481
|
+
run: accepted.run,
|
|
475
482
|
requestedDelivery,
|
|
476
483
|
effectiveDelivery: accepted.delivery,
|
|
477
484
|
queuePosition: accepted.queuePosition,
|
|
@@ -500,6 +507,8 @@ export async function sendTaskInput(
|
|
|
500
507
|
if (current.status !== "ok") throw new Error(`Could not read accepted task ${id}`)
|
|
501
508
|
return {
|
|
502
509
|
id,
|
|
510
|
+
run: accepted.run,
|
|
511
|
+
...(accepted.conversionReason ? { conversionReason: accepted.conversionReason } : {}),
|
|
503
512
|
requestedDelivery: delivery,
|
|
504
513
|
effectiveDelivery: accepted.delivery,
|
|
505
514
|
queuePosition: accepted.queuePosition,
|
|
@@ -515,15 +524,14 @@ export async function controlTaskLifecycle(ctx: ExtensionContext, id: string, ac
|
|
|
515
524
|
const paths = taskStoragePaths(lease.paths, id)
|
|
516
525
|
if (action === "discard") {
|
|
517
526
|
await discardTask(paths)
|
|
518
|
-
const archived = archivedTaskStoragePaths(paths)
|
|
519
527
|
return {
|
|
520
528
|
id,
|
|
521
529
|
action,
|
|
522
|
-
state: "
|
|
530
|
+
state: "discarded",
|
|
523
531
|
latestOutcome: null,
|
|
524
532
|
discarded: true,
|
|
525
533
|
queuedFollowUps: 0,
|
|
526
|
-
|
|
534
|
+
taskDirectory: displayWorkspacePath(ctx.cwd, paths.taskDirectory)
|
|
527
535
|
}
|
|
528
536
|
}
|
|
529
537
|
let loaded = await readTaskMetadata(paths)
|
|
@@ -542,7 +550,7 @@ export async function controlTaskLifecycle(ctx: ExtensionContext, id: string, ac
|
|
|
542
550
|
latestOutcome: metadata.latestOutcome,
|
|
543
551
|
discarded: metadata.discardedAt !== null,
|
|
544
552
|
queuedFollowUps: metadata.queuedFollowUps.length,
|
|
545
|
-
|
|
553
|
+
taskDirectory: displayWorkspacePath(ctx.cwd, paths.taskDirectory),
|
|
546
554
|
paths: retainedPaths(paths)
|
|
547
555
|
}
|
|
548
556
|
}
|
|
@@ -716,7 +724,12 @@ class AgentRuntime implements ResidentAgent {
|
|
|
716
724
|
if (index >= 0) this.#pendingSteers.splice(index, 1)
|
|
717
725
|
throw error
|
|
718
726
|
}
|
|
719
|
-
result = {
|
|
727
|
+
result = {
|
|
728
|
+
run: metadata.activeRun.sequence,
|
|
729
|
+
delivery: "steer",
|
|
730
|
+
queuePosition: null,
|
|
731
|
+
queuedFollowUps: metadata.queuedFollowUps.length
|
|
732
|
+
}
|
|
720
733
|
return metadata
|
|
721
734
|
}
|
|
722
735
|
if (
|
|
@@ -733,7 +746,13 @@ class AgentRuntime implements ResidentAgent {
|
|
|
733
746
|
const runId = createRunId()
|
|
734
747
|
const acceptanceOrder = getAgentCoordinator().nextAcceptanceOrder()
|
|
735
748
|
const queuePosition = metadata.activeRun ? metadata.queuedFollowUps.length + 1 : 1
|
|
736
|
-
result = {
|
|
749
|
+
result = {
|
|
750
|
+
run: sequence,
|
|
751
|
+
delivery: "followup",
|
|
752
|
+
queuePosition,
|
|
753
|
+
queuedFollowUps: metadata.queuedFollowUps.length + 1,
|
|
754
|
+
...(delivery === "steer" ? { conversionReason: `task ${metadata.state}, no live stream to steer` } : {})
|
|
755
|
+
}
|
|
737
756
|
acceptedRun = {
|
|
738
757
|
id: runId,
|
|
739
758
|
sequence,
|
|
@@ -892,7 +911,8 @@ class AgentRuntime implements ResidentAgent {
|
|
|
892
911
|
})
|
|
893
912
|
}
|
|
894
913
|
this.#awaitingPrimaryInput = true
|
|
895
|
-
await this.#child.
|
|
914
|
+
await this.#child.prompt(
|
|
915
|
+
run.id,
|
|
896
916
|
recovering ? "Continue." : run.input,
|
|
897
917
|
childPromptOptions(recovering ? false : this.#expandPromptTemplates)
|
|
898
918
|
)
|
|
@@ -1003,7 +1023,15 @@ class AgentRuntime implements ResidentAgent {
|
|
|
1003
1023
|
const activeRun = metadata.activeRun
|
|
1004
1024
|
if (activeRun?.id !== run.id) return clearFollowUps ? { ...metadata, queuedFollowUps: [], updatedAt: timestamp } : metadata
|
|
1005
1025
|
won = true
|
|
1006
|
-
completed = {
|
|
1026
|
+
completed = {
|
|
1027
|
+
...metadata,
|
|
1028
|
+
state: "idle",
|
|
1029
|
+
latestOutcome: outcome,
|
|
1030
|
+
lastSettledRun: run.sequence,
|
|
1031
|
+
activeRun: null,
|
|
1032
|
+
queuedFollowUps: [],
|
|
1033
|
+
updatedAt: timestamp
|
|
1034
|
+
}
|
|
1007
1035
|
if (
|
|
1008
1036
|
activeRun.background &&
|
|
1009
1037
|
(outcome === "succeeded" || outcome === "failed") &&
|
|
@@ -1285,10 +1313,11 @@ function buildAgentCreationToolResult(
|
|
|
1285
1313
|
if (metadata.kind !== "agent") throw new Error("Expected an Agent task")
|
|
1286
1314
|
const result: AgentCreationResult = {
|
|
1287
1315
|
id: metadata.taskRef,
|
|
1316
|
+
run: 1,
|
|
1288
1317
|
label: metadata.label,
|
|
1289
1318
|
definition: metadata.definitionName,
|
|
1290
|
-
state:
|
|
1291
|
-
latestOutcome:
|
|
1319
|
+
state: output.state,
|
|
1320
|
+
latestOutcome: output.latestOutcome,
|
|
1292
1321
|
model: `${metadata.model.provider}/${metadata.model.id}`,
|
|
1293
1322
|
thinking: metadata.thinking,
|
|
1294
1323
|
depth: metadata.depth,
|
|
@@ -1305,6 +1334,7 @@ function buildAgentCreationToolResult(
|
|
|
1305
1334
|
type: "text",
|
|
1306
1335
|
text: [
|
|
1307
1336
|
`task: ${result.id}`,
|
|
1337
|
+
`run: ${result.run}`,
|
|
1308
1338
|
`state: ${result.state}`,
|
|
1309
1339
|
...(output.queueReason
|
|
1310
1340
|
? [`waiting: ${output.queueReason} (${output.capacity.active}/${output.capacity.limit} execution permits)`]
|
|
@@ -51,6 +51,7 @@ const BashParameters = Type.Object(
|
|
|
51
51
|
export type BashToolInput = Static<typeof BashParameters>
|
|
52
52
|
export type BashCreationResult = {
|
|
53
53
|
id: string
|
|
54
|
+
run: number
|
|
54
55
|
label: string
|
|
55
56
|
state: BashTaskMetadata["state"]
|
|
56
57
|
latestOutcome: BashTaskMetadata["latestOutcome"]
|
|
@@ -69,6 +70,7 @@ export function registerBashTool(pi: ExtensionAPI, options: { getConfig: () => A
|
|
|
69
70
|
promptSnippet: "Run a background Bash command with durable output and task controls",
|
|
70
71
|
promptGuidelines: [
|
|
71
72
|
"Use bash_bg for background shell work, not as a replacement for normal bash. Use task_list/task_output/task_stop/task_discard with its b_ ID.",
|
|
73
|
+
"Detached bash_bg tasks automatically notify you on success or failure and wake an idle parent; no completion polling is needed. Synchronous completion and explicit stops do not notify.",
|
|
72
74
|
"Use task_input on a running bash_bg task for literal stdin, optionally eof:true to close stdin; agent Follow-up/Steer modes are not supported. Commands never restart automatically."
|
|
73
75
|
],
|
|
74
76
|
parameters: BashParameters,
|
|
@@ -176,6 +178,7 @@ export function registerBashTool(pi: ExtensionAPI, options: { getConfig: () => A
|
|
|
176
178
|
if (loaded.status !== "ok" || loaded.metadata.kind !== "bash") throw new Error(`Could not read accepted task ${paths.taskRef}`)
|
|
177
179
|
const result: BashCreationResult = {
|
|
178
180
|
id: paths.taskRef,
|
|
181
|
+
run: 1,
|
|
179
182
|
label: loaded.metadata.label,
|
|
180
183
|
state: loaded.metadata.state,
|
|
181
184
|
latestOutcome: loaded.metadata.latestOutcome,
|
|
@@ -188,7 +191,7 @@ export function registerBashTool(pi: ExtensionAPI, options: { getConfig: () => A
|
|
|
188
191
|
content: [
|
|
189
192
|
{
|
|
190
193
|
type: "text" as const,
|
|
191
|
-
text: `${result.id} ${JSON.stringify(result.label)}: ${result.state}${result.latestOutcome ? ` / ${result.latestOutcome}` : ""}${detached ? " (detached)" : ""}\n${result.output.text}`
|
|
194
|
+
text: `${result.id} run=1 ${JSON.stringify(result.label)}: ${result.state}${result.latestOutcome ? ` / ${result.latestOutcome}` : ""}${detached ? " (detached)" : ""}${result.output.queueReason ? ` waiting=${result.output.queueReason}` : ""}\n${result.output.text}`
|
|
192
195
|
}
|
|
193
196
|
],
|
|
194
197
|
details: result
|
|
@@ -346,7 +349,7 @@ class BashRuntime implements ResidentAgent {
|
|
|
346
349
|
this.fail(error)
|
|
347
350
|
throw error
|
|
348
351
|
}
|
|
349
|
-
return { delivery: "stdin" as const, queuePosition: null, queuedFollowUps: 0 }
|
|
352
|
+
return { run: 1, delivery: "stdin" as const, queuePosition: null, queuedFollowUps: 0 }
|
|
350
353
|
})
|
|
351
354
|
this.#inputLane = operation.catch(() => {})
|
|
352
355
|
if (!options.signal) return operation
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
1
2
|
import { lstat, open } from "node:fs/promises"
|
|
2
3
|
import {
|
|
3
4
|
type AgentSession,
|
|
@@ -12,10 +13,12 @@ import {
|
|
|
12
13
|
SettingsManager,
|
|
13
14
|
type Skill
|
|
14
15
|
} from "@earendil-works/pi-coding-agent"
|
|
16
|
+
import { Type } from "typebox"
|
|
17
|
+
import { Value } from "typebox/value"
|
|
15
18
|
import { MODEL_ALIASES, type ModelAliasChoice } from "./config.js"
|
|
16
19
|
import { getAgentCoordinator } from "./coordinator.js"
|
|
17
20
|
import type { AgentDefinition, AgentThinkingLevel } from "./definitions.js"
|
|
18
|
-
import type
|
|
21
|
+
import { mutateTaskMetadata, TaskProgressSchema, type TaskStoragePaths } from "./state.js"
|
|
19
22
|
|
|
20
23
|
const PROMPT_EXTENSION_PATH = "<inline:lovely-agent-prompt>"
|
|
21
24
|
const CREATION_TOOL_NAMES = new Set(["agent"])
|
|
@@ -51,6 +54,8 @@ export type ChildSessionHandle = {
|
|
|
51
54
|
extensionsResult: LoadExtensionsResult
|
|
52
55
|
depth: number
|
|
53
56
|
allowAgents: boolean
|
|
57
|
+
/** Binds tool execution to this immutable run, including delayed async callbacks. */
|
|
58
|
+
prompt(runId: string, text: string, options?: PromptOptions): Promise<void>
|
|
54
59
|
dispose(): void
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -126,6 +131,8 @@ export async function createChildSession(options: CreateChildSessionOptions): Pr
|
|
|
126
131
|
const agentDir = options.agentDir ?? getAgentDir()
|
|
127
132
|
const settingsManager = SettingsManager.create(options.cwd, agentDir)
|
|
128
133
|
settingsManager.setProjectTrusted(options.projectTrusted)
|
|
134
|
+
const runContext = new AsyncLocalStorage<string>()
|
|
135
|
+
const lifetime = new AbortController()
|
|
129
136
|
const resourceLoader = new DefaultResourceLoader({
|
|
130
137
|
cwd: options.cwd,
|
|
131
138
|
agentDir,
|
|
@@ -137,6 +144,40 @@ export async function createChildSession(options: CreateChildSessionOptions): Pr
|
|
|
137
144
|
name: "lovely-agent-prompt",
|
|
138
145
|
hidden: true,
|
|
139
146
|
factory(pi) {
|
|
147
|
+
pi.registerTool({
|
|
148
|
+
name: "task_update",
|
|
149
|
+
label: "Task Update",
|
|
150
|
+
description:
|
|
151
|
+
"Report a short progress line for your own current task run (up to 240 characters). Does not change its label or lifecycle state, or notify the parent.",
|
|
152
|
+
promptSnippet: "Update your task's progress in the panel and task inspection",
|
|
153
|
+
promptGuidelines: [
|
|
154
|
+
"Use task_update for meaningful phase changes or blockers: what is achieved and what remains. Do not report every tool call or invent percentages. Updates do not interrupt or wake the parent."
|
|
155
|
+
],
|
|
156
|
+
parameters: Type.Object({ progress: TaskProgressSchema }, { additionalProperties: false }),
|
|
157
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
158
|
+
const runId = runContext.getStore()
|
|
159
|
+
if (!runId) throw new Error("task_update requires a managed task run")
|
|
160
|
+
if (!Value.Check(TaskProgressSchema, params.progress)) throw new Error("progress must be 1–240 characters")
|
|
161
|
+
const progress = params.progress.replace(/\s+/g, " ").trim()
|
|
162
|
+
if (!progress || /\p{Cc}/u.test(progress)) throw new Error("progress must be nonempty text without control characters")
|
|
163
|
+
const childSessionId = ctx.sessionManager.getSessionId()
|
|
164
|
+
await mutateTaskMetadata(options.paths, metadata => {
|
|
165
|
+
signal?.throwIfAborted()
|
|
166
|
+
lifetime.signal.throwIfAborted()
|
|
167
|
+
if (
|
|
168
|
+
metadata.kind !== "agent" ||
|
|
169
|
+
metadata.childSessionId !== childSessionId ||
|
|
170
|
+
metadata.activeRun?.id !== runId ||
|
|
171
|
+
metadata.state !== "running" ||
|
|
172
|
+
metadata.discardedAt !== null
|
|
173
|
+
) {
|
|
174
|
+
throw new Error("task_update can only update its own active run")
|
|
175
|
+
}
|
|
176
|
+
return { ...metadata, progress, updatedAt: Date.now() }
|
|
177
|
+
})
|
|
178
|
+
return { content: [{ type: "text", text: "Progress updated." }], details: { progress } }
|
|
179
|
+
}
|
|
180
|
+
})
|
|
140
181
|
pi.on("before_agent_start", event => ({
|
|
141
182
|
systemPrompt: buildDefinitionSystemPrompt(event.systemPromptOptions)
|
|
142
183
|
}))
|
|
@@ -173,7 +214,6 @@ export async function createChildSession(options: CreateChildSessionOptions): Pr
|
|
|
173
214
|
throw new Error(`Child session identity mismatch: expected ${options.expectedSessionId}, found ${result.session.sessionId}`)
|
|
174
215
|
}
|
|
175
216
|
|
|
176
|
-
const lifetime = new AbortController()
|
|
177
217
|
const unbindContext = getAgentCoordinator().bindSessionContext(result.session.sessionId, {
|
|
178
218
|
depth: policy.depth,
|
|
179
219
|
allowAgents: policy.allowAgents,
|
|
@@ -193,6 +233,7 @@ export async function createChildSession(options: CreateChildSessionOptions): Pr
|
|
|
193
233
|
extensionsResult: result.extensionsResult,
|
|
194
234
|
depth: policy.depth,
|
|
195
235
|
allowAgents: policy.allowAgents,
|
|
236
|
+
prompt: (runId, text, promptOptions) => runContext.run(runId, () => result.session.prompt(text, promptOptions)),
|
|
196
237
|
dispose() {
|
|
197
238
|
if (disposed) return
|
|
198
239
|
disposed = true
|
|
@@ -39,7 +39,9 @@ export type ResidentAgent = {
|
|
|
39
39
|
export type ResidentInputOptions = { background?: boolean; signal?: AbortSignal; eof?: boolean }
|
|
40
40
|
|
|
41
41
|
export type ResidentInputResult = {
|
|
42
|
+
run: number
|
|
42
43
|
delivery: "followup" | "steer" | "stdin"
|
|
44
|
+
conversionReason?: string
|
|
43
45
|
queuePosition: number | null
|
|
44
46
|
queuedFollowUps: number
|
|
45
47
|
completed?: TaskMetadata
|
|
@@ -290,7 +290,8 @@ function parseTools(value: unknown, knownTools: readonly string[]): { value?: st
|
|
|
290
290
|
const tools = raw.map(tool => (tool as string).trim())
|
|
291
291
|
const duplicate = tools.find((tool, index) => tools.indexOf(tool) !== index)
|
|
292
292
|
if (duplicate) return { error: `duplicate tool "${duplicate}"` }
|
|
293
|
-
|
|
293
|
+
// This tool is registered only inside managed children, not in the parent's roster.
|
|
294
|
+
const unknown = tools.filter(tool => tool !== "task_update" && !knownTools.includes(tool))
|
|
294
295
|
if (unknown.length > 0) return { error: `unknown tools: ${unknown.sort(compareText).join(", ")}` }
|
|
295
296
|
return { value: tools }
|
|
296
297
|
}
|
|
@@ -164,7 +164,7 @@ export default function lovelyAgentsExtension(pi: ExtensionAPI) {
|
|
|
164
164
|
pi.sendMessage(
|
|
165
165
|
{
|
|
166
166
|
customType: NOTIFICATION_CUSTOM_TYPE,
|
|
167
|
-
content: `User manually discarded task ${id} and its descendants.\nFiles
|
|
167
|
+
content: `User manually discarded task ${id} and its descendants.\nFiles stay at their original paths for read-only inspection; these tasks cannot receive further input.`,
|
|
168
168
|
display: true
|
|
169
169
|
},
|
|
170
170
|
{ deliverAs: "steer", triggerTurn: false }
|