Skip to content

Commit 0697820

Browse files
More sentinel fixes (#401)
1 parent 6e55115 commit 0697820

File tree

11 files changed

+234
-123
lines changed

11 files changed

+234
-123
lines changed

frontend/src/components/store.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const defaultConfig: GeneralConfig = {
4848
run_without_docker: false,
4949
browser_headless: true,
5050
sentinel_plan: {
51-
enable_sentinel_steps: false,
51+
enable_sentinel_steps: true,
5252
dynamic_sentinel_sleep: false,
5353
},
5454
model_client_configs: {

frontend/src/components/views/chat/plan.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ const PlanView: React.FC<PlanProps> = ({
159159

160160
const updateSentinelField = (index: number, field: 'sleep_duration' | 'condition', value: number | string) => {
161161
const newPlan = [...localPlan];
162+
// Ensure sleep_duration is never negative
163+
if (field === 'sleep_duration' && typeof value === 'number') {
164+
value = Math.max(0, value);
165+
}
162166
newPlan[index] = {
163167
...newPlan[index],
164168
[field]: value,

frontend/src/components/views/chat/rendermessage.tsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ interface RenderStepExecutionProps {
7272

7373
interface ParsedContent {
7474
text:
75-
| string
76-
| FunctionCall[]
77-
| (string | ImageContent)[]
78-
| FunctionExecutionResult[];
75+
| string
76+
| FunctionCall[]
77+
| (string | ImageContent)[]
78+
| FunctionExecutionResult[];
7979
metadata?: Record<string, string>;
8080
plan?: IPlanStep[];
8181
}
@@ -191,7 +191,7 @@ const parseorchestratorContent = (
191191
if (messageUtils.isStepExecution(metadata)) {
192192
return { type: "step-execution" as const, content: parsedContent };
193193
}
194-
} catch {}
194+
} catch { }
195195

196196
return { type: "default" as const, content };
197197
};
@@ -297,7 +297,7 @@ const RenderToolResult: React.FC<{ content: FunctionExecutionResult[] }> = memo(
297297
return (
298298
<div key={result.call_id} className="rounded p-2">
299299
<div className="font-medium">Result ID: {result.call_id}</div>
300-
<div
300+
<div
301301
className="cursor-pointer hover:bg-secondary/50 rounded p-1"
302302
onClick={() => toggleExpand(result.call_id)}
303303
>
@@ -555,6 +555,14 @@ export const messageUtils = {
555555
return metadata?.type === "sentinel_complete";
556556
},
557557

558+
isSentinelStatus(metadata?: Record<string, any>): boolean {
559+
return metadata?.type === "sentinel_status";
560+
},
561+
562+
isSentinelSleeping(metadata?: Record<string, any>): boolean {
563+
return metadata?.type === "sentinel_sleeping";
564+
},
565+
558566
findUserPlan(content: unknown): IPlanStep[] {
559567
if (typeof content !== "string") return [];
560568
try {
@@ -656,9 +664,9 @@ const RenderUserMessage: React.FC<{
656664
<PlanView
657665
task={""}
658666
plan={parsedContent.plan}
659-
setPlan={() => {}} // No-op since it's read-only
667+
setPlan={() => { }} // No-op since it's read-only
660668
viewOnly={true}
661-
onSavePlan={() => {}} // No-op since it's read-only
669+
onSavePlan={() => { }} // No-op since it's read-only
662670
/>
663671
)}
664672
</div>
@@ -696,7 +704,9 @@ export const RenderMessage: React.FC<MessageProps> = memo(
696704
if (!skipSentinelHiding) {
697705
if (
698706
messageUtils.isSentinelCheck(message.metadata) ||
699-
messageUtils.isSentinelComplete(message.metadata)
707+
messageUtils.isSentinelComplete(message.metadata) ||
708+
messageUtils.isSentinelStatus(message.metadata) ||
709+
messageUtils.isSentinelSleeping(message.metadata)
700710
) {
701711
return null;
702712
}
@@ -709,6 +719,11 @@ export const RenderMessage: React.FC<MessageProps> = memo(
709719

710720
// Handle sentinel start message
711721
if (messageUtils.isSentinelStart(message.metadata)) {
722+
// Hide sentinel step if hidden prop is true
723+
if (hidden) {
724+
return null;
725+
}
726+
712727
try {
713728
const sentinelData = JSON.parse(message.content as string);
714729
return (
@@ -768,29 +783,25 @@ export const RenderMessage: React.FC<MessageProps> = memo(
768783

769784
return (
770785
<div
771-
className={`relative group mb-3 ${className} w-full break-words ${
772-
hidden &&
773-
(!orchestratorContent ||
774-
orchestratorContent.type !== "step-execution")
786+
className={`relative group mb-3 ${className} w-full break-words ${hidden &&
787+
(!orchestratorContent ||
788+
orchestratorContent.type !== "step-execution")
775789
? "hidden"
776790
: ""
777-
}`}
791+
}`}
778792
>
779793
<div
780-
className={`flex ${
781-
isUser || isUserProxy ? "justify-end" : "justify-start"
782-
} items-start w-full transition-all duration-200`}
794+
className={`flex ${isUser || isUserProxy ? "justify-end" : "justify-start"
795+
} items-start w-full transition-all duration-200`}
783796
>
784797
<div
785-
className={`${
786-
isUser || isUserProxy
787-
? `text-primary rounded-2xl bg-tertiary rounded-tr-sm px-4 py-2 ${
788-
parsedContent.plan && parsedContent.plan.length > 0
789-
? "w-[80%]"
790-
: "max-w-[80%]"
791-
}`
798+
className={`${isUser || isUserProxy
799+
? `text-primary rounded-2xl bg-tertiary rounded-tr-sm px-4 py-2 ${parsedContent.plan && parsedContent.plan.length > 0
800+
? "w-[80%]"
801+
: "max-w-[80%]"
802+
}`
792803
: "w-full text-primary"
793-
} break-words overflow-hidden`}
804+
} break-words overflow-hidden`}
794805
>
795806
{/* Show user message content first */}
796807
{(isUser || isUserProxy) && (

frontend/src/components/views/chat/rendersentinelstep.tsx

Lines changed: 71 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
3232
}) => {
3333
const [currentCheckIndex, setCurrentCheckIndex] = useState(0);
3434
const [checks, setChecks] = useState<SentinelCheck[]>([]);
35-
const [expandedCheck, setExpandedCheck] = useState<number | null>(null);
35+
const [collapsedChecks, setCollapsedChecks] = useState<Set<number>>(new Set());
3636
const [totalChecks, setTotalChecks] = useState(0);
3737
const [runtime, setRuntime] = useState(0);
38-
const [nextCheckIn, setNextCheckIn] = useState<number>(sleepDuration);
3938
const [currentStatus, setCurrentStatus] = useState<"checking" | "sleeping" | "complete">("checking");
4039
const [countdown, setCountdown] = useState<number>(0);
41-
const [lastCheckTime, setLastCheckTime] = useState<number>(Date.now());
40+
const [sleepStartTimestamp, setSleepStartTimestamp] = useState<string | null>(null);
41+
const [sleepDurationSeconds, setSleepDurationSeconds] = useState<number>(0);
4242

4343
useEffect(() => {
4444
// Collect all messages related to this sentinel step
@@ -48,7 +48,6 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
4848
msg.config.metadata?.sentinel_id === sentinelId
4949
);
5050

51-
5251
// Group messages by check number
5352
const checkMap = new Map<number, SentinelCheck>();
5453

@@ -66,89 +65,71 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
6665

6766
const check = checkMap.get(checkNumber)!;
6867

69-
// If this is a sentinel_check message, extract check info
70-
if (metadata?.type === "sentinel_check") {
68+
// If this is a sentinel_check or sentinel_sleeping message, extract check info
69+
if (metadata?.type === "sentinel_check" || metadata?.type === "sentinel_sleeping") {
7170
check.reason = metadata.reason;
72-
check.nextCheckIn = parseInt(metadata.next_check_in || "0");
73-
} else {
71+
check.nextCheckIn = parseInt(metadata.next_check_in || metadata.sleep_duration || "0");
72+
} else if (
73+
metadata?.type !== "sentinel_status" &&
74+
metadata?.type !== "sentinel_complete" &&
75+
metadata?.type !== "sentinel_start"
76+
) {
7477
// This is an agent message during the check
7578
check.messages.push(msg);
7679
}
7780
}
7881
});
7982

80-
// Find the latest sentinel_check or sentinel_complete message
81-
const latestStatusMsg = sentinelMessages.reverse().find(msg =>
82-
msg.config.metadata?.type === "sentinel_check" ||
83+
// Find the latest status message (sentinel_status, sentinel_sleeping, or sentinel_complete)
84+
const latestStatusMsg = [...sentinelMessages].reverse().find(msg =>
85+
msg.config.metadata?.type === "sentinel_status" ||
86+
msg.config.metadata?.type === "sentinel_sleeping" ||
8387
msg.config.metadata?.type === "sentinel_complete"
8488
);
85-
const latestStatusCheckNumber = parseInt(latestStatusMsg?.config.metadata?.check_number || "0");
8689

87-
// Determine current status
90+
// Determine current status from the latest status message
8891
if (latestStatusMsg) {
8992
const metadata = latestStatusMsg.config.metadata;
9093
setTotalChecks(parseInt(metadata?.total_checks || "0"));
9194
setRuntime(parseInt(metadata?.runtime || "0"));
9295

9396
if (metadata?.type === "sentinel_complete") {
94-
setNextCheckIn(0);
9597
setCurrentStatus("complete");
9698
setCountdown(0);
97-
} else if (metadata?.type === "sentinel_check") {
98-
const checkInSeconds = parseInt(metadata?.next_check_in || sleepDuration);
99-
setNextCheckIn(checkInSeconds);
100-
101-
// Check if there are any agent messages with a check_number higher than the latest sentinel_check
102-
// This indicates the agent is actively working on the next check
103-
const activeMessages = sentinelMessages.filter(msg => {
104-
const msgCheckNumber = parseInt(msg.config.metadata?.check_number || "0");
105-
const isAgentMessage = msg.config.metadata?.type !== "sentinel_check" &&
106-
msg.config.metadata?.type !== "sentinel_complete" &&
107-
msg.config.metadata?.sentinel_id === sentinelId;
108-
return isAgentMessage && msgCheckNumber > latestStatusCheckNumber;
109-
});
110-
111-
if (activeMessages.length > 0) {
112-
const activeCheckNumber = parseInt(activeMessages[0].config.metadata?.check_number || "0");
113-
114-
// Add these active messages to the check map
115-
if (!checkMap.has(activeCheckNumber)) {
116-
checkMap.set(activeCheckNumber, {
117-
checkNumber: activeCheckNumber,
118-
messages: activeMessages,
119-
reason: "Actively checking...",
120-
});
121-
} else {
122-
const activeCheck = checkMap.get(activeCheckNumber)!;
123-
activeCheck.messages = activeMessages;
124-
activeCheck.reason = "Actively checking...";
125-
}
126-
127-
setCurrentStatus("checking");
128-
setCountdown(0);
99+
setSleepStartTimestamp(null);
100+
setSleepDurationSeconds(0);
101+
} else if (metadata?.type === "sentinel_status") {
102+
// Orchestrator says it's actively checking
103+
setCurrentStatus("checking");
104+
setCountdown(0);
105+
setSleepStartTimestamp(null);
106+
setSleepDurationSeconds(0);
107+
} else if (metadata?.type === "sentinel_sleeping") {
108+
// Orchestrator sent a sleeping message with timestamp
109+
setCurrentStatus("sleeping");
110+
setSleepStartTimestamp(metadata.sleep_start_timestamp || null);
111+
const duration = parseInt(metadata?.sleep_duration || "0");
112+
setSleepDurationSeconds(duration);
113+
114+
// Calculate initial countdown from timestamp
115+
if (metadata.sleep_start_timestamp) {
116+
const sleepStart = new Date(metadata.sleep_start_timestamp).getTime();
117+
const now = Date.now();
118+
const elapsed = Math.floor((now - sleepStart) / 1000);
119+
const remaining = Math.max(0, duration - elapsed);
120+
setCountdown(remaining);
129121
} else {
130-
// Use the timestamp from the sentinel_check message
131-
const checkTimestamp = latestStatusMsg.config.timestamp;
132-
if (checkTimestamp) {
133-
const checkTime = new Date(checkTimestamp).getTime();
134-
const elapsed = Math.floor((Date.now() - checkTime) / 1000);
135-
const remaining = Math.max(0, checkInSeconds - elapsed);
136-
setCountdown(remaining);
137-
setLastCheckTime(checkTime);
138-
} else {
139-
setCountdown(checkInSeconds);
140-
setLastCheckTime(Date.now());
141-
}
142-
setCurrentStatus("sleeping");
122+
setCountdown(duration);
143123
}
144124
}
145125
} else {
146126
// If no status messages yet, we're still checking (check #1)
147127
const check1Messages = sentinelMessages.filter(msg => {
148128
const msgCheckNumber = parseInt(msg.config.metadata?.check_number || "0");
149129
return msgCheckNumber === 1 &&
150-
msg.config.metadata?.type !== "sentinel_check" &&
151-
msg.config.metadata?.type !== "sentinel_complete";
130+
msg.config.metadata?.type !== "sentinel_sleeping" &&
131+
msg.config.metadata?.type !== "sentinel_complete" &&
132+
msg.config.metadata?.type !== "sentinel_status";
152133
});
153134

154135
if (!checkMap.has(1)) {
@@ -160,6 +141,8 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
160141
}
161142

162143
setCurrentStatus("checking");
144+
setSleepStartTimestamp(null);
145+
setSleepDurationSeconds(0);
163146
}
164147

165148
// Convert map to sorted array and set current check to the latest
@@ -170,26 +153,31 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
170153
}
171154
}, [allMessages, currentMessageIndex, sentinelId, sleepDuration]);
172155

173-
// Countdown timer effect
156+
// Countdown timer effect - computes countdown from timestamp
174157
useEffect(() => {
175-
if (currentStatus !== "sleeping" || countdown <= 0) {
158+
if (currentStatus !== "sleeping" || !sleepStartTimestamp || sleepDurationSeconds <= 0) {
176159
return;
177160
}
178161

179162
const interval = setInterval(() => {
180-
const elapsed = Math.floor((Date.now() - lastCheckTime) / 1000);
181-
const remaining = Math.max(0, nextCheckIn - elapsed);
163+
const sleepStart = new Date(sleepStartTimestamp).getTime();
164+
const now = Date.now();
165+
const elapsed = Math.floor((now - sleepStart) / 1000);
166+
const remaining = Math.max(0, sleepDurationSeconds - elapsed);
182167
setCountdown(remaining);
183168

184169
if (remaining === 0) {
185170
clearInterval(interval);
186-
// Auto-switch to checking when timeout reaches 0
171+
// Auto-switch to checking when countdown reaches 0
187172
setCurrentStatus("checking");
188173
}
189174
}, 100); // Update every 100ms for smooth countdown
190175

191176
return () => clearInterval(interval);
192-
}, [currentStatus, countdown, nextCheckIn, lastCheckTime]);
177+
}, [currentStatus, sleepStartTimestamp, sleepDurationSeconds]);
178+
179+
// Helper to check if a check is expanded (expanded by default, collapsed if in set)
180+
const isCheckExpanded = (checkNumber: number) => !collapsedChecks.has(checkNumber);
193181

194182
const currentCheck = checks[currentCheckIndex];
195183

@@ -251,7 +239,9 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
251239
<div className="font-semibold text-primary">{title}</div>
252240
<div className="flex items-center gap-2">
253241
{getStatusIcon()}
254-
<span className="text-sm">{getStatusText()}</span>
242+
{(runStatus === "active" && (currentStatus === "checking" || currentStatus === "sleeping")) && (
243+
<span className="text-sm">{getStatusText()}</span>
244+
)}
255245
</div>
256246
</div>
257247
<div className="text-sm space-y-1">
@@ -305,22 +295,28 @@ const RenderSentinelStep: React.FC<RenderSentinelStepProps> = ({
305295
<div className="space-y-2">
306296
<div className="text-sm">
307297
{currentCheck.reason ||
308-
`Check #${currentCheck.checkNumber} - Condition not yet satisfied`}
298+
`Actively Checking...`}
309299
</div>
310300

311301
{/* Expandable section for agent messages */}
312302
{currentCheck.messages.length > 0 && (
313303
<div className="mt-2">
314304
<button
315-
onClick={() => setExpandedCheck(
316-
expandedCheck === currentCheck.checkNumber ? null : currentCheck.checkNumber
317-
)}
318-
className="text-sm text-magenta-800 hover:text-magenta-900 underline"
305+
onClick={() => setCollapsedChecks((prev) => {
306+
const newSet = new Set(prev);
307+
if (newSet.has(currentCheck.checkNumber)) {
308+
newSet.delete(currentCheck.checkNumber);
309+
} else {
310+
newSet.add(currentCheck.checkNumber);
311+
}
312+
return newSet;
313+
})}
314+
className="text-magenta-800 hover:text-magenta-900 underline"
319315
>
320-
{expandedCheck === currentCheck.checkNumber ? "Hide" : "Show"} check steps ({currentCheck.messages.length})
316+
{isCheckExpanded(currentCheck.checkNumber) ? "Hide" : "Show"} check steps ({currentCheck.messages.length})
321317
</button>
322318

323-
{expandedCheck === currentCheck.checkNumber && (
319+
{isCheckExpanded(currentCheck.checkNumber) && (
324320
<div className="mt-2 space-y-1">
325321
{currentCheck.messages.map((msg, idx) => (
326322
<RenderMessage

0 commit comments

Comments
 (0)