Skip to content

Commit 85ebd75

Browse files
MaxLinCodeclaude
andcommitted
Replace committedSlots/resolvedContract with PendingWriteOperation
Replaces the old parallel slot-tracking model (committedSlots + resolved_slots + pending_write_contract) with a single structured PendingWriteOperation that carries operationKind, resolvedFields, missingFields, originatingText, and startedAt through the turn pipeline. Changes: - packages/core: commit-policy now outputs resolvedFields/missingFields instead of committedSlots/missingSlots; ambiguity.ts switches to missingFields; write-contract and proposal-rules updated accordingly - apps/web/turn-router: builds a PendingWriteOperation from commit results and passes resolvedOperation through policy instead of committedSlots + resolvedContract - apps/web/decide-turn-policy: drops committedSlots from all policy branches; uses missingFields/resolvedFields throughout - apps/web/conversation-state: persists pending_write_operation; drops resolved_slots and pending_write_contract - apps/web/telegram-webhook: threads resolvedOperation instead of committedSlots/resolvedContract - All tests updated to reflect the new shape Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cbbaad1 commit 85ebd75

17 files changed

Lines changed: 473 additions & 416 deletions

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import type {
22
ConversationEntity,
33
InboxPlanningOutput,
4+
PendingWriteOperation,
45
RoutedTurn,
5-
TimeSpec,
66
TurnInterpretationType,
77
TurnPolicyAction,
88
} from "@atlas/core";
99

10-
function t(hour: number, minute: number): TimeSpec {
11-
return { kind: "absolute", hour, minute };
12-
}
13-
1410
import {
1511
getDefaultFollowUpRuntimeStore,
1612
getDefaultGoogleCalendarConnectionStore,
@@ -155,8 +151,8 @@ function buildRoutedTurn(input: {
155151
confidence?: number;
156152
ambiguity?: "none" | "low" | "high";
157153
resolvedProposalId?: string;
158-
committedSlots?: Record<string, string | number | TimeSpec>;
159154
clarificationSlots?: string[];
155+
resolvedOperation?: PendingWriteOperation;
160156
}): RoutedTurn {
161157
return {
162158
interpretation: {
@@ -186,10 +182,12 @@ function buildRoutedTurn(input: {
186182
: input.action === "execute_mutation"
187183
? { mutationInputSource: "direct_user_turn" as const }
188184
: {}),
189-
committedSlots: input.committedSlots ?? {},
190185
...(input.clarificationSlots
191186
? { clarificationSlots: input.clarificationSlots }
192187
: {}),
188+
...(input.resolvedOperation
189+
? { resolvedOperation: input.resolvedOperation }
190+
: {}),
193191
},
194192
};
195193
}
@@ -1192,7 +1190,16 @@ describe("telegram webhook route", () => {
11921190
turnType: "confirmation",
11931191
action: "recover_and_execute",
11941192
resolvedProposalId: "proposal-1",
1195-
committedSlots: { time: t(15, 0) },
1193+
resolvedOperation: {
1194+
operationKind: "plan",
1195+
targetRef: null,
1196+
resolvedFields: {
1197+
scheduleFields: { time: { kind: "absolute", hour: 15, minute: 0 } },
1198+
},
1199+
missingFields: [],
1200+
originatingText: "Schedule the dentist reminder",
1201+
startedAt: new Date().toISOString(),
1202+
},
11961203
}),
11971204
},
11981205
);

apps/web/src/lib/server/conversation-state.test.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
import type { ConversationStateSnapshot, TimeSpec } from "@atlas/core";
1+
import type { ConversationStateSnapshot, PendingWriteOperation, TimeSpec } from "@atlas/core";
22
import type { ProcessedInboxResult } from "@atlas/db";
33
import { describe, expect, it } from "vitest";
44

55
function t(hour: number, minute: number): TimeSpec {
66
return { kind: "absolute", hour, minute };
77
}
88

