Files
pi-config/extensions/pi-crew/src/extension/team-tool/respond.ts

104 lines
4.7 KiB
TypeScript

import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
import { withRunLockSync } from "../../state/locks.ts";
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
import { appendEvent } from "../../state/event-log.ts";
import { appendMailboxMessage } from "../../state/mailbox.ts";
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
import { logInternalError } from "../../utils/internal-error.ts";
import type { PiTeamsToolResult } from "../tool-result.ts";
import { result, type TeamContext } from "./context.ts";
/**
* Handle `respond` action: send a message to a waiting (interactive) task.
* The task must be in "waiting" status. The message is stored in the task's
* mailbox and the task is re-queued for durable scheduler resume.
*/
export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
if (!params.runId) return result("Respond requires runId.", { action: "respond", status: "error" }, true);
if (!params.message && !params.taskId) return result("Respond requires taskId and/or message.", { action: "respond", status: "error" }, true);
const loaded = loadRunManifestById(ctx.cwd, params.runId);
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
return withRunLockSync(loaded.manifest, () => {
const fresh = loadRunManifestById(ctx.cwd, params.runId!);
if (!fresh) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
const foreignRun = typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId;
if (foreignRun) return result(`Run ${fresh.manifest.runId} belongs to another session; not responding.`, { action: "respond", status: "error", runId: fresh.manifest.runId }, true);
const taskId = params.taskId;
const message = params.message ?? "";
const targetTasks = taskId
? fresh.tasks.filter((t) => t.id === taskId && t.status === "waiting")
: fresh.tasks.filter((t) => t.status === "waiting");
if (targetTasks.length === 0) {
const existing = taskId ? fresh.tasks.find((t) => t.id === taskId) : undefined;
const hint = " Use api operation=follow-up-agent for continuation prompts or api operation=steer-agent to interrupt active work.";
return result(
(taskId
? existing
? `Task '${taskId}' is ${existing.status}, not waiting.`
: `Task '${taskId}' not found.`
: `No waiting tasks in run ${fresh.manifest.runId}.`) + hint,
{ action: "respond", status: "error", runId: fresh.manifest.runId },
true,
);
}
const resumed = new Set(targetTasks.map((t) => t.id));
const mailboxIds: string[] = [];
for (const task of targetTasks) {
const mailbox = appendMailboxMessage(fresh.manifest, {
direction: "inbox",
from: "leader",
to: task.id,
taskId: task.id,
body: message || "(resume)",
kind: "response",
priority: "normal",
deliveryMode: "next_turn",
data: { action: "respond", kind: "response" },
});
mailboxIds.push(mailbox.id);
}
// Re-queue waiting tasks so durable scheduler/resume can pick them up again.
const updatedTasks = fresh.tasks.map((task) => {
if (!resumed.has(task.id)) return task;
return {
...task,
status: "queued" as const,
startedAt: undefined,
finishedAt: undefined,
error: undefined,
adaptive: {
...task.adaptive,
phase: "resumed",
task: message || task.adaptive?.task || "",
},
};
});
saveRunTasks(fresh.manifest, updatedTasks);
let manifest = fresh.manifest;
if (manifest.status === "blocked" || manifest.status === "completed" || manifest.status === "failed" || manifest.status === "cancelled") {
manifest = updateRunStatus(manifest, "running", `Resumed ${resumed.size} waiting task(s).`);
}
for (const taskId of resumed) {
appendEvent(manifest.eventsPath, { type: "task.resumed", runId: manifest.runId, taskId, message: message || "Task re-queued after respond.", data: { mailboxIds } });
}
try {
saveCrewAgents(fresh.manifest, updatedTasks.map((task) => recordFromTask(fresh.manifest, task, "child-process")));
} catch (error) {
logInternalError("team-tool.handleRespond.crewAgents", error, `runId=${fresh.manifest.runId}`);
}
const resumedIds = targetTasks.map((t) => t.id);
return result(
`Resumed ${resumedIds.length} waiting task(s): ${resumedIds.join(", ")}. Message: ${message || "(no message)"}`,
{ action: "respond", status: "ok", runId: fresh.manifest.runId, resumedIds, mailboxIds },
);
});
}