Skip to content

Commit 730866d

Browse files
authored
Merge pull request #83 from MaxLinCode/claude/mutation-executor-plan
feat: replace planner LLM with deterministic mutation executor
2 parents 3ed04fc + 67a44a2 commit 730866d

46 files changed

Lines changed: 4490 additions & 6651 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/src/app/api/telegram/webhook/route.test.ts

Lines changed: 97 additions & 248 deletions
Large diffs are not rendered by default.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { ScheduleBlock, Task } from "@atlas/core";
2+
3+
export function hasAmbiguousTaskTitle(tasks: Task[], task: Task) {
4+
return findActionableTasksWithSameTitle(tasks, task).length > 1;
5+
}
6+
7+
export function hasAmbiguousScheduledTitle(
8+
tasks: Task[],
9+
scheduleBlocks: ScheduleBlock[],
10+
taskId: string,
11+
) {
12+
const task = tasks.find((candidate) => candidate.id === taskId);
13+
14+
if (!task) {
15+
return false;
16+
}
17+
18+
const normalizedTitle = normalizeTaskTitle(task.title);
19+
let matchingScheduledCount = 0;
20+
21+
for (const block of scheduleBlocks) {
22+
const scheduledTask = tasks.find(
23+
(candidate) => candidate.id === block.taskId,
24+
);
25+
26+
if (
27+
scheduledTask &&
28+
normalizeTaskTitle(scheduledTask.title) === normalizedTitle
29+
) {
30+
matchingScheduledCount += 1;
31+
}
32+
}
33+
34+
return matchingScheduledCount > 1;
35+
}
36+
37+
export function findActionableTasksWithSameTitle(tasks: Task[], task: Task) {
38+
const normalizedTitle = normalizeTaskTitle(task.title);
39+
40+
return tasks.filter(
41+
(candidate) =>
42+
isTaskActionable(candidate) &&
43+
normalizeTaskTitle(candidate.title) === normalizedTitle,
44+
);
45+
}
46+
47+
export function findScheduledTasksWithSameTitle(
48+
tasks: Task[],
49+
scheduleBlocks: ScheduleBlock[],
50+
taskId: string,
51+
) {
52+
const task = tasks.find((candidate) => candidate.id === taskId);
53+
54+
if (!task) {
55+
return [];
56+
}
57+
58+
const normalizedTitle = normalizeTaskTitle(task.title);
59+
60+
return scheduleBlocks.flatMap((block) => {
61+
const scheduledTask = tasks.find(
62+
(candidate) => candidate.id === block.taskId,
63+
);
64+
65+
if (
66+
scheduledTask &&
67+
normalizeTaskTitle(scheduledTask.title) === normalizedTitle
68+
) {
69+
return [scheduledTask];
70+
}
71+
72+
return [];
73+
});
74+
}
75+
76+
function normalizeTaskTitle(title: string) {
77+
return title.trim().toLocaleLowerCase();
78+
}
79+
80+
function isTaskActionable(task: Task) {
81+
return task.lifecycleState !== "done" && task.lifecycleState !== "archived";
82+
}
83+
84+
export function buildAmbiguousTaskReply(input: {
85+
tasks: Task[];
86+
title: string;
87+
actionPrompt: "update" | "move";
88+
timeZone: string;
89+
}) {
90+
const header =
91+
input.actionPrompt === "move"
92+
? `I found multiple scheduled tasks named '${input.title}'. Tell me which one you want me to move:`
93+
: `I found multiple tasks named '${input.title}'. Tell me which one you want me to update:`;
94+
95+
return `${header}\n${input.tasks
96+
.map(
97+
(task, index) =>
98+
`${index + 1}. ${describeTaskOption(task, input.timeZone)}`,
99+
)
100+
.join("\n")}`;
101+
}
102+
103+
function describeTaskOption(task: Task, timeZone: string) {
104+
if (task.scheduledStartAt) {
105+
return `scheduled for ${formatClarificationTime(task.scheduledStartAt, timeZone)}`;
106+
}
107+
108+
if (task.lifecycleState === "pending_schedule") {
109+
return "not scheduled yet";
110+
}
111+
112+
if (task.lifecycleState === "awaiting_followup") {
113+
return "waiting for follow-up";
114+
}
115+
116+
return task.lifecycleState.replaceAll("_", " ");
117+
}
118+
119+
function formatClarificationTime(iso: string, timeZone: string) {
120+
return new Intl.DateTimeFormat("en-US", {
121+
timeZone,
122+
month: "short",
123+
day: "numeric",
124+
hour: "numeric",
125+
minute: "2-digit",
126+
}).format(new Date(iso));
127+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import {
2+
buildBusyScheduleBlocks,
3+
type ScheduleBlock,
4+
type Task,
5+
} from "@atlas/core";
6+
import type {
7+
CalendarBusyPeriod,
8+
CalendarEventSnapshot,
9+
ExternalCalendarAdapter,
10+
} from "@atlas/integrations";
11+
12+
export const CALENDAR_BUSY_LOOKAHEAD_DAYS = 14;
13+
14+
export async function scheduleTaskWithCalendar(input: {
15+
calendar: ExternalCalendarAdapter;
16+
task: Task;
17+
selectedCalendarId: string | null;
18+
proposedBlock: ScheduleBlock;
19+
}): Promise<ScheduleBlock> {
20+
const currentEvent = await getCurrentCalendarEvent(
21+
input.calendar,
22+
input.task,
23+
);
24+
const operation = currentEvent ? "update" : "create";
25+
const externalCalendarId =
26+
currentEvent?.externalCalendarId ??
27+
input.task.externalCalendarId ??
28+
input.proposedBlock.externalCalendarId ??
29+
input.selectedCalendarId;
30+
31+
logCalendarWriteAttempt({
32+
operation,
33+
taskId: input.task.id,
34+
userId: input.task.userId,
35+
externalCalendarEventId: currentEvent?.externalCalendarEventId ?? null,
36+
externalCalendarId,
37+
startAt: input.proposedBlock.startAt,
38+
endAt: input.proposedBlock.endAt,
39+
});
40+
41+
try {
42+
const calendarEvent = currentEvent
43+
? await input.calendar.updateEvent({
44+
externalCalendarEventId: currentEvent.externalCalendarEventId,
45+
externalCalendarId: currentEvent.externalCalendarId,
46+
title: input.task.title,
47+
startAt: input.proposedBlock.startAt,
48+
endAt: input.proposedBlock.endAt,
49+
})
50+
: await input.calendar.createEvent({
51+
title: input.task.title,
52+
startAt: input.proposedBlock.startAt,
53+
endAt: input.proposedBlock.endAt,
54+
externalCalendarId,
55+
});
56+
57+
logCalendarWriteSuccess({
58+
operation,
59+
taskId: input.task.id,
60+
userId: input.task.userId,
61+
externalCalendarEventId: calendarEvent.externalCalendarEventId,
62+
externalCalendarId: calendarEvent.externalCalendarId,
63+
startAt: calendarEvent.scheduledStartAt,
64+
endAt: calendarEvent.scheduledEndAt,
65+
});
66+
67+
return buildScheduleBlockFromCalendarEvent({
68+
taskId: input.proposedBlock.taskId,
69+
userId: input.task.userId,
70+
reason: input.proposedBlock.reason,
71+
rescheduleCount: input.proposedBlock.rescheduleCount,
72+
confidence: input.proposedBlock.confidence,
73+
calendarEvent,
74+
});
75+
} catch (error) {
76+
logCalendarWriteFailure({
77+
operation,
78+
taskId: input.task.id,
79+
userId: input.task.userId,
80+
externalCalendarEventId: currentEvent?.externalCalendarEventId ?? null,
81+
externalCalendarId,
82+
startAt: input.proposedBlock.startAt,
83+
endAt: input.proposedBlock.endAt,
84+
error,
85+
});
86+
throw error;
87+
}
88+
}
89+
90+
export async function buildRuntimeScheduleBlocks(input: {
91+
scheduleBlocks: ScheduleBlock[];
92+
tasks: Task[];
93+
userId: string;
94+
googleCalendarConnection: { selectedCalendarId: string } | null;
95+
calendar: ExternalCalendarAdapter;
96+
referenceTime: string;
97+
}): Promise<ScheduleBlock[]> {
98+
const existingBlocks = [...input.scheduleBlocks];
99+
100+
if (!input.googleCalendarConnection) {
101+
return existingBlocks;
102+
}
103+
104+
const busyWindowStart = new Date(input.referenceTime);
105+
const busyWindowEnd = new Date(
106+
busyWindowStart.getTime() +
107+
CALENDAR_BUSY_LOOKAHEAD_DAYS * 24 * 60 * 60 * 1000,
108+
);
109+
110+
const busyPeriods = await input.calendar.listBusyPeriods({
111+
startAt: busyWindowStart.toISOString(),
112+
endAt: busyWindowEnd.toISOString(),
113+
externalCalendarId: input.googleCalendarConnection.selectedCalendarId,
114+
});
115+
116+
return [
117+
...existingBlocks,
118+
...buildBusyScheduleBlocks({
119+
userId: input.userId,
120+
periods: filterBusyPeriodsAgainstAtlasTasks(input.tasks, busyPeriods),
121+
}),
122+
];
123+
}
124+
125+
export function filterBusyPeriodsAgainstAtlasTasks(
126+
tasks: Task[],
127+
busyPeriods: CalendarBusyPeriod[],
128+
) {
129+
return busyPeriods.filter(
130+
(period) =>
131+
!tasks.some(
132+
(task) =>
133+
task.externalCalendarId === period.externalCalendarId &&
134+
task.scheduledStartAt === period.startAt &&
135+
task.scheduledEndAt === period.endAt,
136+
),
137+
);
138+
}
139+
140+
export async function getCurrentCalendarEvent(
141+
calendar: ExternalCalendarAdapter,
142+
task: Task,
143+
) {
144+
if (
145+
task.externalCalendarEventId === null ||
146+
task.externalCalendarId === null
147+
) {
148+
return null;
149+
}
150+
151+
return calendar.getEvent({
152+
externalCalendarEventId: task.externalCalendarEventId,
153+
externalCalendarId: task.externalCalendarId,
154+
});
155+
}
156+
157+
export function buildScheduleBlockFromCalendarEvent(input: {
158+
taskId: string;
159+
userId: string;
160+
reason: string;
161+
rescheduleCount: number;
162+
confidence: number;
163+
calendarEvent: CalendarEventSnapshot;
164+
}): ScheduleBlock {
165+
return {
166+
id: input.calendarEvent.externalCalendarEventId,
167+
userId: input.userId,
168+
taskId: input.taskId,
169+
startAt: input.calendarEvent.scheduledStartAt,
170+
endAt: input.calendarEvent.scheduledEndAt,
171+
confidence: input.confidence,
172+
reason: input.reason,
173+
rescheduleCount: input.rescheduleCount,
174+
externalCalendarId: input.calendarEvent.externalCalendarId,
175+
};
176+
}
177+
178+
export function logCalendarWriteAttempt(input: {
179+
operation: "create" | "update";
180+
taskId: string;
181+
userId: string;
182+
externalCalendarEventId: string | null;
183+
externalCalendarId: string | null;
184+
startAt: string;
185+
endAt: string;
186+
}) {
187+
console.info("calendar_write_attempt", input);
188+
}
189+
190+
export function logCalendarWriteSuccess(input: {
191+
operation: "create" | "update";
192+
taskId: string;
193+
userId: string;
194+
externalCalendarEventId: string;
195+
externalCalendarId: string;
196+
startAt: string;
197+
endAt: string;
198+
}) {
199+
console.info("calendar_write_succeeded", input);
200+
}
201+
202+
export function logCalendarWriteFailure(input: {
203+
operation: "create" | "update";
204+
taskId: string;
205+
userId: string;
206+
externalCalendarEventId: string | null;
207+
externalCalendarId: string | null;
208+
startAt: string;
209+
endAt: string;
210+
error: unknown;
211+
}) {
212+
console.error("calendar_write_failed", {
213+
operation: input.operation,
214+
taskId: input.taskId,
215+
userId: input.userId,
216+
externalCalendarEventId: input.externalCalendarEventId,
217+
externalCalendarId: input.externalCalendarId,
218+
startAt: input.startAt,
219+
endAt: input.endAt,
220+
error:
221+
input.error instanceof Error
222+
? {
223+
name: input.error.name,
224+
message: input.error.message,
225+
}
226+
: {
227+
message: String(input.error),
228+
},
229+
});
230+
}

0 commit comments

Comments
 (0)