bep40 commited on
Commit
fefda99
·
verified ·
1 Parent(s): 68e54e0

V8: Safe auto-continue via useEffect (no hoisting)

Browse files
Files changed (1) hide show
  1. patch_auto_continue_v8.py +239 -0
patch_auto_continue_v8.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """V8: Safe auto-continue via useEffect pattern (no hoisting issues)."""
2
+
3
+ import os
4
+
5
+ events_ts = "/source/frontend/src/types/events.ts"
6
+ sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
7
+ hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
8
+ input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
9
+ session_tsx = "/source/frontend/src/components/SessionChat.tsx"
10
+ agent_py = "/app/agent/core/agent_loop.py"
11
+
12
+
13
+ def patch_events():
14
+ with open(events_ts) as f:
15
+ c = f.read()
16
+ if "'task_incomplete'" not in c:
17
+ c = c.replace(
18
+ " | 'interrupted'",
19
+ " | 'interrupted'\n | 'task_incomplete'"
20
+ )
21
+ with open(events_ts, 'w') as f: f.write(c)
22
+ print("OK: events.ts")
23
+
24
+
25
+ def patch_sse():
26
+ with open(sse_ts) as f:
27
+ c = f.read()
28
+
29
+ if "onTaskIncomplete:" not in c:
30
+ c = c.replace(
31
+ " onInterrupted: () => void;",
32
+ " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
33
+ )
34
+
35
+ if "case 'task_incomplete'" not in c:
36
+ case_handler = """ case 'task_incomplete':
37
+ sideChannel.onTaskIncomplete(
38
+ (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
39
+ );
40
+ break;
41
+
42
+ default:"""
43
+ c = c.replace("\n default:", "\n" + case_handler)
44
+
45
+ with open(sse_ts, 'w') as f: f.write(c)
46
+ print("OK: sse.ts")
47
+
48
+
49
+ def patch_hook():
50
+ with open(hook_ts) as f:
51
+ c = f.read()
52
+
53
+ # 1. Import useState
54
+ if "useState" not in c:
55
+ c = c.replace(
56
+ "import { useCallback, useEffect, useMemo, useRef } from 'react';",
57
+ "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
58
+ )
59
+
60
+ # 2. State after callbacksRef
61
+ state = """
62
+ // Auto-continue state for free models
63
+ const [_showAc, _setShowAc] = useState(false);
64
+ const _acPlanRef = useRef<Array<{ id: string; content: string; status: string }>>([]);"""
65
+ if "_showAc" not in c:
66
+ c = c.replace(
67
+ "callbacksRef.current = { onReady, onError, onSessionDead };",
68
+ "callbacksRef.current = { onReady, onError, onSessionDead };" + state
69
+ )
70
+
71
+ # 3. onTaskIncomplete in interface
72
+ if "onTaskIncomplete:" not in c:
73
+ c = c.replace(
74
+ " onInterrupted: () => void;",
75
+ " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
76
+ )
77
+
78
+ # 4. Handler in sideChannel (only set state + ref, no chat.sendMessage)
79
+ handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
80
+ _acPlanRef.current = incompletePlan;
81
+ _setShowAc(true);
82
+ },
83
+ onInterrupted: () => { /* no-op */ },"""
84
+
85
+ if "onTaskIncomplete: (incompletePlan" not in c:
86
+ old = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },'
87
+ if old in c:
88
+ c = c.replace(old, handler)
89
+ else:
90
+ c = c.replace(' onInterrupted: () => { /* no-op */ },', handler)
91
+
92
+ # 5. useEffect to watch _showAc - runs timer, calls chat.sendMessage via chatActionsRef
93
+ effect = """
94
+ // Auto-continue effect: when _showAc becomes true, start 10s timer
95
+ useEffect(() => {
96
+ if (!_showAc) return;
97
+ const timer = setTimeout(() => {
98
+ const plan = _acPlanRef.current;
99
+ const setMsgs = chatActionsRef.current.setMessages;
100
+ const msgs = chatActionsRef.current.messages;
101
+ if (setMsgs && plan.length > 0) {
102
+ const planStr = plan.map(i => `- ${i.content}`).join('\\\\n');
103
+ const newMsg = { id: 'ac-' + Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}` }], content: '' };
104
+ setMsgs([...msgs, newMsg]);
105
+ }
106
+ _setShowAc(false);
107
+ }, 10000);
108
+ return () => clearTimeout(timer);
109
+ }, [_showAc]);
110
+ """
111
+ if "Auto-continue effect" not in c:
112
+ # Insert before return
113
+ c = c.replace("\n return {\n messages: chat.messages,", effect + "\n return {\n messages: chat.messages,")
114
+
115
+ # 6. Cancel + return
116
+ cancel = """
117
+ const _acCancel = useCallback(() => { _setShowAc(false); }, []);"""
118
+ if "_acCancel" not in c:
119
+ c = c.replace("\n return {\n messages: chat.messages,", cancel + "\n return {\n messages: chat.messages,")
120
+ if "_showAc," not in c:
121
+ c = c.replace(
122
+ "refreshMessages,\n };",
123
+ "refreshMessages,\n _showAc,\n _acCancel,\n };"
124
+ )
125
+
126
+ with open(hook_ts, 'w') as f: f.write(c)
127
+ print("OK: useAgentChat.ts")
128
+
129
+
130
+ def patch_chat_input():
131
+ with open(input_tsx) as f:
132
+ c = f.read()
133
+
134
+ if "Button" not in c:
135
+ c = c.replace(" Tooltip,", " Tooltip,\n Button,")
136
+
137
+ if "_showAc" not in c:
138
+ old_iface = """interface ChatInputProps {
139
+ sessionId?: string;
140
+ initialModelPath?: string | null;
141
+ onSend: (text: string) => void;
142
+ onStop?: () => void;
143
+ onDatasetUploaded?: () => Promise<boolean> | boolean;
144
+ isProcessing?: boolean;
145
+ disabled?: boolean;
146
+ placeholder?: string;
147
+ }"""
148
+ new_iface = """interface ChatInputProps {
149
+ sessionId?: string;
150
+ initialModelPath?: string | null;
151
+ onSend: (text: string) => void;
152
+ onStop?: () => void;
153
+ onDatasetUploaded?: () => Promise<boolean> | boolean;
154
+ isProcessing?: boolean;
155
+ disabled?: boolean;
156
+ placeholder?: string;
157
+ _showAc?: boolean;
158
+ _acCancel?: () => void;
159
+ }"""
160
+ c = c.replace(old_iface, new_iface)
161
+
162
+ old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
163
+ new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
164
+ if old_props not in c:
165
+ old_props2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
166
+ new_props2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
167
+ c = c.replace(old_props2, new_props2)
168
+ else:
169
+ c = c.replace(old_props, new_props)
170
+
171
+ if "T\u1ea1m d\u1eebng" not in c:
172
+ btn = """
173
+ {_showAc && _acCancel && (
174
+ <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
175
+ <Button variant="outlined" size="small" onClick={_acCancel}
176
+ sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
177
+ T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
178
+ </Button>
179
+ </Box>
180
+ )}
181
+ """
182
+ if "<JobsUpgradeDialog" in c:
183
+ c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
184
+
185
+ with open(input_tsx, 'w') as f: f.write(c)
186
+ print("OK: ChatInput.tsx")
187
+
188
+
189
+ def patch_session():
190
+ if not os.path.exists(session_tsx):
191
+ print(f"SKIP: {session_tsx}")
192
+ return
193
+ with open(session_tsx) as f:
194
+ c = f.read()
195
+ if "_showAc" not in c:
196
+ c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
197
+ with open(session_tsx, 'w') as f: f.write(c)
198
+ print("OK: SessionChat.tsx")
199
+
200
+
201
+ def patch_agent():
202
+ if not os.path.exists(agent_py):
203
+ print(f"SKIP: {agent_py}")
204
+ return
205
+ with open(agent_py) as f:
206
+ c = f.read()
207
+
208
+ if "_unfinished_plan" not in c:
209
+ fn = """
210
+ def _unfinished_plan(s: Session) -> list[dict[str, str]]:
211
+ p = getattr(s, "current_plan", None) or []
212
+ return [it for it in p if it.get("status") in ("pending", "in_progress")]
213
+ """
214
+ c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
215
+
216
+ if "task_incomplete" not in c:
217
+ check = """
218
+ # Auto-continue detection
219
+ if not llm_result.tool_calls_acc and llm_result.content:
220
+ unfinished = _unfinished_plan(session)
221
+ if unfinished:
222
+ await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}))
223
+ """
224
+ marker = " # -- End of turn --"
225
+ if marker in c:
226
+ c = c.replace(marker, check + "\n" + marker)
227
+
228
+ with open(agent_py, 'w') as f: f.write(c)
229
+ print("OK: agent_loop.py")
230
+
231
+
232
+ if __name__ == "__main__":
233
+ patch_events()
234
+ patch_sse()
235
+ patch_hook()
236
+ patch_chat_input()
237
+ patch_session()
238
+ patch_agent()
239
+ print("\nDONE: V8 - safe useEffect pattern")