iris-chatbot 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +772 -84
- package/template/src/app/api/contacts/search/route.ts +71 -0
- package/template/src/app/api/tool-approval/route.ts +30 -2
- package/template/src/app/globals.css +223 -1
- package/template/src/components/ChatView.tsx +247 -27
- package/template/src/components/Composer.tsx +11 -8
- package/template/src/components/MessageCard.tsx +549 -29
- package/template/src/components/SettingsModal.tsx +7 -0
- package/template/src/lib/data.ts +5 -0
- package/template/src/lib/tooling/approvals.ts +24 -9
- package/template/src/lib/tooling/tools/communication.ts +178 -31
- package/template/src/lib/types.ts +12 -2
|
@@ -44,6 +44,8 @@ const LOCAL_TOOL_SYSTEM_INSTRUCTIONS = [
|
|
|
44
44
|
"Calendar: You have direct access to the user's Apple Calendar via calendar_list_events and related tools. You CAN see their events—call the tools; do not say you cannot access or see their calendar. Never ask the user to paste their calendar, export it, or send a screenshot; you already have access. When they ask what's on the calendar, what's happening this/next weekend/month, or say 'check' or 'you can see', call calendar_list_events immediately with from/to for the range (e.g. next month → from: 'today', to: 'end of next month'; specific day → YYYY-MM-DD for both). Always pass calendar: 'all'. from/to support 'today', 'tomorrow', 'next week', 'end of next week', 'next month', 'end of next month'. Report the exact queried range from the tool result. For move/delete/reschedule: call calendar_list_events first to get uids. For rescheduling to a new time on the same calendar use calendar_update_event (preserves recurrence); for delete use calendar_delete_event; use calendar_move_event only when update is not sufficient (e.g. user says move and you need delete+recreate). When the user says to add or put something on their calendar (e.g. 'Add 1:1 with Alex for Thursday at 9am to my calendar', 'Put team standup on Friday at 10', 'Schedule meeting with John next Monday at 2pm'), use calendar_create_event with title = only the event name (e.g. '1:1 with Alex', 'team standup', 'meeting with John') and start = the date/time phrase (e.g. 'Thursday at 9am', 'Friday at 10', 'next Monday at 2pm'). Do not put the date/time in the title.",
|
|
45
45
|
"Files and Folders: You HAVE direct access to the user's files via file_list and file_find. Never say you cannot see, access, or list their files—call the tools. When the user asks 'what files do I have,' 'files from the last 7 days,' 'recent files,' or similar: call file_list immediately with path set to a folder (e.g. ~/Downloads, ~/Desktop, ~/Documents—or call once per root if you want all) and modifiedInLastDays: 7 (or the number of days they said). You have full file system access via file_list, file_mkdir, file_move, file_copy, file_delete_to_trash, file_batch_move, and file_find. When the user gives a folder or file name (e.g. 'us debt clock'), use file_find with searchPath '~' and the name as given. When moving MULTIPLE files, use file_batch_move with { operations: [ ... ] } or { destination, sources }. For organizing: file_find if location unknown, then file_list (use modifiedInLastDays for 'recent'), then file_mkdir/file_batch_move as needed. Never refuse file tasks or ask the user to use Finder manually—you have the tools.",
|
|
46
46
|
"For iMessage/text requests, use messages_send; if contact matching is ambiguous, ask one concise clarification with candidate names.",
|
|
47
|
+
"For requests to write, draft, compose, or send an email/message to someone, call mail_send or messages_send so the user can review/edit in the pre-send draft template when enabled.",
|
|
48
|
+
"When drafting email or message content, write complete, natural prose tailored to the request. Avoid placeholder text unless the user explicitly asks for a template with placeholders.",
|
|
47
49
|
"For media requests, assume Apple Music unless the user specifies otherwise.",
|
|
48
50
|
"For note creation, write structured markdown with clear headings, bolded key labels, and tables when they improve clarity.",
|
|
49
51
|
"If the user requests multiple actions in one prompt, execute every requested action sequentially before your final response.",
|
|
@@ -117,16 +119,50 @@ function buildDetectedActionHint(messages: ChatMessageInput[]): string | null {
|
|
|
117
119
|
return `Detected user actions:\n${lines.join("\n")}\nExecute each action in order and confirm each result.`;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
type DraftEditPromptContext = {
|
|
123
|
+
toolName: "mail_send" | "messages_send";
|
|
124
|
+
to: string[];
|
|
125
|
+
cc: string[];
|
|
126
|
+
subject: string;
|
|
127
|
+
body: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function buildDraftEditPromptContextMessage(context: DraftEditPromptContext): string {
|
|
131
|
+
const lines = [
|
|
132
|
+
"You are editing an existing pre-send draft.",
|
|
133
|
+
`Channel: ${context.toolName === "mail_send" ? "email" : "message"}.`,
|
|
134
|
+
`Current To: ${context.to.join(", ") || "(none)"}`,
|
|
135
|
+
];
|
|
136
|
+
if (context.toolName === "mail_send") {
|
|
137
|
+
lines.push(`Current Cc: ${context.cc.join(", ") || "(none)"}`);
|
|
138
|
+
lines.push(`Current Subject: ${context.subject || "(none)"}`);
|
|
139
|
+
}
|
|
140
|
+
lines.push("Current Body:");
|
|
141
|
+
lines.push(context.body || "(empty)");
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("Apply the user's latest instruction to this draft.");
|
|
144
|
+
lines.push("Keep recipients/channel unchanged unless user explicitly requests changes.");
|
|
145
|
+
lines.push("Use mail_send or messages_send with updated args so the draft template refreshes.");
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildToolSystemPrompt(
|
|
150
|
+
system: string | undefined,
|
|
151
|
+
messages: ChatMessageInput[],
|
|
152
|
+
draftEditContext?: DraftEditPromptContext,
|
|
153
|
+
): string {
|
|
121
154
|
const base = system?.trim();
|
|
122
155
|
const actionHint = buildDetectedActionHint(messages);
|
|
156
|
+
const draftEditHint = draftEditContext ? buildDraftEditPromptContextMessage(draftEditContext) : null;
|
|
157
|
+
const extraBlocks = [actionHint, draftEditHint].filter(Boolean).join("\n\n");
|
|
158
|
+
|
|
123
159
|
if (!base) {
|
|
124
|
-
return
|
|
125
|
-
? `${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${
|
|
160
|
+
return extraBlocks
|
|
161
|
+
? `${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${extraBlocks}`
|
|
126
162
|
: LOCAL_TOOL_SYSTEM_INSTRUCTIONS;
|
|
127
163
|
}
|
|
128
|
-
return
|
|
129
|
-
? `${base}\n\n${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${
|
|
164
|
+
return extraBlocks
|
|
165
|
+
? `${base}\n\n${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${extraBlocks}`
|
|
130
166
|
: `${base}\n\n${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}`;
|
|
131
167
|
}
|
|
132
168
|
|
|
@@ -210,6 +246,68 @@ type RuntimeConnection = {
|
|
|
210
246
|
supportsTools: boolean;
|
|
211
247
|
};
|
|
212
248
|
|
|
249
|
+
type NormalizedRequestMeta = {
|
|
250
|
+
threadId?: string;
|
|
251
|
+
conversationId?: string;
|
|
252
|
+
assistantMessageId?: string;
|
|
253
|
+
forceToolRouting: boolean;
|
|
254
|
+
draftEditContext?: DraftEditPromptContext;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
function sanitizeChatRequestMeta(meta: ChatRequest["meta"] | undefined): NormalizedRequestMeta {
|
|
258
|
+
const threadId = typeof meta?.threadId === "string" && meta.threadId.trim()
|
|
259
|
+
? meta.threadId.trim()
|
|
260
|
+
: undefined;
|
|
261
|
+
const conversationId = typeof meta?.conversationId === "string" && meta.conversationId.trim()
|
|
262
|
+
? meta.conversationId.trim()
|
|
263
|
+
: undefined;
|
|
264
|
+
const assistantMessageId = typeof meta?.assistantMessageId === "string" && meta.assistantMessageId.trim()
|
|
265
|
+
? meta.assistantMessageId.trim()
|
|
266
|
+
: undefined;
|
|
267
|
+
const forceToolRouting = meta?.forceToolRouting === true;
|
|
268
|
+
|
|
269
|
+
let draftEditContext: DraftEditPromptContext | undefined;
|
|
270
|
+
const rawDraft = meta?.draftEditContext;
|
|
271
|
+
if (rawDraft && typeof rawDraft === "object" && !Array.isArray(rawDraft)) {
|
|
272
|
+
const record = rawDraft as {
|
|
273
|
+
toolName?: unknown;
|
|
274
|
+
to?: unknown;
|
|
275
|
+
cc?: unknown;
|
|
276
|
+
subject?: unknown;
|
|
277
|
+
body?: unknown;
|
|
278
|
+
};
|
|
279
|
+
const toolName =
|
|
280
|
+
record.toolName === "mail_send" || record.toolName === "messages_send"
|
|
281
|
+
? record.toolName
|
|
282
|
+
: null;
|
|
283
|
+
const to = Array.isArray(record.to)
|
|
284
|
+
? record.to.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
285
|
+
: [];
|
|
286
|
+
const cc = Array.isArray(record.cc)
|
|
287
|
+
? record.cc.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
288
|
+
: [];
|
|
289
|
+
const subject = typeof record.subject === "string" ? record.subject : "";
|
|
290
|
+
const body = typeof record.body === "string" ? record.body : "";
|
|
291
|
+
if (toolName && to.length > 0 && body.trim().length > 0) {
|
|
292
|
+
draftEditContext = {
|
|
293
|
+
toolName,
|
|
294
|
+
to,
|
|
295
|
+
cc,
|
|
296
|
+
subject,
|
|
297
|
+
body,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
threadId,
|
|
304
|
+
conversationId,
|
|
305
|
+
assistantMessageId,
|
|
306
|
+
forceToolRouting,
|
|
307
|
+
draftEditContext,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
213
311
|
function headersArrayToRecord(headers: RuntimeConnection["headers"]): Record<string, string> | undefined {
|
|
214
312
|
if (!Array.isArray(headers) || headers.length === 0) {
|
|
215
313
|
return undefined;
|
|
@@ -311,6 +409,10 @@ function normalizeLocalTools(settings: LocalToolsSettings | undefined): LocalToo
|
|
|
311
409
|
enabled: Boolean(settings.enabled),
|
|
312
410
|
approvalMode: settings.approvalMode ?? DEFAULT_LOCAL_TOOLS_SETTINGS.approvalMode,
|
|
313
411
|
safetyProfile: settings.safetyProfile ?? DEFAULT_LOCAL_TOOLS_SETTINGS.safetyProfile,
|
|
412
|
+
preSendDraftReview:
|
|
413
|
+
typeof settings.preSendDraftReview === "boolean"
|
|
414
|
+
? settings.preSendDraftReview
|
|
415
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.preSendDraftReview,
|
|
314
416
|
allowedRoots:
|
|
315
417
|
Array.isArray(settings.allowedRoots) && settings.allowedRoots.length > 0
|
|
316
418
|
? settings.allowedRoots
|
|
@@ -363,6 +465,7 @@ function normalizeLocalTools(settings: LocalToolsSettings | undefined): LocalToo
|
|
|
363
465
|
normalized.enabled === true &&
|
|
364
466
|
normalized.approvalMode === "always_confirm_writes" &&
|
|
365
467
|
normalized.safetyProfile === "balanced" &&
|
|
468
|
+
normalized.preSendDraftReview === DEFAULT_LOCAL_TOOLS_SETTINGS.preSendDraftReview &&
|
|
366
469
|
normalized.allowedRoots.length === DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots.length &&
|
|
367
470
|
normalized.allowedRoots.every(
|
|
368
471
|
(value, index) => value === DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots[index],
|
|
@@ -731,6 +834,10 @@ function summarizeApprovalReason(toolName: string, args: Record<string, unknown>
|
|
|
731
834
|
return `Approval required for ${toolName}.`;
|
|
732
835
|
}
|
|
733
836
|
|
|
837
|
+
function isPreSendDraftTool(toolName: string): boolean {
|
|
838
|
+
return toolName === "mail_send" || toolName === "messages_send";
|
|
839
|
+
}
|
|
840
|
+
|
|
734
841
|
function normalizeAliasText(input: string): string {
|
|
735
842
|
return input
|
|
736
843
|
.toLowerCase()
|
|
@@ -994,6 +1101,452 @@ function parseDirectMessageIntent(params: {
|
|
|
994
1101
|
};
|
|
995
1102
|
}
|
|
996
1103
|
|
|
1104
|
+
type DraftCommunicationIntent = {
|
|
1105
|
+
intentType: "explicit_draft_request" | "send_request";
|
|
1106
|
+
toolName: "mail_send" | "messages_send";
|
|
1107
|
+
args: Record<string, unknown>;
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
type DraftHintKind = "body" | "topic";
|
|
1111
|
+
type DraftCommunicationIntentType = "explicit_draft_request" | "send_request" | "draft_edit_request";
|
|
1112
|
+
|
|
1113
|
+
function isLikelyDraftEditInstruction(input: string): boolean {
|
|
1114
|
+
const text = input.replace(/\s+/g, " ").trim();
|
|
1115
|
+
if (!text) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (
|
|
1120
|
+
/\b(?:send|write|draft|compose|create)\b[\s\S]{0,32}\b(?:email|mail|message|text)\b[\s\S]{0,40}\b(?:to|for)\b/i
|
|
1121
|
+
.test(text)
|
|
1122
|
+
) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const editVerb = /\b(edit|rewrite|change|update|improve|polish|adjust|expand|shorten|lengthen|make)\b/i.test(text);
|
|
1127
|
+
const draftField = /\b(email|mail|message|text|draft|subject|body)\b/i.test(text);
|
|
1128
|
+
const toneOrLength =
|
|
1129
|
+
/\b(longer|shorter|more formal|less formal|more casual|more polite|more professional|concise|friendlier)\b/i
|
|
1130
|
+
.test(text);
|
|
1131
|
+
const pronounEdit = /\b(make|rewrite|edit|change|update)\s+(?:it|this)\b/i.test(text);
|
|
1132
|
+
|
|
1133
|
+
return (editVerb && draftField) || toneOrLength || pronounEdit;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function classifyDraftCommunicationIntent(input: string): DraftCommunicationIntentType | null {
|
|
1137
|
+
const text = input.replace(/\s+/g, " ").trim();
|
|
1138
|
+
if (!text) {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
if (isLikelyDraftEditInstruction(text)) {
|
|
1142
|
+
return "draft_edit_request";
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const communicationMention = /\b(email|mail|text|message|imessage|sms|draft)\b/i.test(text);
|
|
1146
|
+
if (!communicationMention) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const explicitDraftPattern =
|
|
1151
|
+
/\b(?:write|draft|compose|create)\b/i.test(text) ||
|
|
1152
|
+
/\b(?:email|mail|message|text)\s+draft\b/i.test(text) ||
|
|
1153
|
+
/\bdraft\s+(?:an?\s+)?(?:email|mail|message|text)\b/i.test(text);
|
|
1154
|
+
if (explicitDraftPattern) {
|
|
1155
|
+
return "explicit_draft_request";
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (/\bsend\b/i.test(text) || /^\s*(?:email|mail|text(?:\s+message)?|message)\b/i.test(text)) {
|
|
1159
|
+
return "send_request";
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function normalizeRecipientList(
|
|
1165
|
+
recipientChunk: string,
|
|
1166
|
+
peopleAliases: MemoryContextPayload["aliases"]["people"] | undefined,
|
|
1167
|
+
resolveAliases: boolean,
|
|
1168
|
+
): string[] {
|
|
1169
|
+
const recipients = recipientChunk
|
|
1170
|
+
.replace(/^to\s+/i, "")
|
|
1171
|
+
.split(/\s*,\s*|\s+and\s+/i)
|
|
1172
|
+
.map((value) =>
|
|
1173
|
+
value
|
|
1174
|
+
.trim()
|
|
1175
|
+
.replace(/[,:;]+$/g, "")
|
|
1176
|
+
.replace(/^['"]|['"]$/g, ""),
|
|
1177
|
+
)
|
|
1178
|
+
.filter(Boolean)
|
|
1179
|
+
.slice(0, 3);
|
|
1180
|
+
|
|
1181
|
+
const normalized = recipients
|
|
1182
|
+
.map((value) => {
|
|
1183
|
+
const compact = normalizeAliasText(value);
|
|
1184
|
+
if (!compact || compact === "me" || compact === "myself" || compact === "us") {
|
|
1185
|
+
return "";
|
|
1186
|
+
}
|
|
1187
|
+
if (!resolveAliases) {
|
|
1188
|
+
return value;
|
|
1189
|
+
}
|
|
1190
|
+
return resolvePersonAlias(value, peopleAliases) ?? value;
|
|
1191
|
+
})
|
|
1192
|
+
.filter(Boolean);
|
|
1193
|
+
return [...new Set(normalized)];
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function normalizeDraftText(input: string): string {
|
|
1197
|
+
return input
|
|
1198
|
+
.replace(/\s+/g, " ")
|
|
1199
|
+
.trim()
|
|
1200
|
+
.replace(/^['"]|['"]$/g, "")
|
|
1201
|
+
.replace(/^[,.;:\-]+|[,.;:\-]+$/g, "")
|
|
1202
|
+
.trim();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function capitalizeFirst(input: string): string {
|
|
1206
|
+
if (!input) {
|
|
1207
|
+
return input;
|
|
1208
|
+
}
|
|
1209
|
+
return input.charAt(0).toUpperCase() + input.slice(1);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function toSentence(input: string): string {
|
|
1213
|
+
const normalized = normalizeDraftText(input);
|
|
1214
|
+
if (!normalized) {
|
|
1215
|
+
return "";
|
|
1216
|
+
}
|
|
1217
|
+
const withPronoun = normalized
|
|
1218
|
+
.replace(/\bi\b/g, "I")
|
|
1219
|
+
.replace(/^cant\b/i, "can't")
|
|
1220
|
+
.replace(/^dont\b/i, "don't")
|
|
1221
|
+
.replace(/^wont\b/i, "won't")
|
|
1222
|
+
.replace(/^im\b/i, "I'm");
|
|
1223
|
+
const withSubject = /^(can't|cannot|won't|will not|unable|am unable|not able|couldn't|didn't|don't)\b/i.test(withPronoun)
|
|
1224
|
+
? `I ${withPronoun}`
|
|
1225
|
+
: withPronoun;
|
|
1226
|
+
const capitalized = capitalizeFirst(withSubject);
|
|
1227
|
+
return /[.!?]$/.test(capitalized) ? capitalized : `${capitalized}.`;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function normalizeNameLabel(input: string): string {
|
|
1231
|
+
return input
|
|
1232
|
+
.split(/\s+/)
|
|
1233
|
+
.map((token) => {
|
|
1234
|
+
if (!token) {
|
|
1235
|
+
return token;
|
|
1236
|
+
}
|
|
1237
|
+
if (token.includes("@") || /\d/.test(token)) {
|
|
1238
|
+
return token;
|
|
1239
|
+
}
|
|
1240
|
+
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase();
|
|
1241
|
+
})
|
|
1242
|
+
.join(" ");
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function toClause(input: string): string {
|
|
1246
|
+
const sentence = toSentence(input);
|
|
1247
|
+
return sentence ? sentence.replace(/[.!?]+$/g, "").trim() : "";
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function ensureFirstPersonClause(input: string): string {
|
|
1251
|
+
const clause = toClause(input);
|
|
1252
|
+
if (!clause) {
|
|
1253
|
+
return "";
|
|
1254
|
+
}
|
|
1255
|
+
if (/^i\b/i.test(clause)) {
|
|
1256
|
+
return `I${clause.slice(1)}`;
|
|
1257
|
+
}
|
|
1258
|
+
if (/^(can't|cannot|won't|will not|am|have|had|need|would|could|should|must|did|do|was|were)\b/i.test(clause)) {
|
|
1259
|
+
return `I ${clause}`;
|
|
1260
|
+
}
|
|
1261
|
+
return clause;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function inferEmailSalutation(recipient: string): string {
|
|
1265
|
+
const compact = normalizeAliasText(recipient);
|
|
1266
|
+
if (/^(?:a|an|the)?\s*(?:professor|teacher|instructor|ta|lecturer|advisor)\b/i.test(compact)) {
|
|
1267
|
+
return "Dear Professor [Last Name],";
|
|
1268
|
+
}
|
|
1269
|
+
if (/^(?:a|an|the)?\s*(?:dr|doctor)\b/i.test(compact)) {
|
|
1270
|
+
return "Dear Dr. [Last Name],";
|
|
1271
|
+
}
|
|
1272
|
+
if (/@|\d{7,}/.test(recipient)) {
|
|
1273
|
+
return "Hello,";
|
|
1274
|
+
}
|
|
1275
|
+
const label = normalizeNameLabel(recipient).replace(/^(?:A|An|The)\s+/g, "").trim();
|
|
1276
|
+
if (!label) {
|
|
1277
|
+
return "Hello,";
|
|
1278
|
+
}
|
|
1279
|
+
return `Dear ${label},`;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function inferEmailSubject(params: { explicitSubject: string; bodyText: string; topicText: string }): string {
|
|
1283
|
+
const explicit = normalizeDraftText(params.explicitSubject);
|
|
1284
|
+
if (explicit) {
|
|
1285
|
+
return explicit;
|
|
1286
|
+
}
|
|
1287
|
+
const basis = normalizeDraftText(params.bodyText || params.topicText);
|
|
1288
|
+
const lower = basis.toLowerCase();
|
|
1289
|
+
if (!basis) {
|
|
1290
|
+
return "Quick Update";
|
|
1291
|
+
}
|
|
1292
|
+
if (/can't make it today|cannot make it today|unable to make it today|won't be able to make it today/.test(lower)) {
|
|
1293
|
+
return "Unable to Make It Today";
|
|
1294
|
+
}
|
|
1295
|
+
if (/can't make it|cannot make it|unable to make it|won't be able to/.test(lower)) {
|
|
1296
|
+
return "Unable to Make It";
|
|
1297
|
+
}
|
|
1298
|
+
if (/miss(ing)? class.*today|today.*miss(ing)? class/.test(lower)) {
|
|
1299
|
+
return "Absence from Class Today";
|
|
1300
|
+
}
|
|
1301
|
+
if (/miss(ing)? class|absence|absent/.test(lower)) {
|
|
1302
|
+
return "Absence from Class";
|
|
1303
|
+
}
|
|
1304
|
+
if (/running late|late/.test(lower)) {
|
|
1305
|
+
return "Running Late";
|
|
1306
|
+
}
|
|
1307
|
+
const clipped = basis.length > 64 ? basis.slice(0, 64).trim() : basis;
|
|
1308
|
+
return titleCase(clipped.replace(/[.!?]+$/g, ""));
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function extractRecipientAndHint(input: string): {
|
|
1312
|
+
recipientChunk: string;
|
|
1313
|
+
hintKind: DraftHintKind | null;
|
|
1314
|
+
hintText: string;
|
|
1315
|
+
} {
|
|
1316
|
+
const compact = normalizeDraftText(input);
|
|
1317
|
+
if (!compact) {
|
|
1318
|
+
return { recipientChunk: "", hintKind: null, hintText: "" };
|
|
1319
|
+
}
|
|
1320
|
+
const lowered = compact.toLowerCase();
|
|
1321
|
+
const separators: Array<{ token: string; kind: DraftHintKind }> = [
|
|
1322
|
+
{ token: " that says ", kind: "body" },
|
|
1323
|
+
{ token: " saying that ", kind: "body" },
|
|
1324
|
+
{ token: " saying ", kind: "body" },
|
|
1325
|
+
{ token: " with message ", kind: "body" },
|
|
1326
|
+
{ token: " message: ", kind: "body" },
|
|
1327
|
+
{ token: " about ", kind: "topic" },
|
|
1328
|
+
{ token: " regarding ", kind: "topic" },
|
|
1329
|
+
{ token: " re: ", kind: "topic" },
|
|
1330
|
+
{ token: " that ", kind: "body" },
|
|
1331
|
+
];
|
|
1332
|
+
|
|
1333
|
+
let bestIndex = -1;
|
|
1334
|
+
let bestSeparator: { token: string; kind: DraftHintKind } | null = null;
|
|
1335
|
+
for (const separator of separators) {
|
|
1336
|
+
const index = lowered.indexOf(separator.token);
|
|
1337
|
+
if (index <= 0) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (bestIndex === -1 || index < bestIndex) {
|
|
1341
|
+
bestIndex = index;
|
|
1342
|
+
bestSeparator = separator;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!bestSeparator) {
|
|
1347
|
+
return {
|
|
1348
|
+
recipientChunk: compact,
|
|
1349
|
+
hintKind: null,
|
|
1350
|
+
hintText: "",
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
recipientChunk: compact.slice(0, bestIndex).trim(),
|
|
1355
|
+
hintKind: bestSeparator.kind,
|
|
1356
|
+
hintText: compact.slice(bestIndex + bestSeparator.token.length).trim(),
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function buildEmailDraftBody(params: {
|
|
1361
|
+
topicText: string;
|
|
1362
|
+
explicitBody: string;
|
|
1363
|
+
recipient: string;
|
|
1364
|
+
sourceText?: string;
|
|
1365
|
+
}): string {
|
|
1366
|
+
const salutation = inferEmailSalutation(params.recipient);
|
|
1367
|
+
const combined = normalizeDraftText([params.sourceText ?? "", params.explicitBody, params.topicText].join(" "));
|
|
1368
|
+
const loweredCombined = combined.toLowerCase();
|
|
1369
|
+
const wantsLong = /\b(long|detailed|thorough)\b/i.test(loweredCombined);
|
|
1370
|
+
const wantsShort = /\b(short|brief|concise)\b/i.test(loweredCombined);
|
|
1371
|
+
const absenceLike = /\b(can't make|cannot make|unable to|won't be able|miss(?:ing)? class|absence|absent|not attend)\b/i
|
|
1372
|
+
.test(loweredCombined);
|
|
1373
|
+
|
|
1374
|
+
if (absenceLike) {
|
|
1375
|
+
const baselineAbsence = /\btoday\b/i.test(loweredCombined)
|
|
1376
|
+
? "I won't be able to attend class today"
|
|
1377
|
+
: "I won't be able to attend class";
|
|
1378
|
+
const absenceClause = ensureFirstPersonClause(params.explicitBody) || baselineAbsence;
|
|
1379
|
+
const detailParagraph = wantsLong
|
|
1380
|
+
? "I will review the material covered and catch up on any notes or assignments. If there is anything specific you would like me to prioritize, please let me know."
|
|
1381
|
+
: "I will review the material covered and catch up on any notes or assignments.";
|
|
1382
|
+
|
|
1383
|
+
if (wantsShort) {
|
|
1384
|
+
return `${salutation}\n\n${absenceClause}.\n\nThank you for your understanding.\n\nBest,\n[Your Name]`;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return `${salutation}\n\nI hope you are doing well. I wanted to let you know that ${absenceClause}.\n\n${detailParagraph}\n\nThank you for your understanding.\n\nBest,\n[Your Name]`;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const explicitSentence = toSentence(params.explicitBody);
|
|
1391
|
+
if (explicitSentence) {
|
|
1392
|
+
return `${salutation}\n\n${explicitSentence}\n\nPlease let me know if you need anything else from me.\n\nBest,\n[Your Name]`;
|
|
1393
|
+
}
|
|
1394
|
+
const topicClause = toClause(params.topicText);
|
|
1395
|
+
if (topicClause) {
|
|
1396
|
+
const detailLine = wantsLong
|
|
1397
|
+
? "I wanted to share a bit more context so you have the full picture."
|
|
1398
|
+
: "I wanted to share a quick update.";
|
|
1399
|
+
return `${salutation}\n\n${detailLine} I wanted to reach out regarding ${topicClause}.\n\nThank you for your time.\n\nBest,\n[Your Name]`;
|
|
1400
|
+
}
|
|
1401
|
+
return `${salutation}\n\nI wanted to share a quick update.\n\nThank you for your time.\n\nBest,\n[Your Name]`;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function buildMessageDraftBody(params: { topicText: string; explicitBody: string; recipient: string }): string {
|
|
1405
|
+
const firstLabel = normalizeNameLabel(params.recipient).split(/\s+/)[0] || params.recipient;
|
|
1406
|
+
const explicitSentence = toSentence(params.explicitBody);
|
|
1407
|
+
if (explicitSentence) {
|
|
1408
|
+
const apology = /can't make|cannot make|unable to|won't be able|miss class|absence|late/i.test(explicitSentence)
|
|
1409
|
+
? " Sorry for the short notice."
|
|
1410
|
+
: "";
|
|
1411
|
+
return `Hey ${firstLabel}, ${explicitSentence}${apology}`;
|
|
1412
|
+
}
|
|
1413
|
+
const topicSentence = toSentence(params.topicText);
|
|
1414
|
+
if (topicSentence) {
|
|
1415
|
+
return `Hey ${firstLabel}, just wanted to message you about ${topicSentence}`;
|
|
1416
|
+
}
|
|
1417
|
+
return `Hey ${firstLabel}, wanted to check in when you have a minute.`;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function parseDraftCommunicationIntent(params: {
|
|
1421
|
+
text: string;
|
|
1422
|
+
peopleAliases?: MemoryContextPayload["aliases"]["people"];
|
|
1423
|
+
}): DraftCommunicationIntent | null {
|
|
1424
|
+
const text = params.text.replace(/\s+/g, " ").trim();
|
|
1425
|
+
if (!text) {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
const intentType = classifyDraftCommunicationIntent(text);
|
|
1429
|
+
if (intentType === "draft_edit_request" || !intentType) {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
const actionVerbMention = /\b(write|draft|compose|create|send)\b/i.test(text);
|
|
1433
|
+
const communicationNounMention = /\b(email|mail|text|message|imessage|sms|draft)\b/i.test(text);
|
|
1434
|
+
if (!actionVerbMention && !communicationNounMention) {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const emailMention = /\b(email|mail)\b/i.test(text);
|
|
1439
|
+
const messageMention = /\b(text|message|imessage|sms)\b/i.test(text);
|
|
1440
|
+
const genericDraftToMention =
|
|
1441
|
+
/(?:write|draft|compose|create)(?:\s+me)?\s+(?:an?\s+)?(?:[a-z]+\s+){0,3}draft\s+(?:for|to)\b/i.test(text) ||
|
|
1442
|
+
/\bdraft\s+(?:for|to)\b/i.test(text) ||
|
|
1443
|
+
/(?:write|draft|compose|create)\s+to\b/i.test(text);
|
|
1444
|
+
if (!emailMention && !messageMention && !genericDraftToMention) {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
let channel: "email" | "message" = emailMention ? "email" : "message";
|
|
1449
|
+
if (!emailMention && !messageMention) {
|
|
1450
|
+
channel = "email";
|
|
1451
|
+
}
|
|
1452
|
+
if (emailMention && messageMention) {
|
|
1453
|
+
const lowered = text.toLowerCase();
|
|
1454
|
+
const emailIndex = lowered.search(/\b(email|mail)\b/);
|
|
1455
|
+
const messageIndex = lowered.search(/\b(text|message|imessage|sms)\b/);
|
|
1456
|
+
channel = emailIndex !== -1 && (messageIndex === -1 || emailIndex < messageIndex) ? "email" : "message";
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
let recipientChunk = "";
|
|
1460
|
+
let hintKind: DraftHintKind | null = null;
|
|
1461
|
+
let hintText = "";
|
|
1462
|
+
const recipientPatterns = [
|
|
1463
|
+
/(?:write|draft|compose|create|send)(?:\s+me)?\s+(?:an?\s+)?(?:[a-z]+\s+){0,2}(?:email|mail|text(?:\s+message)?|message)(?:\s+draft)?\s+(?:for|to)\s+(.+)$/i,
|
|
1464
|
+
/(?:write|draft|compose|create)(?:\s+me)?\s+(?:an?\s+)?(?:[a-z]+\s+){0,3}draft\s+(?:for|to)\s+(.+)$/i,
|
|
1465
|
+
/(?:email|mail|text(?:\s+message)?|message)\s+(?:for|to)\s+(.+)$/i,
|
|
1466
|
+
/^\s*(?:email|mail|text(?:\s+message)?|message)\s+(.+)$/i,
|
|
1467
|
+
/\bdraft\s+(?:for|to)\s+(.+)$/i,
|
|
1468
|
+
/(?:write|draft|compose|create)\s+to\s+(.+)$/i,
|
|
1469
|
+
];
|
|
1470
|
+
for (const pattern of recipientPatterns) {
|
|
1471
|
+
const match = text.match(pattern);
|
|
1472
|
+
if (match?.[1]) {
|
|
1473
|
+
const extracted = extractRecipientAndHint(match[1].trim());
|
|
1474
|
+
recipientChunk = extracted.recipientChunk;
|
|
1475
|
+
hintKind = extracted.hintKind;
|
|
1476
|
+
hintText = extracted.hintText;
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (!recipientChunk) {
|
|
1481
|
+
const fallbackRecipientMatch = text.match(/\b(?:to|for)\s+(.+)$/i);
|
|
1482
|
+
if (fallbackRecipientMatch?.[1]) {
|
|
1483
|
+
const extracted = extractRecipientAndHint(fallbackRecipientMatch[1].trim());
|
|
1484
|
+
recipientChunk = extracted.recipientChunk;
|
|
1485
|
+
hintKind = extracted.hintKind;
|
|
1486
|
+
hintText = extracted.hintText;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (!recipientChunk) {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const recipients = normalizeRecipientList(
|
|
1494
|
+
recipientChunk,
|
|
1495
|
+
params.peopleAliases,
|
|
1496
|
+
channel === "message",
|
|
1497
|
+
);
|
|
1498
|
+
if (recipients.length === 0) {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const subjectMatch = text.match(/\bsubject(?:\s*[:\-]\s*|\s+)(.+?)(?=\s+(?:body|message|that says|saying)\b|$)/i);
|
|
1503
|
+
const messageMatch = text.match(/\b(?:that says|saying(?: that)?|with message|message:)\s+(.+)$/i);
|
|
1504
|
+
const topicMatch = text.match(/\b(?:about|regarding|re(?:\:)?)(?:\s+the)?\s+(.+)$/i);
|
|
1505
|
+
const topic = normalizeDraftText(
|
|
1506
|
+
(topicMatch?.[1] ?? "") || (hintKind === "topic" ? hintText : ""),
|
|
1507
|
+
);
|
|
1508
|
+
const explicitBody = normalizeDraftText(
|
|
1509
|
+
(messageMatch?.[1] ?? "") || (hintKind === "body" ? hintText : ""),
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
if (channel === "email") {
|
|
1513
|
+
const subject = inferEmailSubject({
|
|
1514
|
+
explicitSubject: subjectMatch?.[1] ?? "",
|
|
1515
|
+
bodyText: explicitBody,
|
|
1516
|
+
topicText: topic,
|
|
1517
|
+
});
|
|
1518
|
+
const body = buildEmailDraftBody({
|
|
1519
|
+
topicText: topic,
|
|
1520
|
+
explicitBody,
|
|
1521
|
+
recipient: recipients[0],
|
|
1522
|
+
sourceText: text,
|
|
1523
|
+
});
|
|
1524
|
+
return {
|
|
1525
|
+
intentType,
|
|
1526
|
+
toolName: "mail_send",
|
|
1527
|
+
args: {
|
|
1528
|
+
to: recipients,
|
|
1529
|
+
subject,
|
|
1530
|
+
body,
|
|
1531
|
+
},
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const body = buildMessageDraftBody({
|
|
1536
|
+
topicText: topic,
|
|
1537
|
+
explicitBody,
|
|
1538
|
+
recipient: recipients[0],
|
|
1539
|
+
});
|
|
1540
|
+
return {
|
|
1541
|
+
intentType,
|
|
1542
|
+
toolName: "messages_send",
|
|
1543
|
+
args: {
|
|
1544
|
+
to: recipients,
|
|
1545
|
+
body,
|
|
1546
|
+
},
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
|
|
997
1550
|
function extractDirectMusicIntent(messages: ChatMessageInput[], aliases?: MemoryMusicAlias[]): {
|
|
998
1551
|
query: string;
|
|
999
1552
|
title: string | null;
|
|
@@ -1482,59 +2035,111 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1482
2035
|
}
|
|
1483
2036
|
}
|
|
1484
2037
|
|
|
1485
|
-
|
|
1486
|
-
const messageIntent = parseDirectMessageIntent({
|
|
1487
|
-
text: lastUserText,
|
|
1488
|
-
peopleAliases: params.memory?.enabled ? params.memory.aliases.people : [],
|
|
1489
|
-
});
|
|
1490
|
-
if (messageIntent) {
|
|
2038
|
+
const runDraftableCommunication = async (intent: DraftCommunicationIntent): Promise<boolean> => {
|
|
1491
2039
|
if (!params.localTools.enableMail) {
|
|
1492
2040
|
params.send({
|
|
1493
2041
|
type: "token",
|
|
1494
2042
|
value:
|
|
1495
|
-
"
|
|
2043
|
+
"Mail/messages tools are disabled. Enable **mail/messages tools** in Settings > Local Tools, then retry.",
|
|
1496
2044
|
});
|
|
1497
2045
|
return true;
|
|
1498
2046
|
}
|
|
1499
2047
|
|
|
1500
|
-
const
|
|
1501
|
-
if (
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}
|
|
2048
|
+
const tool = findToolByName(tools, intent.toolName);
|
|
2049
|
+
if (!tool) {
|
|
2050
|
+
return false;
|
|
2051
|
+
}
|
|
1505
2052
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2053
|
+
const requiresDraftReview =
|
|
2054
|
+
isPreSendDraftTool(tool.name) &&
|
|
2055
|
+
(params.localTools.preSendDraftReview || intent.intentType === "explicit_draft_request");
|
|
2056
|
+
if (!requiresDraftReview && requiresApprovalForRisk(tool.risk, params.localTools.approvalMode)) {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const callId = intent.toolName === "mail_send" ? "fastpath_mail_send" : "fastpath_messages_send";
|
|
2061
|
+
let executionArgs: Record<string, unknown> = { ...intent.args };
|
|
2062
|
+
params.send({
|
|
2063
|
+
type: "tool_call",
|
|
2064
|
+
callId,
|
|
2065
|
+
name: tool.name,
|
|
2066
|
+
args: intent.args,
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
if (requiresDraftReview) {
|
|
2070
|
+
const approval = createApprovalRequest();
|
|
1511
2071
|
params.send({
|
|
1512
|
-
type: "
|
|
2072
|
+
type: "approval_requested",
|
|
2073
|
+
approvalId: approval.approvalId,
|
|
1513
2074
|
callId,
|
|
1514
|
-
name:
|
|
1515
|
-
args,
|
|
2075
|
+
name: tool.name,
|
|
2076
|
+
args: intent.args,
|
|
2077
|
+
reason: summarizeApprovalReason(tool.name, intent.args),
|
|
1516
2078
|
});
|
|
2079
|
+
const approvalResolution = await approval.promise;
|
|
1517
2080
|
params.send({
|
|
1518
|
-
type: "
|
|
2081
|
+
type: "approval_resolved",
|
|
2082
|
+
approvalId: approval.approvalId,
|
|
1519
2083
|
callId,
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2084
|
+
decision: approvalResolution.decision,
|
|
2085
|
+
message:
|
|
2086
|
+
approvalResolution.decision === "approve"
|
|
2087
|
+
? "Approved"
|
|
2088
|
+
: approvalResolution.decision === "supersede"
|
|
2089
|
+
? "Replaced"
|
|
2090
|
+
: approvalResolution.decision === "timeout"
|
|
2091
|
+
? "Timed out"
|
|
2092
|
+
: "Denied",
|
|
1523
2093
|
});
|
|
2094
|
+
if (approvalResolution.decision !== "approve") {
|
|
2095
|
+
if (approvalResolution.decision === "deny") {
|
|
2096
|
+
params.send({
|
|
2097
|
+
type: "token",
|
|
2098
|
+
value: "Draft discarded. Nothing was sent.",
|
|
2099
|
+
});
|
|
2100
|
+
} else if (approvalResolution.decision === "timeout") {
|
|
2101
|
+
params.send({
|
|
2102
|
+
type: "token",
|
|
2103
|
+
value: "Draft review expired. Nothing was sent.",
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
2108
|
+
if (
|
|
2109
|
+
approvalResolution.args &&
|
|
2110
|
+
typeof approvalResolution.args === "object" &&
|
|
2111
|
+
!Array.isArray(approvalResolution.args)
|
|
2112
|
+
) {
|
|
2113
|
+
executionArgs = approvalResolution.args;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1524
2116
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2117
|
+
const progressMessage = tool.name === "mail_send" ? "Sending email." : "Sending message.";
|
|
2118
|
+
params.send({
|
|
2119
|
+
type: "tool_progress",
|
|
2120
|
+
callId,
|
|
2121
|
+
name: tool.name,
|
|
2122
|
+
status: "started",
|
|
2123
|
+
message: progressMessage,
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
try {
|
|
2127
|
+
const output = await tool.execute(executionArgs, {
|
|
2128
|
+
localTools: params.localTools,
|
|
2129
|
+
signal: params.signal,
|
|
2130
|
+
});
|
|
2131
|
+
params.send({
|
|
2132
|
+
type: "tool_result",
|
|
2133
|
+
callId,
|
|
2134
|
+
name: tool.name,
|
|
2135
|
+
ok: true,
|
|
2136
|
+
result: output,
|
|
2137
|
+
});
|
|
1537
2138
|
|
|
2139
|
+
const argsRecipients = Array.isArray(executionArgs.to)
|
|
2140
|
+
? executionArgs.to.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
2141
|
+
: [];
|
|
2142
|
+
if (tool.name === "messages_send") {
|
|
1538
2143
|
const outputRecord =
|
|
1539
2144
|
output && typeof output === "object"
|
|
1540
2145
|
? (output as { recipients?: Array<{ name?: string | null; input?: string; handle?: string }> })
|
|
@@ -1542,30 +2147,66 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1542
2147
|
const recipientLabel =
|
|
1543
2148
|
outputRecord?.recipients?.map((recipient) => recipient.name || recipient.input || recipient.handle)
|
|
1544
2149
|
.filter(Boolean)
|
|
1545
|
-
.join(", ") ||
|
|
1546
|
-
|
|
2150
|
+
.join(", ") || argsRecipients.join(", ");
|
|
1547
2151
|
params.send({
|
|
1548
2152
|
type: "token",
|
|
1549
2153
|
value: `**Done.** Sent message to ${recipientLabel}.`,
|
|
1550
2154
|
});
|
|
1551
|
-
}
|
|
1552
|
-
const
|
|
1553
|
-
messageTool.name,
|
|
1554
|
-
error,
|
|
1555
|
-
"Could not send message.",
|
|
1556
|
-
);
|
|
1557
|
-
params.send({
|
|
1558
|
-
type: "tool_result",
|
|
1559
|
-
callId,
|
|
1560
|
-
name: messageTool.name,
|
|
1561
|
-
ok: false,
|
|
1562
|
-
result: { error: message },
|
|
1563
|
-
});
|
|
2155
|
+
} else {
|
|
2156
|
+
const recipientLabel = argsRecipients.join(", ");
|
|
1564
2157
|
params.send({
|
|
1565
2158
|
type: "token",
|
|
1566
|
-
value:
|
|
2159
|
+
value: `**Done.** Sent email to ${recipientLabel}.`,
|
|
1567
2160
|
});
|
|
1568
2161
|
}
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
const fallback = tool.name === "mail_send" ? "Could not send email." : "Could not send message.";
|
|
2164
|
+
const message = formatToolErrorForUser(tool.name, error, fallback);
|
|
2165
|
+
params.send({
|
|
2166
|
+
type: "tool_result",
|
|
2167
|
+
callId,
|
|
2168
|
+
name: tool.name,
|
|
2169
|
+
ok: false,
|
|
2170
|
+
result: { error: message },
|
|
2171
|
+
});
|
|
2172
|
+
params.send({
|
|
2173
|
+
type: "token",
|
|
2174
|
+
value:
|
|
2175
|
+
tool.name === "mail_send"
|
|
2176
|
+
? `I couldn't send that email: ${message}.`
|
|
2177
|
+
: `I couldn't send that message: ${message}.`,
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
return true;
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
// Fast path 2: direct text/iMessage
|
|
2184
|
+
const messageIntent = parseDirectMessageIntent({
|
|
2185
|
+
text: lastUserText,
|
|
2186
|
+
peopleAliases: params.memory?.enabled ? params.memory.aliases.people : [],
|
|
2187
|
+
});
|
|
2188
|
+
if (messageIntent) {
|
|
2189
|
+
const handled = await runDraftableCommunication({
|
|
2190
|
+
intentType: "send_request",
|
|
2191
|
+
toolName: "messages_send",
|
|
2192
|
+
args: {
|
|
2193
|
+
to: messageIntent.to,
|
|
2194
|
+
body: messageIntent.body,
|
|
2195
|
+
},
|
|
2196
|
+
});
|
|
2197
|
+
if (handled) {
|
|
2198
|
+
return true;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// Fast path 2b: draft or general write/send email/message
|
|
2203
|
+
const draftCommunicationIntent = parseDraftCommunicationIntent({
|
|
2204
|
+
text: lastUserText,
|
|
2205
|
+
peopleAliases: params.memory?.enabled ? params.memory.aliases.people : [],
|
|
2206
|
+
});
|
|
2207
|
+
if (draftCommunicationIntent) {
|
|
2208
|
+
const handled = await runDraftableCommunication(draftCommunicationIntent);
|
|
2209
|
+
if (handled) {
|
|
1569
2210
|
return true;
|
|
1570
2211
|
}
|
|
1571
2212
|
}
|
|
@@ -2040,13 +2681,15 @@ async function runToolOrchestrator(params: {
|
|
|
2040
2681
|
messages: ChatMessageInput[];
|
|
2041
2682
|
localTools: LocalToolsSettings;
|
|
2042
2683
|
enableWebSources: boolean;
|
|
2684
|
+
forceDraftReviewForSend?: boolean;
|
|
2685
|
+
draftEditContext?: DraftEditPromptContext;
|
|
2043
2686
|
send: (chunk: ChatStreamChunk) => void;
|
|
2044
2687
|
signal?: AbortSignal;
|
|
2045
2688
|
}) {
|
|
2046
2689
|
const { tools, session } = getToolSession({
|
|
2047
2690
|
connection: params.connection,
|
|
2048
2691
|
model: params.model,
|
|
2049
|
-
system: buildToolSystemPrompt(params.system, params.messages),
|
|
2692
|
+
system: buildToolSystemPrompt(params.system, params.messages, params.draftEditContext),
|
|
2050
2693
|
messages: params.messages,
|
|
2051
2694
|
localTools: params.localTools,
|
|
2052
2695
|
enableWebSources: params.enableWebSources,
|
|
@@ -2157,7 +2800,10 @@ async function runToolOrchestrator(params: {
|
|
|
2157
2800
|
continue;
|
|
2158
2801
|
}
|
|
2159
2802
|
|
|
2160
|
-
|
|
2803
|
+
const requiresDraftReview =
|
|
2804
|
+
isPreSendDraftTool(call.name) &&
|
|
2805
|
+
(params.localTools.preSendDraftReview || params.forceDraftReviewForSend === true);
|
|
2806
|
+
if (requiresDraftReview || requiresApprovalForRisk(tool.risk, params.localTools.approvalMode)) {
|
|
2161
2807
|
const approval = createApprovalRequest();
|
|
2162
2808
|
params.send({
|
|
2163
2809
|
type: "approval_requested",
|
|
@@ -2169,23 +2815,47 @@ async function runToolOrchestrator(params: {
|
|
|
2169
2815
|
});
|
|
2170
2816
|
|
|
2171
2817
|
emitThinkingUpdate(`Waiting for approval to run ${call.name}.`);
|
|
2172
|
-
const
|
|
2818
|
+
const approvalResolution = await approval.promise;
|
|
2173
2819
|
params.send({
|
|
2174
2820
|
type: "approval_resolved",
|
|
2175
2821
|
approvalId: approval.approvalId,
|
|
2176
2822
|
callId: call.id,
|
|
2177
|
-
decision,
|
|
2823
|
+
decision: approvalResolution.decision,
|
|
2178
2824
|
message:
|
|
2179
|
-
decision === "approve"
|
|
2825
|
+
approvalResolution.decision === "approve"
|
|
2180
2826
|
? "Approved"
|
|
2181
|
-
: decision === "
|
|
2827
|
+
: approvalResolution.decision === "supersede"
|
|
2828
|
+
? "Replaced"
|
|
2829
|
+
: approvalResolution.decision === "timeout"
|
|
2182
2830
|
? "Timed out"
|
|
2183
2831
|
: "Denied",
|
|
2184
2832
|
});
|
|
2185
2833
|
|
|
2186
|
-
if (decision !== "approve") {
|
|
2834
|
+
if (approvalResolution.decision !== "approve") {
|
|
2835
|
+
if (requiresDraftReview && isPreSendDraftTool(call.name)) {
|
|
2836
|
+
const message =
|
|
2837
|
+
approvalResolution.decision === "deny"
|
|
2838
|
+
? "Draft discarded. Nothing was sent."
|
|
2839
|
+
: approvalResolution.decision === "timeout"
|
|
2840
|
+
? "Draft review expired. Nothing was sent."
|
|
2841
|
+
: "Draft replaced by a newer instruction.";
|
|
2842
|
+
if (approvalResolution.decision !== "supersede") {
|
|
2843
|
+
params.send({
|
|
2844
|
+
type: "token",
|
|
2845
|
+
value: message,
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
session.addToolResult({
|
|
2849
|
+
callId: call.id,
|
|
2850
|
+
name: call.name,
|
|
2851
|
+
result: message,
|
|
2852
|
+
isError: false,
|
|
2853
|
+
});
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2187
2857
|
const message =
|
|
2188
|
-
decision === "timeout"
|
|
2858
|
+
approvalResolution.decision === "timeout"
|
|
2189
2859
|
? "Tool call timed out waiting for approval."
|
|
2190
2860
|
: "Tool call denied by user.";
|
|
2191
2861
|
params.send({
|
|
@@ -2203,6 +2873,13 @@ async function runToolOrchestrator(params: {
|
|
|
2203
2873
|
});
|
|
2204
2874
|
continue;
|
|
2205
2875
|
}
|
|
2876
|
+
if (
|
|
2877
|
+
approvalResolution.args &&
|
|
2878
|
+
typeof approvalResolution.args === "object" &&
|
|
2879
|
+
!Array.isArray(approvalResolution.args)
|
|
2880
|
+
) {
|
|
2881
|
+
callInput = approvalResolution.args;
|
|
2882
|
+
}
|
|
2206
2883
|
}
|
|
2207
2884
|
|
|
2208
2885
|
params.send({
|
|
@@ -2330,6 +3007,7 @@ export async function POST(request: Request) {
|
|
|
2330
3007
|
webSourcesEnabled && supportsWebSourcesViaOpenAI(connection);
|
|
2331
3008
|
const localToolsRequested = normalizeLocalTools(body.localTools);
|
|
2332
3009
|
const memoryContext = sanitizeMemoryContext(body.memory);
|
|
3010
|
+
const requestMeta = sanitizeChatRequestMeta(body.meta);
|
|
2333
3011
|
const encoder = new TextEncoder();
|
|
2334
3012
|
|
|
2335
3013
|
const stream = new ReadableStream({
|
|
@@ -2369,28 +3047,36 @@ export async function POST(request: Request) {
|
|
|
2369
3047
|
}
|
|
2370
3048
|
|
|
2371
3049
|
if (localTools.enabled) {
|
|
2372
|
-
const
|
|
3050
|
+
const lastUserText =
|
|
3051
|
+
[...sanitizedMessages].reverse().find((message) => message.role === "user")?.content ?? "";
|
|
3052
|
+
const communicationIntentType = classifyDraftCommunicationIntent(lastUserText);
|
|
3053
|
+
const forceDraftReviewForSend =
|
|
3054
|
+
communicationIntentType === "explicit_draft_request" || Boolean(requestMeta.draftEditContext);
|
|
3055
|
+
const routeThroughTools =
|
|
3056
|
+
requestMeta.forceToolRouting || shouldRouteThroughToolOrchestrator(sanitizedMessages);
|
|
2373
3057
|
if (routeThroughTools) {
|
|
2374
3058
|
sendThinking("update", "Checking whether this can run through a direct action path.");
|
|
2375
3059
|
let fastHandled = false;
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
3060
|
+
if (!requestMeta.forceToolRouting) {
|
|
3061
|
+
try {
|
|
3062
|
+
fastHandled = await tryDirectAutomationFastPath({
|
|
3063
|
+
localTools,
|
|
3064
|
+
messages: sanitizedMessages,
|
|
3065
|
+
memory: memoryContext,
|
|
3066
|
+
send,
|
|
3067
|
+
signal: request.signal,
|
|
3068
|
+
});
|
|
3069
|
+
} catch (error) {
|
|
3070
|
+
const message =
|
|
3071
|
+
error instanceof Error ? truncateMessage(error.message) : "Fast-path unavailable.";
|
|
3072
|
+
send({
|
|
3073
|
+
type: "tool_progress",
|
|
3074
|
+
callId: "tooling",
|
|
3075
|
+
name: "tooling",
|
|
3076
|
+
status: "warning",
|
|
3077
|
+
message: `Direct automation fast-path failed: ${message}. Continuing with tool orchestration.`,
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
2394
3080
|
}
|
|
2395
3081
|
|
|
2396
3082
|
if (fastHandled) {
|
|
@@ -2407,6 +3093,8 @@ export async function POST(request: Request) {
|
|
|
2407
3093
|
messages: sanitizedMessages,
|
|
2408
3094
|
localTools,
|
|
2409
3095
|
enableWebSources: enableWebSourcesForConnection,
|
|
3096
|
+
forceDraftReviewForSend,
|
|
3097
|
+
draftEditContext: requestMeta.draftEditContext,
|
|
2410
3098
|
send,
|
|
2411
3099
|
signal: request.signal,
|
|
2412
3100
|
});
|