feat: refine subagents + add chat.inject
Co-authored-by: Tyler Yust <tyler6204@users.noreply.github.com>
This commit is contained in:
@@ -8,11 +8,7 @@ import {
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
import { AGENT_LANE_NESTED } from "./lanes.js";
|
||||
import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js";
|
||||
import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js";
|
||||
import { isAnnounceSkip } from "./tools/sessions-send-helpers.js";
|
||||
import { readLatestAssistantReply } from "./tools/agent-step.js";
|
||||
|
||||
function formatDurationShort(valueMs?: number) {
|
||||
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) return undefined;
|
||||
@@ -149,27 +145,27 @@ export function buildSubagentSystemPrompt(params: {
|
||||
"",
|
||||
"## Your Role",
|
||||
`- You were created to handle: ${taskText}`,
|
||||
"- Complete this task and report back. That's your entire purpose.",
|
||||
"- Complete this task. That's your entire purpose.",
|
||||
"- You are NOT the main agent. Don't try to be.",
|
||||
"",
|
||||
"## Rules",
|
||||
"1. **Stay focused** - Do your assigned task, nothing else",
|
||||
"2. **Report completion** - When done, summarize results clearly",
|
||||
"2. **Complete the task** - Your final message will be automatically reported to the main agent",
|
||||
"3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
|
||||
"4. **Ask the spawner** - If blocked or confused, report back rather than improvising",
|
||||
"5. **Be ephemeral** - You may be terminated after task completion. That's fine.",
|
||||
"4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
|
||||
"",
|
||||
"## Output Format",
|
||||
"When complete, your final response should include:",
|
||||
"- What you accomplished or found",
|
||||
"- Any relevant details the main agent should know",
|
||||
"- Keep it concise but informative",
|
||||
"",
|
||||
"## What You DON'T Do",
|
||||
"- NO user conversations (that's main agent's job)",
|
||||
"- NO external messages (email, tweets, etc.) unless explicitly tasked",
|
||||
"- NO cron jobs or persistent state",
|
||||
"- NO pretending to be the main agent",
|
||||
"",
|
||||
"## Output Format",
|
||||
"When complete, respond with:",
|
||||
"- **Status:** success | failed | blocked",
|
||||
"- **Result:** [what you accomplished]",
|
||||
"- **Notes:** [anything the main agent should know] - discuss gimme options",
|
||||
"- NO using the `message` tool directly",
|
||||
"",
|
||||
"## Session Context",
|
||||
params.label ? `- Label: ${params.label}` : undefined,
|
||||
@@ -177,8 +173,6 @@ export function buildSubagentSystemPrompt(params: {
|
||||
params.requesterChannel ? `- Requester channel: ${params.requesterChannel}.` : undefined,
|
||||
`- Your session: ${params.childSessionKey}.`,
|
||||
"",
|
||||
"Run the task. Provide a clear final answer (plain text).",
|
||||
'After you finish, you may be asked to produce an "announce" message to post back to the requester chat.',
|
||||
].filter((line): line is string => line !== undefined);
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -188,109 +182,6 @@ export type SubagentRunOutcome = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const ANNOUNCE_SECTION_RE = /^\s*[-*]?\s*(?:\*\*)?(status|result|notes)(?:\*\*)?\s*:\s*(.*)$/i;
|
||||
|
||||
function parseAnnounceSections(announce: string) {
|
||||
const sections = {
|
||||
status: [] as string[],
|
||||
result: [] as string[],
|
||||
notes: [] as string[],
|
||||
};
|
||||
let current: keyof typeof sections | null = null;
|
||||
let sawSection = false;
|
||||
|
||||
for (const line of announce.split(/\r?\n/)) {
|
||||
const match = line.match(ANNOUNCE_SECTION_RE);
|
||||
if (match) {
|
||||
const key = match[1]?.toLowerCase() as keyof typeof sections;
|
||||
current = key;
|
||||
sawSection = true;
|
||||
const rest = match[2]?.trim();
|
||||
if (rest) sections[key].push(rest);
|
||||
continue;
|
||||
}
|
||||
if (current) sections[current].push(line);
|
||||
}
|
||||
|
||||
const normalize = (lines: string[]) => {
|
||||
const joined = lines.join("\n").trim();
|
||||
return joined.length > 0 ? joined : undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
sawSection,
|
||||
status: normalize(sections.status),
|
||||
result: normalize(sections.result),
|
||||
notes: normalize(sections.notes),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAnnounceBody(params: {
|
||||
outcome: SubagentRunOutcome;
|
||||
announceReply: string;
|
||||
statsLine?: string;
|
||||
}) {
|
||||
const announce = params.announceReply.trim();
|
||||
const statsLine = params.statsLine?.trim();
|
||||
|
||||
const statusLabel =
|
||||
params.outcome.status === "ok"
|
||||
? "success"
|
||||
: params.outcome.status === "timeout"
|
||||
? "timeout"
|
||||
: params.outcome.status === "unknown"
|
||||
? "unknown"
|
||||
: "error";
|
||||
|
||||
const parsed = parseAnnounceSections(announce);
|
||||
const resultText = parsed.result ?? (announce || "(not available)");
|
||||
const notesParts: string[] = [];
|
||||
if (parsed.notes) notesParts.push(parsed.notes);
|
||||
if (params.outcome.error) notesParts.push(`- Error: ${params.outcome.error}`);
|
||||
const notesBlock = notesParts.length ? notesParts.join("\n") : "- (none)";
|
||||
|
||||
const message = [
|
||||
`Status: ${statusLabel}`,
|
||||
"",
|
||||
"Result:",
|
||||
resultText,
|
||||
"",
|
||||
"Notes:",
|
||||
notesBlock,
|
||||
].join("\n");
|
||||
|
||||
return statsLine ? `${message}\n\n${statsLine}` : message;
|
||||
}
|
||||
|
||||
function buildSubagentAnnouncePrompt(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterChannel?: string;
|
||||
announceChannel: string;
|
||||
task: string;
|
||||
subagentReply?: string;
|
||||
}) {
|
||||
const lines = [
|
||||
"Sub-agent announce step:",
|
||||
params.requesterSessionKey ? `Requester session: ${params.requesterSessionKey}.` : undefined,
|
||||
params.requesterChannel ? `Requester channel: ${params.requesterChannel}.` : undefined,
|
||||
`Post target channel: ${params.announceChannel}.`,
|
||||
`Original task: ${params.task}`,
|
||||
params.subagentReply
|
||||
? `Sub-agent result: ${params.subagentReply}`
|
||||
: "Sub-agent result: (not available).",
|
||||
"",
|
||||
"**You MUST announce your result.** The requester is waiting for your response.",
|
||||
"Provide a brief, useful summary of what you accomplished.",
|
||||
"Reply with Result and Notes only (no Status line; status is added by the system).",
|
||||
"Format:",
|
||||
"Result: <summary>",
|
||||
"Notes: <extra context>",
|
||||
'Only reply "ANNOUNCE_SKIP" if the task completely failed with no useful output.',
|
||||
"Your reply will be posted to the requester chat.",
|
||||
].filter(Boolean);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function runSubagentAnnounceFlow(params: {
|
||||
childSessionKey: string;
|
||||
childRunId: string;
|
||||
@@ -340,8 +231,6 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
params.endedAt = wait.endedAt;
|
||||
}
|
||||
if (wait?.status === "timeout") {
|
||||
// No lifecycle end seen before timeout. Still attempt an announce so
|
||||
// requesters are not left hanging.
|
||||
if (!outcome) outcome = { status: "timeout" };
|
||||
}
|
||||
reply = await readLatestAssistantReply({
|
||||
@@ -357,53 +246,50 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
|
||||
if (!outcome) outcome = { status: "unknown" };
|
||||
|
||||
const announceTarget = await resolveAnnounceTarget({
|
||||
sessionKey: params.requesterSessionKey,
|
||||
displayKey: params.requesterDisplayKey,
|
||||
});
|
||||
if (!announceTarget) return false;
|
||||
|
||||
const announcePrompt = buildSubagentAnnouncePrompt({
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
requesterChannel: params.requesterChannel,
|
||||
announceChannel: announceTarget.channel,
|
||||
task: params.task,
|
||||
subagentReply: reply,
|
||||
});
|
||||
|
||||
const announceReply = await runAgentStep({
|
||||
sessionKey: params.childSessionKey,
|
||||
message: "Sub-agent announce step.",
|
||||
extraSystemPrompt: announcePrompt,
|
||||
timeoutMs: params.timeoutMs,
|
||||
channel: INTERNAL_MESSAGE_CHANNEL,
|
||||
lane: AGENT_LANE_NESTED,
|
||||
});
|
||||
|
||||
if (!announceReply || !announceReply.trim() || isAnnounceSkip(announceReply)) return false;
|
||||
|
||||
// Build stats
|
||||
const statsLine = await buildSubagentStatsLine({
|
||||
sessionKey: params.childSessionKey,
|
||||
startedAt: params.startedAt,
|
||||
endedAt: params.endedAt,
|
||||
});
|
||||
const message = normalizeAnnounceBody({
|
||||
outcome,
|
||||
announceReply,
|
||||
statsLine,
|
||||
});
|
||||
|
||||
// Build status label
|
||||
const statusLabel =
|
||||
outcome.status === "ok"
|
||||
? "completed successfully"
|
||||
: outcome.status === "timeout"
|
||||
? "timed out"
|
||||
: outcome.status === "error"
|
||||
? `failed: ${outcome.error || "unknown error"}`
|
||||
: "finished with unknown status";
|
||||
|
||||
// Build instructional message for main agent
|
||||
const taskLabel = params.label || params.task || "background task";
|
||||
const triggerMessage = [
|
||||
`A background task "${taskLabel}" just ${statusLabel}.`,
|
||||
"",
|
||||
"Findings:",
|
||||
reply || "(no output)",
|
||||
"",
|
||||
statsLine,
|
||||
"",
|
||||
"Summarize this naturally for the user. Keep it brief (1-2 sentences). Flow it into the conversation naturally.",
|
||||
"Do not mention technical details like tokens, stats, or that this was a background task.",
|
||||
"You can respond with NO_REPLY if no announcement is needed (e.g., internal task with no user-facing result).",
|
||||
].join("\n");
|
||||
|
||||
// Send to main agent - it will respond in its own voice
|
||||
await callGateway({
|
||||
method: "send",
|
||||
method: "agent",
|
||||
params: {
|
||||
to: announceTarget.to,
|
||||
message,
|
||||
channel: announceTarget.channel,
|
||||
accountId: announceTarget.accountId,
|
||||
sessionKey: params.requesterSessionKey,
|
||||
message: triggerMessage,
|
||||
deliver: true,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
|
||||
didAnnounce = true;
|
||||
} catch {
|
||||
// Best-effort follow-ups; ignore failures to avoid breaking the caller response.
|
||||
|
||||
Reference in New Issue
Block a user