mirror of
https://github.com/wassname/pi-lgtm.git
synced 2026-06-27 17:01:35 +08:00
fix TaskExecute UX: debug logging, agent ID resolution, TaskGet consistency
- Add PI_TASKS_DEBUG=1 env flag to trace RPC communication to stderr - TaskOutput/TaskStop now accept agent IDs (resolve via agentTaskMap) - TaskGet filters completed blockers (consistent with TaskList) - TaskGet shows non-empty metadata - Soften TaskExecute description to not deter agents from using it - TaskExecute success message guides agents to use TaskOutput - Add promptGuidelines to prevent duplicate agent spawns - Update changelog
This commit is contained in:
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **RPC-based subagent spawning** — `TaskExecute` now communicates with `@tintinweb/pi-subagents` via a standardized RPC envelope (`rpcCall` helper) with protocol version negotiation and timeout handling.
|
||||||
|
- **RPC-based subagent stopping** — `stopSubagent` sends stop requests via `subagents:rpc:stop` event bus RPC.
|
||||||
|
- **TaskOutput supports subagent tasks** — can wait for subagent completion with blocking/timeout, using `subagents:completed` and `subagents:failed` events.
|
||||||
|
- **TaskStop supports subagent tasks** — stops running subagents via RPC and marks the task as completed.
|
||||||
|
- **Debug logging** — set `PI_TASKS_DEBUG=1` to trace RPC communication (request/reply/timeout) and spawn errors to stderr.
|
||||||
|
- **TaskExecute prompt guidelines** — agents are instructed not to use the Agent tool for tasks already launched via TaskExecute.
|
||||||
|
- **Biome linter** — added [Biome](https://biomejs.dev/) for correctness linting.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **TaskOutput/TaskStop accept agent IDs** — both tools now resolve agent IDs (including partial prefixes) to task IDs via `agentTaskMap`, fixing the mismatch where TaskExecute returns agent IDs but TaskOutput/TaskStop only accepted task IDs.
|
||||||
|
- **TaskGet shows metadata** — non-empty metadata is now displayed in TaskGet output as JSON.
|
||||||
|
- **TaskGet filters completed blockers** — consistent with TaskList, TaskGet now only shows open (non-completed) blockers instead of all dependency edges.
|
||||||
|
- **TaskExecute success message** — now includes guidance to use TaskOutput for progress and not spawn duplicate agents.
|
||||||
|
- **Softened TaskExecute description** — removed "Requires @tintinweb/pi-subagents extension" from the tool description to prevent agents from refusing to use it when the extension is loaded.
|
||||||
|
- **Stopped subagents handled gracefully** — `subagents:failed` listener now distinguishes intentional stops (status `"stopped"` → mark completed, preserve partial result) from actual errors (revert to pending).
|
||||||
|
|
||||||
## [0.3.3] - 2026-03-17
|
## [0.3.3] - 2026-03-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+61
-9
@@ -24,6 +24,13 @@ import { openSettingsMenu } from "./ui/settings-menu.js";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
// ---- Debug ----
|
||||||
|
|
||||||
|
const DEBUG = !!process.env.PI_TASKS_DEBUG;
|
||||||
|
function debug(...args: unknown[]) {
|
||||||
|
if (DEBUG) console.error("[pi-tasks]", ...args);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
function textResult(msg: string) {
|
function textResult(msg: string) {
|
||||||
@@ -84,21 +91,30 @@ export default function (pi: ExtensionAPI) {
|
|||||||
/** Call a subagents RPC method: emit request, wait for scoped reply, unwrap envelope. */
|
/** Call a subagents RPC method: emit request, wait for scoped reply, unwrap envelope. */
|
||||||
function rpcCall<T>(channel: string, params: Record<string, unknown>, timeoutMs: number): Promise<T> {
|
function rpcCall<T>(channel: string, params: Record<string, unknown>, timeoutMs: number): Promise<T> {
|
||||||
const requestId = randomUUID();
|
const requestId = randomUUID();
|
||||||
|
debug(`rpc:send ${channel}`, { requestId });
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const timer = setTimeout(() => { unsub(); reject(new Error(`${channel} timeout`)); }, timeoutMs);
|
const timer = setTimeout(() => {
|
||||||
|
unsub();
|
||||||
|
debug(`rpc:timeout ${channel}`, { requestId });
|
||||||
|
reject(new Error(`${channel} timeout`));
|
||||||
|
}, timeoutMs);
|
||||||
const unsub = pi.events.on(`${channel}:reply:${requestId}`, (raw: unknown) => {
|
const unsub = pi.events.on(`${channel}:reply:${requestId}`, (raw: unknown) => {
|
||||||
unsub(); clearTimeout(timer);
|
unsub(); clearTimeout(timer);
|
||||||
|
debug(`rpc:reply ${channel}`, { requestId, raw });
|
||||||
const reply = raw as RpcReply<T>;
|
const reply = raw as RpcReply<T>;
|
||||||
if (reply.success) resolve(reply.data as T);
|
if (reply.success) resolve(reply.data as T);
|
||||||
else reject(new Error(reply.error));
|
else reject(new Error(reply.error));
|
||||||
});
|
});
|
||||||
pi.events.emit(channel, { requestId, ...params });
|
pi.events.emit(channel, { requestId, ...params });
|
||||||
|
debug(`rpc:emitted ${channel}`, { requestId });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spawn a subagent via pi.events RPC (requires @tintinweb/pi-subagents extension). */
|
/** Spawn a subagent via pi.events RPC (requires @tintinweb/pi-subagents extension). */
|
||||||
function spawnSubagent(type: string, prompt: string, options?: any): Promise<string> {
|
function spawnSubagent(type: string, prompt: string, options?: any): Promise<string> {
|
||||||
return rpcCall<{ id: string }>("subagents:rpc:spawn", { type, prompt, options }, 30_000).then(d => d.id);
|
debug("spawn:call", { type, options: { ...options, prompt: undefined } });
|
||||||
|
return rpcCall<{ id: string }>("subagents:rpc:spawn", { type, prompt, options }, 30_000)
|
||||||
|
.then(d => { debug("spawn:ok", d); return d.id; });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop a subagent via pi.events RPC (requires @tintinweb/pi-subagents extension). */
|
/** Stop a subagent via pi.events RPC (requires @tintinweb/pi-subagents extension). */
|
||||||
@@ -508,12 +524,24 @@ Returns full task details:
|
|||||||
lines.push(`Description: ${desc}`);
|
lines.push(`Description: ${desc}`);
|
||||||
|
|
||||||
if (task.blockedBy.length > 0) {
|
if (task.blockedBy.length > 0) {
|
||||||
lines.push(`Blocked by: ${task.blockedBy.map(id => "#" + id).join(", ")}`);
|
const openBlockers = task.blockedBy.filter(bid => {
|
||||||
|
const blocker = store.get(bid);
|
||||||
|
return blocker && blocker.status !== "completed";
|
||||||
|
});
|
||||||
|
if (openBlockers.length > 0) {
|
||||||
|
lines.push(`Blocked by: ${openBlockers.map(id => "#" + id).join(", ")}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (task.blocks.length > 0) {
|
if (task.blocks.length > 0) {
|
||||||
lines.push(`Blocks: ${task.blocks.map(id => "#" + id).join(", ")}`);
|
lines.push(`Blocks: ${task.blocks.map(id => "#" + id).join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show metadata if non-empty
|
||||||
|
const metaKeys = Object.keys(task.metadata);
|
||||||
|
if (metaKeys.length > 0) {
|
||||||
|
lines.push(`Metadata: ${JSON.stringify(task.metadata)}`);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(textResult(lines.join("\n")));
|
return Promise.resolve(textResult(lines.join("\n")));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -671,7 +699,15 @@ Set up task dependencies:
|
|||||||
const processOutput = tracker.getOutput(task_id);
|
const processOutput = tracker.getOutput(task_id);
|
||||||
if (!processOutput) {
|
if (!processOutput) {
|
||||||
// No shell process — check if this is a subagent task
|
// No shell process — check if this is a subagent task
|
||||||
const task = store.get(task_id);
|
// Support both task IDs and agent IDs (resolve agent ID → task ID)
|
||||||
|
let resolvedId = task_id;
|
||||||
|
if (!store.get(resolvedId)) {
|
||||||
|
// Check if this is an agent ID mapped to a task
|
||||||
|
for (const [agentId, taskId] of agentTaskMap) {
|
||||||
|
if (agentId === task_id || agentId.startsWith(task_id)) { resolvedId = taskId; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const task = store.get(resolvedId);
|
||||||
if (!task) throw new Error(`No task found with ID ${task_id}`);
|
if (!task) throw new Error(`No task found with ID ${task_id}`);
|
||||||
|
|
||||||
if (task.metadata?.agentId) {
|
if (task.metadata?.agentId) {
|
||||||
@@ -737,7 +773,14 @@ Set up task dependencies:
|
|||||||
const stopped = await tracker.stop(taskId);
|
const stopped = await tracker.stop(taskId);
|
||||||
if (!stopped) {
|
if (!stopped) {
|
||||||
// No shell process — check if this is a subagent task
|
// No shell process — check if this is a subagent task
|
||||||
const task = store.get(taskId);
|
// Support both task IDs and agent IDs
|
||||||
|
let resolvedId = taskId;
|
||||||
|
if (!store.get(resolvedId)) {
|
||||||
|
for (const [agentId, tId] of agentTaskMap) {
|
||||||
|
if (agentId === taskId || agentId.startsWith(taskId)) { resolvedId = tId; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const task = store.get(resolvedId);
|
||||||
if (task?.metadata?.agentId && task.status === "in_progress") {
|
if (task?.metadata?.agentId && task.status === "in_progress") {
|
||||||
store.update(taskId, { status: "completed" });
|
store.update(taskId, { status: "completed" });
|
||||||
await stopSubagent(task.metadata.agentId);
|
await stopSubagent(task.metadata.agentId);
|
||||||
@@ -762,7 +805,7 @@ Set up task dependencies:
|
|||||||
pi.registerTool({
|
pi.registerTool({
|
||||||
name: "TaskExecute",
|
name: "TaskExecute",
|
||||||
label: "TaskExecute",
|
label: "TaskExecute",
|
||||||
description: `Execute one or more tasks as subagents. Requires @tintinweb/pi-subagents extension.
|
description: `Execute one or more tasks as subagents.
|
||||||
|
|
||||||
## When to Use This Tool
|
## When to Use This Tool
|
||||||
|
|
||||||
@@ -776,6 +819,9 @@ Set up task dependencies:
|
|||||||
- **additional_context**: Extra context appended to each agent's prompt
|
- **additional_context**: Extra context appended to each agent's prompt
|
||||||
- **model**: Model override for agents (e.g., "sonnet", "haiku")
|
- **model**: Model override for agents (e.g., "sonnet", "haiku")
|
||||||
- **max_turns**: Maximum turns per agent`,
|
- **max_turns**: Maximum turns per agent`,
|
||||||
|
promptGuidelines: [
|
||||||
|
"Never use the Agent tool for tasks launched via TaskExecute — agents are already running.",
|
||||||
|
],
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
task_ids: Type.Array(Type.String(), { description: "Task IDs to execute as subagents" }),
|
task_ids: Type.Array(Type.String(), { description: "Task IDs to execute as subagents" }),
|
||||||
additional_context: Type.Optional(Type.String({ description: "Extra context for agent prompts" })),
|
additional_context: Type.Optional(Type.String({ description: "Extra context for agent prompts" })),
|
||||||
@@ -786,8 +832,8 @@ Set up task dependencies:
|
|||||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||||
if (!subagentsAvailable) {
|
if (!subagentsAvailable) {
|
||||||
return textResult(
|
return textResult(
|
||||||
"TaskExecute requires the @tintinweb/pi-subagents extension to be loaded. " +
|
"Subagent execution is currently unavailable. " +
|
||||||
"Install and enable it, then try again."
|
"Ensure the @tintinweb/pi-subagents extension is loaded and try again."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,6 +879,7 @@ Set up task dependencies:
|
|||||||
widget.setActiveTask(taskId);
|
widget.setActiveTask(taskId);
|
||||||
launched.push(`#${taskId} → agent ${agentId}`);
|
launched.push(`#${taskId} → agent ${agentId}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
debug(`spawn:error task=#${taskId}`, err);
|
||||||
store.update(taskId, { status: "pending" });
|
store.update(taskId, { status: "pending" });
|
||||||
results.push(`#${taskId}: spawn failed — ${err.message}`);
|
results.push(`#${taskId}: spawn failed — ${err.message}`);
|
||||||
}
|
}
|
||||||
@@ -848,7 +895,12 @@ Set up task dependencies:
|
|||||||
widget.update();
|
widget.update();
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
if (launched.length > 0) lines.push(`Launched ${launched.length} agent(s):\n${launched.join("\n")}`);
|
if (launched.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
`Launched ${launched.length} agent(s):\n${launched.join("\n")}\n` +
|
||||||
|
`Use TaskOutput to check progress. Do not spawn additional agents for these tasks.`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (results.length > 0) lines.push(`Skipped:\n${results.join("\n")}`);
|
if (results.length > 0) lines.push(`Skipped:\n${results.join("\n")}`);
|
||||||
if (lines.length === 0) lines.push("No tasks to execute.");
|
if (lines.length === 0) lines.push("No tasks to execute.");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user