File size: 3,242 Bytes
68d7816 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | import type { ContextMessage } from '#/agent/contextMemory/types';
import { NOTIFY_USER_TOOL_NAME } from './tools/notify-user/notify-user';
export const NOTIFY_USER_NUDGE_VARIANT = 'notify_user_nudge';
export const NOTIFY_USER_NUDGE_THRESHOLD = 8;
function startsNewTurn(message: ContextMessage): boolean {
const origin = message.origin;
if (origin === undefined) return false;
switch (origin.kind) {
case 'user':
case 'cron_job':
case 'cron_missed':
case 'task':
case 'retry':
return true;
case 'system_trigger':
return origin.name !== 'stop_hook';
case 'skill_activation':
return origin.trigger === 'user-slash';
case 'plugin_command':
return true;
default:
return false;
}
}
export function toolCallsSinceLastNotify(history: readonly ContextMessage[]): number {
let count = 0;
for (let index = history.length - 1; index >= 0; index -= 1) {
const message = history[index]!;
if (startsNewTurn(message)) break;
if (message.role !== 'assistant') continue;
if (message.toolCalls.some((call) => call.name === NOTIFY_USER_TOOL_NAME)) break;
count += message.toolCalls.length;
}
return count;
}
export function toolCallsSincePosition(
history: readonly ContextMessage[],
position: number,
): number {
let count = 0;
for (const message of history.slice(position + 1)) {
if (message.role !== 'assistant') continue;
count += message.toolCalls.length;
}
return count;
}
export function lastMidResponsePosition(history: readonly ContextMessage[]): number {
for (let index = history.length - 1; index >= 0; index -= 1) {
const message = history[index]!;
if (startsNewTurn(message)) break;
if (message.role !== 'assistant') continue;
if (message.toolCalls.some((call) => call.name === NOTIFY_USER_TOOL_NAME)) break;
const hasVisibleText = message.content.some(
(part) => part.type === 'text' && part.text.trim().length > 0,
);
if (hasVisibleText) return index;
}
return -1;
}
export function shouldNudgeNotifyUser(
streak: number,
callsSinceLastNudge: number | null,
): boolean {
if (streak < NOTIFY_USER_NUDGE_THRESHOLD) return false;
return callsSinceLastNudge === null || callsSinceLastNudge >= NOTIFY_USER_NUDGE_THRESHOLD;
}
export function shouldNudgeMidResponse(
midResponsePosition: number,
lastInjectedAt: number | null,
): boolean {
if (midResponsePosition < 0) return false;
return lastInjectedAt === null || lastInjectedAt < midResponsePosition;
}
export function renderNotifyUserNudge(count: number): string {
return `You have gone ${String(count)} tool calls without a NotifyUser update — the user has seen nothing in the meantime. Send one now: a structured, chat-style update (a well structured paragraph with a few bullet points, under ~1000 characters) covering what you have concluded so far and what you will do next, batched with your next tool calls.`;
}
export function renderMidResponseHint(): string {
return 'You just sent a mid-turn text reply — easy for the user to miss between tool calls. Repost its substance as a NotifyUser update (a short intro plus a few bullet points), batched with your next tool calls, then continue.';
}
|