@signalridge/pi-subagents 1.10.0 → 1.10.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 +61 -0
- package/README.md +13 -13
- package/package.json +5 -5
- package/src/agent-runner.ts +54 -37
- package/src/supervisor.ts +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.10.2
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 65e2481: Show supervisor questions in subagent input dialogs and identify the asking agent by its actual name.
|
|
7
|
+
|
|
8
|
+
## 1.10.1
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 1f586f8: Preserve resumable cancellation when a workflow pauses during successful timeout cleanup, and retain finalized subagent output alongside observer failures. Avoid retaining descendant-owned mouse gestures in bordered components. Track successful writes using final rewritten paths, and distinguish uncertain changes observed during failed calls from confirmed agent modifications while keeping both discoverable in file browsing. Preserve exact filesystem paths and lazy-directory scan state when discovering session-observed changes, including changes injected during an active scan.
|
|
12
|
+
- 1f586f8: - Bound workflow cancellation drains and durably fence unconfirmed cleanup, aborting sibling agents and releasing foreground callers.
|
|
13
|
+
- Await managed timeout cleanup before script continuation and terminal publication.
|
|
14
|
+
- Keep failed retry text and length responses discarded by overflow-compaction retry out of successful subagent fallbacks, while retaining valid pre-compaction and non-retried length output.
|
|
15
|
+
- Account for native prompts spanning reporter activation without dropping out-of-order wait debt.
|
|
16
|
+
- Reconcile settlement after observer-triggered manual compaction with run/session ownership.
|
|
17
|
+
- Preserve clipped overlay hit ranges and native container ancestry for editor autocomplete focus.
|
|
18
|
+
- Reconcile errored writes and edits against pre/post mutation evidence, retaining invocation cwd and releasing evidence when blocked or aborted preflight bypasses tool results.
|
|
19
|
+
- d9219d4: Correct shipped documentation that contradicted the code or the manifest.
|
|
20
|
+
|
|
21
|
+
- `pi-subagents`: the summary line still advertised protocol-v3 spawning while
|
|
22
|
+
the feature list and the managed-spawn section described v4.
|
|
23
|
+
- `pi-worktree`: the workspace-switching section named `/tree`; the registered
|
|
24
|
+
command is `/worktree`.
|
|
25
|
+
- `pi-workflows`: removed an orphaned sentence fragment left behind by an edit
|
|
26
|
+
to the saved-workflow paragraph.
|
|
27
|
+
- `pi-plan-mode`, `pi-goal`, `pi-usage-extension`: the stated Pi floor
|
|
28
|
+
(`0.80.6`, `0.42.4+`) contradicted the declared `^0.84.0` peer range, which
|
|
29
|
+
resolves to `0.84.x`. `pi-usage-extension` also carried a "Last updated"
|
|
30
|
+
stamp from a version that predates the workspace baseline.
|
|
31
|
+
- `pi-welcome`: the example card was a real session capture. It is now a
|
|
32
|
+
generic sample, and Install moved above the configuration sections instead of
|
|
33
|
+
sitting below them.
|
|
34
|
+
- 1f586f8: Expand Pi peer support to `^0.84.0 || ^0.85.0`, retaining 0.84 compatibility while admitting 0.85 releases. The previous zero-major caret range excluded Pi 0.85.1. Update existing Pi development dependency pins to 0.85.1.
|
|
35
|
+
- d9219d4: Converge every README on one house style: plain sentence-case headings, no
|
|
36
|
+
decorative emoji.
|
|
37
|
+
|
|
38
|
+
Ten packages carried an emoji heading scheme inherited from their upstream
|
|
39
|
+
forks while the other nineteen used plain headings, so the same monorepo
|
|
40
|
+
rendered as two unrelated projects on npmjs.com. Headings are now emoji-free
|
|
41
|
+
and titles are sentence case.
|
|
42
|
+
|
|
43
|
+
Also removes the `Keywords` section from those ten. It duplicated each
|
|
44
|
+
package's `package.json` `keywords` field, which is what npm actually indexes,
|
|
45
|
+
and no plain-style README carried one. `Installation` is now `Install`
|
|
46
|
+
everywhere.
|
|
47
|
+
|
|
48
|
+
Headings that were Title Case are sentence case too, so one convention now
|
|
49
|
+
covers the whole monorepo. Existing in-page anchor links are unaffected:
|
|
50
|
+
GitHub lowercases heading slugs already.
|
|
51
|
+
|
|
52
|
+
Documentation only — no runtime change.
|
|
53
|
+
- 1f586f8: Keep goal iteration and continuation numbering correct when Pi compacts between tool results and the next response within one run. Preserve existing response limits and manual/post-run compaction handling.
|
|
54
|
+
|
|
55
|
+
Capture subagent invocation results from finalized assistant events with bounded message retention so compaction cannot hide a final error or mix in historical output. Recognize explicitly selected PowerShell when the host provides it, without requiring its export on older Pi versions or expanding default wildcard tools.
|
|
56
|
+
|
|
57
|
+
Use real session contexts for workflow replay and live TUI progress, release widget bindings on shutdown, and allow foreground checkpoint dialogs in RPC mode while retaining print-mode defaults. Cancel unanswered dialogs with their owning execution on tool abort, pause, stop, disposal, or fatal failure without journaling cancelled answers.
|
|
58
|
+
- Updated dependencies [1f586f8]
|
|
59
|
+
- Updated dependencies [1f586f8]
|
|
60
|
+
- Updated dependencies [1f586f8]
|
|
61
|
+
- Updated dependencies [1f586f8]
|
|
62
|
+
- @signalridge/pi-ui@1.3.1
|
|
63
|
+
|
|
3
64
|
## 1.10.0
|
|
4
65
|
### Minor Changes
|
|
5
66
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @signalridge/pi-subagents
|
|
2
2
|
|
|
3
|
-
Signalridge's managed subagent package for Pi, including protocol-
|
|
3
|
+
Signalridge's managed subagent package for Pi, including protocol-v4 spawning, lifecycle isolation, Fleet UI, persistence, and recovery.
|
|
4
4
|
|
|
5
5
|
A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
|
|
6
6
|
|
|
@@ -50,7 +50,7 @@ pi -e ./packages/pi-subagents
|
|
|
50
50
|
|
|
51
51
|
Formatter checks remain disabled for this package, while legacy `noExplicitAny`, control-character regex, and empty-interface rules remain off. Signalridge additions follow the package AGENTS rules and the strict `--error-on-warnings` lint gate.
|
|
52
52
|
|
|
53
|
-
## Quick
|
|
53
|
+
## Quick start
|
|
54
54
|
|
|
55
55
|
The parent agent spawns sub-agents using the `Agent` tool:
|
|
56
56
|
|
|
@@ -161,7 +161,7 @@ Find auth files · completed
|
|
|
161
161
|
|
|
162
162
|
Group completions render each agent as a separate block. The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
|
|
163
163
|
|
|
164
|
-
## Default
|
|
164
|
+
## Default agent types
|
|
165
165
|
|
|
166
166
|
| Type | Tools | Model | Prompt Mode | Description |
|
|
167
167
|
|------|-------|-------|-------------|-------------|
|
|
@@ -173,7 +173,7 @@ The `general-purpose` agent is a **parent twin** — it receives the parent's en
|
|
|
173
173
|
|
|
174
174
|
Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
|
|
175
175
|
|
|
176
|
-
## Custom
|
|
176
|
+
## Custom agents
|
|
177
177
|
|
|
178
178
|
Define custom agent types by creating `.md` files. The filename becomes the agent type name. Any name is allowed — using a default agent's name overrides it.
|
|
179
179
|
|
|
@@ -216,7 +216,7 @@ Then spawn it like any built-in type:
|
|
|
216
216
|
Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
### Frontmatter
|
|
219
|
+
### Frontmatter fields
|
|
220
220
|
|
|
221
221
|
All fields are optional — sensible defaults for everything.
|
|
222
222
|
|
|
@@ -376,7 +376,7 @@ Settings
|
|
|
376
376
|
- **Model tiers** — create, edit and delete the [tier](#model-tiers) profiles. Each row shows what the tier resolves to on this machine (`small — claude-haiku-4-5 · thinking max`), and a tier dropped as malformed is listed as `blocked` so it can be fixed rather than staying invisible. The model picker offers this machine's available models (narrowed to your scope when **Scope models** is on) plus `inherit` and a typed escape hatch; the thinking picker offers only the levels the chosen model actually supports, since the rest would be silently clamped at spawn. Deleting the tier that `defaultTier` names clears the default in the same step
|
|
377
377
|
- **Settings** — configure max concurrency, default max turns, grace turns, default model, default tier, and join mode at runtime
|
|
378
378
|
|
|
379
|
-
## Graceful
|
|
379
|
+
## Graceful max turns
|
|
380
380
|
|
|
381
381
|
Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
|
|
382
382
|
|
|
@@ -397,7 +397,7 @@ Background agents are subject to a configurable concurrency limit (default: 4).
|
|
|
397
397
|
|
|
398
398
|
Foreground agents bypass the queue — they block the parent anyway.
|
|
399
399
|
|
|
400
|
-
## Join
|
|
400
|
+
## Join strategies
|
|
401
401
|
|
|
402
402
|
When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered. It applies only to background agents.
|
|
403
403
|
|
|
@@ -640,7 +640,7 @@ then `defaultModel`, or the parent's model when none is set.
|
|
|
640
640
|
Programmatic callers and the legacy RPC may still pass `model`/`thinking`
|
|
641
641
|
directly. That is the escape hatch for code, not a way to configure an agent.
|
|
642
642
|
|
|
643
|
-
## Model
|
|
643
|
+
## Model scope
|
|
644
644
|
|
|
645
645
|
**Opt-in:** off by default. Enable via `/agents → Settings → Scope models`.
|
|
646
646
|
|
|
@@ -662,7 +662,7 @@ When on, each subagent spawn's effective model is validated against pi's own `en
|
|
|
662
662
|
|
|
663
663
|
**No-op safety:** if `enabledModels` is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.
|
|
664
664
|
|
|
665
|
-
## Persistent
|
|
665
|
+
## Persistent settings
|
|
666
666
|
|
|
667
667
|
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default model, default tier, default join mode, scheduling on/off, scope models on/off, strict agent files on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, fleet view on/off) persist across pi restarts. Two files, merged on load:
|
|
668
668
|
|
|
@@ -839,7 +839,7 @@ pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
|
|
|
839
839
|
Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
|
|
840
840
|
Workflow callers should pass a foreground signal only to their own wait. Managed child cancellation is explicit through the owner-scoped lifecycle RPC; aborting an RPC wait does not abort the child.
|
|
841
841
|
|
|
842
|
-
## Persistent
|
|
842
|
+
## Persistent agent memory
|
|
843
843
|
|
|
844
844
|
Agents can have persistent memory across sessions. Set `memory` in frontmatter to enable:
|
|
845
845
|
|
|
@@ -861,7 +861,7 @@ Memory uses a `MEMORY.md` index file and individual memory files with frontmatte
|
|
|
861
861
|
|
|
862
862
|
The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
|
|
863
863
|
|
|
864
|
-
## Worktree
|
|
864
|
+
## Worktree isolation
|
|
865
865
|
|
|
866
866
|
Set `isolation: worktree` to run an agent in a temporary git worktree:
|
|
867
867
|
|
|
@@ -881,7 +881,7 @@ If cleanup cannot be confirmed, the result keeps the worktree path and includes
|
|
|
881
881
|
|
|
882
882
|
The child prompt also states that the temporary worktree is the only writable checkout and names the original base repository as off-limits. This prevents inherited parent instructions from sending the agent back into the shared tree.
|
|
883
883
|
|
|
884
|
-
## Skill
|
|
884
|
+
## Skill preloading
|
|
885
885
|
|
|
886
886
|
Skills can be preloaded by name and injected into the agent's system prompt:
|
|
887
887
|
|
|
@@ -911,7 +911,7 @@ Recursion skips dotfile directories and `node_modules`. A directory that itself
|
|
|
911
911
|
|
|
912
912
|
**Security:** symlinks are rejected at every layer (root, flat file, skill directory, `SKILL.md` inside a skill directory) — intentional deviation from Pi, which follows symlinks. Skill names with path-traversal characters (`..`, `/`, `\`, spaces, leading dot, >128 chars) are rejected.
|
|
913
913
|
|
|
914
|
-
## Tool
|
|
914
|
+
## Tool denylist
|
|
915
915
|
|
|
916
916
|
Block specific tools from an agent even if extensions provide them:
|
|
917
917
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalridge/pi-subagents",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "Signalridge's managed subagent runtime with workflow-owned orchestration RPC.",
|
|
5
5
|
"author": "tintinweb and signalridge contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
"autonomous"
|
|
26
26
|
],
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-ai": "^0.84.0",
|
|
29
|
-
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
30
|
-
"@earendil-works/pi-tui": "^0.84.0"
|
|
28
|
+
"@earendil-works/pi-ai": "^0.84.0 || ^0.85.0",
|
|
29
|
+
"@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0",
|
|
30
|
+
"@earendil-works/pi-tui": "^0.84.0 || ^0.85.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@sinclair/typebox": "^0.34.50",
|
|
34
34
|
"croner": "^10.0.1",
|
|
35
35
|
"@signalridge/pi-subagents-protocol": "^1.5.0",
|
|
36
|
-
"@signalridge/pi-ui": "^1.3.
|
|
36
|
+
"@signalridge/pi-ui": "^1.3.1",
|
|
37
37
|
"nanoid": "^5.0.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
package/src/agent-runner.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
8
|
import type { Model } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { ExtensionContext, LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import {
|
|
11
12
|
type AgentSession,
|
|
12
13
|
type AgentSessionEvent,
|
|
@@ -641,6 +642,11 @@ export interface RunResult {
|
|
|
641
642
|
*/
|
|
642
643
|
function collectResponseText(session: AgentSession) {
|
|
643
644
|
let text = "";
|
|
645
|
+
// Invocation-owned events survive compaction replacing session history.
|
|
646
|
+
// Retain only the final message and latest nonempty text, not a transcript.
|
|
647
|
+
let lastMessage: AgentSession["messages"][number] | undefined;
|
|
648
|
+
let lastNonemptyText = "";
|
|
649
|
+
let beforeLastMessageText = "";
|
|
644
650
|
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
645
651
|
// message_start also fires for user and toolResult messages — resetting on
|
|
646
652
|
// those would wipe assistant text already collected. Reset only when a new
|
|
@@ -651,25 +657,33 @@ function collectResponseText(session: AgentSession) {
|
|
|
651
657
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
652
658
|
text += event.assistantMessageEvent.delta;
|
|
653
659
|
}
|
|
660
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
661
|
+
beforeLastMessageText = lastNonemptyText;
|
|
662
|
+
lastMessage = event.message;
|
|
663
|
+
text = extractText(event.message.content);
|
|
664
|
+
// Failed attempts remain visible as the current result, but must never
|
|
665
|
+
// replace the valid fallback: Pi removes them when retrying. Compaction
|
|
666
|
+
// may already have removed the earlier valid message from history.
|
|
667
|
+
if (text.trim() && event.message.stopReason !== "error" && event.message.stopReason !== "aborted") {
|
|
668
|
+
lastNonemptyText = text.trim();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (event.type === "compaction_end" && event.reason === "overflow" && !event.aborted && event.willRetry &&
|
|
672
|
+
lastMessage?.role === "assistant" && lastMessage.stopReason === "length") {
|
|
673
|
+
// A length result is legitimate until the SDK actually retries it. On
|
|
674
|
+
// overflow recovery Pi removes this candidate, including any occurrence
|
|
675
|
+
// restored by compaction AFTER this notification. Retract its fallback
|
|
676
|
+
// contribution from invocation state rather than rescanning that history.
|
|
677
|
+
lastNonemptyText = beforeLastMessageText;
|
|
678
|
+
text = "";
|
|
679
|
+
lastMessage = undefined;
|
|
680
|
+
}
|
|
654
681
|
});
|
|
655
|
-
return {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
* `startIndex` is the message count captured before the prompt, so the walk-back
|
|
661
|
-
* never crosses into a previous turn: on a resume whose new turn failed empty,
|
|
662
|
-
* this returns "" instead of the prior turn's answer (#144). Defaults to 0 (a
|
|
663
|
-
* fresh spawn, where the whole history belongs to this run).
|
|
664
|
-
*/
|
|
665
|
-
function getLastAssistantText(session: AgentSession, startIndex = 0): string {
|
|
666
|
-
for (let i = session.messages.length - 1; i >= startIndex; i--) {
|
|
667
|
-
const msg = session.messages[i];
|
|
668
|
-
if (msg.role !== "assistant") continue;
|
|
669
|
-
const text = extractText(msg.content).trim();
|
|
670
|
-
if (text) return text;
|
|
671
|
-
}
|
|
672
|
-
return "";
|
|
682
|
+
return {
|
|
683
|
+
getText: () => text.trim() || lastNonemptyText,
|
|
684
|
+
getFailure: () => finalTurnError(lastMessage ? [lastMessage] : []),
|
|
685
|
+
unsubscribe,
|
|
686
|
+
};
|
|
673
687
|
}
|
|
674
688
|
|
|
675
689
|
/**
|
|
@@ -683,12 +697,11 @@ function getLastAssistantText(session: AgentSession, startIndex = 0): string {
|
|
|
683
697
|
* Everything else completes: a clean "stop"/"toolUse" final, and — crucially — a
|
|
684
698
|
* "length" stop that DID produce text (a legitimate truncated-but-useful answer).
|
|
685
699
|
* "aborted" is handled by the manager's abort flag / "stopped" guard, not here.
|
|
686
|
-
*
|
|
687
|
-
* assistant message of its own never inherits a PRIOR turn's stop reason.
|
|
700
|
+
* Invocation-owned messages exclude historical failures, even after compaction.
|
|
688
701
|
*/
|
|
689
|
-
function finalTurnError(
|
|
690
|
-
for (let i =
|
|
691
|
-
const msg =
|
|
702
|
+
function finalTurnError(messages: AgentSession["messages"]): string | undefined {
|
|
703
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
704
|
+
const msg = messages[i];
|
|
692
705
|
if (msg.role !== "assistant") continue;
|
|
693
706
|
if (msg.stopReason === "error") {
|
|
694
707
|
return (msg as { errorMessage?: string }).errorMessage?.trim() || "provider error with no output";
|
|
@@ -984,6 +997,10 @@ export async function runAgent(
|
|
|
984
997
|
// into the allowlist, then dropped it at registration with no signal back.
|
|
985
998
|
if (agentConfig?.builtinToolNames?.length) {
|
|
986
999
|
const knownBuiltins = new Set(BUILTIN_TOOL_NAMES);
|
|
1000
|
+
// Optional public capability: do not require a new named export on Pi 0.84.
|
|
1001
|
+
if ("createPowerShellTool" in PiCodingAgent && typeof Reflect.get(PiCodingAgent, "createPowerShellTool") === "function") {
|
|
1002
|
+
knownBuiltins.add("powershell");
|
|
1003
|
+
}
|
|
987
1004
|
for (const name of agentConfig.builtinToolNames) {
|
|
988
1005
|
if (!knownBuiltins.has(name)) {
|
|
989
1006
|
options.onToolActivity?.({
|
|
@@ -1109,7 +1126,9 @@ export async function runAgent(
|
|
|
1109
1126
|
const supervisorTools =
|
|
1110
1127
|
ctx.hasUI && !isolated && options.supervisorQuestions !== false
|
|
1111
1128
|
? createSupervisorTool({
|
|
1112
|
-
|
|
1129
|
+
// Use the canonical agent type, not display_name: the question must
|
|
1130
|
+
// identify the actual subagent that requested the decision.
|
|
1131
|
+
agentLabel: agentConfig?.name ?? type,
|
|
1113
1132
|
ask: {
|
|
1114
1133
|
input: (title, placeholder) => ctx.ui.input(title, placeholder),
|
|
1115
1134
|
select: (title, choices) => ctx.ui.select(title, choices),
|
|
@@ -1167,6 +1186,10 @@ export async function runAgent(
|
|
|
1167
1186
|
const denyTools = new Set<string>(
|
|
1168
1187
|
EXCLUDED_TOOL_NAMES.filter((t) => !nestedToolNames.has(t)),
|
|
1169
1188
|
);
|
|
1189
|
+
// Keep the historical wildcard defaults; PowerShell is explicit opt-in.
|
|
1190
|
+
if ("createPowerShellTool" in PiCodingAgent && typeof Reflect.get(PiCodingAgent, "createPowerShellTool") === "function" && !builtinToolNameSet.has("powershell")) {
|
|
1191
|
+
denyTools.add("powershell");
|
|
1192
|
+
}
|
|
1170
1193
|
// Keep only the built-ins the agent asked for — deny the rest.
|
|
1171
1194
|
for (const name of BUILTIN_TOOL_NAMES) {
|
|
1172
1195
|
if (!builtinToolNameSet.has(name)) denyTools.add(name);
|
|
@@ -1392,6 +1415,9 @@ export async function runAgent(
|
|
|
1392
1415
|
handle.unref?.();
|
|
1393
1416
|
pendingToolTimeouts.set(toolCallId, handle);
|
|
1394
1417
|
};
|
|
1418
|
+
// Own finalized output before observer dispatch: a synchronous callback
|
|
1419
|
+
// failure must remain visible without erasing the answer it interrupted.
|
|
1420
|
+
const collector = collectResponseText(session);
|
|
1395
1421
|
const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
|
|
1396
1422
|
if (event.type === "turn_end") {
|
|
1397
1423
|
turnCount++;
|
|
@@ -1442,8 +1468,6 @@ export async function runAgent(
|
|
|
1442
1468
|
}
|
|
1443
1469
|
});
|
|
1444
1470
|
|
|
1445
|
-
const collector = collectResponseText(session);
|
|
1446
|
-
|
|
1447
1471
|
// Build the effective prompt: optionally prepend parent context
|
|
1448
1472
|
let effectivePrompt = prompt;
|
|
1449
1473
|
const inheritContext = internalOverride?.inheritContext ?? options.inheritContext;
|
|
@@ -1454,9 +1478,6 @@ export async function runAgent(
|
|
|
1454
1478
|
}
|
|
1455
1479
|
}
|
|
1456
1480
|
|
|
1457
|
-
// Boundary for the history fallback: only assistant text produced from here
|
|
1458
|
-
// on counts as this run's output (a fresh session, so usually 0).
|
|
1459
|
-
const startLen = session.messages.length;
|
|
1460
1481
|
try {
|
|
1461
1482
|
throwIfAborted(options.signal);
|
|
1462
1483
|
await session.prompt(effectivePrompt);
|
|
@@ -1467,7 +1488,7 @@ export async function runAgent(
|
|
|
1467
1488
|
pendingToolTimeouts.clear();
|
|
1468
1489
|
}
|
|
1469
1490
|
|
|
1470
|
-
const baseText = collector.getText()
|
|
1491
|
+
const baseText = collector.getText();
|
|
1471
1492
|
// The acceptance gate runs AFTER the agent is done and its verdict is
|
|
1472
1493
|
// appended to the result, so the parent reads the check and the claim it is
|
|
1473
1494
|
// checking side by side. It deliberately does not steer the agent to fix
|
|
@@ -1476,7 +1497,7 @@ export async function runAgent(
|
|
|
1476
1497
|
? `${baseText}${await runConfiguredGate(agentConfig.gate, effectiveCwd, options.pi)}`
|
|
1477
1498
|
: baseText;
|
|
1478
1499
|
completed = true;
|
|
1479
|
-
return { responseText, session, aborted, steered: softLimitReached, failure:
|
|
1500
|
+
return { responseText, session, aborted, steered: softLimitReached, failure: collector.getFailure() };
|
|
1480
1501
|
} finally {
|
|
1481
1502
|
cleanupAbort();
|
|
1482
1503
|
// The manager takes ownership only after onSessionCreated is invoked. If
|
|
@@ -1505,11 +1526,7 @@ export async function resumeAgent(
|
|
|
1505
1526
|
signal?: AbortSignal;
|
|
1506
1527
|
} = {},
|
|
1507
1528
|
): Promise<{ text: string; failure?: string }> {
|
|
1508
|
-
// Boundary for the history fallback: the session already holds prior turns,
|
|
1509
|
-
// so only assistant text produced by THIS resume prompt counts as its output
|
|
1510
|
-
// — a failed resume must not surface the previous turn's answer (#144).
|
|
1511
1529
|
throwIfAborted(options.signal);
|
|
1512
|
-
const startLen = session.messages.length;
|
|
1513
1530
|
const collector = collectResponseText(session);
|
|
1514
1531
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
|
1515
1532
|
|
|
@@ -1574,8 +1591,8 @@ export async function resumeAgent(
|
|
|
1574
1591
|
}
|
|
1575
1592
|
|
|
1576
1593
|
return {
|
|
1577
|
-
text: collector.getText()
|
|
1578
|
-
failure:
|
|
1594
|
+
text: collector.getText(),
|
|
1595
|
+
failure: collector.getFailure(),
|
|
1579
1596
|
};
|
|
1580
1597
|
}
|
|
1581
1598
|
|
package/src/supervisor.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface SupervisorAsk {
|
|
|
36
36
|
export interface SupervisorToolContext {
|
|
37
37
|
/** The parent's UI. Omitted when there is no human to ask. */
|
|
38
38
|
ask?: SupervisorAsk;
|
|
39
|
-
/**
|
|
39
|
+
/** Actual type/name of the asking agent, for the prompt title. */
|
|
40
40
|
agentLabel: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -82,7 +82,10 @@ export function createSupervisorTool(context: SupervisorToolContext) {
|
|
|
82
82
|
const question = truncateCodePoints(sanitizeDisplayText(params.question), MAX_QUESTION, "…").trim();
|
|
83
83
|
if (!question) return textResult("Ask a non-empty question.", true);
|
|
84
84
|
|
|
85
|
-
const title = `${context.agentLabel} asks`;
|
|
85
|
+
const title = `${sanitizeDisplayText(context.agentLabel)} asks`;
|
|
86
|
+
// Pi 0.85.1 ignores ui.input()'s placeholder argument. Keep the
|
|
87
|
+
// question in the title so the human sees it in both old and new hosts.
|
|
88
|
+
const promptTitle = `${title}: ${question}`;
|
|
86
89
|
const options = (params.options ?? [])
|
|
87
90
|
.map((option) => truncateCodePoints(sanitizeDisplayText(option), MAX_OPTION, "…").trim())
|
|
88
91
|
.filter((option) => option.length > 0)
|
|
@@ -92,8 +95,8 @@ export function createSupervisorTool(context: SupervisorToolContext) {
|
|
|
92
95
|
try {
|
|
93
96
|
answer =
|
|
94
97
|
options.length > 0
|
|
95
|
-
? await ask.select(
|
|
96
|
-
: await ask.input(
|
|
98
|
+
? await ask.select(promptTitle, options)
|
|
99
|
+
: await ask.input(promptTitle);
|
|
97
100
|
} catch (error: unknown) {
|
|
98
101
|
// A dialog that cannot open must not fail the child's whole run; it
|
|
99
102
|
// is told nobody answered and carries on under its own judgement.
|