akseljoonas HF Staff commited on
Commit
f915e8e
·
1 Parent(s): e9c82b7

fix: protect system prompt in undo, scan backwards for dangling tool calls, gate input on SDK status

Browse files
agent/context_manager/manager.py CHANGED
@@ -142,18 +142,35 @@ class ContextManager:
142
  return self.items
143
 
144
  def _patch_dangling_tool_calls(self) -> None:
145
- """Add stub tool results for any tool_calls that lack a matching result."""
 
 
 
 
146
  if not self.items:
147
  return
148
- last = self.items[-1]
149
- if getattr(last, "role", None) != "assistant" or not getattr(last, "tool_calls", None):
 
 
 
 
 
 
 
 
 
 
 
 
150
  return
 
151
  answered_ids = {
152
  getattr(m, "tool_call_id", None)
153
  for m in self.items
154
  if getattr(m, "role", None) == "tool"
155
  }
156
- for tc in last.tool_calls:
157
  if tc.id not in answered_ids:
158
  self.items.append(
159
  Message(
@@ -168,14 +185,14 @@ class ContextManager:
168
  """Remove the last complete turn (user msg + all assistant/tool msgs that follow).
169
 
170
  Pops from the end until the last user message is removed, keeping the
171
- tool_use/tool_result pairing valid.
172
 
173
  Returns True if a user message was found and removed.
174
  """
175
- if not self.items:
176
  return False
177
 
178
- while self.items:
179
  msg = self.items.pop()
180
  if getattr(msg, "role", None) == "user":
181
  return True
 
142
  return self.items
143
 
144
  def _patch_dangling_tool_calls(self) -> None:
145
+ """Add stub tool results for any tool_calls that lack a matching result.
146
+
147
+ Scans backwards to find the last assistant message with tool_calls,
148
+ which may not be items[-1] if some tool results were already added.
149
+ """
150
  if not self.items:
151
  return
152
+
153
+ # Find the last assistant message with tool_calls
154
+ assistant_msg = None
155
+ for i in range(len(self.items) - 1, -1, -1):
156
+ msg = self.items[i]
157
+ if getattr(msg, "role", None) == "assistant" and getattr(msg, "tool_calls", None):
158
+ assistant_msg = msg
159
+ break
160
+ # Stop scanning once we hit a user message — anything before
161
+ # that belongs to a previous (complete) turn.
162
+ if getattr(msg, "role", None) == "user":
163
+ break
164
+
165
+ if not assistant_msg:
166
  return
167
+
168
  answered_ids = {
169
  getattr(m, "tool_call_id", None)
170
  for m in self.items
171
  if getattr(m, "role", None) == "tool"
172
  }
173
+ for tc in assistant_msg.tool_calls:
174
  if tc.id not in answered_ids:
175
  self.items.append(
176
  Message(
 
185
  """Remove the last complete turn (user msg + all assistant/tool msgs that follow).
186
 
187
  Pops from the end until the last user message is removed, keeping the
188
+ tool_use/tool_result pairing valid. Never removes the system message.
189
 
190
  Returns True if a user message was found and removed.
191
  """
192
+ if len(self.items) <= 1:
193
  return False
194
 
195
+ while len(self.items) > 1:
196
  msg = self.items.pop()
197
  if getattr(msg, "role", None) == "user":
198
  return True
frontend/src/components/SessionChat.tsx CHANGED
@@ -25,7 +25,7 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
25
  const { isConnected, isProcessing, setProcessing, activityStatus } = useAgentStore();
26
  const { updateSessionTitle } = useSessionStore();
27
 
28
- const { messages, sendMessage, stop, undoLastTurn, approveTools } = useAgentChat({
29
  sessionId,
30
  isActive,
31
  onReady: () => logger.log(`Session ${sessionId} ready`),
@@ -105,9 +105,13 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
105
  prevActiveRef.current = isActive;
106
  }, [isActive, messages]);
107
 
 
 
 
 
108
  const handleSendMessage = useCallback(
109
  async (text: string) => {
110
- if (!text.trim() || isProcessing) return;
111
 
112
  setProcessing(true);
113
  sendMessage({ text: text.trim(), metadata: { createdAt: new Date().toISOString() } });
@@ -129,7 +133,7 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
129
  });
130
  }
131
  },
132
- [sessionId, sendMessage, messages, updateSessionTitle, isProcessing, setProcessing],
133
  );
134
 
135
  // Don't render UI for background sessions — hooks still run
@@ -139,14 +143,14 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
139
  <>
140
  <MessageList
141
  messages={messages}
142
- isProcessing={isProcessing}
143
  approveTools={approveTools}
144
  onUndoLastTurn={undoLastTurn}
145
  />
146
  <ChatInput
147
  onSend={handleSendMessage}
148
  onStop={stop}
149
- isProcessing={isProcessing}
150
  disabled={!isConnected || activityStatus.type === 'waiting-approval'}
151
  placeholder={activityStatus.type === 'waiting-approval' ? 'Approve or reject pending tools first...' : undefined}
152
  />
 
25
  const { isConnected, isProcessing, setProcessing, activityStatus } = useAgentStore();
26
  const { updateSessionTitle } = useSessionStore();
27
 
28
+ const { messages, sendMessage, stop, status, undoLastTurn, approveTools } = useAgentChat({
29
  sessionId,
30
  isActive,
31
  onReady: () => logger.log(`Session ${sessionId} ready`),
 
105
  prevActiveRef.current = isActive;
106
  }, [isActive, messages]);
107
 
108
+ // SDK status is the ground truth — if it's streaming/submitted, agent is busy
109
+ const sdkBusy = status === 'streaming' || status === 'submitted';
110
+ const busy = isProcessing || sdkBusy;
111
+
112
  const handleSendMessage = useCallback(
113
  async (text: string) => {
114
+ if (!text.trim() || busy) return;
115
 
116
  setProcessing(true);
117
  sendMessage({ text: text.trim(), metadata: { createdAt: new Date().toISOString() } });
 
133
  });
134
  }
135
  },
136
+ [sessionId, sendMessage, messages, updateSessionTitle, busy, setProcessing],
137
  );
138
 
139
  // Don't render UI for background sessions — hooks still run
 
143
  <>
144
  <MessageList
145
  messages={messages}
146
+ isProcessing={busy}
147
  approveTools={approveTools}
148
  onUndoLastTurn={undoLastTurn}
149
  />
150
  <ChatInput
151
  onSend={handleSendMessage}
152
  onStop={stop}
153
+ isProcessing={busy}
154
  disabled={!isConnected || activityStatus.type === 'waiting-approval'}
155
  placeholder={activityStatus.type === 'waiting-approval' ? 'Approve or reject pending tools first...' : undefined}
156
  />