9+
function buildPendingWriteOperation(
10+
overrides?: Partial<PendingWriteOperation>,
11+
): PendingWriteOperation {
12+
return {
13+
operationKind: "plan",
14+
targetRef: null,
15+
resolvedFields: {},
16+
missingFields: [],
17+
originatingText: "schedule gym tomorrow",
18+
startedAt: "2026-03-22T16:00:00.000Z",
19+
...overrides,
20+
};
21+
}
22+
923
import {
1024
deriveConversationReplyState,
1125
deriveMutationState,
@@ -42,7 +56,6 @@ describe("deriveConversationReplyState", () => {
4256
policy: {
4357
action: "ask_clarification",
4458
clarificationSlots: ["time"],
45-
committedSlots: {},
4659
},
4760
interpretation: {
4861
turnType: "planning_request",
@@ -80,7 +93,6 @@ describe("deriveConversationReplyState", () => {
8093
policy: {
8194
action: "ask_clarification",
8295
clarificationSlots: ["proposal"],
83-
committedSlots: {},
8496
},
8597
interpretation: {
8698
turnType: "confirmation",
@@ -104,7 +116,6 @@ describe("deriveConversationReplyState", () => {
104116
snapshot: buildSnapshot(),
105117
policy: {
106118
action: "ask_clarification",
107-
committedSlots: {},
108119
},
109120
interpretation: {
110121
turnType: "unknown",
@@ -165,7 +176,6 @@ describe("deriveConversationReplyState", () => {
165176
snapshot,
166177
policy: {
167178
action: "present_proposal",
168-
committedSlots: {},
169179
},
170180
interpretation: {
171181
turnType: "clarification_answer",
@@ -197,24 +207,18 @@ describe("deriveConversationReplyState", () => {
197207
expect(result.discourseState?.mode).toBe("confirming");
198208
});
199209

200-
it("persists pending_write_contract when resolvedContract is provided", () => {
201-
const contract = {
202-
requiredSlots: ["day", "time"] as (
203-
| "day"
204-
| "time"
205-
| "duration"
206-
| "target"
207-
)[],
208-
intentKind: "plan" as const,
209-
};
210+
it("persists pending_write_operation when resolvedOperation is provided", () => {
211+
const op = buildPendingWriteOperation({
212+
resolvedFields: { scheduleFields: { day: "tomorrow" } },
213+
missingFields: ["scheduleFields.time"],
214+
});
210215

211216
const result = deriveConversationReplyState({
212217
snapshot: buildSnapshot(),
213218
policy: {
214219
action: "ask_clarification",
215220
clarificationSlots: ["time"],
216-
committedSlots: { day: "tomorrow" },
217-
resolvedContract: contract,
221+
resolvedOperation: op,
218222
},
219223
interpretation: {
220224
turnType: "planning_request",
@@ -229,15 +233,14 @@ describe("deriveConversationReplyState", () => {
229233
occurredAt: "2026-03-22T16:05:00.000Z",
230234
});
231235

232-
expect(result.discourseState?.pending_write_contract).toEqual(contract);
236+
expect(result.discourseState?.pending_write_operation).toEqual(op);
233237
});
234238

235-
it("does not set pending_write_contract when resolvedContract is absent", () => {
239+
it("does not set pending_write_operation when resolvedOperation is absent", () => {
236240
const result = deriveConversationReplyState({
237241
snapshot: buildSnapshot(),
238242
policy: {
239243
action: "reply_only",
240-
committedSlots: {},
241244
},
242245
interpretation: {
243246
turnType: "informational",
@@ -251,7 +254,7 @@ describe("deriveConversationReplyState", () => {
251254
occurredAt: "2026-03-22T16:05:00.000Z",
252255
});
253256

254-
expect(result.discourseState?.pending_write_contract).toBeUndefined();
257+
expect(result.discourseState?.pending_write_operation).toBeUndefined();
255258
});
256259

257260
it("clears active clarifications when a clarification_answer resolves all slots", () => {
@@ -294,7 +297,10 @@ describe("deriveConversationReplyState", () => {
294297
snapshot,
295298
policy: {
296299
action: "present_proposal",
297-
committedSlots: { day: "tomorrow", time: t(17, 0) },
300+
resolvedOperation: buildPendingWriteOperation({
301+
resolvedFields: { scheduleFields: { day: "tomorrow", time: t(17, 0) } },
302+
missingFields: [],
303+
}),
298304
},
299305
interpretation: {
300306
turnType: "clarification_answer",
@@ -428,15 +434,13 @@ describe("deriveMutationState", () => {
428434
expect(result.discourseState.mode).toBe("editing");
429435
});
430436

431-
it("clears resolved_slots and pending_write_contract on successful mutation", () => {
437+
it("clears pending_write_operation on successful mutation", () => {
432438
const snapshot = buildSnapshot();
433439
snapshot.discourseState = {
434440
...snapshot.discourseState!,
435-
resolved_slots: { day: "tomorrow", time: t(17, 0) },
436-
pending_write_contract: {
437-
requiredSlots: ["day", "time"],
438-
intentKind: "plan",
439-
},
441+
pending_write_operation: buildPendingWriteOperation({
442+
resolvedFields: { scheduleFields: { day: "tomorrow", time: t(17, 0) } },
443+
}),
440444
};
441445

442446
const processing: ProcessedInboxResult = {
@@ -504,19 +508,18 @@ describe("deriveMutationState", () => {
504508
occurredAt: "2026-03-22T16:10:00.000Z",
505509
});
506510

507-
expect(result.discourseState.resolved_slots).toEqual({});
508-
expect(result.discourseState.pending_write_contract).toBeUndefined();
511+
expect(result.discourseState.pending_write_operation).toBeUndefined();
509512
});
510513

511-
it("preserves resolved_slots and pending_write_contract on needs_clarification", () => {
514+
it("preserves pending_write_operation on needs_clarification", () => {
515+
const op = buildPendingWriteOperation({
516+
resolvedFields: { scheduleFields: { day: "tomorrow" } },
517+
missingFields: ["scheduleFields.time"],
518+
});
512519
const snapshot = buildSnapshot();
513520
snapshot.discourseState = {
514521
...snapshot.discourseState!,
515-
resolved_slots: { day: "tomorrow" },
516-
pending_write_contract: {
517-
requiredSlots: ["day", "time"],
518-
intentKind: "plan",
519-
},
522+
pending_write_operation: op,
520523
};
521524

522525
const processing: ProcessedInboxResult = {
@@ -549,10 +552,6 @@ describe("deriveMutationState", () => {
549552
occurredAt: "2026-03-22T16:10:00.000Z",
550553
});
551554

552-
expect(result.discourseState.resolved_slots).toEqual({ day: "tomorrow" });
553-
expect(result.discourseState.pending_write_contract).toEqual({
554-
requiredSlots: ["day", "time"],
555-
intentKind: "plan",
556-
});
555+
expect(result.discourseState.pending_write_operation).toEqual(op);
557556
});
558557
});

apps/web/src/lib/server/conversation-state.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ type DeriveConversationReplyStateInput = {
2424
| "action"
2525
| "clarificationSlots"
2626
| "targetProposalId"
27-
| "committedSlots"
28-
| "resolvedContract"
27+
| "resolvedOperation"
2928
> & {
3029
action: Extract<
3130
TurnPolicyAction,
@@ -109,7 +108,7 @@ export function deriveConversationReplyState(
109108
confirmationRequired: true,
110109
originatingTurnText: input.userTurnText,
111110
missingSlots: input.policy.clarificationSlots,
112-
slotSnapshot: input.policy.committedSlots ?? {},
111+
slotSnapshot: input.policy.resolvedOperation?.resolvedFields.scheduleFields ?? {},
113112
},
114113
}),
115114
);
@@ -176,14 +175,10 @@ export function deriveConversationReplyState(
176175
},
177176
).state;
178177

179-
const committedSlots = input.policy.committedSlots;
180178
const nextDiscourseState = {
181179
...updatedDiscourseState,
182-
...(committedSlots && Object.keys(committedSlots).length > 0
183-
? { resolved_slots: committedSlots }
184-
: {}),
185-
...(input.policy.resolvedContract
186-
? { pending_write_contract: input.policy.resolvedContract }
180+
...(input.policy.resolvedOperation
181+
? { pending_write_operation: input.policy.resolvedOperation }
187182
: {}),
188183
};
189184

@@ -319,8 +314,7 @@ export function deriveMutationState(input: DeriveMutationStateInput) {
319314
const finalDiscourseState = isFlowComplete
320315
? {
321316
...discourseState,
322-
resolved_slots: {},
323-
pending_write_contract: undefined,
317+
pending_write_operation: undefined,
324318
}
325319
: discourseState;
326320

0 commit comments

Comments
 (0)