diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index ab2fbb1140e..08797c32de0 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -1262,6 +1262,7 @@ export async function runSubagentAnnounceFlow(params: { wakeOnDescendantSettle?: boolean; signal?: AbortSignal; bestEffortDeliver?: boolean; + maxAnnounceChars?: number; }): Promise { let didAnnounce = false; const expectsCompletionMessage = params.expectsCompletionMessage === true; @@ -1488,6 +1489,16 @@ export async function runSubagentAnnounceFlow(params: { startedAt: params.startedAt, endedAt: params.endedAt, }); + + // Apply maxAnnounceChars truncation if specified + const TRUNCATION_MARKER = "\n\n[truncated — full output in transcript]"; + const truncatedFindings = + typeof params.maxAnnounceChars === "number" && + params.maxAnnounceChars >= 1 && + findings.length > params.maxAnnounceChars + ? findings.slice(0, params.maxAnnounceChars - TRUNCATION_MARKER.length) + TRUNCATION_MARKER + : findings; + const internalEvents: AgentInternalEvent[] = [ { type: "task_completion", @@ -1498,7 +1509,7 @@ export async function runSubagentAnnounceFlow(params: { taskLabel, status: outcome.status, statusLabel, - result: findings, + result: truncatedFindings, statsLine, replyInstruction, }, diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index 3c11f1850a2..97e9eb0d866 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -666,6 +666,7 @@ function startSubagentAnnounceCleanupFlow(runId: string, entry: SubagentRunRecor spawnMode: entry.spawnMode, expectsCompletionMessage: entry.expectsCompletionMessage, wakeOnDescendantSettle: entry.wakeOnDescendantSettle === true, + maxAnnounceChars: entry.maxAnnounceChars, }) .then((didAnnounce) => { finalizeAnnounceCleanup(didAnnounce); @@ -1349,6 +1350,7 @@ export function registerSubagentRun(params: { runTimeoutSeconds?: number; expectsCompletionMessage?: boolean; spawnMode?: "run" | "session"; + maxAnnounceChars?: number; attachmentsDir?: string; attachmentsRootDir?: string; retainAttachmentsOnKeep?: boolean; @@ -1388,6 +1390,10 @@ export function registerSubagentRun(params: { archiveAtMs, cleanupHandled: false, wakeOnDescendantSettle: undefined, + maxAnnounceChars: + typeof params.maxAnnounceChars === "number" && Number.isFinite(params.maxAnnounceChars) + ? Math.max(1, Math.floor(params.maxAnnounceChars)) + : undefined, attachmentsDir: params.attachmentsDir, attachmentsRootDir: params.attachmentsRootDir, retainAttachmentsOnKeep: params.retainAttachmentsOnKeep, diff --git a/src/agents/subagent-registry.types.ts b/src/agents/subagent-registry.types.ts index 299adb83e33..da84a95f8db 100644 --- a/src/agents/subagent-registry.types.ts +++ b/src/agents/subagent-registry.types.ts @@ -60,4 +60,6 @@ export type SubagentRunRecord = { attachmentsDir?: string; attachmentsRootDir?: string; retainAttachmentsOnKeep?: boolean; + /** Maximum character limit for announce message delivery (truncates with marker if exceeded). */ + maxAnnounceChars?: number; }; diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index d75a8717a22..509ee6839ef 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -62,6 +62,7 @@ export type SpawnSubagentParams = { cleanup?: "delete" | "keep"; sandbox?: SpawnSubagentSandboxMode; expectsCompletionMessage?: boolean; + maxAnnounceChars?: number; attachments?: Array<{ name: string; content: string; @@ -753,6 +754,7 @@ export async function spawnSubagentDirect( runTimeoutSeconds, expectsCompletionMessage, spawnMode, + maxAnnounceChars: params.maxAnnounceChars, attachmentsDir: attachmentAbsDir, attachmentsRootDir: attachmentRootDir, retainAttachmentsOnKeep: retainOnSessionKeep, diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index b735084d2b0..1aa2f9b19a3 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -63,6 +63,13 @@ const SessionsSpawnToolSchema = Type.Object({ mountPath: Type.Optional(Type.String()), }), ), + maxAnnounceChars: Type.Optional( + Type.Number({ + minimum: 1, + description: + "Maximum character limit for the sub-agent completion announce message. If exceeded, the message is truncated with a marker.", + }), + ), }); export function createSessionsSpawnTool( @@ -81,7 +88,7 @@ export function createSessionsSpawnTool( label: "Sessions", name: "sessions_spawn", description: - 'Spawn an isolated session (runtime="subagent" or runtime="acp"). mode="run" is one-shot and mode="session" is persistent/thread-bound. Subagents inherit the parent workspace directory automatically.', + 'Spawn an isolated session (runtime="subagent" or runtime="acp"). mode="run" is one-shot and mode="session" is persistent/thread-bound. Subagents inherit the parent workspace directory automatically. Use maxAnnounceChars to limit completion message length for platforms with character limits (e.g., Telegram 4096 chars).', parameters: SessionsSpawnToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; @@ -118,6 +125,10 @@ export function createSessionsSpawnTool( ? Math.max(0, Math.floor(timeoutSecondsCandidate)) : undefined; const thread = params.thread === true; + const maxAnnounceCharsCandidate = + typeof params.maxAnnounceChars === "number" && Number.isFinite(params.maxAnnounceChars) + ? Math.max(1, Math.floor(params.maxAnnounceChars)) + : undefined; const attachments = Array.isArray(params.attachments) ? (params.attachments as Array<{ name: string; @@ -186,6 +197,7 @@ export function createSessionsSpawnTool( cleanup, sandbox, expectsCompletionMessage: true, + maxAnnounceChars: maxAnnounceCharsCandidate, attachments, attachMountPath: params.attachAs && typeof params.attachAs === "object" diff --git a/src/auto-reply/reply/dispatch-acp.ts b/src/auto-reply/reply/dispatch-acp.ts index 8fc7110fc4c..7fba9c6d28b 100644 --- a/src/auto-reply/reply/dispatch-acp.ts +++ b/src/auto-reply/reply/dispatch-acp.ts @@ -314,7 +314,7 @@ export async function tryDispatchAcpReply(params: { await projector.flush(true); const ttsMode = resolveTtsConfig(params.cfg).mode ?? "final"; const accumulatedBlockText = delivery.getAccumulatedBlockText(); - if (ttsMode === "final" && delivery.getBlockCount() > 0 && accumulatedBlockText.trim()) { + if (ttsMode === "final" && accumulatedBlockText.trim()) { try { const ttsSyntheticReply = await maybeApplyTtsToPayload({ payload: { text: accumulatedBlockText },