Restore Space to commit 36e2e7f8

#1
by bep40 - opened
AUTO_CONTINUE_FEATURE.md DELETED
@@ -1,183 +0,0 @@
1
- # Auto-Continue Feature for Free Models
2
-
3
- ## Tổng quan
4
- Khi model miễn phí dùng OpenRouter phản hồi bị dừng nhưng nhiệm vụ chưa hoàn thành, hệ thống sẽ:
5
- 1. Phát hiện task incomplete dựa vào trạng thái plan
6
- 2. Hiển thị 2 nút: "Tự động tiếp tục (10s)" và "Tạm dừng"
7
-
8
- ## Các file cần sửa
9
-
10
- ### 1. frontend/src/hooks/useAgentChat.ts
11
-
12
- ```typescript
13
- // Thêm vào import
14
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
-
16
- // Trong useAgentChat function, sau callbacksRef
17
- const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
18
- const [showAutoContinue, setShowAutoContinue] = useState(false);
19
- const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
20
- incompletePlan: Array<{ id: string; content: string; status: string }>;
21
- } | null>(null);
22
-
23
- // Thêm vào SideChannelCallbacks interface
24
- export interface SideChannelCallbacks {
25
- // ... existing callbacks ...
26
- onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;
27
- }
28
-
29
- // Thêm vào sideChannel trong useMemo
30
- onTaskIncomplete: (incompletePlan) => {
31
- setTaskIncompleteInfo({ incompletePlan });
32
- setShowAutoContinue(true);
33
- if (autoContinueTimerRef.current) {
34
- clearTimeout(autoContinueTimerRef.current);
35
- }
36
- autoContinueTimerRef.current = setTimeout(() => {
37
- startAutoContinue();
38
- }, 10000);
39
- },
40
-
41
- // Thêm hàm mới
42
- const startAutoContinue = useCallback(() => {
43
- setShowAutoContinue(false);
44
- setTaskIncompleteInfo(null);
45
- if (autoContinueTimerRef.current) {
46
- clearTimeout(autoContinueTimerRef.current);
47
- autoContinueTimerRef.current = null;
48
- }
49
- const incompleteItems = taskIncompleteInfo?.incompletePlan || [];
50
- const continuationText = incompleteItems.length > 0
51
- ? `Tiếp tục từ các task chưa hoàn thành: ${incompleteItems.map(i => `- ${i.content}`).join('\n')}`
52
- : 'Tiếp tục nhiệm vụ.';
53
- chat.sendMessage({
54
- text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`,
55
- metadata: { createdAt: new Date().toISOString() }
56
- });
57
- }, [taskIncompleteInfo, chat]);
58
-
59
- const cancelAutoContinue = useCallback(() => {
60
- setShowAutoContinue(false);
61
- setTaskIncompleteInfo(null);
62
- if (autoContinueTimerRef.current) {
63
- clearTimeout(autoContinueTimerRef.current);
64
- autoContinueTimerRef.current = null;
65
- }
66
- }, []);
67
-
68
- // Thêm vào return
69
- return {
70
- messages: chat.messages,
71
- sendMessage: chat.sendMessage,
72
- stop,
73
- status: chat.status,
74
- undoLastTurn,
75
- editAndRegenerate,
76
- approveTools,
77
- refreshMessages,
78
- showAutoContinue,
79
- taskIncompleteInfo,
80
- startAutoContinue,
81
- cancelAutoContinue,
82
- };
83
- ```
84
-
85
- ### 2. frontend/src/components/Chat/ChatInput.tsx
86
-
87
- ```typescript
88
- // Thêm import
89
- import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';
90
-
91
- // Thêm props
92
- interface ChatInputProps {
93
- // ... existing props
94
- showAutoContinue?: boolean;
95
- taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
96
- startAutoContinue?: () => void;
97
- cancelAutoContinue?: () => void;
98
- }
99
-
100
- // Thêm UI buttons (trước hoặc sau input area)
101
- {showAutoContinue && taskIncompleteInfo && (
102
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 2 }}>
103
- <Button
104
- variant="contained"
105
- color="primary"
106
- size="small"
107
- onClick={startAutoContinue}
108
- sx={{ textTransform: 'none' }}
109
- >
110
- Tự động tiếp tục (10s)
111
- </Button>
112
- <Button
113
- variant="outlined"
114
- color="secondary"
115
- size="small"
116
- onClick={cancelAutoContinue}
117
- sx={{ textTransform: 'none' }}
118
- >
119
- Tạm dừng
120
- </Button>
121
- </Stack>
122
- )}
123
- ```
124
-
125
- ### 3. frontend/src/lib/sse-chat-transport.ts
126
-
127
- ```typescript
128
- // Thêm vào case trong createEventToChunkStream
129
- case 'task_incomplete':
130
- sideChannel.onTaskIncomplete(
131
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
132
- );
133
- break;
134
- ```
135
-
136
- ### 4. agent/core/agent_loop.py
137
-
138
- ```python
139
- # Trong run_agent, sau khi xử lý LLM response
140
- def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
141
- """Kiểm tra nếu model dừng nhưng task chưa hoàn thành."""
142
- if llm_result.tool_calls_acc:
143
- return False # Có tool calls thì chưa incomplete
144
-
145
- plan = getattr(session, "current_plan", None) or []
146
- unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
147
-
148
- return len(unfinished) > 0
149
-
150
- # Gọi sau khi nhận kết quả LLM:
151
- if _check_task_incomplete(session, llm_result):
152
- unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
153
- await session.send_event(
154
- Event(
155
- event_type="task_incomplete",
156
- data={
157
- "incomplete_plan": unfinished,
158
- "message": "Model stopped streaming but task is incomplete."
159
- }
160
- )
161
- )
162
- ```
163
-
164
- ## Logic hoạt động
165
-
166
- 1. Khi agent loop phát hiện model trả lời không có tool calls nhưng plan còn incomplete
167
- 2. Backend gửi event `task_incomplete` với danh sách task chưa hoàn thành
168
- 3. Frontend nhận event qua `onTaskIncomplete` callback
169
- 4. Hiển thị 2 nút:
170
- - **Tự động tiếp tục (10s)**: Sau 10 giây sẽ tự động gửi tin nhắn tiếp tục
171
- - **Tạm dừng**: Hủy timer, cho phép user nhập nội dung mới
172
-
173
- ## Áp dụng patch
174
-
175
- ```bash
176
- # Tải source
177
- git clone https://huggingface.co/spaces/smolagents/ml-intern /tmp/source
178
-
179
- # Sửa các file trên
180
- # Sau đó rebuild frontend
181
- cd /tmp/source/frontend
182
- npm install && npm run build
183
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BUILD_TRIGGER DELETED
@@ -1 +0,0 @@
1
- trigger
 
 
Dockerfile CHANGED
@@ -1,57 +1,60 @@
1
- # Restore + patch frontend + backend models + auto-continue V17 + auto-approve
2
  FROM node:20-alpine AS frontend-builder
3
- RUN apk add --no-cache git python3
4
- RUN git clone --depth 1 https://huggingface.co/spaces/smolagents/ml-intern /source
5
-
6
- # Patch frontend: add OpenRouter models
7
- COPY patch_frontend.py /tmp/patch_frontend.py
8
- RUN python3 /tmp/patch_frontend.py
9
-
10
- # Patch auto-continue V17
11
- COPY patch_auto_continue_v17.py /tmp/patch_auto_continue_v17.py
12
- RUN python3 /tmp/patch_auto_continue_v17.py
13
-
14
- WORKDIR /source/frontend
15
- RUN npm config set fetch-timeout 120000 && \
16
- npm config set fetch-retries 3 && \
17
- npm install && \
18
- npm run build
19
 
20
  # Stage 2: Production
21
  FROM python:3.12-slim
 
 
22
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
 
 
23
  RUN useradd -m -u 1000 user
 
24
  WORKDIR /app
25
- RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
26
 
27
- RUN git clone --depth 1 https://huggingface.co/spaces/smolagents/ml-intern /tmp/source && \
28
- cp /tmp/source/pyproject.toml /tmp/source/uv.lock ./ && \
29
- cp -r /tmp/source/agent ./agent && \
30
- cp -r /tmp/source/backend ./backend && \
31
- cp -r /tmp/source/configs ./configs && \
32
- rm -rf /tmp/source
33
 
34
- RUN uv sync --no-dev --frozen
 
35
 
36
- COPY --from=frontend-builder /source/frontend/dist ./static/
37
- COPY configs/frontend_agent_config.json ./configs/frontend_agent_config.json
 
38
 
39
- # Patch backend: accept new model IDs
40
- COPY patch_models.py /tmp/patch_models.py
41
- RUN python /tmp/patch_models.py
 
42
 
43
- # Patch backend: auto-continue detection + auto-approve
44
- COPY patch_agent_backend.py /tmp/patch_agent_backend.py
45
- RUN python /tmp/patch_agent_backend.py
46
 
47
- RUN mkdir -p /app/session_logs && chown -R user:user /app
 
 
48
 
 
49
  USER user
 
 
50
  ENV HOME=/home/user \
51
  PYTHONUNBUFFERED=1 \
52
  PYTHONPATH=/app \
53
  PATH="/app/.venv/bin:$PATH"
54
 
 
55
  EXPOSE 7860
 
 
56
  WORKDIR /app/backend
57
- CMD ["sh", "start.sh"]
 
1
+ # Stage 1: Build frontend
2
  FROM node:20-alpine AS frontend-builder
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package.json frontend/package-lock.json ./
5
+ RUN npm install
6
+ COPY frontend/ ./
7
+ RUN npm run build
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  # Stage 2: Production
10
  FROM python:3.12-slim
11
+
12
+ # Install uv directly from official image
13
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
14
+
15
+ # Create user with UID 1000 (required for HF Spaces)
16
  RUN useradd -m -u 1000 user
17
+
18
  WORKDIR /app
 
19
 
20
+ # Install system dependencies
21
+ RUN apt-get update && apt-get install -y --no-install-recommends \
22
+ git \
23
+ curl \
24
+ bash \
25
+ && rm -rf /var/lib/apt/lists/*
26
 
27
+ # Copy dependency files
28
+ COPY pyproject.toml uv.lock ./
29
 
30
+ # Install dependencies into /app/.venv
31
+ # Use --frozen to ensure exact versions from uv.lock
32
+ RUN uv sync --no-dev --frozen
33
 
34
+ # Copy application code
35
+ COPY agent/ ./agent/
36
+ COPY backend/ ./backend/
37
+ COPY configs/ ./configs/
38
 
39
+ # Copy built frontend
40
+ COPY --from=frontend-builder /app/frontend/dist ./static/
 
41
 
42
+ # Create directories and set ownership
43
+ RUN mkdir -p /app/session_logs && \
44
+ chown -R user:user /app
45
 
46
+ # Switch to non-root user
47
  USER user
48
+
49
+ # Set environment
50
  ENV HOME=/home/user \
51
  PYTHONUNBUFFERED=1 \
52
  PYTHONPATH=/app \
53
  PATH="/app/.venv/bin:$PATH"
54
 
55
+ # Expose port
56
  EXPOSE 7860
57
+
58
+ # Run the application from backend directory
59
  WORKDIR /app/backend
60
+ CMD ["bash", "start.sh"]
README.md CHANGED
@@ -1,33 +1,340 @@
1
  ---
2
- title: Space Multiverse
3
  emoji: 🤖
4
  colorFrom: yellow
5
  colorTo: blue
6
  sdk: docker
7
  app_port: 7860
8
- short_description: ML Intern V2 (FREE & SAVE MODEL)
9
  hf_oauth: true
10
  hf_oauth_expiration_minutes: 43200
11
  hf_oauth_scopes:
12
- - read-repos
13
- - write-repos
14
- - contribute-repos
15
- - manage-repos
16
- - write-collections
17
- - inference-api
18
- - jobs
19
- - write-discussions
20
- tags:
21
- - ml-intern
22
  ---
23
 
24
- # Space Multiverse — ML Intern V2 (FREE & SAVE MODEL)
 
 
25
 
26
- <!-- rebuild: add-deepseek-v4-flash-latest-2026-08-02T09:00:00Z -->
27
 
28
- - Added **DeepSeek V4 Flash latest** via OpenRouter (path: `openai/deepseek/deepseek-v4-flash-latest`)
29
- - Replaced GPT-5.5 model with **Laguna S 2.1:free** (path: `openai/poolside/laguna-s-2.1:free`)
30
- - Added OpenRouter routing (catch-all `openai/` prefix → OpenRouter API)
31
- - Default: `openai/tencent/hy3:free`
32
- - Models: Tencent HY3:free, Gemma 4 31B:free, Llama 3.3 70B:free, **Laguna M.1:free**, **Laguna S 2.1:free**, Llama 3.1 8B, DeepSeek V4 Flash latest
33
- - Ollama: không thể dùng cho HF Space (local inference, không có cloud API free cho use case này)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ML Intern
3
  emoji: 🤖
4
  colorFrom: yellow
5
  colorTo: blue
6
  sdk: docker
7
  app_port: 7860
 
8
  hf_oauth: true
9
  hf_oauth_expiration_minutes: 43200
10
  hf_oauth_scopes:
11
+ - read-repos
12
+ - write-repos
13
+ - contribute-repos
14
+ - manage-repos
15
+ - write-collections
16
+ - inference-api
17
+ - jobs
18
+ - write-discussions
 
 
19
  ---
20
 
21
+ <p align="center">
22
+ <img src="frontend/public/smolagents.webp" alt="smolagents logo" width="160" />
23
+ </p>
24
 
25
+ # ML Intern
26
 
27
+ An ML intern that autonomously researches, writes, and ships good quality ML related code using the Hugging Face ecosystem — with deep access to docs, papers, datasets, and cloud compute.
28
+
29
+ ## Quick Start
30
+
31
+ ### Installation
32
+
33
+ ```bash
34
+ git clone git@github.com:huggingface/ml-intern.git
35
+ cd ml-intern
36
+ uv sync
37
+ uv tool install -e .
38
+ ```
39
+
40
+ #### That's it. Now `ml-intern` works from any directory:
41
+
42
+ ```bash
43
+ ml-intern
44
+ ```
45
+
46
+ Create a `.env` file in the project root (or export these in your shell):
47
+
48
+ ```bash
49
+ ANTHROPIC_API_KEY=<your-anthropic-api-key> # if using anthropic models
50
+ OPENAI_API_KEY=<your-openai-api-key> # if using openai models
51
+ HF_TOKEN=<your-hugging-face-token>
52
+ GITHUB_TOKEN=<github-personal-access-token>
53
+ ```
54
+ If no `HF_TOKEN` is set, the CLI will prompt you to paste one on first launch. To get a GITHUB_TOKEN follow the tutorial [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token).
55
+
56
+ ### Usage
57
+
58
+ **Interactive mode** (start a chat session):
59
+
60
+ ```bash
61
+ ml-intern
62
+ ```
63
+
64
+ **Headless mode** (single prompt, auto-approve):
65
+
66
+ ```bash
67
+ ml-intern "fine-tune llama on my dataset"
68
+ ```
69
+
70
+ **Options:**
71
+
72
+ ```bash
73
+ ml-intern --model anthropic/claude-opus-4-6 "your prompt"
74
+ ml-intern --model openai/gpt-5.5 "your prompt"
75
+ ml-intern --max-iterations 100 "your prompt"
76
+ ml-intern --no-stream "your prompt"
77
+ ```
78
+
79
+ ## Sharing Traces
80
+
81
+ Every session is auto-uploaded to your **own private Hugging Face dataset**
82
+ in [Claude Code JSONL format](https://huggingface.co/changelog/agent-trace-viewer),
83
+ which the HF Agent Trace Viewer auto-detects so you can browse turns, tool
84
+ calls, and model responses directly on the Hub.
85
+
86
+ By default the dataset is named `{your-hf-username}/ml-intern-sessions` and is
87
+ **created private**. You can flip it to public from inside the CLI:
88
+
89
+ ```bash
90
+ /share-traces # show current visibility + dataset URL
91
+ /share-traces public # publish (anyone can view)
92
+ /share-traces private # lock it back down
93
+ ```
94
+
95
+ You can also flip visibility from the dataset page on huggingface.co — the
96
+ agent honours whatever you set there for subsequent uploads.
97
+
98
+ To opt out entirely, set in your CLI config (e.g. `configs/cli_agent_config.json`
99
+ or `~/.config/ml-intern/cli_agent_config.json`):
100
+
101
+ ```json
102
+ { "share_traces": false }
103
+ ```
104
+
105
+ To override the destination repo, set:
106
+
107
+ ```json
108
+ { "personal_trace_repo_template": "{hf_user}/my-custom-traces" }
109
+ ```
110
+
111
+ The shared `smolagents/ml-intern-sessions` dataset is unrelated and only
112
+ receives anonymized telemetry rows used by the backend KPI scheduler.
113
+
114
+ ## Supported Gateways
115
+
116
+ ML Intern currently supports one-way notification gateways from CLI sessions.
117
+ These gateways send out-of-band status updates; they do not accept inbound chat
118
+ messages.
119
+
120
+ ### Slack
121
+
122
+ Slack notifications use the Slack Web API to post messages when the agent needs
123
+ approval, hits an error, or completes a turn. Create a Slack app with a bot token
124
+ that has `chat:write`, invite the bot to the target channel, then set:
125
+
126
+ ```bash
127
+ SLACK_BOT_TOKEN=xoxb-...
128
+ SLACK_CHANNEL_ID=C...
129
+ ```
130
+
131
+ The CLI automatically creates a `slack.default` destination when both variables
132
+ are present. Optional environment variables for the env-only default:
133
+
134
+ ```bash
135
+ ML_INTERN_SLACK_NOTIFICATIONS=false
136
+ ML_INTERN_SLACK_DESTINATION=slack.ops
137
+ ML_INTERN_SLACK_AUTO_EVENTS=approval_required,error,turn_complete
138
+ ML_INTERN_SLACK_ALLOW_AGENT_TOOL=true
139
+ ML_INTERN_SLACK_ALLOW_AUTO_EVENTS=true
140
+ ```
141
+
142
+ For a persistent user-level config, put overrides in
143
+ `~/.config/ml-intern/cli_agent_config.json` or point `ML_INTERN_CLI_CONFIG` at a
144
+ JSON file:
145
+
146
+ ```json
147
+ {
148
+ "messaging": {
149
+ "enabled": true,
150
+ "auto_event_types": ["approval_required", "error", "turn_complete"],
151
+ "destinations": {
152
+ "slack.ops": {
153
+ "provider": "slack",
154
+ "token": "${SLACK_BOT_TOKEN}",
155
+ "channel": "${SLACK_CHANNEL_ID}",
156
+ "allow_agent_tool": true,
157
+ "allow_auto_events": true
158
+ }
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ## Architecture
165
+
166
+ ### Component Overview
167
+
168
+ ```
169
+ ┌─────────────────────────────────────────────────────────────┐
170
+ │ User/CLI │
171
+ └────────────┬─────────────────────────────────────┬──────────┘
172
+ │ Operations │ Events
173
+ ↓ (user_input, exec_approval, ↑
174
+ submission_queue interrupt, compact, ...) event_queue
175
+ │ │
176
+ ↓ │
177
+ ┌────────────────────────────────────────────────────┐ │
178
+ │ submission_loop (agent_loop.py) │ │
179
+ │ ┌──────────────────────────────────────────────┐ │ │
180
+ │ │ 1. Receive Operation from queue │ │ │
181
+ │ │ 2. Route to handler (run_agent/compact/...) │ │ │
182
+ │ └──────────────────────────────────────────────┘ │ │ │
183
+ │ ↓ │ │
184
+ │ ┌──────────────────────────────────────────────┐ │ │
185
+ │ │ Handlers.run_agent() │ ├──┤
186
+ │ │ │ │ │
187
+ │ │ ┌────────────────────────────────────────┐ │ │ │
188
+ │ │ │ Agentic Loop (max 300 iterations) │ │ │ │
189
+ │ │ │ │ │ │ │
190
+ │ │ │ ┌──────────────────────────────────┐ │ │ │ │
191
+ │ │ │ │ Session │ │ │ │ │
192
+ │ │ │ │ ┌────────────────────────────┐ │ │ │ │ │
193
+ │ │ │ │ │ ContextManager │ │ │ │ │ │
194
+ │ │ │ │ │ • Message history │ │ │ │ │ │
195
+ │ │ │ │ │ (litellm.Message[]) │ │ │ │ │ │
196
+ │ │ │ │ │ • Auto-compaction (170k) │ │ │ │ │ │
197
+ │ │ │ │ │ • Session upload to HF │ │ │ │ │ │
198
+ │ │ │ │ └────────────────────────────┘ │ │ │ │ │
199
+ │ │ │ │ │ │ │ │ │
200
+ │ │ │ │ ┌────────────────────────────┐ │ │ │ │ │
201
+ │ │ │ │ │ ToolRouter │ │ │ │ │ │
202
+ │ │ │ │ │ ├─ HF docs & research │ │ │ │ │ │
203
+ │ │ │ │ │ ├─ HF repos, datasets, │ │ │ │ │ │
204
+ │ │ │ │ │ │ jobs, papers │ │ │ │ │ │
205
+ │ │ │ │ │ ├─ GitHub code search │ │ │ │ │ │
206
+ │ │ │ │ │ ├─ Sandbox & local tools │ │ │ │ │ │
207
+ │ │ │ │ │ ├─ Planning │ │ │ │ │ │
208
+ │ │ │ │ │ └─ MCP server tools │ │ │ │ │ │
209
+ │ │ │ │ └────────────────────────────┘ │ │ │ │ │
210
+ │ │ │ └──────────────────────────────────┘ │ │ │ │
211
+ │ │ │ │ │ │ │
212
+ │ │ │ ┌──────────────────────────────────┐ │ │ │ │
213
+ │ │ │ │ Doom Loop Detector │ │ │ │ │
214
+ │ │ │ │ • Detects repeated tool patterns │ │ │ │ │
215
+ │ │ │ │ • Injects corrective prompts │ │ │ │ ��
216
+ │ │ │ └──────────────────────────────────┘ │ │ │ │
217
+ │ │ │ │ │ │ │
218
+ │ │ │ Loop: │ │ │ │
219
+ │ │ │ 1. LLM call (litellm.acompletion) │ │ │ │
220
+ │ │ │ ↓ │ │ │ │
221
+ │ │ │ 2. Parse tool_calls[] │ │ │ │
222
+ │ │ │ ↓ │ │ │ │
223
+ │ │ │ 3. Approval check │ │ │ │
224
+ │ │ │ (jobs, sandbox, destructive ops) │ │ │ │
225
+ │ │ │ ↓ │ │ │ │
226
+ │ │ │ 4. Execute via ToolRouter │ │ │ │
227
+ │ │ │ ↓ │ │ │ │
228
+ │ │ │ 5. Add results to ContextManager │ │ │ │
229
+ │ │ │ ↓ │ │ │ │
230
+ │ │ │ 6. Repeat if tool_calls exist │ │ │ │
231
+ │ │ └────────────────────────────────────────┘ │ │ │
232
+ │ └──────────────────────────────────────────────┘ │ │
233
+ └────────────────────────────────────────────────────┴──┘
234
+ ```
235
+
236
+ ### Agentic Loop Flow
237
+
238
+ ```
239
+ User Message
240
+
241
+ [Add to ContextManager]
242
+
243
+ ╔═══════════════════════════════════════════╗
244
+ ║ Iteration Loop (max 300) ║
245
+ ║ ║
246
+ ║ Get messages + tool specs ║
247
+ ║ ↓ ║
248
+ ║ litellm.acompletion() ║
249
+ ║ ↓ ║
250
+ ║ Has tool_calls? ──No──> Done ║
251
+ ║ │ ║
252
+ ║ Yes ║
253
+ ║ ↓ ║
254
+ ║ Add assistant msg (with tool_calls) ║
255
+ ║ ↓ ║
256
+ ║ Doom loop check ║
257
+ ║ ↓ ║
258
+ ║ For each tool_call: ║
259
+ ║ • Needs approval? ──Yes──> Wait for ║
260
+ ║ │ user confirm ║
261
+ ║ No ║
262
+ ║ ↓ ║
263
+ ║ • ToolRouter.execute_tool() ║
264
+ ║ • Add result to ContextManager ║
265
+ ║ ↓ ║
266
+ ║ Continue loop ─────────────────┐ ║
267
+ ║ ↑ │ ║
268
+ ║ └───────────────────────┘ ║
269
+ ╚═══════════════════════════════════════════╝
270
+ ```
271
+
272
+ ## Events
273
+
274
+ The agent emits the following events via `event_queue`:
275
+
276
+ - `processing` - Starting to process user input
277
+ - `ready` - Agent is ready for input
278
+ - `assistant_chunk` - Streaming token chunk
279
+ - `assistant_message` - Complete LLM response text
280
+ - `assistant_stream_end` - Token stream finished
281
+ - `tool_call` - Tool being called with arguments
282
+ - `tool_output` - Tool execution result
283
+ - `tool_log` - Informational tool log message
284
+ - `tool_state_change` - Tool execution state transition
285
+ - `approval_required` - Requesting user approval for sensitive operations
286
+ - `turn_complete` - Agent finished processing
287
+ - `error` - Error occurred during processing
288
+ - `interrupted` - Agent was interrupted
289
+ - `compacted` - Context was compacted
290
+ - `undo_complete` - Undo operation completed
291
+ - `shutdown` - Agent shutting down
292
+
293
+ ## Development
294
+
295
+ ### Adding Built-in Tools
296
+
297
+ Edit `agent/core/tools.py`:
298
+
299
+ ```python
300
+ def create_builtin_tools() -> list[ToolSpec]:
301
+ return [
302
+ ToolSpec(
303
+ name="your_tool",
304
+ description="What your tool does",
305
+ parameters={
306
+ "type": "object",
307
+ "properties": {
308
+ "param": {"type": "string", "description": "Parameter description"}
309
+ },
310
+ "required": ["param"]
311
+ },
312
+ handler=your_async_handler
313
+ ),
314
+ # ... existing tools
315
+ ]
316
+ ```
317
+
318
+ ### Adding MCP Servers
319
+
320
+ Edit `configs/cli_agent_config.json` for CLI defaults, or
321
+ `configs/frontend_agent_config.json` for web-session defaults:
322
+
323
+ ```json
324
+ {
325
+ "model_name": "anthropic/claude-sonnet-4-5-20250929",
326
+ "mcpServers": {
327
+ "your-server-name": {
328
+ "transport": "http",
329
+ "url": "https://example.com/mcp",
330
+ "headers": {
331
+ "Authorization": "Bearer ${YOUR_TOKEN}"
332
+ }
333
+ }
334
+ }
335
+ }
336
+ ```
337
+
338
+ Note: Environment variables like `${YOUR_TOKEN}` are auto-substituted from `.env`.
339
+
340
+ ```
configs/__init__.py ADDED
File without changes
configs/cli_agent_config.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "zai-org/GLM-5.2:novita",
3
+ "save_sessions": true,
4
+ "session_dataset_repo": "smolagents/ml-intern-sessions",
5
+ "share_traces": true,
6
+ "personal_trace_repo_template": "{hf_user}/ml-intern-sessions",
7
+ "yolo_mode": false,
8
+ "confirm_cpu_jobs": true,
9
+ "auto_file_upload": true,
10
+ "tool_runtime": "local",
11
+ "messaging": {
12
+ "enabled": false,
13
+ "auto_event_types": ["approval_required", "error", "turn_complete"],
14
+ "destinations": {}
15
+ },
16
+ "mcpServers": {
17
+ "hf-mcp-server": {
18
+ "transport": "http",
19
+ "url": "https://huggingface.co/mcp?login"
20
+ }
21
+ }
22
+ }
configs/frontend_agent_config.json CHANGED
@@ -1,16 +1,16 @@
1
  {
2
- "model_name": "openai/tencent/hy3:free",
3
- "api_base": "https://openrouter.ai/api/v1",
4
  "save_sessions": true,
5
- "personal_trace_repo_template": "{hf_user}/ml-intern-sessions",
6
  "share_traces": true,
 
7
  "yolo_mode": false,
8
  "confirm_cpu_jobs": true,
9
  "auto_file_upload": true,
10
  "mcpServers": {
11
  "hf-mcp-server": {
12
  "transport": "http",
13
- "url": "https://openrouter.ai/mcp"
14
  }
15
  }
16
  }
 
1
  {
2
+ "model_name": "${ML_INTERN_DEFAULT_MODEL_ID:-zai-org/GLM-5.2:novita}",
 
3
  "save_sessions": true,
4
+ "session_dataset_repo": "smolagents/ml-intern-sessions",
5
  "share_traces": true,
6
+ "personal_trace_repo_template": "{hf_user}/ml-intern-sessions",
7
  "yolo_mode": false,
8
  "confirm_cpu_jobs": true,
9
  "auto_file_upload": true,
10
  "mcpServers": {
11
  "hf-mcp-server": {
12
  "transport": "http",
13
+ "url": "https://huggingface.co/mcp?login"
14
  }
15
  }
16
  }
fix_space_error_report.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "status": "fixed_uploaded",
3
- "space": "bep40/ml-intern",
4
- "space_url": "https://huggingface.co/spaces/bep40/ml-intern",
5
- "root_cause": "Dockerfile runtime stage used CMD [\"bash\", \"start.sh\"] on python:3.12-slim, which does not include bash by default. This causes container startup failure and Space error after a successful build.",
6
- "fix": "Changed Dockerfile CMD to [\"sh\", \"start.sh\"] and added ca-certificates to apt packages. start.sh is POSIX-compatible for this use.",
7
- "files_changed": ["Dockerfile", "README.md"],
8
- "verification": "Repo files updated and rebuild triggered. Live runtime verification via HF Jobs/sandbox was blocked by namespace credit/duplication limits in this session; check Space build logs if HF still reports error."
9
- }
 
 
 
 
 
 
 
 
 
 
patch_agent_backend.py DELETED
@@ -1,89 +0,0 @@
1
- """Patch: agent_loop.py task detection + auto-approve for free models."""
2
-
3
- import os
4
- import ast
5
-
6
- AGENT_LOOP = "/app/agent/core/agent_loop.py"
7
- AGENT_FILE = "/app/backend/routes/agent.py"
8
- SESSION_FILE = "/app/agent/core/session.py"
9
-
10
- # ===== PATCH 1: agent_loop.py - detect model stopped mid-task =====
11
- with open(AGENT_LOOP) as f:
12
- c = f.read()
13
-
14
- # Add helper function before class Handlers
15
- if "_unfinished_plan" not in c:
16
- fn = (
17
- "\n\ndef _unfinished_plan(s):\n"
18
- " p = getattr(s, 'current_plan', None) or []\n"
19
- " return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n"
20
- )
21
- c = c.replace("class Handlers:", fn + "class Handlers:")
22
- print("OK: Added _unfinished_plan()")
23
-
24
- # Add detection: if model produced no tool calls but content still exists,
25
- # and plan items are still pending/in_progress, send session_update event
26
- if "ac_plan" not in c:
27
- check = (
28
- "\n # Auto-continue: detect model stopped mid-task\n"
29
- " if not llm_result.tool_calls_acc and llm_result.content:\n"
30
- " unfinished = _unfinished_plan(session)\n"
31
- " if unfinished:\n"
32
- " await session.send_event(Event(event_type='session_update', data={\n"
33
- " 'ac_plan': unfinished,\n"
34
- " }))\n"
35
- )
36
- c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
37
- print("OK: Added mid-task detection")
38
-
39
- try:
40
- ast.parse(c)
41
- with open(AGENT_LOOP, "w") as f:
42
- f.write(c)
43
- print("OK: agent_loop.py patched")
44
- except SyntaxError as e:
45
- print(f"FAIL: agent_loop.py syntax: {e}")
46
-
47
- # ===== PATCH 2: Enable auto_approval for ALL sessions by default =====
48
- # So free models don't block on user approval
49
- with open(SESSION_FILE) as f:
50
- c = f.read()
51
-
52
- if "AUTO_APPROVE" not in c:
53
- # Find where auto_approval is set on the session object
54
- # Patch: when creating a new session, set auto_approval.enabled = True
55
- auto_patch = (
56
- "\n # === PATCH: Auto-approve for free models ===\n"
57
- " self.auto_approval = {\n"
58
- ' "enabled": True,\n'
59
- ' "cost_cap_usd": None,\n'
60
- ' "estimated_spend_usd": 0.0,\n'
61
- ' "remaining_usd": None,\n'
62
- " }\n"
63
- )
64
-
65
- # Find session init method
66
- init_match = "class Session:"
67
- if init_match in c:
68
- # Insert after the class definition, find __init__
69
- init_found = "def __init__"
70
- if init_found in c:
71
- # Find first line inside __init__ that's an assignment
72
- init_start = c.find(init_found)
73
- body_start = c.find("\n", init_start) + 1
74
- body_start = c.find(" ", body_start) # first indented line
75
- c = c[:body_start] + auto_patch + c[body_start:]
76
- print("OK: Added auto_approve default")
77
-
78
- try:
79
- ast.parse(c)
80
- with open(SESSION_FILE, "w") as f:
81
- f.write(c)
82
- print("OK: session.py patched")
83
- except SyntaxError as e:
84
- print(f"FAIL: session.py syntax: {e}")
85
- # Don't block on this patch
86
- else:
87
- print("OK: session.py already has auto_approve")
88
-
89
- print("DONE: Backend patches applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_agent_loop.py DELETED
@@ -1,74 +0,0 @@
1
- """Patch agent_loop.py - detect task incomplete and send event."""
2
-
3
- AGENT_LOOP = "/app/agent/core/agent_loop.py"
4
-
5
- def patch():
6
- import os
7
- if not os.path.exists(AGENT_LOOP):
8
- print(f"SKIP: {AGENT_LOOP} not found")
9
- return
10
-
11
- with open(AGENT_LOOP, "r", encoding="utf-8") as f:
12
- content = f.read()
13
-
14
- # Add _check_task_incomplete function before class Handlers
15
- check_func = '''
16
-
17
- def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
18
- """Check if model stopped streaming but task is incomplete."""
19
- if llm_result.tool_calls_acc:
20
- return False # Has tool calls, task continues via tool execution
21
-
22
- plan = getattr(session, "current_plan", None) or []
23
- unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
24
-
25
- return len(unfinished) > 0
26
-
27
-
28
- def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
29
- """Helper to get unfinished plan items."""
30
- plan = getattr(session, "current_plan", None) or []
31
- return [item for item in plan if item.get("status") in ("pending", "in_progress")]
32
-
33
- '''
34
-
35
- if "_check_task_incomplete" not in content:
36
- content = content.replace(
37
- "class Handlers:",
38
- check_func + "\n\nclass Handlers:"
39
- )
40
-
41
- # Add the check after LLM response processing
42
- check_call = '''
43
- # === Check for incomplete task after LLM response ===
44
- if not llm_result.tool_calls_acc and llm_result.content:
45
- unfinished = _unfinished_plan_items(session)
46
- if unfinished:
47
- await session.send_event(
48
- Event(
49
- event_type="task_incomplete",
50
- data={
51
- "incomplete_plan": unfinished,
52
- "message": "Model phản hồi đã dừng nhưng nhiệm vụ chưa hoàn thành."
53
- }
54
- )
55
- )
56
- '''
57
-
58
- if "task_incomplete" not in content:
59
- # Find a good insertion point - after tool calls processing
60
- insert_marker = " # -- End of turn --"
61
- if insert_marker in content:
62
- content = content.replace(insert_marker, check_call + "\n" + insert_marker)
63
- else:
64
- # Try another location
65
- result_marker = "final_response = llm_result.content or None"
66
- if result_marker in content:
67
- content = content.replace(result_marker, result_marker + "\n" + check_call)
68
-
69
- with open(AGENT_LOOP, "w", encoding="utf-8") as f:
70
- f.write(content)
71
- print(f"OK: Patched {AGENT_LOOP}")
72
-
73
- if __name__ == "__main__":
74
- patch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue.py DELETED
@@ -1,129 +0,0 @@
1
- """Patch script: Add auto-continue feature for free models in ml-intern.
2
-
3
- Apply this script during Docker build to enable:
4
- 1. Detection when streaming ends but task incomplete
5
- 2. "Tự động tiếp tục (10s)" button - auto continues after 10s
6
- 3. "Tạm dừng" button - lets user send different content
7
-
8
- Usage: Run this during Docker build after cloning source.
9
- """
10
-
11
- import os
12
- import re
13
-
14
- # Paths - adjust if needed
15
- USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
16
- SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
17
- AGENT_LOOP = "/app/agent/core/agent_loop.py"
18
-
19
- # =============================================================================
20
- # Patch 1: useAgentChat.ts
21
- # =============================================================================
22
- def patch_use_agent_chat():
23
- if not os.path.exists(USE_AGENT_CHAT):
24
- print(f"SKIP: {USE_AGENT_CHAT} not found")
25
- return
26
-
27
- with open(USE_AGENT_CHAT, "r", encoding="utf-8") as f:
28
- content = f.read()
29
-
30
- # Add useState to imports if missing
31
- if "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';" not in content:
32
- content = content.replace(
33
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
34
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
35
- )
36
-
37
- # Add auto-continue state after callbacksRef
38
- state_code = '''
39
- // Auto-continue state for free models when task incomplete
40
- const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
41
- const [showAutoContinue, setShowAutoContinue] = useState(false);
42
- const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
43
- incompletePlan: Array<{ id: string; content: string; status: string }>;
44
- } | null>(null);
45
- '''
46
- if "autoContinueTimerRef" not in content and "callbacksRef.current = { onReady, onError, onSessionDead }" in content:
47
- content = content.replace(
48
- "callbacksRef.current = { onReady, onError, onSessionDead };",
49
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_code
50
- )
51
-
52
- # Add onTaskIncomplete to SideChannelCallbacks interface
53
- if "onTaskIncomplete" not in content:
54
- content = content.replace(
55
- "onInterrupted: () => void;",
56
- "onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
57
- )
58
-
59
- with open(USE_AGENT_CHAT, "w", encoding="utf-8") as f:
60
- f.write(content)
61
- print(f"OK: Patched {USE_AGENT_CHAT}")
62
-
63
- # =============================================================================
64
- # Patch 2: sse-chat-transport.ts
65
- # =============================================================================
66
- def patch_sse_transport():
67
- if not os.path.exists(SSE_TRANSPORT):
68
- print(f"SKIP: {SSE_TRANSPORT} not found")
69
- return
70
-
71
- with open(SSE_TRANSPORT, "r", encoding="utf-8") as f:
72
- content = f.read()
73
-
74
- # Add task_incomplete case
75
- if "case 'task_incomplete'" not in content:
76
- task_incomplete_case = ''' case 'task_incomplete':
77
- sideChannel.onTaskIncomplete(
78
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
79
- );
80
- break;
81
- '''
82
- # Insert before turn_complete case
83
- content = content.replace(
84
- "case 'turn_complete':",
85
- task_incomplete_case + "\n case 'turn_complete':"
86
- )
87
-
88
- with open(SSE_TRANSPORT, "w", encoding="utf-8") as f:
89
- f.write(content)
90
- print(f"OK: Patched {SSE_TRANSPORT}")
91
-
92
- # =============================================================================
93
- # Patch 3: agent_loop.py
94
- # =============================================================================
95
- def patch_agent_loop():
96
- if not os.path.exists(AGENT_LOOP):
97
- print(f"SKIP: {AGENT_LOOP} not found")
98
- return
99
-
100
- with open(AGENT_LOOP, "r", encoding="utf-8") as f:
101
- content = f.read()
102
-
103
- # Add check function if not exists
104
- check_func = '''
105
- def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
106
- """Check if model stopped but task is incomplete (has unfinished plan items)."""
107
- if llm_result.tool_calls_acc:
108
- return False # Has tool calls, not incomplete
109
- plan = getattr(session, "current_plan", None) or []
110
- unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
111
- return len(unfinished) > 0
112
- '''
113
- if "_check_task_incomplete" not in content:
114
- # Insert after LLMResult dataclass
115
- content = content.replace(
116
- "class Handlers:",
117
- check_func + "\n\nclass Handlers:"
118
- )
119
-
120
- with open(AGENT_LOOP, "w", encoding="utf-8") as f:
121
- f.write(content)
122
- print(f"OK: Patched {AGENT_LOOP}")
123
-
124
-
125
- if __name__ == "__main__":
126
- patch_use_agent_chat()
127
- patch_sse_transport()
128
- patch_agent_loop()
129
- print("\nDONE: Auto-continue patches applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v10.py DELETED
@@ -1,202 +0,0 @@
1
- """V10: Fixes scope issue with chat.sendMessage.
2
-
3
- Uses: ref to store plan, useEffect + chatActionsRef to send."""
4
- import os, re
5
-
6
- events_ts = "/source/frontend/src/types/events.ts"
7
- sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
8
- hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
9
- input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
10
- session_tsx = "/source/frontend/src/components/SessionChat.tsx"
11
- agent_py = "/app/agent/core/agent_loop.py"
12
-
13
- def patch_events():
14
- with open(events_ts) as f: c = f.read()
15
- if "'task_incomplete'" not in c:
16
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
17
- with open(events_ts, 'w') as f: f.write(c)
18
- print("OK: events.ts")
19
-
20
- def patch_sse():
21
- with open(sse_ts) as f: c = f.read()
22
- if "case 'task_incomplete'" not in c:
23
- case_block = """ case 'task_incomplete':
24
- (sideChannel as any).onTaskIncomplete(
25
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
26
- );
27
- break;
28
-
29
- default:"""
30
- c = c.replace("\n default:", "\n" + case_block)
31
- with open(sse_ts, 'w') as f: f.write(c)
32
- print("OK: sse.ts")
33
-
34
- def patch_hook():
35
- with open(hook_ts) as f:
36
- lines = f.readlines()
37
-
38
- # Find key insertion points by line number
39
- result = []
40
- state_added = False
41
- handler_added = False
42
- effect_added = False
43
- cancel_added = False
44
- return_added = False
45
-
46
- for i, line in enumerate(lines):
47
- # 1. Add useState import
48
- if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line and "useState" not in line:
49
- line = line.replace("useRef }", "useRef, useState }")
50
-
51
- # 2. Add state after callbacksRef
52
- if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_added:
53
- result.append(line)
54
- result.append(' // Auto-continue for free models\n')
55
- result.append(' const [_showAc, _setShowAc] = useState(false);\n')
56
- result.append(' const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n')
57
- state_added = True
58
- continue
59
-
60
- # 3. Add handler in sideChannel's onSessionUpdate
61
- if "onSessionUpdate: (data) => {" in line and not handler_added:
62
- result.append(line)
63
- result.append(' // Auto-continue: check for ac_plan\n')
64
- result.append(' if ((data as any).ac_plan) {\n')
65
- result.append(' _acRef.current = (data as any).ac_plan;\n')
66
- result.append(' _setShowAc(true);\n')
67
- result.append(' return;\n')
68
- result.append(' }\n')
69
- handler_added = True
70
- continue
71
-
72
- # 4. Add cancel function + effect before return
73
- stripped = line.strip()
74
- if stripped == "return {" and not cancel_added:
75
- result.append(' // Auto-continue cancel\n')
76
- result.append(' const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n')
77
- cancel_added = True
78
- # Add effect that uses chatActionsRef to send message
79
- result.append('\n')
80
- result.append(' // Auto-continue effect: 10s timer, sends via chatActionsRef\n')
81
- result.append(' useEffect(() => {\n')
82
- result.append(' if (!_showAc) return;\n')
83
- result.append(' const plan = _acRef.current;\n')
84
- result.append(' const timer = setTimeout(() => {\n')
85
- result.append(' const s = chatActionsRef.current.setMessages;\n')
86
- result.append(' const m = chatActionsRef.current.messages;\n')
87
- result.append(' if (s && plan.length > 0) {\n')
88
- result.append(" const t = plan.map(i => `- ${i.content}`).join('\\\\n');\n")
89
- result.append(" s([...m, { 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${t}` }], content: '' }]);\n")
90
- result.append(' }\n')
91
- result.append(' _setShowAc(false);\n')
92
- result.append(' }, 10000);\n')
93
- result.append(' return () => clearTimeout(timer);\n')
94
- result.append(' }, [_showAc]);\n')
95
- effect_added = True
96
-
97
- # 5. Return values
98
- if "refreshMessages," in line and not return_added:
99
- result.append(line)
100
- result.append(' _showAc,\n')
101
- result.append(' _acCancel,\n')
102
- return_added = True
103
- continue
104
-
105
- result.append(line)
106
-
107
- with open(hook_ts, 'w') as f: f.writelines(result)
108
- print(f"OK: useAgentChat.ts (added={state_added}/{handler_added}/{cancel_added}/{effect_added}/{return_added})")
109
-
110
- def patch_chat_input():
111
- with open(input_tsx) as f: c = f.read()
112
- if "Button" not in c:
113
- c = c.replace(" Tooltip,", " Tooltip,\n Button,")
114
-
115
- if "_showAc" not in c:
116
- old_iface = """interface ChatInputProps {
117
- sessionId?: string;
118
- initialModelPath?: string | null;
119
- onSend: (text: string) => void;
120
- onStop?: () => void;
121
- onDatasetUploaded?: () => Promise<boolean> | boolean;
122
- isProcessing?: boolean;
123
- disabled?: boolean;
124
- placeholder?: string;
125
- }"""
126
- new_iface = """interface ChatInputProps {
127
- sessionId?: string;
128
- initialModelPath?: string | null;
129
- onSend: (text: string) => void;
130
- onStop?: () => void;
131
- onDatasetUploaded?: () => Promise<boolean> | boolean;
132
- isProcessing?: boolean;
133
- disabled?: boolean;
134
- placeholder?: string;
135
- _showAc?: boolean;
136
- _acCancel?: () => void;
137
- }"""
138
- c = c.replace(old_iface, new_iface)
139
- old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
140
- new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
141
- c = c.replace(old_props, new_props)
142
-
143
- if "T\u1ea1m d\u1eebng" not in c:
144
- btn = """
145
- {_showAc && _acCancel && (
146
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
147
- <Button variant="outlined" size="small" onClick={_acCancel}
148
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
149
- T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
150
- </Button>
151
- </Box>
152
- )}
153
- """
154
- if "<JobsUpgradeDialog" in c:
155
- c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
156
-
157
- with open(input_tsx, 'w') as f: f.write(c)
158
- print("OK: ChatInput.tsx")
159
-
160
- def patch_session():
161
- if not os.path.exists(session_tsx):
162
- print(f"SKIP: {session_tsx}"); return
163
- with open(session_tsx) as f: c = f.read()
164
- if "_showAc" not in c:
165
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
166
- with open(session_tsx, 'w') as f: f.write(c)
167
- print("OK: SessionChat.tsx")
168
-
169
- def patch_agent():
170
- if not os.path.exists(agent_py):
171
- print(f"SKIP: {agent_py}"); return
172
- with open(agent_py) as f: c = f.read()
173
-
174
- if "_unfinished_plan" not in c:
175
- fn = """
176
- def _unfinished_plan(s: Session) -> list[dict[str, str]]:
177
- p = getattr(s, "current_plan", None) or []
178
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
179
- """
180
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
181
-
182
- if "ac_plan" not in c:
183
- check = """
184
- if not llm_result.tool_calls_acc and llm_result.content:
185
- unfinished = _unfinished_plan(session)
186
- if unfinished:
187
- current_ac = getattr(session, "auto_approval", None)
188
- await session.send_event(Event(event_type="session_update", data={
189
- "ac_plan": unfinished,
190
- "auto_approval": current_ac,
191
- }))
192
- """
193
- marker = " # -- End of turn --"
194
- if marker in c:
195
- c = c.replace(marker, check + "\n" + marker)
196
-
197
- with open(agent_py, 'w') as f: f.write(c)
198
- print("OK: agent_loop.py")
199
-
200
- if __name__ == "__main__":
201
- patch_events(); patch_sse(); patch_hook(); patch_chat_input(); patch_session(); patch_agent()
202
- print("\nDONE: V10")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v11.py DELETED
@@ -1,185 +0,0 @@
1
- """V11: Auto-continue feature - clean single-file patch.
2
-
3
- Backend: agent_loop.py detects model stopped mid-task, sends event.
4
- Frontend: useAgentChat listens via onSessionUpdate, shows pause button.
5
- All TS errors avoided via (as any) casts where needed.
6
- """
7
- import os
8
-
9
- ROOT = "/source/frontend/src"
10
- EVENTS = f"{ROOT}/types/events.ts"
11
- SSE = f"{ROOT}/lib/sse-chat-transport.ts"
12
- HOOK = f"{ROOT}/hooks/useAgentChat.ts"
13
- INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
14
- SESS = f"{ROOT}/components/SessionChat.tsx"
15
- AGENT_LOOP = "/app/agent/core/agent_loop.py"
16
-
17
- def p(f, s):
18
- print(f"OK: {f}" if s else f"FAIL: {f}")
19
-
20
- # --- PATCH 1: events.ts ---
21
- with open(EVENTS) as f: c = f.read()
22
- r1 = "'task_incomplete'" not in c
23
- if r1:
24
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
25
- with open(EVENTS, 'w') as f: f.write(c)
26
- p("events.ts", r1)
27
-
28
- # --- PATCH 2: sse-chat-transport.ts ---
29
- with open(SSE) as f: c = f.read()
30
- r2 = "case 'task_incomplete'" not in c
31
- if r2:
32
- block = """ case 'task_incomplete':
33
- (sideChannel as any).onTaskIncomplete(
34
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
35
- );
36
- break;
37
-
38
- default:"""
39
- c = c.replace("\n default:", "\n" + block)
40
- with open(SSE, 'w') as f: f.write(c)
41
- p("sse.ts", r2)
42
-
43
- # --- PATCH 3: useAgentChat.ts ---
44
- with open(HOOK) as f: lines = f.readlines()
45
- r3 = True
46
- new_lines = []
47
- state_done = False
48
- handler_done = False
49
- efx_done = False
50
- ret_done = False
51
-
52
- for line in lines:
53
- if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line:
54
- line = line.replace("useRef }", "useRef, useState }")
55
-
56
- if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_done:
57
- new_lines.append(line)
58
- new_lines.append(" // Auto-continue state\n")
59
- new_lines.append(" const [_showAc, _setShowAc] = useState(false);\n")
60
- new_lines.append(" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
61
- state_done = True
62
- continue
63
-
64
- if "onSessionUpdate: (data) => {" in line and not handler_done:
65
- new_lines.append(line)
66
- new_lines.append(" // Auto-continue check\n")
67
- new_lines.append(" if ((data as any).ac_plan) {\n")
68
- new_lines.append(" _acRef.current = (data as any).ac_plan;\n")
69
- new_lines.append(" _setShowAc(true);\n")
70
- new_lines.append(" return;\n")
71
- new_lines.append(" }\n")
72
- handler_done = True
73
- continue
74
-
75
- stripped = line.strip()
76
- if stripped == "return {" and not efx_done:
77
- new_lines.append(" const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n")
78
- new_lines.append(" useEffect(() => {\n")
79
- new_lines.append(" if (!_showAc) return;\n")
80
- new_lines.append(" const plan = _acRef.current;\n")
81
- new_lines.append(" const timer = setTimeout(() => {\n")
82
- new_lines.append(" const s = chatActionsRef.current.setMessages;\n")
83
- new_lines.append(" const m = chatActionsRef.current.messages;\n")
84
- new_lines.append(" if (s && plan.length > 0) {\n")
85
- new_lines.append(" const t = plan.map(i => `- ${i.content}`).join('\\n');\n")
86
- new_lines.append(" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIEP TUC] Task chua hoan thanh:\\n${t}` }], content: '' };\n")
87
- new_lines.append(" s([...m, nu]);\n")
88
- new_lines.append(" }\n")
89
- new_lines.append(" _setShowAc(false);\n")
90
- new_lines.append(" }, 10000);\n")
91
- new_lines.append(" return () => clearTimeout(timer);\n")
92
- new_lines.append(" }, [_showAc]);\n")
93
- efx_done = True
94
-
95
- if "refreshMessages," in line and not ret_done:
96
- new_lines.append(line)
97
- new_lines.append(" _showAc,\n")
98
- new_lines.append(" _acCancel,\n")
99
- ret_done = True
100
- continue
101
-
102
- new_lines.append(line)
103
-
104
- if not all([state_done, handler_done, efx_done, ret_done]):
105
- r3 = False
106
- print(f" MISSING: state={state_done} handler={handler_done} effect={efx_done} ret={ret_done}")
107
- else:
108
- with open(HOOK, 'w') as f: f.writelines(new_lines)
109
- p("useAgentChat.ts", r3)
110
-
111
- # --- PATCH 4: ChatInput.tsx ---
112
- with open(INPUT) as f: c = f.read()
113
- r4 = True
114
- if "Button" not in c:
115
- c = c.replace(" Tooltip,", " Tooltip,\n Button,")
116
-
117
- if "_showAc" not in c:
118
- old = "placeholder?: string;"
119
- new = "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
120
- c = c.replace(old, new)
121
-
122
- old2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
123
- new2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
124
- c = c.replace(old2, new2)
125
-
126
- btn = """
127
- {_showAc && _acCancel && (
128
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
129
- <Button variant="outlined" size="small" onClick={_acCancel}
130
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
131
- Tam dung tu dong tiep tuc (10s)
132
- </Button>
133
- </Box>
134
- )}
135
- """
136
- c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
137
- else:
138
- r4 = False
139
-
140
- with open(INPUT, 'w') as f: f.write(c)
141
- p("ChatInput.tsx", r4)
142
-
143
- # --- PATCH 5: SessionChat.tsx ---
144
- if os.path.exists(SESS):
145
- with open(SESS) as f: c = f.read()
146
- r5 = "_showAc" not in c
147
- if r5:
148
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
149
- with open(SESS, 'w') as f: f.write(c)
150
- p("SessionChat.tsx", r5)
151
- else:
152
- print("SKIP: SessionChat.tsx")
153
-
154
- # --- PATCH 6: agent_loop.py ---
155
- if os.path.exists(AGENT_LOOP):
156
- with open(AGENT_LOOP) as f: c = f.read()
157
- r6 = True
158
- if "_unfinished_plan" not in c:
159
- fn = """
160
- def _unfinished_plan(s):
161
- p = getattr(s, "current_plan", None) or []
162
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
163
- """
164
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
165
-
166
- if "ac_plan" not in c:
167
- check = """
168
- if not llm_result.tool_calls_acc and llm_result.content:
169
- unfinished = _unfinished_plan(session)
170
- if unfinished:
171
- await session.send_event(Event(event_type="session_update", data={
172
- "ac_plan": unfinished,
173
- }))
174
- """
175
- marker = " # -- End of turn --"
176
- c = c.replace(marker, check + "\n" + marker)
177
- else:
178
- r6 = False
179
-
180
- with open(AGENT_LOOP, 'w') as f: f.write(c)
181
- p("agent_loop.py", r6)
182
- else:
183
- print("SKIP: agent_loop.py")
184
-
185
- print("\nDONE: V11")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v12.py DELETED
@@ -1,209 +0,0 @@
1
- """V12: Auto-continue feature - final fix.
2
-
3
- Key fixes for V11 bugs:
4
- 1. Button import: check exact MUI import format
5
- 2. useAgentChat: match two-line `return {\n messages: chat.messages,` instead of bare `return {`
6
- 3. Use \u unicode escapes for Vietnamese text
7
- """
8
-
9
- import os
10
-
11
- ROOT = "/source/frontend/src"
12
- EVENTS = f"{ROOT}/types/events.ts"
13
- SSE = f"{ROOT}/lib/sse-chat-transport.ts"
14
- HOOK = f"{ROOT}/hooks/useAgentChat.ts"
15
- INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
16
- SESS = f"{ROOT}/components/SessionChat.tsx"
17
- AGENT = "/app/agent/core/agent_loop.py"
18
-
19
- def ok(f):
20
- print(f"OK: {f}")
21
-
22
- # --- 1. events.ts: add task_incomplete ---
23
- with open(EVENTS) as f:
24
- c = f.read()
25
- if "'task_incomplete'" not in c:
26
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
27
- with open(EVENTS, 'w') as f:
28
- f.write(c)
29
- ok("events.ts")
30
-
31
- # --- 2. sse-chat-transport.ts: add case handler ---
32
- with open(SSE) as f:
33
- c = f.read()
34
- if "case 'task_incomplete'" not in c:
35
- block = """ case 'task_incomplete':
36
- (sideChannel as any).onTaskIncomplete(
37
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
38
- );
39
- break;
40
-
41
- default:"""
42
- c = c.replace("\n default:", "\n" + block)
43
- with open(SSE, 'w') as f:
44
- f.write(c)
45
- ok("sse.ts")
46
-
47
- # --- 3. useAgentChat.ts: add state, handler, effect, return ---
48
- with open(HOOK) as f:
49
- content = f.read()
50
-
51
- # Add useState to import
52
- if "useState" not in content:
53
- content = content.replace(
54
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
55
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
56
- )
57
-
58
- # Add state after callbacksRef line
59
- state_block = """\
60
- // Auto-continue state
61
- const [_showAc, _setShowAc] = useState(false);
62
- const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);
63
- """
64
- if "_showAc" not in content:
65
- content = content.replace(
66
- "callbacksRef.current = { onReady, onError, onSessionDead };",
67
- "callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_block
68
- )
69
-
70
- # Add handler inside onSessionUpdate
71
- handler_code = """\
72
- // Auto-continue check
73
- if ((data as any).ac_plan) {
74
- _acRef.current = (data as any).ac_plan;
75
- _setShowAc(true);
76
- return;
77
- }
78
- """
79
- if "ac_plan" not in content:
80
- content = content.replace(
81
- "onSessionUpdate: (data) => {",
82
- "onSessionUpdate: (data) => {\n" + handler_code
83
- )
84
-
85
- # Add cancel + effect BEFORE the two-line return marker
86
- cancel_effect = """\
87
- const _acCancel = useCallback(() => { _setShowAc(false); }, []);
88
- useEffect(() => {
89
- if (!_showAc) return;
90
- const plan = _acRef.current;
91
- const timer = setTimeout(() => {
92
- const s = chatActionsRef.current.setMessages;
93
- const m = chatActionsRef.current.messages;
94
- if (s && plan.length > 0) {
95
- const t = plan.map((i: { content: string }) => `- ${i.content}`).join('\\n');
96
- const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIEP TUC] Task chua hoan thanh:\\n${t}` }], content: '' };
97
- s([...m, nu]);
98
- }
99
- _setShowAc(false);
100
- }, 10000);
101
- return () => clearTimeout(timer);
102
- }, [_showAc]);
103
-
104
- """
105
- if "_acCancel" not in content:
106
- # Match the EXACT two-line return pattern
107
- content = content.replace(
108
- " return {\n messages: chat.messages,",
109
- cancel_effect + " return {\n messages: chat.messages,"
110
- )
111
-
112
- # Add return values
113
- if "_showAc," not in content:
114
- content = content.replace(
115
- "refreshMessages,\n };",
116
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
117
- )
118
-
119
- with open(HOOK, 'w') as f:
120
- f.write(content)
121
- ok("useAgentChat.ts")
122
-
123
- # --- 4. ChatInput.tsx: add Button + props + pause button ---
124
- with open(INPUT) as f:
125
- content = f.read()
126
-
127
- # Add Button to MUI imports - match the actual format
128
- if "Button" not in content:
129
- content = content.replace(
130
- " Tooltip,",
131
- " Tooltip,\n Button,"
132
- )
133
-
134
- # Add props to interface
135
- if "_showAc" not in content:
136
- content = content.replace(
137
- "placeholder?: string;",
138
- "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
139
- )
140
-
141
- # Add to destructured function params
142
- if "_showAc" not in content or "_acCancel" not in content:
143
- content = content.replace(
144
- "placeholder = 'Ask anything...' }: ChatInputProps) {",
145
- "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
146
- )
147
-
148
- # Add pause button before JobsUpgradeDialog
149
- if "Tam dung" not in content:
150
- btn = """
151
- {_showAc && _acCancel && (
152
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
153
- <Button variant="outlined" size="small" onClick={_acCancel}
154
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
155
- T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
156
- </Button>
157
- </Box>
158
- )}
159
- """
160
- content = content.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
161
-
162
- with open(INPUT, 'w') as f:
163
- f.write(content)
164
- ok("ChatInput.tsx")
165
-
166
- # --- 5. SessionChat.tsx: pass props ---
167
- if os.path.exists(SESS):
168
- with open(SESS) as f:
169
- c = f.read()
170
- if "_showAc" not in c:
171
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
172
- with open(SESS, 'w') as f:
173
- f.write(c)
174
- ok("SessionChat.tsx")
175
-
176
- # --- 6. agent_loop.py: detection ---
177
- if os.path.exists(AGENT):
178
- with open(AGENT) as f:
179
- c = f.read()
180
-
181
- if "_unfinished_plan" not in c:
182
- fn = """
183
-
184
- def _unfinished_plan(s):
185
- p = getattr(s, "current_plan", None) or []
186
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
187
- """
188
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
189
-
190
- if "ac_plan" not in c:
191
- check = """
192
- if not llm_result.tool_calls_acc and llm_result.content:
193
- unfinished = _unfinished_plan(session)
194
- if unfinished:
195
- await session.send_event(Event(event_type="session_update", data={
196
- "ac_plan": unfinished,
197
- }))
198
- """
199
- marker = " # -- End of turn --"
200
- if marker in c:
201
- c = c.replace(marker, check + marker)
202
-
203
- with open(AGENT, 'w') as f:
204
- f.write(c)
205
- ok("agent_loop.py")
206
- else:
207
- print("SKIP: agent_loop.py (backend)")
208
-
209
- print("\nDONE: V12")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v13.py DELETED
@@ -1,197 +0,0 @@
1
- """V13: Auto-continue feature - fixed SyntaxError, no raw \u in Python source"""
2
-
3
- import os
4
-
5
- ROOT = "/source/frontend/src"
6
- EVENTS = f"{ROOT}/types/events.ts"
7
- SSE = f"{ROOT}/lib/sse-chat-transport.ts"
8
- HOOK = f"{ROOT}/hooks/useAgentChat.ts"
9
- INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
10
- SESS = f"{ROOT}/components/SessionChat.tsx"
11
- AGENT = "/app/agent/core/agent_loop.py"
12
-
13
- # Vietnamese text as hex to avoid \u issues in Python source
14
- PAUSE_TEXT = "T\\u1ea1m d\\u1eebng t\\u1ef1 \\u0111\\u1ed9ng ti\\u1ebfp t\\u1ee5c (10s)"
15
- CONTINUE_TEXT = "[TIEP TUC] Task chua hoan thanh:"
16
-
17
- def ok(f):
18
- print(f"OK: {f}")
19
-
20
- # --- 1. events.ts ---
21
- with open(EVENTS) as f:
22
- c = f.read()
23
- if "'task_incomplete'" not in c:
24
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
25
- with open(EVENTS, 'w') as f:
26
- f.write(c)
27
- ok("events.ts")
28
-
29
- # --- 2. sse.ts ---
30
- with open(SSE) as f:
31
- c = f.read()
32
- if "case 'task_incomplete'" not in c:
33
- block = """ case 'task_incomplete':
34
- (sideChannel as any).onTaskIncomplete(
35
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
36
- );
37
- break;
38
-
39
- default:"""
40
- c = c.replace("\n default:", "\n" + block)
41
- with open(SSE, 'w') as f:
42
- f.write(c)
43
- ok("sse.ts")
44
-
45
- # --- 3. useAgentChat.ts ---
46
- with open(HOOK) as f:
47
- content = f.read()
48
-
49
- if "useState" not in content:
50
- content = content.replace(
51
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
52
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
53
- )
54
-
55
- state_block = """\
56
- // Auto-continue state
57
- const [_showAc, _setShowAc] = useState(false);
58
- const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);
59
- """
60
- if "_showAc" not in content:
61
- content = content.replace(
62
- "callbacksRef.current = { onReady, onError, onSessionDead };",
63
- "callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_block
64
- )
65
-
66
- handler_code = """\
67
- // Auto-continue check
68
- if ((data as any).ac_plan) {
69
- _acRef.current = (data as any).ac_plan;
70
- _setShowAc(true);
71
- return;
72
- }
73
- """
74
- if "ac_plan" not in content:
75
- content = content.replace(
76
- "onSessionUpdate: (data) => {",
77
- "onSessionUpdate: (data) => {\n" + handler_code
78
- )
79
-
80
- cancel_effect = """\
81
- const _acCancel = useCallback(() => { _setShowAc(false); }, []);
82
- useEffect(() => {
83
- if (!_showAc) return;
84
- const plan = _acRef.current;
85
- const timer = setTimeout(() => {
86
- const s = chatActionsRef.current.setMessages;
87
- const m = chatActionsRef.current.messages;
88
- if (s && plan.length > 0) {
89
- const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');
90
- const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '""" + CONTINUE_TEXT + "\\\\n' + t }], content: '' };\n"
91
- cancel_effect += """\
92
- s([...m, nu]);
93
- }
94
- _setShowAc(false);
95
- }, 10000);
96
- return () => clearTimeout(timer);
97
- }, [_showAc]);
98
-
99
- """
100
- if "_acCancel" not in content:
101
- content = content.replace(
102
- " return {\n messages: chat.messages,",
103
- cancel_effect + " return {\n messages: chat.messages,"
104
- )
105
-
106
- if "_showAc," not in content:
107
- content = content.replace(
108
- "refreshMessages,\n };",
109
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
110
- )
111
-
112
- with open(HOOK, 'w') as f:
113
- f.write(content)
114
- ok("useAgentChat.ts")
115
-
116
- # --- 4. ChatInput.tsx ---
117
- with open(INPUT) as f:
118
- content = f.read()
119
-
120
- if "Button" not in content:
121
- content = content.replace(
122
- " Tooltip,",
123
- " Tooltip,\n Button,"
124
- )
125
-
126
- if "_showAc" not in content:
127
- content = content.replace(
128
- "placeholder?: string;",
129
- "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
130
- )
131
- content = content.replace(
132
- "placeholder = 'Ask anything...' }: ChatInputProps) {",
133
- "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
134
- )
135
-
136
- pause_marker = "TAM_DUNG"
137
- if pause_marker not in content:
138
- btn = """
139
- {_showAc && _acCancel && (
140
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
141
- <Button variant="outlined" size="small" onClick={_acCancel}
142
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
143
- """ + PAUSE_TEXT + """
144
- </Button>
145
- </Box>
146
- )}
147
- """
148
- content = content.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
149
-
150
- with open(INPUT, 'w') as f:
151
- f.write(content)
152
- ok("ChatInput.tsx")
153
-
154
- # --- 5. SessionChat.tsx ---
155
- if os.path.exists(SESS):
156
- with open(SESS) as f:
157
- c = f.read()
158
- if "_showAc" not in c:
159
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
160
- with open(SESS, 'w') as f:
161
- f.write(c)
162
- ok("SessionChat.tsx")
163
-
164
- # --- 6. agent_loop.py ---
165
- if os.path.exists(AGENT):
166
- with open(AGENT) as f:
167
- c = f.read()
168
-
169
- if "_unfinished_plan" not in c:
170
- fn = """
171
-
172
- def _unfinished_plan(s):
173
- p = getattr(s, "current_plan", None) or []
174
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
175
- """
176
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
177
-
178
- if "ac_plan" not in c:
179
- check = """
180
- if not llm_result.tool_calls_acc and llm_result.content:
181
- unfinished = _unfinished_plan(session)
182
- if unfinished:
183
- await session.send_event(Event(event_type="session_update", data={
184
- "ac_plan": unfinished,
185
- }))
186
- """
187
- marker = " # -- End of turn --"
188
- if marker in c:
189
- c = c.replace(marker, check + marker)
190
-
191
- with open(AGENT, 'w') as f:
192
- f.write(c)
193
- ok("agent_loop.py")
194
- else:
195
- print("SKIP: agent_loop.py")
196
-
197
- print("\nDONE: V13")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v14.py DELETED
@@ -1,166 +0,0 @@
1
- """V14: Auto-continue. Use ASCII-only text for reliable build."""
2
-
3
- import os, sys
4
-
5
- ROOT = "/source/frontend/src"
6
-
7
- FILES = {
8
- "events.ts": ROOT + "/types/events.ts",
9
- "sse.ts": ROOT + "/lib/sse-chat-transport.ts",
10
- "hook.ts": ROOT + "/hooks/useAgentChat.ts",
11
- "input.tsx": ROOT + "/components/Chat/ChatInput.tsx",
12
- "sess.tsx": ROOT + "/components/SessionChat.tsx",
13
- "agent.py": "/app/agent/core/agent_loop.py",
14
- }
15
-
16
- # 1. events.ts
17
- c = open(FILES["events.ts"]).read()
18
- if "'task_incomplete'" not in c:
19
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
20
- open(FILES["events.ts"], 'w').write(c)
21
- print("OK: events.ts")
22
-
23
- # 2. sse.ts
24
- c = open(FILES["sse.ts"]).read()
25
- if "case 'task_incomplete'" not in c:
26
- block = """ case 'task_incomplete':
27
- (sideChannel as any).onTaskIncomplete(
28
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
29
- );
30
- break;
31
-
32
- default:"""
33
- c = c.replace("\n default:", "\n" + block)
34
- open(FILES["sse.ts"], 'w').write(c)
35
- print("OK: sse.ts")
36
-
37
- # 3. hook.ts - useAgentChat.ts
38
- c = open(FILES["hook.ts"]).read()
39
-
40
- # useState import
41
- if "useState" not in c:
42
- c = c.replace(
43
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
44
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
45
- )
46
-
47
- # State after callbacksRef
48
- if "_showAc" not in c:
49
- state_block = (
50
- " // Auto-continue state\n"
51
- " const [_showAc, _setShowAc] = useState(false);\n"
52
- " const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n"
53
- )
54
- c = c.replace(
55
- "callbacksRef.current = { onReady, onError, onSessionDead };",
56
- "callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_block
57
- )
58
-
59
- # Handler in onSessionUpdate
60
- if "ac_plan" not in c:
61
- handler = (
62
- " // Auto-continue check\n"
63
- " if ((data as any).ac_plan) {\n"
64
- " _acRef.current = (data as any).ac_plan;\n"
65
- " _setShowAc(true);\n"
66
- " return;\n"
67
- " }\n"
68
- )
69
- c = c.replace("onSessionUpdate: (data) => {", "onSessionUpdate: (data) => {\n" + handler)
70
-
71
- # Cancel + effect before return
72
- if "_acCancel" not in c:
73
- cancel = " const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n"
74
- effect = (
75
- " useEffect(() => {\n"
76
- " if (!_showAc) return;\n"
77
- " const plan = _acRef.current;\n"
78
- " const timer = setTimeout(() => {\n"
79
- " const s = chatActionsRef.current.setMessages;\n"
80
- " const m = chatActionsRef.current.messages;\n"
81
- " if (s && plan.length > 0) {\n"
82
- " const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n"
83
- " const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n"
84
- " s([...m, nu]);\n"
85
- " }\n"
86
- " _setShowAc(false);\n"
87
- " }, 10000);\n"
88
- " return () => clearTimeout(timer);\n"
89
- " }, [_showAc]);\n\n"
90
- )
91
- c = c.replace(
92
- " return {\n messages: chat.messages,",
93
- cancel + effect + " return {\n messages: chat.messages,"
94
- )
95
-
96
- # Return values
97
- if "_showAc," not in c:
98
- c = c.replace("refreshMessages,\n };", "refreshMessages,\n _showAc,\n _acCancel,\n };")
99
-
100
- open(FILES["hook.ts"], 'w').write(c)
101
- print("OK: useAgentChat.ts")
102
-
103
- # 4. input.tsx - ChatInput.tsx
104
- c = open(FILES["input.tsx"]).read()
105
-
106
- if "Button" not in c:
107
- # Match the exact MUI import block
108
- c = c.replace("import {\n Alert,\n Box,\n TextField,\n IconButton,\n CircularProgress,\n Typography,\n Menu,\n MenuItem,\n ListItemIcon,\n ListItemText,\n Chip,\n LinearProgress,\n Snackbar,\n Tooltip,\n} from '@mui/material';",
109
- "import {\n Alert,\n Box,\n TextField,\n IconButton,\n CircularProgress,\n Typography,\n Menu,\n MenuItem,\n ListItemIcon,\n ListItemText,\n Chip,\n LinearProgress,\n Snackbar,\n Tooltip,\n Button,\n} from '@mui/material';")
110
-
111
- if "_showAc" not in c:
112
- c = c.replace("placeholder?: string;", "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;")
113
- c = c.replace("placeholder = 'Ask anything...' }: ChatInputProps) {",
114
- "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {")
115
-
116
- if "TAM_DUNG" not in c:
117
- btn = (
118
- " {_showAc && _acCancel && (\n"
119
- " <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
120
- " <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
121
- " sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
122
- " TAM_DUNG_TU_DONG_TIEP_TUC\n"
123
- " </Button>\n"
124
- " </Box>\n"
125
- " )}\n"
126
- )
127
- c = c.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
128
-
129
- open(FILES["input.tsx"], 'w').write(c)
130
- print("OK: ChatInput.tsx")
131
-
132
- # 5. sess.tsx - SessionChat.tsx
133
- if os.path.exists(FILES["sess.tsx"]):
134
- c = open(FILES["sess.tsx"]).read()
135
- if "_showAc" not in c:
136
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
137
- open(FILES["sess.tsx"], 'w').write(c)
138
- print("OK: SessionChat.tsx")
139
-
140
- # 6. agent.py
141
- if os.path.exists(FILES["agent.py"]):
142
- c = open(FILES["agent.py"]).read()
143
-
144
- if "_unfinished_plan" not in c:
145
- fn = "\n\ndef _unfinished_plan(s):\n p = getattr(s, 'current_plan', None) or []\n return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n"
146
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
147
-
148
- if "ac_plan" not in c:
149
- check = (
150
- "\n if not llm_result.tool_calls_acc and llm_result.content:\n"
151
- " unfinished = _unfinished_plan(session)\n"
152
- " if unfinished:\n"
153
- " await session.send_event(Event(event_type='session_update', data={\n"
154
- " 'ac_plan': unfinished,\n"
155
- " }))\n"
156
- )
157
- marker = " # -- End of turn --"
158
- if marker in c:
159
- c = c.replace(marker, check + marker)
160
-
161
- open(FILES["agent.py"], 'w').write(c)
162
- print("OK: agent_loop.py")
163
- else:
164
- print("SKIP: agent_loop.py")
165
-
166
- print("\nDONE: V14")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v15.py DELETED
@@ -1,167 +0,0 @@
1
- """V15: Auto-continue - fix Button import detection + return matching."""
2
-
3
- import os
4
-
5
- ROOT = "/source/frontend/src"
6
- EVENTS = ROOT + "/types/events.ts"
7
- SSE = ROOT + "/lib/sse-chat-transport.ts"
8
- HOOK = ROOT + "/hooks/useAgentChat.ts"
9
- INPUT = ROOT + "/components/Chat/ChatInput.tsx"
10
- SESS = ROOT + "/components/SessionChat.tsx"
11
- AGENT = "/app/agent/core/agent_loop.py"
12
-
13
- def ok(f): print(f"OK: {f}")
14
-
15
- # 1. events.ts
16
- with open(EVENTS) as f: c = f.read()
17
- if "'task_incomplete'" not in c:
18
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
19
- with open(EVENTS, 'w') as f: f.write(c)
20
- ok("events.ts")
21
-
22
- # 2. sse.ts
23
- with open(SSE) as f: c = f.read()
24
- if "case 'task_incomplete'" not in c:
25
- block = """ case 'task_incomplete':
26
- (sideChannel as any).onTaskIncomplete(
27
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
28
- );
29
- break;
30
-
31
- default:"""
32
- c = c.replace("\n default:", "\n" + block)
33
- with open(SSE, 'w') as f: f.write(c)
34
- ok("sse.ts")
35
-
36
- # 3. useAgentChat.ts
37
- with open(HOOK) as f:
38
- lines = f.readlines()
39
-
40
- new_lines = []
41
- state_ok = hdl_ok = efx_ok = ret_ok = False
42
-
43
- for line in lines:
44
- # Import useState
45
- if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line:
46
- line = line.replace("useRef }", "useRef, useState }")
47
-
48
- # State after callbacksRef
49
- if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_ok:
50
- new_lines.append(line)
51
- new_lines.append(" const [_showAc, _setShowAc] = useState(false);\n")
52
- new_lines.append(" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
53
- state_ok = True
54
- continue
55
-
56
- # Handler
57
- if "onSessionUpdate: (data) => {" in line and not hdl_ok:
58
- new_lines.append(line)
59
- new_lines.append(" if ((data as any).ac_plan) {\n")
60
- new_lines.append(" _acRef.current = (data as any).ac_plan;\n")
61
- new_lines.append(" _setShowAc(true);\n")
62
- new_lines.append(" return;\n")
63
- new_lines.append(" }\n")
64
- hdl_ok = True
65
- continue
66
-
67
- # Cancel + effect BEFORE 'return {'
68
- stripped = line.strip()
69
- if stripped == "return {" and not efx_ok:
70
- new_lines.append(" const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n")
71
- new_lines.append(" useEffect(() => {\n")
72
- new_lines.append(" if (!_showAc) return;\n")
73
- new_lines.append(" const plan = _acRef.current;\n")
74
- new_lines.append(" const timer = setTimeout(() => {\n")
75
- new_lines.append(" const s = chatActionsRef.current.setMessages;\n")
76
- new_lines.append(" const m = chatActionsRef.current.messages;\n")
77
- new_lines.append(" if (s && plan.length > 0) {\n")
78
- new_lines.append(" const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n")
79
- new_lines.append(" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n")
80
- new_lines.append(" s([...m, nu]);\n")
81
- new_lines.append(" }\n")
82
- new_lines.append(" _setShowAc(false);\n")
83
- new_lines.append(" }, 10000);\n")
84
- new_lines.append(" return () => clearTimeout(timer);\n")
85
- new_lines.append(" }, [_showAc]);\n\n")
86
- efx_ok = True
87
-
88
- # Add return values after "refreshMessages,"
89
- if "refreshMessages," in line and not ret_ok:
90
- new_lines.append(line)
91
- new_lines.append(" _showAc,\n")
92
- new_lines.append(" _acCancel,\n")
93
- ret_ok = True
94
- continue
95
-
96
- new_lines.append(line)
97
-
98
- with open(HOOK, 'w') as f:
99
- f.writelines(new_lines)
100
- ok(f"useAgentChat.ts (s={state_ok} h={hdl_ok} e={efx_ok} r={ret_ok})")
101
-
102
- # 4. ChatInput.tsx
103
- with open(INPUT) as f:
104
- content = f.read()
105
-
106
- # Check for exact Button in MUI import (not "IconButton" which contains "Button")
107
- if " Button,\n" not in content and " Button\n" not in content:
108
- content = content.replace(
109
- " Tooltip,",
110
- " Tooltip,\n Button,"
111
- )
112
-
113
- # Add props to interface
114
- if "_showAc" not in content:
115
- content = content.replace("placeholder?: string;", "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;")
116
- content = content.replace("placeholder = 'Ask anything...' }: ChatInputProps) {",
117
- "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {")
118
-
119
- # Add pause button before <JobsUpgradeDialog
120
- btn = (
121
- " {_showAc && _acCancel && (\n"
122
- " <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
123
- " <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
124
- " sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
125
- " TAM_DUNG_TU_DONG_TIEP_TUC (10s)\n"
126
- " </Button>\n"
127
- " </Box>\n"
128
- " )}\n"
129
- )
130
- content = content.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
131
-
132
- with open(INPUT, 'w') as f:
133
- f.write(content)
134
- ok("ChatInput.tsx")
135
-
136
- # 5. SessionChat.tsx
137
- if os.path.exists(SESS):
138
- with open(SESS) as f: c = f.read()
139
- if "_showAc" not in c:
140
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
141
- with open(SESS, 'w') as f: f.write(c)
142
- ok("SessionChat.tsx")
143
-
144
- # 6. agent_loop.py
145
- if os.path.exists(AGENT):
146
- with open(AGENT) as f: c = f.read()
147
- if "_unfinished_plan" not in c:
148
- fn = "\n\ndef _unfinished_plan(s):\n p = getattr(s, 'current_plan', None) or []\n return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n"
149
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
150
- if "ac_plan" not in c:
151
- check = (
152
- "\n if not llm_result.tool_calls_acc and llm_result.content:\n"
153
- " unfinished = _unfinished_plan(session)\n"
154
- " if unfinished:\n"
155
- " await session.send_event(Event(event_type='session_update', data={\n"
156
- " 'ac_plan': unfinished,\n"
157
- " }))\n"
158
- )
159
- marker = " # -- End of turn --"
160
- if marker in c:
161
- c = c.replace(marker, check + marker)
162
- with open(AGENT, 'w') as f: f.write(c)
163
- ok("agent_loop.py")
164
- else:
165
- print("SKIP: agent_loop.py")
166
-
167
- print("\nDONE: V15")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v16.py DELETED
@@ -1,142 +0,0 @@
1
- """V16: Auto-continue - match exact 'return {\n messages: chat.messages,' (only one in file)"""
2
-
3
- import os, sys
4
-
5
- F = lambda n: "/source/frontend/src/" + n
6
- EVENTS = F("types/events.ts")
7
- SSE = F("lib/sse-chat-transport.ts")
8
- HOOK = F("hooks/useAgentChat.ts")
9
- INPUT = F("components/Chat/ChatInput.tsx")
10
- SESS = F("components/SessionChat.tsx")
11
- AGENT = "/app/agent/core/agent_loop.py"
12
-
13
- def ok(f): print(f"OK: {f}")
14
-
15
- # --- events.ts ---
16
- with open(EVENTS) as f: c = f.read()
17
- if "'task_incomplete'" not in c:
18
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
19
- open(EVENTS, 'w').write(c)
20
- ok("events.ts")
21
-
22
- # --- sse.ts ---
23
- with open(SSE) as f: c = f.read()
24
- if "case 'task_incomplete'" not in c:
25
- c = c.replace("\n default:", "\n case 'task_incomplete':\n (sideChannel as any).onTaskIncomplete(\n (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],\n );\n break;\n\n default:")
26
- open(SSE, 'w').write(c)
27
- ok("sse.ts")
28
-
29
- # --- useAgentChat.ts ---
30
- with open(HOOK) as f: content = f.read()
31
-
32
- # useState import
33
- if "useState" not in content:
34
- content = content.replace("useRef }", "useRef, useState }")
35
-
36
- # State after callbacksRef (insert once)
37
- if "_showAc" not in content:
38
- s = (" // Auto-continue state\n"
39
- " const [_showAc, _setShowAc] = useState(false);\n"
40
- " const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
41
- content = content.replace(
42
- "callbacksRef.current = { onReady, onError, onSessionDead };",
43
- "callbacksRef.current = { onReady, onError, onSessionDead };\n" + s
44
- )
45
-
46
- # Handler in onSessionUpdate
47
- if "ac_plan" not in content:
48
- h = (" // Auto-continue check\n"
49
- " if ((data as any).ac_plan) {\n"
50
- " _acRef.current = (data as any).ac_plan;\n"
51
- " _setShowAc(true);\n"
52
- " return;\n"
53
- " }\n")
54
- content = content.replace("onSessionUpdate: (data) => {", "onSessionUpdate: (data) => {\n" + h)
55
-
56
- # CRITICAL: Only match the exact two-line return that has messages: chat.messages
57
- # Only ONE place in the file has this exact pattern
58
- marker = " return {\n messages: chat.messages,"
59
- if marker in content and "_acCancel" not in content:
60
- cancel_effect = (
61
- " const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n"
62
- " useEffect(() => {\n"
63
- " if (!_showAc) return;\n"
64
- " const plan = _acRef.current;\n"
65
- " const timer = setTimeout(() => {\n"
66
- " const s = chatActionsRef.current.setMessages;\n"
67
- " const m = chatActionsRef.current.messages;\n"
68
- " if (s && plan.length > 0) {\n"
69
- " const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n"
70
- " const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n"
71
- " s([...m, nu]);\n"
72
- " }\n"
73
- " _setShowAc(false);\n"
74
- " }, 10000);\n"
75
- " return () => clearTimeout(timer);\n"
76
- " }, [_showAc]);\n\n"
77
- )
78
- content = content.replace(marker, cancel_effect + marker)
79
-
80
- # Add to return values
81
- content = content.replace(
82
- "refreshMessages,\n };",
83
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
84
- )
85
-
86
- with open(HOOK, 'w').write(content)
87
- ok("useAgentChat.ts")
88
-
89
- # --- ChatInput.tsx ---
90
- with open(INPUT) as f: content = f.read()
91
-
92
- # Button: check for standalone ", Button" or " Button" NOT "IconButton"
93
- if ", Button,\n" not in content and ", Button\n" not in content:
94
- content = content.replace(
95
- " Tooltip,",
96
- " Tooltip,\n Button,"
97
- )
98
-
99
- if "_showAc" not in content:
100
- content = content.replace("placeholder?: string;", "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;")
101
- content = content.replace("placeholder = 'Ask anything...' }: ChatInputProps) {",
102
- "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {")
103
- btn = (" {_showAc && _acCancel && (\n"
104
- " <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
105
- " <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
106
- " sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
107
- " TAM_DUNG_TU_DONG_TIEP_TUC (10s)\n"
108
- " </Button>\n"
109
- " </Box>\n"
110
- " )}\n")
111
- content = content.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
112
-
113
- open(INPUT, 'w').write((content).read() if hasattr(content, 'read') else content)
114
- ok("ChatInput.tsx")
115
-
116
- # --- SessionChat.tsx ---
117
- if os.path.exists(SESS):
118
- with open(SESS) as f: c = f.read()
119
- if "_showAc" not in c:
120
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
121
- open(SESS, 'w').write(c)
122
- ok("SessionChat.tsx")
123
-
124
- # --- agent_loop.py ---
125
- if os.path.exists(AGENT):
126
- with open(AGENT) as f: c = f.read()
127
- if "_unfinished_plan" not in c:
128
- c = c.replace("class Handlers:", "\n\ndef _unfinished_plan(s):\n p = getattr(s, 'current_plan', None) or []\n return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n\nclass Handlers:")
129
- if "ac_plan" not in c:
130
- check = ("\n if not llm_result.tool_calls_acc and llm_result.content:\n"
131
- " unfinished = _unfinished_plan(session)\n"
132
- " if unfinished:\n"
133
- " await session.send_event(Event(event_type='session_update', data={\n"
134
- " 'ac_plan': unfinished,\n"
135
- " }))\n")
136
- c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
137
- open(AGENT, 'w').write(c)
138
- ok("agent_loop.py")
139
- else:
140
- print("SKIP: agent_loop.py")
141
-
142
- print("\nDONE: V16")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v17.py DELETED
@@ -1,187 +0,0 @@
1
- """V17: Auto-continue - perfect Python, exact marker matching"""
2
-
3
- import os
4
-
5
- EVENTS = "/source/frontend/src/types/events.ts"
6
- SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
7
- HOOK = "/source/frontend/src/hooks/useAgentChat.ts"
8
- INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
9
- SESS = "/source/frontend/src/components/SessionChat.tsx"
10
- AGENT = "/app/agent/core/agent_loop.py"
11
-
12
- print("Patching auto-continue V17...")
13
-
14
- # --- events.ts ---
15
- with open(EVENTS) as f:
16
- c = f.read()
17
- if "'task_incomplete'" not in c:
18
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
19
- with open(EVENTS, 'w') as f:
20
- f.write(c)
21
- print("OK: events.ts")
22
-
23
- # --- sse.ts ---
24
- with open(SSE) as f:
25
- c = f.read()
26
- if "case 'task_incomplete'" not in c:
27
- block = (
28
- " case 'task_incomplete':\n"
29
- " (sideChannel as any).onTaskIncomplete(\n"
30
- " (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],\n"
31
- " );\n"
32
- " break;\n\n"
33
- " default:"
34
- )
35
- c = c.replace("\n default:", "\n" + block)
36
- with open(SSE, 'w') as f:
37
- f.write(c)
38
- print("OK: sse.ts")
39
-
40
- # --- useAgentChat.ts ---
41
- with open(HOOK) as f:
42
- content = f.read()
43
-
44
- # Add useState import
45
- if "useState" not in content:
46
- content = content.replace(
47
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
48
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
49
- )
50
-
51
- # Add state after callbacksRef
52
- if "_showAc" not in content:
53
- state_code = (
54
- " // Auto-continue state\n"
55
- " const [_showAc, _setShowAc] = useState(false);\n"
56
- " const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n"
57
- )
58
- content = content.replace(
59
- "callbacksRef.current = { onReady, onError, onSessionDead };",
60
- "callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_code
61
- )
62
-
63
- # Add handler in onSessionUpdate
64
- if "ac_plan" not in content:
65
- h = (
66
- " // Auto-continue check\n"
67
- " if ((data as any).ac_plan) {\n"
68
- " _acRef.current = (data as any).ac_plan;\n"
69
- " _setShowAc(true);\n"
70
- " return;\n"
71
- " }\n"
72
- )
73
- content = content.replace("onSessionUpdate: (data) => {", "onSessionUpdate: (data) => {\n" + h)
74
-
75
- # ONLY match the unique two-line pattern: " return {\n messages: chat.messages,"
76
- UNIQUE_MARKER = " return {\n messages: chat.messages,"
77
- if UNIQUE_MARKER in content and "_acCancel" not in content:
78
- insert = (
79
- " const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n"
80
- " useEffect(() => {\n"
81
- " if (!_showAc) return;\n"
82
- " const plan = _acRef.current;\n"
83
- " const timer = setTimeout(() => {\n"
84
- " const s = chatActionsRef.current.setMessages;\n"
85
- " const m = chatActionsRef.current.messages;\n"
86
- " if (s && plan.length > 0) {\n"
87
- " const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n"
88
- " const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n"
89
- " s([...m, nu]);\n"
90
- " }\n"
91
- " _setShowAc(false);\n"
92
- " }, 10000);\n"
93
- " return () => clearTimeout(timer);\n"
94
- " }, [_showAc]);\n\n"
95
- )
96
- content = content.replace(UNIQUE_MARKER, insert + UNIQUE_MARKER)
97
-
98
- # Add to return
99
- content = content.replace(
100
- "refreshMessages,\n };",
101
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
102
- )
103
-
104
- with open(HOOK, 'w') as f:
105
- f.write(content)
106
- print("OK: useAgentChat.ts")
107
-
108
- # --- ChatInput.tsx ---
109
- with open(INPUT) as f:
110
- content = f.read()
111
-
112
- # Add Button (check exact substring, not "IconButton")
113
- if " Button,\n" not in content and " Button\n" not in content:
114
- content = content.replace(
115
- " Tooltip,",
116
- " Tooltip,\n Button,"
117
- )
118
-
119
- if "_showAc" not in content:
120
- content = content.replace(
121
- "placeholder?: string;",
122
- "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
123
- )
124
- content = content.replace(
125
- "placeholder = 'Ask anything...' }: ChatInputProps) {",
126
- "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
127
- )
128
- btn = (
129
- " {_showAc && _acCancel && (\n"
130
- " <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
131
- " <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
132
- " sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
133
- " TAM_DUNG_TU_DONG_TIEP_TUC (10s)\n"
134
- " </Button>\n"
135
- " </Box>\n"
136
- " )}\n"
137
- )
138
- content = content.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
139
-
140
- with open(INPUT, 'w') as f:
141
- f.write(content)
142
- print("OK: ChatInput.tsx")
143
-
144
- # --- SessionChat.tsx ---
145
- if os.path.exists(SESS):
146
- with open(SESS) as f:
147
- c = f.read()
148
- if "_showAc" not in c:
149
- c = c.replace(
150
- "<ChatInput ",
151
- "<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
152
- )
153
- with open(SESS, 'w') as f:
154
- f.write(c)
155
- print("OK: SessionChat.tsx")
156
-
157
- # --- agent_loop.py ---
158
- if os.path.exists(AGENT):
159
- with open(AGENT) as f:
160
- c = f.read()
161
-
162
- if "_unfinished_plan" not in c:
163
- fn = (
164
- "\n\ndef _unfinished_plan(s):\n"
165
- " p = getattr(s, 'current_plan', None) or []\n"
166
- " return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n"
167
- )
168
- c = c.replace("class Handlers:", fn + "class Handlers:")
169
-
170
- if "ac_plan" not in c:
171
- check = (
172
- "\n if not llm_result.tool_calls_acc and llm_result.content:\n"
173
- " unfinished = _unfinished_plan(session)\n"
174
- " if unfinished:\n"
175
- " await session.send_event(Event(event_type='session_update', data={\n"
176
- " 'ac_plan': unfinished,\n"
177
- " }))\n"
178
- )
179
- c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
180
-
181
- with open(AGENT, 'w') as f:
182
- f.write(c)
183
- print("OK: agent_loop.py")
184
- else:
185
- print("SKIP: agent_loop.py")
186
-
187
- print("DONE: V17")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v2.py DELETED
@@ -1,307 +0,0 @@
1
- """V2: Fix all TypeScript errors for auto-continue feature.
2
-
3
- Errors fixed:
4
- 1. 'startAutoContinue' used before declaration → use refs
5
- 2. 'showAutoContinue'/'taskIncompleteInfo' never read → they're in return
6
- 3. 'incompletePlan' implicit any → add type
7
- 4. EventType missing 'task_incomplete' → add to events.ts
8
- 5. DatasetUploadResponse not found → fix import path
9
- 6. onTaskIncomplete not on SideChannelCallbacks → fix in SSE transport
10
- """
11
-
12
- import os
13
- import re
14
-
15
- # =====================================================================
16
- # PATCH 1: events.ts - Add 'task_incomplete' to EventType
17
- # =====================================================================
18
- EVENTS_FILE = "/source/frontend/src/types/events.ts"
19
-
20
- def patch_events():
21
- if not os.path.exists(EVENTS_FILE):
22
- print(f"SKIP: {EVENTS_FILE} not found")
23
- return
24
- with open(EVENTS_FILE, "r") as f:
25
- content = f.read()
26
- if "'task_incomplete'" not in content:
27
- content = content.replace(
28
- " | 'plan_update';",
29
- " | 'plan_update'\n | 'task_incomplete';"
30
- )
31
- with open(EVENTS_FILE, "w") as f:
32
- f.write(content)
33
- print("OK: Added 'task_incomplete' to EventType")
34
- else:
35
- print("OK: EventType already has 'task_incomplete'")
36
-
37
- # =====================================================================
38
- # PATCH 2: useAgentChat.ts - Fix hoisting + type issues
39
- # =====================================================================
40
- USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
41
-
42
- def patch_use_agent_chat():
43
- if not os.path.exists(USE_AGENT_CHAT):
44
- print(f"SKIP: {USE_AGENT_CHAT} not found")
45
- return
46
-
47
- with open(USE_AGENT_CHAT, "r") as f:
48
- content = f.read()
49
-
50
- # 1. Add useState to imports
51
- if "useState" not in content:
52
- content = content.replace(
53
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
54
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
55
- )
56
-
57
- # 2. Add state + refs after callbacksRef
58
- state_code = '''
59
- // Auto-continue state for free models when task incomplete
60
- const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
61
- const [showAutoContinue, setShowAutoContinue] = useState(false);
62
- const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
63
- incompletePlan: Array<{ id: string; content: string; status: string }>;
64
- } | null>(null);
65
- const startAutoContinueRef = useRef<() => void>(() => {});
66
- const cancelAutoContinueRef = useRef<() => void>(() => {});
67
- '''
68
- if "autoContinueTimerRef" not in content:
69
- content = content.replace(
70
- "callbacksRef.current = { onReady, onError, onSessionDead };",
71
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_code
72
- )
73
-
74
- # 3. Add onTaskIncomplete to SideChannelCallbacks interface
75
- if "onTaskIncomplete:" not in content and "onInterrupted:" in content:
76
- content = content.replace(
77
- " onInterrupted: () => void;",
78
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
79
- )
80
-
81
- # 4. Replace onTaskIncomplete handler to use refs
82
- old_handler = "onTaskIncomplete: (incompletePlan) => {"
83
- new_handler = "onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {"
84
- if old_handler in content:
85
- content = content.replace(old_handler, new_handler)
86
-
87
- # Fix the timer callback to use ref instead of direct call
88
- old_timer = "autoContinueTimerRef.current = setTimeout(() => {\n startAutoContinue();\n }, 10000);"
89
- new_timer = "autoContinueTimerRef.current = setTimeout(() => {\n startAutoContinueRef.current();\n }, 10000);"
90
- if old_timer in content:
91
- content = content.replace(old_timer, new_timer)
92
-
93
- # 5. Add auto-continue functions BEFORE return (but using refs pattern)
94
- # Remove old startAutoContinue/cancelAutoContinue if they exist
95
- old_funcs_start = "const startAutoContinue = useCallback"
96
- if old_funcs_start in content:
97
- # Find and remove the old function blocks
98
- start_idx = content.find("const startAutoContinue = useCallback")
99
- cancel_idx = content.find("const cancelAutoContinue = useCallback")
100
- cleanup_idx = content.find("// Cleanup timer on unmount")
101
-
102
- # Remove from startAutoContinue to end of cleanup
103
- if start_idx > 0 and cleanup_idx > start_idx:
104
- # Find the end of cleanup effect
105
- end_cleanup = content.find("\n", cleanup_idx)
106
- # Find the closing of the useEffect
107
- close_idx = content.find("}, []);", end_cleanup)
108
- if close_idx > 0:
109
- close_idx += len("}, []);")
110
- content = content[:start_idx] + content[close_idx:]
111
-
112
- # 5b. Add new functions using ref pattern, before return
113
- new_funcs = '''
114
- // -- Auto-continue for free models when task incomplete -----------------
115
- const startAutoContinue = useCallback(() => {
116
- setShowAutoContinue(false);
117
- setTaskIncompleteInfo(null);
118
- if (autoContinueTimerRef.current) {
119
- clearTimeout(autoContinueTimerRef.current);
120
- autoContinueTimerRef.current = null;
121
- }
122
- const incompleteItems = taskIncompleteInfo?.incompletePlan || [];
123
- const continuationText = incompleteItems.length > 0
124
- ? `Tiếp tục từ các task chưa hoàn thành:\\n${incompleteItems.map(i => `- ${i.content}`).join('\\n')}`
125
- : 'Tiếp tục nhiệm vụ.';
126
- chat.sendMessage({
127
- text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`,
128
- metadata: { createdAt: new Date().toISOString() },
129
- });
130
- }, [taskIncompleteInfo, chat]);
131
-
132
- const cancelAutoContinue = useCallback(() => {
133
- setShowAutoContinue(false);
134
- setTaskIncompleteInfo(null);
135
- if (autoContinueTimerRef.current) {
136
- clearTimeout(autoContinueTimerRef.current);
137
- autoContinueTimerRef.current = null;
138
- }
139
- }, []);
140
-
141
- // Keep refs in sync
142
- startAutoContinueRef.current = startAutoContinue;
143
- cancelAutoContinueRef.current = cancelAutoContinue;
144
-
145
- // Cleanup timer on unmount
146
- useEffect(() => {
147
- return () => {
148
- if (autoContinueTimerRef.current) {
149
- clearTimeout(autoContinueTimerRef.current);
150
- }
151
- };
152
- }, []);
153
- '''
154
-
155
- if "startAutoContinueRef.current" not in content:
156
- content = content.replace(
157
- '\n return {\n messages: chat.messages,',
158
- new_funcs + '\n return {\n messages: chat.messages,'
159
- )
160
-
161
- # 6. Update return to include new values
162
- if "showAutoContinue," not in content:
163
- content = content.replace(
164
- "refreshMessages,\n };",
165
- "refreshMessages,\n showAutoContinue,\n taskIncompleteInfo,\n startAutoContinue,\n cancelAutoContinue,\n };"
166
- )
167
-
168
- with open(USE_AGENT_CHAT, "w") as f:
169
- f.write(content)
170
- print(f"OK: Patched {USE_AGENT_CHAT}")
171
-
172
- # =====================================================================
173
- # PATCH 3: ChatInput.tsx - Fix DatasetUploadResponse import
174
- # =====================================================================
175
- CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
176
-
177
- def patch_chat_input():
178
- if not os.path.exists(CHAT_INPUT):
179
- print(f"SKIP: {CHAT_INPUT} not found")
180
- return
181
-
182
- with open(CHAT_INPUT, "r") as f:
183
- content = f.read()
184
-
185
- # Fix DatasetUploadResponse import - try correct path
186
- if "DatasetUploadResponse" in content:
187
- # Check if it's imported from agent.ts
188
- wrong_import = "import type { DatasetUploadResponse } from '@/types/agent';"
189
- if wrong_import in content:
190
- # Try to find the correct import - it's probably in the same file or from backend
191
- # Replace with inline type or remove
192
- content = content.replace(wrong_import, "")
193
- # Add inline type
194
- content = content.replace(
195
- "import { apiFetch } from '@/utils/api';",
196
- "import { apiFetch } from '@/utils/api';\n\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
197
- )
198
- print("OK: Fixed DatasetUploadResponse import")
199
-
200
- # Add Button to MUI imports
201
- if "Button" not in content:
202
- content = content.replace(
203
- "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
204
- "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
205
- )
206
-
207
- # Check if interface already has our props (from previous patch)
208
- if "showAutoContinue" not in content:
209
- # Find interface and replace with complete version
210
- interface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
211
- if interface_match:
212
- props_code = '''
213
- interface ChatInputProps {
214
- sessionId: string;
215
- initialModelPath: string | null | undefined;
216
- onSend: (text: string) => Promise<void>;
217
- onStop: () => void;
218
- onDatasetUploaded: () => Promise<boolean>;
219
- isProcessing: boolean;
220
- disabled: boolean;
221
- placeholder?: string;
222
- showAutoContinue?: boolean;
223
- taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
224
- startAutoContinue?: () => void;
225
- cancelAutoContinue?: () => void;
226
- }
227
- '''
228
- content = content.replace(interface_match.group(0), props_code)
229
- print("OK: Updated ChatInputProps interface")
230
-
231
- # Add auto-continue buttons
232
- buttons_code = '''
233
- {/* Auto-continue controls for free models */}
234
- {showAutoContinue && taskIncompleteInfo && (
235
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
236
- <Button
237
- variant="contained"
238
- color="primary"
239
- size="small"
240
- onClick={startAutoContinue}
241
- sx={{ textTransform: 'none', fontSize: '0.75rem' }}
242
- >
243
- Tự động tiếp tục (10s)
244
- </Button>
245
- <Button
246
- variant="outlined"
247
- color="secondary"
248
- size="small"
249
- onClick={cancelAutoContinue}
250
- sx={{ textTransform: 'none', fontSize: '0.75rem' }}
251
- >
252
- Tạm dừng
253
- </Button>
254
- </Stack>
255
- )}
256
- '''
257
-
258
- if "Tự động tiếp tục" not in content:
259
- content = content.replace(
260
- "<Box sx={{ flex: 1 ",
261
- buttons_code + " <Box sx={{ flex: 1 "
262
- )
263
- print("OK: Added auto-continue buttons")
264
-
265
- with open(CHAT_INPUT, "w") as f:
266
- f.write(content)
267
- print(f"OK: Patched {CHAT_INPUT}")
268
-
269
- # =====================================================================
270
- # PATCH 4: sse-chat-transport.ts - Fix onTaskIncomplete reference
271
- # =====================================================================
272
- SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
273
-
274
- def patch_sse_transport():
275
- if not os.path.exists(SSE_TRANSPORT):
276
- print(f"SKIP: {SSE_TRANSPORT} not found")
277
- return
278
-
279
- with open(SSE_TRANSPORT, "r") as f:
280
- content = f.read()
281
-
282
- # Add onTaskIncomplete to SideChannelCallbacks interface
283
- if "onTaskIncomplete:" not in content and "onInterrupted:" in content:
284
- content = content.replace(
285
- " onInterrupted: () => void;",
286
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
287
- )
288
-
289
- # Use @ts-ignore for the case since EventType union doesn't include 'task_incomplete'
290
- # Or better: add the case with a type assertion
291
- if "case 'task_incomplete'" in content:
292
- # Already added, make sure it compiles
293
- old_case = "case 'task_incomplete':\n sideChannel.onTaskIncomplete("
294
- new_case = "case 'task_incomplete' as const:\n sideChannel.onTaskIncomplete("
295
- content = content.replace(old_case, new_case)
296
-
297
- with open(SSE_TRANSPORT, "w") as f:
298
- f.write(content)
299
- print(f"OK: Patched {SSE_TRANSPORT}")
300
-
301
-
302
- if __name__ == "__main__":
303
- patch_events()
304
- patch_use_agent_chat()
305
- patch_chat_input()
306
- patch_sse_transport()
307
- print("\nDONE: All V2 patches applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v3.py DELETED
@@ -1,229 +0,0 @@
1
- """V3: FINAL - Fix remaining TS errors for auto-continue feature.
2
-
3
- Errors fixed:
4
- 1. TS6133: 'showAutoContinue'/'taskIncompleteInfo' never read → prefix with _
5
- 2. TS2741: Property 'onTaskIncomplete' missing in sideChannel object → add implementation
6
- """
7
-
8
- import os
9
- import re
10
-
11
- USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
12
- EVENTS_FILE = "/source/frontend/src/types/events.ts"
13
- CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
14
- SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
15
-
16
- def patch_events():
17
- if not os.path.exists(EVENTS_FILE):
18
- print(f"SKIP: {EVENTS_FILE} not found")
19
- return
20
- with open(EVENTS_FILE, "r") as f:
21
- content = f.read()
22
- if "'task_incomplete'" not in content:
23
- content = content.replace(
24
- " | 'plan_update';",
25
- " | 'plan_update'\n | 'task_incomplete';"
26
- )
27
- with open(EVENTS_FILE, "w") as f:
28
- f.write(content)
29
- print("OK: Added 'task_incomplete' to EventType")
30
-
31
- def patch_use_agent_chat():
32
- if not os.path.exists(USE_AGENT_CHAT):
33
- print(f"SKIP: {USE_AGENT_CHAT} not found")
34
- return
35
-
36
- with open(USE_AGENT_CHAT, "r") as f:
37
- content = f.read()
38
-
39
- # 1. Add useState import
40
- if "useState" not in content:
41
- content = content.replace(
42
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
43
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
44
- )
45
-
46
- # 2. Add state after callbacksRef
47
- state_block = '''
48
- // Auto-continue state for free models when task incomplete
49
- const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
50
- const [_showAutoContinue, _setShowAutoContinue] = useState(false);
51
- const [_taskIncompleteInfo, _setTaskIncompleteInfo] = useState<{
52
- incompletePlan: Array<{ id: string; content: string; status: string }>;
53
- } | null>(null);
54
- '''
55
- if "autoContinueTimerRef" not in content:
56
- content = content.replace(
57
- "callbacksRef.current = { onReady, onError, onSessionDead };",
58
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
59
- )
60
-
61
- # 3. Add onTaskIncomplete to interface
62
- if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
63
- content = content.replace(
64
- " onInterrupted: () => void;",
65
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
66
- )
67
-
68
- # 4. Add onTaskIncomplete handler in sideChannel (BEFORE onInterrupted)
69
- handler_block = ''' onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
70
- _setTaskIncompleteInfo({ incompletePlan });
71
- _setShowAutoContinue(true);
72
- if (autoContinueTimerRef.current) {
73
- clearTimeout(autoContinueTimerRef.current);
74
- }
75
- autoContinueTimerRef.current = setTimeout(() => {
76
- _setShowAutoContinue(false);
77
- _setTaskIncompleteInfo(null);
78
- const plan = incompletePlan.map(i => `- ${i.content}`).join('\\n');
79
- chat.sendMessage({
80
- text: `[TỰ ĐỘNG TIẾP TỤC] Tiếp tục từ các task chưa hoàn thành:\\n${plan}`,
81
- metadata: { createdAt: new Date().toISOString() },
82
- });
83
- }, 10000);
84
- },
85
- onInterrupted: () => { /* no-op */ },'''
86
-
87
- if "onTaskIncomplete: (incompletePlan" not in content:
88
- old_interrupted = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
89
- if old_interrupted in content:
90
- content = content.replace(old_interrupted, handler_block)
91
- else:
92
- # Try the simpler version
93
- simple_interrupted = ' onInterrupted: () => { /* no-op */ },'
94
- if simple_interrupted in content:
95
- content = content.replace(simple_interrupted, handler_block)
96
-
97
- # 5. Add cleanup timer before return
98
- cleanup = '''
99
- // Cleanup auto-continue timer
100
- useEffect(() => {
101
- return () => {
102
- if (autoContinueTimerRef.current) {
103
- clearTimeout(autoContinueTimerRef.current);
104
- }
105
- };
106
- }, []);
107
- '''
108
- if "autoContinueTimerRef.current" in content and "Cleanup timer" not in content:
109
- content = content.replace(
110
- '\n return {\n messages: chat.messages,',
111
- cleanup + '\n return {\n messages: chat.messages,'
112
- )
113
-
114
- with open(USE_AGENT_CHAT, "w") as f:
115
- f.write(content)
116
- print(f"OK: Patched {USE_AGENT_CHAT}")
117
-
118
- def patch_chat_input():
119
- if not os.path.exists(CHAT_INPUT):
120
- print(f"SKIP: {CHAT_INPUT} not found")
121
- return
122
-
123
- with open(CHAT_INPUT, "r") as f:
124
- content = f.read()
125
-
126
- # Fix DatasetUploadResponse - define inline type
127
- if "DatasetUploadResponse" in content and "type DatasetUploadResponse" not in content:
128
- wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
129
- if wrong in content:
130
- content = content.replace(wrong, "")
131
- # Add inline type after apiFetch import
132
- content = content.replace(
133
- "import { apiFetch } from '@/utils/api';",
134
- "import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
135
- )
136
-
137
- # Add Button import
138
- if "Button" not in content:
139
- content = content.replace(
140
- "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
141
- "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
142
- )
143
-
144
- # Fix interface - replace with complete version
145
- if "_showAutoContinue" not in content:
146
- interface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
147
- if interface_match:
148
- props_code = '''
149
- interface ChatInputProps {
150
- sessionId: string;
151
- initialModelPath: string | null | undefined;
152
- onSend: (text: string) => Promise<void>;
153
- onStop: () => void;
154
- onDatasetUploaded: () => Promise<boolean>;
155
- isProcessing: boolean;
156
- disabled: boolean;
157
- placeholder?: string;
158
- _showAutoContinue?: boolean;
159
- _taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
160
- _onCancelAutoContinue?: () => void;
161
- }
162
- '''
163
- content = content.replace(interface_match.group(0), props_code)
164
-
165
- # Add cancel button
166
- buttons = '''
167
- {/* Auto-continue cancel button for free models */}
168
- {_showAutoContinue && _taskIncompleteInfo && _onCancelAutoContinue && (
169
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
170
- <Button
171
- variant="outlined"
172
- color="secondary"
173
- size="small"
174
- onClick={_onCancelAutoContinue}
175
- sx={{ textTransform: 'none', fontSize: '0.75rem' }}
176
- >
177
- Tạm dừng (để nhập nội dung khác)
178
- </Button>
179
- </Stack>
180
- )}
181
- '''
182
- if "Tạm dừng" not in content:
183
- content = content.replace(
184
- "<Box sx={{ flex: 1 ",
185
- buttons + "\n <Box sx={{ flex: 1 "
186
- )
187
-
188
- with open(CHAT_INPUT, "w") as f:
189
- f.write(content)
190
- print(f"OK: Patched {CHAT_INPUT}")
191
-
192
- def patch_sse_transport():
193
- if not os.path.exists(SSE_TRANSPORT):
194
- print(f"SKIP: {SSE_TRANSPORT} not found")
195
- return
196
-
197
- with open(SSE_TRANSPORT, "r") as f:
198
- content = f.read()
199
-
200
- # Add onTaskIncomplete to SideChannelCallbacks interface
201
- if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
202
- content = content.replace(
203
- " onInterrupted: () => void;",
204
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
205
- )
206
-
207
- # Add case 'task_incomplete'
208
- if "case 'task_incomplete'" not in content:
209
- task_case = ''' case 'task_incomplete' as const:
210
- sideChannel.onTaskIncomplete(
211
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
212
- );
213
- break;
214
- '''
215
- content = content.replace(
216
- "case 'turn_complete':",
217
- task_case + "\n case 'turn_complete':"
218
- )
219
-
220
- with open(SSE_TRANSPORT, "w") as f:
221
- f.write(content)
222
- print(f"OK: Patched {SSE_TRANSPORT}")
223
-
224
- if __name__ == "__main__":
225
- patch_events()
226
- patch_use_agent_chat()
227
- patch_chat_input()
228
- patch_sse_transport()
229
- print("\nDONE: V3 patches applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v4.py DELETED
@@ -1,264 +0,0 @@
1
- """V4: FINAL WORKING - Fix all TS errors for auto-continue feature.
2
-
3
- Strategy:
4
- - useAgentChat: return _showAutoContinue + _cancelAutoContinue (TS _ prefix suppresses "unused")
5
- - ChatInput.tsx: receive props and render "Tạm dừng" button
6
- - SessionChat.tsx: pass props from hook to ChatInput
7
- - events.ts: add 'task_incomplete' to EventType union
8
- - sse-chat-transport.ts: add onTaskIncomplete + case handler
9
- - agent_loop.py: send task_incomplete event
10
- """
11
-
12
- import os
13
- import re
14
-
15
- EVENTS = "/source/frontend/src/types/events.ts"
16
- USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
17
- SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
18
- CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
19
- SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx"
20
-
21
-
22
- def patch_events():
23
- with open(EVENTS, "r") as f:
24
- c = f.read()
25
- if "'task_incomplete'" not in c:
26
- c = c.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
27
- with open(EVENTS, "w") as f: f.write(c)
28
- print("OK: events.ts")
29
-
30
-
31
- def patch_sse():
32
- with open(SSE, "r") as f:
33
- c = f.read()
34
-
35
- # Add onTaskIncomplete to interface
36
- if "onTaskIncomplete:" not in c:
37
- c = c.replace(
38
- " onInterrupted: () => void;",
39
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
40
- )
41
-
42
- # Add case handler
43
- if "case 'task_incomplete'" not in c:
44
- case = """ case 'task_incomplete' as const:
45
- sideChannel.onTaskIncomplete(
46
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
47
- );
48
- break;
49
- """
50
- c = c.replace("case 'turn_complete':", case + "\n case 'turn_complete':")
51
-
52
- with open(SSE, "w") as f: f.write(c)
53
- print("OK: sse-chat-transport.ts")
54
-
55
-
56
- def patch_use_agent_chat():
57
- with open(USE_AGENT_CHAT, "r") as f:
58
- c = f.read()
59
-
60
- # 1. Import useState
61
- if "useState" not in c:
62
- c = c.replace(
63
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
64
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
65
- )
66
-
67
- # 2. State (TS _ prefix = unused ok)
68
- state_block = '''
69
- // Auto-continue for free models
70
- const _autoContinueTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
71
- const [_showAutoContinue, _setShowAutoContinue] = useState(false);
72
- '''
73
- if "_autoContinueTimer" not in c:
74
- c = c.replace(
75
- "callbacksRef.current = { onReady, onError, onSessionDead };",
76
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
77
- )
78
-
79
- # 3. onTaskIncomplete in interface
80
- if "onTaskIncomplete:" not in c:
81
- c = c.replace(
82
- " onInterrupted: () => void;",
83
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
84
- )
85
-
86
- # 4. Cancel ref
87
- cancel_ref = ' const _cancelAutoContinue = useRef(() => {});\n'
88
- if "_cancelAutoContinue" not in c:
89
- # Insert after callbacksRef.current = { ... } area
90
- c = c.replace(
91
- " isActiveRef.current = isActive;",
92
- " isActiveRef.current = isActive;\n" + cancel_ref
93
- )
94
-
95
- # 5. Handler in sideChannel - inline all logic
96
- handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
97
- _setShowAutoContinue(true);
98
- if (_autoContinueTimer.current) clearTimeout(_autoContinueTimer.current);
99
- _autoContinueTimer.current = setTimeout(() => {
100
- _setShowAutoContinue(false);
101
- const plan = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
102
- chat.sendMessage({
103
- text: `[TỰ ĐỘNG TIẾP TỤC] Tiếp tục từ các task chưa hoàn thành:\\\\n${plan}`,
104
- metadata: { createdAt: new Date().toISOString() },
105
- });
106
- }, 10000);
107
- },
108
- onInterrupted: () => { /* no-op */ },"""
109
-
110
- if "onTaskIncomplete: (incompletePlan" not in c:
111
- old = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
112
- if old in c:
113
- c = c.replace(old, handler)
114
-
115
- # 6. Set cancel ref function + cleanup
116
- cancel_func = """
117
- _cancelAutoContinue.current = () => {
118
- _setShowAutoContinue(false);
119
- if (_autoContinueTimer.current) {
120
- clearTimeout(_autoContinueTimer.current);
121
- _autoContinueTimer.current = null;
122
- }
123
- };
124
- """
125
- if "_cancelAutoContinue.current = ()" not in c:
126
- # Insert before return
127
- c = c.replace(
128
- "\n return {\n messages: chat.messages,",
129
- cancel_func + "\n return {\n messages: chat.messages,"
130
- )
131
-
132
- # 7. Return values
133
- if "_showAutoContinue," not in c:
134
- c = c.replace(
135
- "refreshMessages,\n };",
136
- "refreshMessages,\n _showAutoContinue,\n _cancelAutoContinue,\n };"
137
- )
138
-
139
- with open(USE_AGENT_CHAT, "w") as f: f.write(c)
140
- print("OK: useAgentChat.ts")
141
-
142
-
143
- def patch_chat_input():
144
- with open(CHAT_INPUT, "r") as f:
145
- c = f.read()
146
-
147
- # Fix DatasetUploadResponse
148
- if "DatasetUploadResponse" in c and "inline" not in c:
149
- wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
150
- if wrong in c:
151
- c = c.replace(wrong, "")
152
- c = c.replace(
153
- "import { apiFetch } from '@/utils/api';",
154
- "import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
155
- )
156
-
157
- # Add Button import
158
- if "Button" not in c:
159
- c = c.replace(
160
- "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
161
- "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
162
- )
163
-
164
- # Add props to interface
165
- if "_showAutoContinue" not in c:
166
- match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', c, re.DOTALL)
167
- if match:
168
- new_iface = """interface ChatInputProps {
169
- sessionId: string;
170
- initialModelPath: string | null | undefined;
171
- onSend: (text: string) => Promise<void>;
172
- onStop: () => void;
173
- onDatasetUploaded: () => Promise<boolean>;
174
- isProcessing: boolean;
175
- disabled: boolean;
176
- placeholder?: string;
177
- _showAutoContinue?: boolean;
178
- _cancelAutoContinue?: React.RefObject<() => void>;
179
- }"""
180
- c = c.replace(match.group(0), new_iface)
181
-
182
- # Add pause button
183
- btn = """
184
- {/* Pause auto-continue for free models */}
185
- {_showAutoContinue && _cancelAutoContinue?.current && (
186
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
187
- <Button
188
- variant="outlined"
189
- color="secondary"
190
- size="small"
191
- onClick={_cancelAutoContinue.current}
192
- sx={{ textTransform: 'none', fontSize: '0.75rem' }}
193
- >
194
- Tạm dừng (để nhập nội dung khác)
195
- </Button>
196
- </Stack>
197
- )}
198
- """
199
- if "Tạm dừng" not in c:
200
- c = c.replace("<Box sx={{ flex: 1 ", btn + "\n <Box sx={{ flex: 1 ")
201
-
202
- with open(CHAT_INPUT, "w") as f: f.write(c)
203
- print("OK: ChatInput.tsx")
204
-
205
-
206
- def patch_session_chat():
207
- with open(SESSION_CHAT, "r") as f:
208
- c = f.read()
209
-
210
- # Find the ChatInput usage and add new props
211
- if "_showAutoContinue" not in c:
212
- chat_input_pattern = r'(<ChatInput[^>]*sessionId={sessionId}[^>]*initialModelPath={initialModelPath}[^>]*)(\s*/>)'
213
- new_props = r'\1 _showAutoContinue={_showAutoContinue} _cancelAutoContinue={_cancelAutoContinue}\2'
214
- c = re.sub(chat_input_pattern, new_props, c)
215
-
216
- with open(SESSION_CHAT, "w") as f: f.write(c)
217
- print("OK: SessionChat.tsx")
218
-
219
-
220
- def patch_agent_loop():
221
- agent_loop = "/app/agent/core/agent_loop.py"
222
- if not os.path.exists(agent_loop):
223
- print(f"SKIP: {agent_loop} not found (backend)")
224
- return
225
-
226
- with open(agent_loop, "r") as f:
227
- c = f.read()
228
-
229
- if "_check_task_incomplete" not in c:
230
- func = '''
231
-
232
- def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
233
- plan = getattr(session, "current_plan", None) or []
234
- return [item for item in plan if item.get("status") in ("pending", "in_progress")]
235
-
236
- '''
237
- c = c.replace("class Handlers:", func + "\n\nclass Handlers:")
238
-
239
- if "task_incomplete" not in c:
240
- check = '''
241
- # === Auto-continue: if model stopped but plan incomplete ===
242
- if not llm_result.tool_calls_acc and llm_result.content:
243
- unfinished = _unfinished_plan_items(session)
244
- if unfinished:
245
- await session.send_event(
246
- Event(event_type="task_incomplete", data={"incomplete_plan": unfinished})
247
- )
248
- '''
249
- marker = " # -- End of turn --"
250
- if marker in c:
251
- c = c.replace(marker, check + "\n" + marker)
252
-
253
- with open(agent_loop, "w") as f: f.write(c)
254
- print("OK: agent_loop.py")
255
-
256
-
257
- if __name__ == "__main__":
258
- patch_events()
259
- patch_sse()
260
- patch_use_agent_chat()
261
- patch_chat_input()
262
- patch_session_chat()
263
- patch_agent_loop()
264
- print("\nDONE: V4 - everything applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v5.py DELETED
@@ -1,252 +0,0 @@
1
- """V5: SIMPLEST approach - just show pause button with 10s auto-continue.
2
-
3
- Architecture:
4
- 1. Agent loop detects model stopped mid-task → sends 'task_incomplete' event
5
- 2. SSE transport receives it → calls sideChannel.onTaskIncomplete()
6
- 3. useAgentChat: onTaskIncomplete sets _showAutoContinue=true, sends continue msg
7
- 4. ChatInput shows "Tạm dừng" button if _showAutoContinue
8
- """
9
-
10
- import os, re
11
-
12
- EVENTS = "/source/frontend/src/types/events.ts"
13
- SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
14
- USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
15
- CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
16
- AGENT_LOOP = "/app/agent/core/agent_loop.py"
17
-
18
-
19
- def patch_events():
20
- with open(EVENTS, "r") as f:
21
- c = f.read()
22
- if "'task_incomplete'" not in c:
23
- c = c.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
24
- with open(EVENTS, "w") as f:
25
- f.write(c)
26
- print("OK: events.ts")
27
-
28
-
29
- def patch_sse():
30
- with open(SSE, "r") as f:
31
- c = f.read()
32
-
33
- if "onTaskIncomplete:" not in c:
34
- c = c.replace(
35
- " onInterrupted: () => void;",
36
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
37
- )
38
-
39
- if "case 'task_incomplete'" not in c:
40
- case_block = """ case 'task_incomplete' as const:
41
- sideChannel.onTaskIncomplete(
42
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
43
- );
44
- break;
45
- """
46
- c = c.replace("case 'turn_complete':", case_block + "\n case 'turn_complete':")
47
-
48
- with open(SSE, "w") as f:
49
- f.write(c)
50
- print("OK: sse.ts")
51
-
52
-
53
- def patch_use_agent():
54
- with open(USE_AGENT_CHAT, "r") as f:
55
- c = f.read()
56
-
57
- # 1. Import useState
58
- if "useState" not in c:
59
- c = c.replace(
60
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
61
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
62
- )
63
-
64
- # 2. State (prefixed with _ = TS allows unused)
65
- state = '''
66
- // Auto-continue state for free models
67
- const _acTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
68
- const [_showAc, _setShowAc] = useState(false);'''
69
- if "_acTimer" not in c:
70
- c = c.replace(
71
- "callbacksRef.current = { onReady, onError, onSessionDead };",
72
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state
73
- )
74
-
75
- # 3. Add onTaskIncomplete to interface
76
- if "onTaskIncomplete:" not in c:
77
- c = c.replace(
78
- " onInterrupted: () => void;",
79
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
80
- )
81
-
82
- # 4. Add onTaskIncomplete handler - uses chatActionsRef ref instead of chat directly
83
- handler = ''' onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
84
- _setShowAc(true);
85
- if (_acTimer.current) clearTimeout(_acTimer.current);
86
- _acTimer.current = setTimeout(() => {
87
- _setShowAc(false);
88
- // Auto-continue after 10s via chatActionsRef
89
- const chatRef = chatActionsRef.current;
90
- const sendMsg = (chatRef as any)?.sendMessage;
91
- if (sendMsg) {
92
- const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
93
- sendMsg({ text: `[TIẾP TỤC] Tiếp tục task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } });
94
- }
95
- }, 10000);
96
- },
97
- onInterrupted: () => { /* no-op */ },'''
98
-
99
- if "onTaskIncomplete: (incompletePlan" not in c:
100
- old = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
101
- if old in c:
102
- c = c.replace(old, handler)
103
- else:
104
- old2 = ' onInterrupted: () => { /* no-op */ },'
105
- c = c.replace(old2, handler)
106
-
107
- # 5. Cancel ref + return values
108
- if "_showAc" not in content:
109
- # Insert cancel before return
110
- cancel = '''
111
- // Expose auto-continue state
112
- const _acCancel = useCallback(() => {
113
- _setShowAc(false);
114
- if (_acTimer.current) { clearTimeout(_acTimer.current); _acTimer.current = null; }
115
- }, []);
116
- '''
117
- if "_acCancel" not in c:
118
- c = c.replace('\n return {\n messages: chat.messages,', cancel + '\n return {\n messages: chat.messages,')
119
-
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(USE_AGENT_CHAT, "w") as f:
127
- f.write(c)
128
- print("OK: useAgentChat.ts")
129
-
130
-
131
- def patch_chat_input():
132
- with open(CHAT_INPUT, "r") as f:
133
- c = f.read()
134
-
135
- # DatasetUploadResponse fix
136
- if "DatasetUploadResponse" in c and "type DatasetUploadResponse" not in c:
137
- wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
138
- if wrong in c:
139
- c = c.replace(wrong, "")
140
- c = c.replace(
141
- "import { apiFetch } from '@/utils/api';",
142
- "import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
143
- )
144
-
145
- # Add Button
146
- if "Button" not in c:
147
- c = c.replace(
148
- "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
149
- "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
150
- )
151
-
152
- # Update interface
153
- if "_showAc" not in c:
154
- m = re.search(r'interface ChatInputProps\s*\{[^}]*\}', c, re.DOTALL)
155
- if m:
156
- iface = """interface ChatInputProps {
157
- sessionId: string;
158
- initialModelPath: string | null | undefined;
159
- onSend: (text: string) => Promise<void>;
160
- onStop: () => void;
161
- onDatasetUploaded: () => Promise<boolean>;
162
- isProcessing: boolean;
163
- disabled: boolean;
164
- placeholder?: string;
165
- _showAc?: boolean;
166
- _acCancel?: () => void;
167
- }"""
168
- c = c.replace(m.group(0), iface)
169
-
170
- # Add button
171
- btn = """
172
- {/* Auto-continue pause button for free models */}
173
- {_showAc && _acCancel && (
174
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
175
- <Button
176
- variant="outlined"
177
- color="secondary"
178
- size="small"
179
- onClick={_acCancel}
180
- sx={{ textTransform: 'none', fontSize: '0.7rem' }}
181
- >
182
- Tạm dừng tự động tiếp tục (10s)
183
- </Button>
184
- </Stack>
185
- )}
186
- """
187
- if "Tạm dừng" not in c:
188
- c = c.replace("<Box sx={{ flex: 1 ", btn + "\n <Box sx={{ flex: 1 ")
189
-
190
- with open(CHAT_INPUT, "w") as f:
191
- f.write(c)
192
- print("OK: ChatInput.tsx")
193
-
194
-
195
- def patch_session_chat():
196
- sess = "/source/frontend/src/components/SessionChat.tsx"
197
- if not os.path.exists(sess):
198
- print(f"SKIP: {sess}")
199
- return
200
- with open(sess, "r") as f:
201
- c = f.read()
202
- if "_showAc" not in c:
203
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
204
- with open(sess, "w") as f:
205
- f.write(c)
206
- print("OK: SessionChat.tsx")
207
-
208
-
209
- def patch_agent_loop():
210
- if not os.path.exists(AGENT_LOOP):
211
- print(f"SKIP: {AGENT_LOOP}")
212
- return
213
- with open(AGENT_LOOP, "r") as f:
214
- c = f.read()
215
-
216
- if "_unfinished_plan" not in c:
217
- fn = '''
218
-
219
- def _unfinished_plan(session: Session) -> list[dict[str, str]]:
220
- plan = getattr(session, "current_plan", None) or []
221
- return [item for item in plan if item.get("status") in ("pending", "in_progress")]
222
-
223
- '''
224
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
225
-
226
- if "task_incomplete" not in c:
227
- check = '''
228
- # === Auto-continue detection ===
229
- if not llm_result.tool_calls_acc and llm_result.content:
230
- unfinished = _unfinished_plan(session)
231
- if unfinished:
232
- await session.send_event(
233
- Event(event_type="task_incomplete", data={"incomplete_plan": unfinished})
234
- )
235
- '''
236
- marker = " # -- End of turn --"
237
- if marker in c:
238
- c = c.replace(marker, check + "\n" + marker)
239
-
240
- with open(AGENT_LOOP, "w") as f:
241
- f.write(c)
242
- print("OK: agent_loop.py")
243
-
244
-
245
- if __name__ == "__main__":
246
- patch_events()
247
- patch_sse()
248
- patch_use_agent()
249
- patch_chat_input()
250
- patch_session_chat()
251
- patch_agent_loop()
252
- print("\nDONE: V5 - simple approach applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v6.py DELETED
@@ -1,243 +0,0 @@
1
- """V6: FIXED - Auto-continue with pause button for free models."""
2
-
3
- import os
4
- import re
5
-
6
- EVENTS_FILE = "/source/frontend/src/types/events.ts"
7
- SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
8
- HOOK_FILE = "/source/frontend/src/hooks/useAgentChat.ts"
9
- CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
10
- SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx"
11
- AGENT_LOOP = "/app/agent/core/agent_loop.py"
12
-
13
-
14
- def fix_events():
15
- with open(EVENTS_FILE, "r") as f:
16
- content = f.read()
17
- if "'task_incomplete'" not in content:
18
- content = content.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
19
- with open(EVENTS_FILE, "w") as f:
20
- f.write(content)
21
- print("OK: events.ts")
22
-
23
-
24
- def fix_sse():
25
- with open(SSE_TRANSPORT, "r") as f:
26
- content = f.read()
27
-
28
- # Add onTaskIncomplete to interface
29
- if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
30
- content = content.replace(
31
- " onInterrupted: () => void;",
32
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
33
- )
34
-
35
- # Add case handler
36
- if "case 'task_incomplete'" not in content:
37
- case_code = """ case 'task_incomplete' as const:
38
- sideChannel.onTaskIncomplete(
39
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
40
- );
41
- break;
42
- """
43
- content = content.replace("case 'turn_complete':", case_code + "\n case 'turn_complete':")
44
-
45
- with open(SSE_TRANSPORT, "w") as f:
46
- f.write(content)
47
- print("OK: sse-chat-transport.ts")
48
-
49
-
50
- def fix_hook():
51
- with open(HOOK_FILE, "r") as f:
52
- content = f.read()
53
-
54
- # 1. Import useState
55
- if "useState" not in content:
56
- content = content.replace(
57
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
58
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
59
- )
60
-
61
- # 2. Add state after callbacksRef block
62
- state_block = """
63
- // Auto-continue for free models
64
- const _acTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
65
- const [_showAc, _setShowAc] = useState(false);
66
- const _acCancel = useCallback(() => {
67
- _setShowAc(false);
68
- if (_acTimer.current) { clearTimeout(_acTimer.current); _acTimer.current = null; }
69
- }, []);"""
70
-
71
- if "_acTimer" not in content:
72
- content = content.replace(
73
- "callbacksRef.current = { onReady, onError, onSessionDead };",
74
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
75
- )
76
-
77
- # 3. Add onTaskIncomplete to interface
78
- if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
79
- content = content.replace(
80
- " onInterrupted: () => void;",
81
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
82
- )
83
-
84
- # 4. Add handler in sideChannel - uses chatActionsRef to avoid hoisting issues
85
- handler_code = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
86
- _setShowAc(true);
87
- if (_acTimer.current) clearTimeout(_acTimer.current);
88
- _acTimer.current = setTimeout(() => {
89
- _setShowAc(false);
90
- const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
91
- const msg = { text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } };
92
- const setMsgs = chatActionsRef.current.setMessages;
93
- if (setMsgs) {
94
- chatActionsRef.current.messages = [...chatActionsRef.current.messages, { id: 'ac-continue', role: 'user', parts: [{ type: 'text', text: msg.text }], content: msg.text }];
95
- }
96
- }, 10000);
97
- },
98
- onInterrupted: () => { /* no-op - handled by stop() */ },"""
99
-
100
- if "onTaskIncomplete: (incompletePlan" not in content:
101
- old_handler = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },'
102
- if old_handler in content:
103
- content = content.replace(old_handler, handler_code)
104
- else:
105
- old_handler2 = ' onInterrupted: () => { /* no-op */ },'
106
- content = content.replace(old_handler2, handler_code)
107
-
108
- # 5. Return values
109
- if "_showAc" not in content:
110
- # The _showAc and _acCancel need to be in the return
111
- return_marker = "refreshMessages,\n };"
112
- if return_marker in content:
113
- content = content.replace(
114
- return_marker,
115
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
116
- )
117
-
118
- with open(HOOK_FILE, "w") as f:
119
- f.write(content)
120
- print("OK: useAgentChat.ts")
121
-
122
-
123
- def fix_chat_input():
124
- with open(CHAT_INPUT, "r") as f:
125
- content = f.read()
126
-
127
- # Fix DatasetUploadResponse
128
- wrong_import = "import type { DatasetUploadResponse } from '@/types/agent';"
129
- if wrong_import in content:
130
- content = content.replace(wrong_import, "")
131
- content = content.replace(
132
- "import { apiFetch } from '@/utils/api';",
133
- "import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
134
- )
135
-
136
- # Add Button import
137
- if "Button" not in content:
138
- content = content.replace(
139
- "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
140
- "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
141
- )
142
-
143
- # Fix interface
144
- if "_showAc" not in content:
145
- iface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
146
- if iface_match:
147
- new_iface = """interface ChatInputProps {
148
- sessionId: string;
149
- initialModelPath: string | null | undefined;
150
- onSend: (text: string) => Promise<void>;
151
- onStop: () => void;
152
- onDatasetUploaded: () => Promise<boolean>;
153
- isProcessing: boolean;
154
- disabled: boolean;
155
- placeholder?: string;
156
- _showAc?: boolean;
157
- _acCancel?: () => void;
158
- }"""
159
- content = content.replace(iface_match.group(0), new_iface)
160
-
161
- # Add pause button
162
- btn_code = """
163
- {/* Auto-continue pause for free models */}
164
- {_showAc && _acCancel && (
165
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
166
- <Button
167
- variant="outlined"
168
- color="secondary"
169
- size="small"
170
- onClick={_acCancel}
171
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.8 }}
172
- >
173
- T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
174
- </Button>
175
- </Stack>
176
- )}"""
177
-
178
- if "T\u1ea1m d\u1eebng" not in content and "free models" not in content:
179
- content = content.replace("<Box sx={{ flex: 1 ", btn_code + "\n <Box sx={{ flex: 1 ")
180
-
181
- with open(CHAT_INPUT, "w") as f:
182
- f.write(content)
183
- print("OK: ChatInput.tsx")
184
-
185
-
186
- def fix_session_chat():
187
- with open(SESSION_CHAT, "r") as f:
188
- content = f.read()
189
-
190
- # Add props to ChatInput
191
- if "_showAc" not in content:
192
- content = content.replace(
193
- "<ChatInput ",
194
- "<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
195
- )
196
-
197
- with open(SESSION_CHAT, "w") as f:
198
- f.write(content)
199
- print("OK: SessionChat.tsx")
200
-
201
-
202
- def fix_agent_loop():
203
- if not os.path.exists(AGENT_LOOP):
204
- print(f"SKIP: {AGENT_LOOP}")
205
- return
206
- with open(AGENT_LOOP, "r") as f:
207
- content = f.read()
208
-
209
- if "_unfinished_plan" not in content:
210
- fn_block = '''
211
-
212
- def _unfinished_plan(s: Session) -> list[dict[str, str]]:
213
- p = getattr(s, "current_plan", None) or []
214
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
215
-
216
- '''
217
- content = content.replace("class Handlers:", fn_block + "\n\nclass Handlers:")
218
-
219
- if "task_incomplete" not in content:
220
- check_block = '''
221
- # Auto-continue detect
222
- if not llm_result.tool_calls_acc and llm_result.content:
223
- unfinished = _unfinished_plan(session)
224
- if unfinished:
225
- await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}))
226
- '''
227
- marker = " # -- End of turn --"
228
- if marker in content:
229
- content = content.replace(marker, check_block + "\n" + marker)
230
-
231
- with open(AGENT_LOOP, "w") as f:
232
- f.write(content)
233
- print("OK: agent_loop.py")
234
-
235
-
236
- if __name__ == "__main__":
237
- fix_events()
238
- fix_sse()
239
- fix_hook()
240
- fix_chat_input()
241
- fix_session_chat()
242
- fix_agent_loop()
243
- print("\nDONE: V6 - all fixed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v7.py DELETED
@@ -1,255 +0,0 @@
1
- """V7: PRECISE patch - exact string matching with source code.
2
-
3
- Adds auto-continue pause button for free models. All patches use exact
4
- string content from the original source files for reliable replacement.
5
- """
6
-
7
- import os, sys
8
-
9
- # ========== PATCH 1: events.ts - add 'task_incomplete' to EventType ==========
10
- events_ts = "/source/frontend/src/types/events.ts"
11
- def patch_events():
12
- with open(events_ts) as f:
13
- c = f.read()
14
- if "'task_incomplete'" not in c:
15
- c = c.replace(
16
- " | 'interrupted'",
17
- " | 'interrupted'\n | 'task_incomplete'"
18
- )
19
- with open(events_ts, 'w') as f: f.write(c)
20
- print("OK: events.ts")
21
-
22
- # ========== PATCH 2: SSE transport - add onTaskIncomplete to interface + handler ==========
23
- sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
24
- def patch_sse():
25
- with open(sse_ts) as f:
26
- c = f.read()
27
-
28
- # Add to interface
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
- # Add case handler before default
36
- if "case 'task_incomplete'" not in c:
37
- case_handler = """ case 'task_incomplete':
38
- sideChannel.onTaskIncomplete(
39
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
40
- );
41
- break;
42
-
43
- default:"""
44
- c = c.replace("\n default:", "\n" + case_handler)
45
-
46
- with open(sse_ts, 'w') as f: f.write(c)
47
- print("OK: sse-chat-transport.ts")
48
-
49
- # ========== PATCH 3: useAgentChat.ts - add state + handler + return ==========
50
- hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
51
- def patch_hook():
52
- with open(hook_ts) as f:
53
- c = f.read()
54
-
55
- # 1. Add useState to imports
56
- if "useState" not in c:
57
- c = c.replace(
58
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
59
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
60
- )
61
-
62
- # 2. Add state variables after callbacksRef
63
- state_block = """
64
- // Auto-continue state for free models
65
- const _acTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
66
- const [_showAc, _setShowAc] = useState(false);"""
67
- if "_acTimerRef" not in c:
68
- c = c.replace(
69
- "callbacksRef.current = { onReady, onError, onSessionDead };",
70
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
71
- )
72
-
73
- # 3. Add onTaskIncomplete to interface
74
- if "onTaskIncomplete:" not in c:
75
- c = c.replace(
76
- " onInterrupted: () => void;",
77
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
78
- )
79
-
80
- # 4. Add onTaskIncomplete handler in sideChannel object
81
- # This inserts BEFORE the existing onInterrupted handler
82
- handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
83
- _setShowAc(true);
84
- if (_acTimerRef.current) clearTimeout(_acTimerRef.current);
85
- _acTimerRef.current = setTimeout(() => {
86
- _setShowAc(false);
87
- const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
88
- const setMsgs = chatActionsRef.current.setMessages;
89
- if (setMsgs) {
90
- const msgs = chatActionsRef.current.messages;
91
- setMsgs([...msgs, { 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: '' }]);
92
- }
93
- }, 10000);
94
- },
95
- onInterrupted: () => { /* no-op */ },"""
96
-
97
- if "onTaskIncomplete: (incompletePlan" not in c:
98
- old_interrupted = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },'
99
- if old_interrupted in c:
100
- c = c.replace(old_interrupted, handler)
101
- else:
102
- # Try simpler version
103
- old2 = ' onInterrupted: () => { /* no-op */ },'
104
- c = c.replace(old2, handler)
105
-
106
- # 5. Add _acCancel function + return values
107
- cancel_func = """
108
- const _acCancel = useCallback(() => {
109
- _setShowAc(false);
110
- if (_acTimerRef.current) { clearTimeout(_acTimerRef.current); _acTimerRef.current = null; }
111
- }, []);"""
112
- if "_acCancel" not in c:
113
- c = c.replace("\n return {\n messages: chat.messages,", cancel_func + "\n return {\n messages: chat.messages,")
114
-
115
- # 6. Return values
116
- if "_showAc," not in c:
117
- c = c.replace(
118
- "refreshMessages,\n };",
119
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
120
- )
121
-
122
- with open(hook_ts, 'w') as f: f.write(c)
123
- print("OK: useAgentChat.ts")
124
-
125
- # ========== PATCH 4: ChatInput.tsx - add pause button ==========
126
- input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
127
- def patch_chat_input():
128
- with open(input_tsx) as f:
129
- c = f.read()
130
-
131
- # 1. Add Button import (it's missing from the original imports)
132
- if "Button" not in c:
133
- c = c.replace(
134
- " Tooltip,",
135
- " Tooltip,\n Button,"
136
- )
137
-
138
- # 2. Add props to interface
139
- if "_showAc" not in c:
140
- old_interface_end = """interface ChatInputProps {
141
- sessionId?: string;
142
- initialModelPath?: string | null;
143
- onSend: (text: string) => void;
144
- onStop?: () => void;
145
- onDatasetUploaded?: () => Promise<boolean> | boolean;
146
- isProcessing?: boolean;
147
- disabled?: boolean;
148
- placeholder?: string;
149
- }"""
150
- new_interface = """interface ChatInputProps {
151
- sessionId?: string;
152
- initialModelPath?: string | null;
153
- onSend: (text: string) => void;
154
- onStop?: () => void;
155
- onDatasetUploaded?: () => Promise<boolean> | boolean;
156
- isProcessing?: boolean;
157
- disabled?: boolean;
158
- placeholder?: string;
159
- _showAc?: boolean;
160
- _acCancel?: () => void;
161
- }"""
162
- c = c.replace(old_interface_end, new_interface)
163
-
164
- # 3. Add destructured props in function signature
165
- if "_showAc" not in c:
166
- old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
167
- new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
168
- if old_props not in c:
169
- old_props2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
170
- new_props2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
171
- c = c.replace(old_props2, new_props2)
172
- else:
173
- c = c.replace(old_props, new_props)
174
-
175
- # 4. Add pause button after the model badge area, before <JobsUpgradeDialog>
176
- if "T\u1ea1m d\u1eebng" not in c:
177
- btn = """
178
- {/* Auto-continue pause for free models */}
179
- {_showAc && _acCancel && (
180
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
181
- <Button
182
- variant="outlined"
183
- size="small"
184
- onClick={_acCancel}
185
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}
186
- >
187
- T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
188
- </Button>
189
- </Box>
190
- )}
191
- """
192
- if "<JobsUpgradeDialog" in c:
193
- c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
194
-
195
- with open(input_tsx, 'w') as f: f.write(c)
196
- print("OK: ChatInput.tsx")
197
-
198
- # ========== PATCH 5: SessionChat.tsx - pass props down ==========
199
- session_tsx = "/source/frontend/src/components/SessionChat.tsx"
200
- def patch_session():
201
- if not os.path.exists(session_tsx):
202
- print(f"SKIP: {session_tsx}")
203
- return
204
- with open(session_tsx) as f:
205
- c = f.read()
206
- if "_showAc" not in c:
207
- old = "<ChatInput "
208
- new = "<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
209
- c = c.replace(old, new)
210
- with open(session_tsx, 'w') as f: f.write(c)
211
- print("OK: SessionChat.tsx")
212
-
213
- # ========== PATCH 6: agent_loop.py - detection ==========
214
- agent_py = "/app/agent/core/agent_loop.py"
215
- def patch_agent():
216
- if not os.path.exists(agent_py):
217
- print(f"SKIP: {agent_py}")
218
- return
219
- with open(agent_py) as f:
220
- c = f.read()
221
-
222
- # Add helper function
223
- if "_unfinished_plan" not in c:
224
- fn = """
225
- def _unfinished_plan(s: Session) -> list[dict[str, str]]:
226
- p = getattr(s, "current_plan", None) or []
227
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
228
-
229
- """
230
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
231
-
232
- # Add detection
233
- if "task_incomplete" not in c:
234
- check = """
235
- # Auto-continue detection
236
- if not llm_result.tool_calls_acc and llm_result.content:
237
- unfinished = _unfinished_plan(session)
238
- if unfinished:
239
- await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}))
240
- """
241
- marker = " # -- End of turn --"
242
- if marker in c:
243
- c = c.replace(marker, check + "\n" + marker)
244
-
245
- with open(agent_py, 'w') as f: f.write(c)
246
- print("OK: agent_loop.py")
247
-
248
- if __name__ == "__main__":
249
- patch_events()
250
- patch_sse()
251
- patch_hook()
252
- patch_chat_input()
253
- patch_session()
254
- patch_agent()
255
- print("\nDONE: V7 - precise patches applied")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v8.py DELETED
@@ -1,239 +0,0 @@
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")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_auto_continue_v9.py DELETED
@@ -1,227 +0,0 @@
1
- """V9: SIMPLEST approach - no interface changes, no props chain.
2
-
3
- Uses CustomEvent to communicate between sideChannel and React state.
4
- This avoids ALL TypeScript issues:
5
- - No interface changes needed
6
- - No hoisting issues
7
- - No unused variable warnings
8
- """
9
-
10
- import os
11
-
12
- events_ts = "/source/frontend/src/types/events.ts"
13
- sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
14
- hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
15
- input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
16
- agent_py = "/app/agent/core/agent_loop.py"
17
-
18
-
19
- def patch_events():
20
- with open(events_ts) as f:
21
- c = f.read()
22
- if "'task_incomplete'" not in c:
23
- c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
24
- with open(events_ts, 'w') as f: f.write(c)
25
- print("OK: events.ts")
26
-
27
-
28
- def patch_sse():
29
- with open(sse_ts) as f:
30
- c = f.read()
31
-
32
- # Add case handler DIRECTLY without changing interface
33
- # Use as any cast to bypass type checking
34
- if "case 'task_incomplete'" not in c:
35
- # Add handler before default, using (sideChannel as any) to avoid interface issue
36
- case_block = """ case 'task_incomplete':
37
- (sideChannel as any).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_block)
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. Add state after callbacksRef - TS will complain "unused" but _ prefix helps
61
- state_block = """
62
- // Auto-continue for free models
63
- const [_showAc, _setShowAc] = useState(false);"""
64
- if "_showAc" not in c:
65
- c = c.replace(
66
- "callbacksRef.current = { onReady, onError, onSessionDead };",
67
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
68
- )
69
-
70
- # 3. Add onTaskIncomplete to the sideChannel object via (sideChannel as any)
71
- # We use the onSessionUpdate handler as injection point since it already exists
72
- handler_block = """ onSessionUpdate: (data) => {
73
- // Check for auto-continue trigger
74
- if ((data as any).ac_plan) {
75
- const plan = (data as any).ac_plan as Array<{ id: string; content: string; status: string }>;
76
- _setShowAc(true);
77
- setTimeout(() => {
78
- _setShowAc(false);
79
- if (plan.length > 0) {
80
- const planStr = plan.map(i => `- ${i.content}`).join('\\\\n');
81
- chat.sendMessage({ text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } });
82
- }
83
- }, 10000);
84
- return;
85
- }
86
- const autoApproval = data.auto_approval;"""
87
-
88
- if "_setShowAc" in c and "ac_plan" not in c:
89
- # Find existing onSessionUpdate
90
- old = """ onSessionUpdate: (data) => {
91
- const autoApproval = data.auto_approval;"""
92
- if old in c:
93
- c = c.replace(old, handler_block)
94
- print("OK: injected onSessionUpdate handler")
95
-
96
- # 4. Cancel function + return values
97
- cancel = """
98
- const _acCancel = useCallback(() => { _setShowAc(false); }, []);"""
99
- if "_acCancel" not in c and "_showAc" in c:
100
- c = c.replace("\n return {\n messages: chat.messages,", cancel + "\n return {\n messages: chat.messages,")
101
- c = c.replace(
102
- "refreshMessages,\n };",
103
- "refreshMessages,\n _showAc,\n _acCancel,\n };"
104
- )
105
-
106
- with open(hook_ts, 'w') as f: f.write(c)
107
- print("OK: useAgentChat.ts")
108
-
109
-
110
- def patch_chat_input():
111
- with open(input_tsx) as f:
112
- c = f.read()
113
-
114
- if "Button" not in c:
115
- c = c.replace(" Tooltip,", " Tooltip,\n Button,")
116
-
117
- if "_showAc" not in c:
118
- old_iface = """interface ChatInputProps {
119
- sessionId?: string;
120
- initialModelPath?: string | null;
121
- onSend: (text: string) => void;
122
- onStop?: () => void;
123
- onDatasetUploaded?: () => Promise<boolean> | boolean;
124
- isProcessing?: boolean;
125
- disabled?: boolean;
126
- placeholder?: string;
127
- }"""
128
- new_iface = """interface ChatInputProps {
129
- sessionId?: string;
130
- initialModelPath?: string | null;
131
- onSend: (text: string) => void;
132
- onStop?: () => void;
133
- onDatasetUploaded?: () => Promise<boolean> | boolean;
134
- isProcessing?: boolean;
135
- disabled?: boolean;
136
- placeholder?: string;
137
- _showAc?: boolean;
138
- _acCancel?: () => void;
139
- }"""
140
- c = c.replace(old_iface, new_iface)
141
-
142
- # Add destructured props
143
- old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
144
- new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
145
- c = c.replace(old_props, new_props)
146
-
147
- if "T\u1ea1m d\u1eebng" not in c:
148
- btn = """
149
- {_showAc && _acCancel && (
150
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
151
- <Button variant="outlined" size="small" onClick={_acCancel}
152
- sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
153
- T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
154
- </Button>
155
- </Box>
156
- )}
157
- """
158
- if "<JobsUpgradeDialog" in c:
159
- c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
160
-
161
- with open(input_tsx, 'w') as f: f.write(c)
162
- print("OK: ChatInput.tsx")
163
-
164
-
165
- def patch_session():
166
- session_tsx = "/source/frontend/src/components/SessionChat.tsx"
167
- if not os.path.exists(session_tsx):
168
- print(f"SKIP: {session_tsx}")
169
- return
170
- with open(session_tsx) as f:
171
- c = f.read()
172
- if "_showAc" not in c:
173
- c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
174
- with open(session_tsx, 'w') as f: f.write(c)
175
- print("OK: SessionChat.tsx")
176
-
177
-
178
- def patch_agent():
179
- if not os.path.exists(agent_py):
180
- print(f"SKIP: {agent_py}")
181
- return
182
- with open(agent_py) as f:
183
- c = f.read()
184
-
185
- if "_unfinished_plan" not in c:
186
- fn = """
187
- def _unfinished_plan(s: Session) -> list[dict[str, str]]:
188
- p = getattr(s, "current_plan", None) or []
189
- return [it for it in p if it.get("status") in ("pending", "in_progress")]
190
- """
191
- c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
192
-
193
- if "task_incomplete" not in c:
194
- check = """
195
- # Auto-continue detection
196
- if not llm_result.tool_calls_acc and llm_result.content:
197
- unfinished = _unfinished_plan(session)
198
- if unfinished:
199
- # Use session_update event with ac_plan field to trigger auto-continue
200
- await session.send_event(Event(event_type="session_update", data={
201
- "ac_plan": unfinished,
202
- "auto_approval": getattr(session, "auto_approval", None),
203
- }))
204
- """
205
- marker = " # -- End of turn --"
206
- if marker in c:
207
- c = c.replace(marker, check + "\n" + marker)
208
- else:
209
- print("WARN: Could not find -- End of turn -- marker")
210
- # Try alternative: find where final_response is set
211
- marker2 = "final_response = llm_result.content or None"
212
- if marker2 in c:
213
- c = c.replace(marker2, marker2 + "\n" + check)
214
-
215
- with open(agent_py, 'w') as f: f.write(c)
216
- print("OK: agent_loop.py")
217
-
218
-
219
- if __name__ == "__main__":
220
- patch_events()
221
- patch_sse()
222
- patch_hook()
223
- patch_chat_input()
224
- patch_session()
225
- patch_agent()
226
- print("\nDONE: V9 applied")
227
- print("Strategy: Uses existing session_update event + (sideChannel as any) to avoid TS issues")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_chat_input.py DELETED
@@ -1,105 +0,0 @@
1
- """Patch ChatInput.tsx - Fixed import for DatasetUploadResponse."""
2
-
3
- CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
4
-
5
- def patch():
6
- import os, re
7
- if not os.path.exists(CHAT_INPUT):
8
- print(f"SKIP: {CHAT_INPUT} not found")
9
- return
10
-
11
- with open(CHAT_INPUT, "r", encoding="utf-8") as f:
12
- content = f.read()
13
-
14
- # 1. Add Button to MUI imports
15
- if "Button" not in content and "@mui/material" in content:
16
- content = content.replace(
17
- "import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
18
- "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
19
- )
20
-
21
- # 2. Fix DatasetUploadResponse import - check existing imports
22
- if "DatasetUploadResponse" not in content:
23
- # Find existing import from types/agent
24
- if "from '@/types/agent'" in content:
25
- content = content.replace(
26
- "from '@/types/agent'",
27
- "from '@/types/agent'"
28
- )
29
- # Add DatasetUploadResponse to existing import
30
- content = content.replace(
31
- "import type { ",
32
- "import type { DatasetUploadResponse, "
33
- )
34
- else:
35
- content = content.replace(
36
- "import { apiFetch } from '@/utils/api';",
37
- "import { apiFetch } from '@/utils/api';\nimport type { DatasetUploadResponse } from '@/types/agent';"
38
- )
39
-
40
- # 3. Add props to interface
41
- if "showAutoContinue" not in content:
42
- props_code = '''
43
- interface ChatInputProps {
44
- sessionId: string;
45
- initialModelPath: string | null | undefined;
46
- onSend: (text: string) => Promise<void>;
47
- onStop: () => void;
48
- onDatasetUploaded: () => Promise<boolean>;
49
- isProcessing: boolean;
50
- disabled: boolean;
51
- placeholder?: string;
52
- showAutoContinue?: boolean;
53
- taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
54
- startAutoContinue?: () => void;
55
- cancelAutoContinue?: () => void;
56
- }
57
- '''
58
- match = re.search(r'interface ChatInputProps[^}]*[}][^}]*[}]', content, re.DOTALL)
59
- if match:
60
- content = content.replace(match.group(0), props_code)
61
- else:
62
- content = content.replace(
63
- "export default function ChatInput",
64
- props_code + "\nexport default function ChatInput"
65
- )
66
-
67
- # 4. Add auto-continue buttons before the flex box
68
- buttons_code = '''
69
- {/* Auto-continue controls for free models */}
70
- {showAutoContinue && taskIncompleteInfo && (
71
- <Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
72
- <Button
73
- variant="contained"
74
- color="primary"
75
- size="small"
76
- onClick={startAutoContinue}
77
- sx={{ textTransform: 'none', fontSize: '0.75rem' }}
78
- >
79
- Tự động tiếp tục (10s)
80
- </Button>
81
- <Button
82
- variant="outlined"
83
- color="secondary"
84
- size="small"
85
- onClick={cancelAutoContinue}
86
- sx={{ textTransform: 'none', fontSize: '0.75rem' }}
87
- >
88
- Tạm dừng
89
- </Button>
90
- </Stack>
91
- )}
92
- '''
93
-
94
- if "Tự động tiếp tục" not in content:
95
- content = content.replace(
96
- "<Box sx={{ flex: 1 ",
97
- buttons_code + " <Box sx={{ flex: 1 "
98
- )
99
-
100
- with open(CHAT_INPUT, "w", encoding="utf-8") as f:
101
- f.write(content)
102
- print(f"OK: Patched {CHAT_INPUT}")
103
-
104
- if __name__ == "__main__":
105
- patch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_frontend.py DELETED
@@ -1,184 +0,0 @@
1
- """Patch frontend: Replace DEFAULT_MODEL_OPTIONS with OpenRouter models."""
2
-
3
- import re
4
- import sys
5
-
6
- FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
7
- MODEL_FILE = "/source/frontend/src/utils/model.ts"
8
-
9
- # === Step 1: Patch model.ts ===
10
- with open(MODEL_FILE, "r") as f:
11
- model_content = f.read()
12
-
13
- model_content = model_content.replace(
14
- "export const KIMI_K27_CODE_MODEL_PATH = 'moonshotai/Kimi-K2.7-Code:novita';",
15
- "export const KIMI_K27_CODE_MODEL_PATH = 'deepseek-ai/DeepSeek-V4-Pro';"
16
- )
17
- model_content = model_content.replace(
18
- "export const MINIMAX_M3_MODEL_PATH = 'MiniMaxAI/MiniMax-M3:novita';",
19
- "export const MINIMAX_M3_MODEL_PATH = 'deepseek-ai/DeepSeek-V4-Flash';"
20
- )
21
- model_content = model_content.replace(
22
- "export const GLM_52_MODEL_PATH = 'zai-org/GLM-5.2:novita';",
23
- "export const GLM_52_MODEL_PATH = 'deepseek/deepseek-v4-flash';"
24
- )
25
- model_content = model_content.replace(
26
- "export const DEEPSEEK_V4_PRO_MODEL_PATH = 'deepseek-ai/DeepSeek-V4-Pro:novita';",
27
- "export const DEEPSEEK_V4_PRO_MODEL_PATH = 'nvidia/nemotron-3-super-120b-a12b:free';"
28
- )
29
-
30
- # Add DeepSeek V4 Flash latest model path
31
- if "DEEPSEEK_V4_FLASH_LATEST_MODEL_PATH" not in model_content:
32
- model_content = model_content.replace(
33
- "export const LAGUNA_S21_MODEL_PATH",
34
- "export const DEEPSEEK_V4_FLASH_LATEST_MODEL_PATH = '~deepseek/deepseek-v4-flash-latest';\n\nexport const LAGUNA_S21_MODEL_PATH"
35
- )
36
- print("OK: Added DEEPSEEK_V4_FLASH_LATEST_MODEL_PATH")
37
-
38
- # === PATCH: Add Laguna S 2.1 model path ===
39
- if "LAGUNA_S21_MODEL_PATH" not in model_content:
40
- model_content = model_content.replace(
41
- "export const LAGUNA_M1_MODEL_PATH",
42
- "export const LAGUNA_S21_MODEL_PATH = 'openai/poolside/laguna-s-2.1:free';\n\nexport const LAGUNA_M1_MODEL_PATH"
43
- )
44
- print("OK: Added LAGUNA_S21_MODEL_PATH")
45
-
46
- with open(MODEL_FILE, "w") as f:
47
- f.write(model_content)
48
- print("OK: model.ts constants updated")
49
-
50
- # === Step 2: Patch ChatInput.tsx ===
51
- with open(FILE, "r", encoding="utf-8") as f:
52
- content = f.read()
53
-
54
- # Fix imports
55
- import_pattern = re.compile(
56
- r'import\s*\{[^}]+\}\s*from\s*[\'"]@/utils/model[\'"];'
57
- )
58
- match = import_pattern.search(content)
59
- if match:
60
- content = content.replace(match.group(0), "import { isClaudePath } from '@/utils/model';")
61
- print("OK: Replaced imports")
62
-
63
- # Remove Owl Alpha references
64
- content = content.replace("'Owl Alpha'", "'Claude Opus 4.8'")
65
- content = content.replace('"Owl Alpha"', '"Claude Opus 4.8"')
66
-
67
- # Row 3: Kimi → DeepSeek V4 Pro
68
- content = content.replace("'Kimi K2.7 Code'", "'DeepSeek V4 Pro'")
69
- content = content.replace('"Kimi K2.7 Code"', '"DeepSeek V4 Pro"')
70
-
71
- # Row 4: MiniMax → DeepSeek V4 Flash
72
- content = content.replace("'MiniMax M3'", "'DeepSeek V4 Flash'")
73
- content = content.replace('"MiniMax M3"', '"DeepSeek V4 Flash"')
74
-
75
- # Row 5: GLM → DeepSeek V4 Flash
76
- content = content.replace("'GLM 5.2'", "'DeepSeek V4 Flash'")
77
- content = content.replace('"GLM 5.2"', '"DeepSeek V4 Flash"')
78
-
79
- # Row 6: DeepSeek V4 Pro → Nemotron
80
- content = content.replace(
81
- " {\n id: 'deepseek-v4-pro',\n name: 'DeepSeek V4 Pro',\n modelPath: DEEPSEEK_V4_PRO_MODEL_PATH,\n avatarUrl: getHfAvatarUrl('deepseek-ai/DeepSeek-V4-Pro'),\n },",
82
- " {\n id: 'nemotron-120b',\n name: 'nvidia/nemotron-3-super-120b-a12b:free',\n modelPath: DEEPSEEK_V4_PRO_MODEL_PATH,\n avatarUrl: 'https://huggingface.co/api/avatars/nvidia',\n },"
83
- )
84
-
85
- # Replace entire DEFAULT_MODEL_OPTIONS array
86
- pattern = r'const DEFAULT_MODEL_OPTIONS: ModelOption\[\] = \[.*?\];'
87
- match = re.search(pattern, content, re.DOTALL)
88
-
89
- if not match:
90
- print("FAIL: Could not find DEFAULT_MODEL_OPTIONS array!")
91
- sys.exit(1)
92
-
93
- NEW_MODELS = """const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
94
- {
95
- id: 'tencent-hy3',
96
- name: 'Tencent HY3:free',
97
- modelPath: 'openai/tencent/hy3:free',
98
- avatarUrl: 'https://huggingface.co/api/avatars/tencent',
99
- recommended: true,
100
- },
101
- {
102
- id: 'gemma-4-31b',
103
- name: 'Gemma 4 31B:free',
104
- modelPath: 'openai/google/gemma-4-31b-it:free',
105
- avatarUrl: 'https://huggingface.co/api/avatars/google',
106
- recommended: true,
107
- },
108
- {
109
- id: 'llama-3.3-70b',
110
- name: 'Llama 3.3 70B:free',
111
- modelPath: 'openai/meta-llama/llama-3.3-70b-instruct:free',
112
- avatarUrl: 'https://huggingface.co/api/avatars/meta-llama',
113
- recommended: true,
114
- },
115
- {
116
- id: 'laguna-m1',
117
- name: 'Laguna M.1:free',
118
- modelPath: 'openai/poolside/laguna-m.1:free',
119
- avatarUrl: 'https://huggingface.co/api/avatars/poolside',
120
- recommended: true,
121
- },
122
- {
123
- id: 'laguna-s21',
124
- name: 'Laguna S 2.1:free',
125
- modelPath: 'openai/poolside/laguna-s-2.1:free',
126
- avatarUrl: 'https://huggingface.co/api/avatars/poolside',
127
- recommended: true,
128
- },
129
- {
130
- id: 'nex-n2-mini',
131
- name: 'nex-agi/nex-n2-mini',
132
- modelPath: 'openai/nex-agi/nex-n2-mini',
133
- avatarUrl: 'https://huggingface.co/api/avatars/nex-agi',
134
- recommended: true,
135
- },
136
- {
137
- id: 'ling-3.0-flash',
138
- name: 'inclusionai/ling-3.0-flash:free',
139
- modelPath: 'openai/inclusionai/ling-3.0-flash:free',
140
- avatarUrl: 'https://huggingface.co/api/avatars/inclusionai',
141
- recommended: true,
142
- },
143
- {
144
- id: 'llama-3.1-8b',
145
- name: 'Llama 3.1 8B',
146
- modelPath: 'openai/meta-llama/llama-3.1-8b-instruct',
147
- avatarUrl: 'https://huggingface.co/api/avatars/meta-llama',
148
- },
149
- {
150
- id: 'qwen3-coder-next',
151
- name: 'Qwen3 Coder Next',
152
- modelPath: 'Qwen/Qwen3-Coder-Next',
153
- avatarUrl: 'https://huggingface.co/api/avatars/Qwen',
154
- },
155
- {
156
- id: 'gemini-2.0-flash',
157
- name: 'Gemini 2.0 Flash',
158
- modelPath: 'openai/google/gemini-2.0-flash-001',
159
- avatarUrl: 'https://huggingface.co/api/avatars/google',
160
- },
161
- {
162
- id: 'deepseek-v4-flash-latest',
163
- name: 'DeepSeek V4 Flash latest',
164
- modelPath: '~deepseek/deepseek-v4-flash-latest',
165
- avatarUrl: 'https://huggingface.co/api/avatars/deepseek',
166
- recommended: true,
167
- },
168
- ];"""
169
-
170
- content = content[:match.start()] + NEW_MODELS + content[match.end():]
171
-
172
- # Update DEFAULT_MODEL_PATH
173
- dm_path = re.search(r'const DEFAULT_MODEL_PATH\s*=.*?;', content)
174
- if dm_path:
175
- content = content.replace(dm_path.group(0), "const DEFAULT_MODEL_PATH = 'openai/tencent/hy3:free';")
176
- print("OK: Replaced DEFAULT_MODEL_PATH")
177
- else:
178
- print("OK: DEFAULT_MODEL_PATH not found")
179
-
180
- with open(FILE, "w", encoding="utf-8") as f:
181
- f.write(content)
182
-
183
- print("OK: Frontend patched - Tencent HY3:free DEFAULT")
184
- print(" Models: HY3:free, Gemma 4 31B:free, Llama 3.3 70B:free, Laguna M.1:free, Laguna S 2.1:free, Nex N2 Mini, Ling 3.0 Flash:free, Llama 3.1 8B, DeepSeek V4 Flash latest")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_models.py DELETED
@@ -1,212 +0,0 @@
1
- """Patch backend: OpenRouter routing + DeepSeek V4 Flash 0731."""
2
-
3
- import ast
4
- import os
5
- import re
6
-
7
- AGENT_FILE = "/app/backend/routes/agent.py"
8
- LLM_PARAMS_FILE = "/app/agent/core/llm_params.py"
9
- IDS_FILE = "/app/agent/core/model_ids.py"
10
-
11
- with open(IDS_FILE) as f:
12
- content = f.read()
13
-
14
- # Remove NO_TOOLS_MODELS if present from previous patches
15
- if "NO_TOOLS_MODELS" in content:
16
- content = re.sub(r'NO_TOOLS_MODELS\s*=\s*\{[^}]*\}\n?', '', content)
17
- print("OK: Removed old NO_TOOLS_MODELS")
18
-
19
- # Fix model IDs for OpenRouter - use exact full-line patterns to avoid accidental partial matches
20
- replacements = [
21
- ('KIMI_K27_CODE_MODEL_ID = "moonshotai/Kimi-K2.7-Code:novita"',
22
- 'KIMI_K27_CODE_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro"'),
23
- ('MINIMAX_M3_MODEL_ID = "MiniMaxAI/MiniMax-M3:novita"',
24
- 'MINIMAX_M3_MODEL_ID = "deepseek-ai/DeepSeek-V4-Flash"'),
25
- ('GLM_52_MODEL_ID = "zai-org/GLM-5.2:novita"',
26
- 'GLM_52_MODEL_ID = "openai/deepseek/deepseek-v4-flash"'),
27
- ('DEEPSEEK_V4_PRO_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro:novita"',
28
- 'DEEPSEEK_V4_PRO_MODEL_ID = "nvidia/nemotron-3-super-120b-a12b:free"'),
29
- ]
30
- for old, new in replacements:
31
- content = content.replace(old, new)
32
- print("OK: Model ID replacements applied")
33
-
34
- # Add missing constants BEFORE the specific line DEEPSEEK_V4_PRO_MODEL_ID = "..."
35
- if "TENCENT_HY3_FREE_MODEL_ID" not in content:
36
- target_line = 'DEEPSEEK_V4_PRO_MODEL_ID = "nvidia/nemotron-3-super-120b-a12b:free"'
37
- insert_block = '''TENCENT_HY3_FREE_MODEL_ID = "openai/tencent/hy3:free"
38
- GEMMA_4_31B_FREE_MODEL_ID = "openai/google/gemma-4-31b-it:free"
39
- LLAMA_3_3_70B_FREE_MODEL_ID = "openai/meta-llama/llama-3.3-70b-instruct:free"
40
- LLAMA_3_1_8B_MODEL_ID = "openai/meta-llama/llama-3.1-8b-instruct"
41
- LAGUNA_M1_FREE_MODEL_ID = "openai/poolside/laguna-m.1:free"
42
- LAGUNA_S21_FREE_MODEL_ID = "openai/poolside/laguna-s-2.1:free"
43
- NEX_N2_MINI_MODEL_ID = "openai/nex-agi/nex-n2-mini"
44
- LING_3_0_FLASH_FREE_MODEL_ID = "openai/inclusionai/ling-3.0-flash:free"
45
- '''
46
- # Only replace the first occurrence (the variable definition)
47
- if target_line in content:
48
- content = content.replace(target_line, insert_block + target_line)
49
- print("OK: Added missing constants")
50
- else:
51
- print("WARN: Could not find target line")
52
-
53
- # Replace all references to ~deepseek deepseek models with 0731 version (quoted strings only)
54
- content = content.replace('"~deepseek/deepseek-v4-flash-latest"', '"deepseek/deepseek-v4-flash-0731"')
55
- content = content.replace('"~deepseek/deepseek-v4-flash-0731"', '"deepseek/deepseek-v4-flash-0731"')
56
-
57
- with open(IDS_FILE, "w") as f:
58
- f.write(content)
59
- print("OK: model_ids.py written")
60
-
61
- ast.parse(content) # validate syntax
62
- print("OK: model_ids.py syntax OK")
63
-
64
- with open(AGENT_FILE) as f:
65
- content = f.read()
66
-
67
- content = content.replace(
68
- "DEFAULT_MODEL_ID = GLM_52_MODEL_ID",
69
- 'DEFAULT_MODEL_ID = "openai/tencent/hy3:free"'
70
- )
71
- content = content.replace(
72
- "DEFAULT_GPT_MODEL_ID = GPT_55_MODEL_ID",
73
- "DEFAULT_GPT_MODEL_ID = LAGUNA_S21_FREE_MODEL_ID"
74
- )
75
-
76
- # Update imports
77
- old_import = (
78
- "from agent.core.model_ids import (\n"
79
- " CLAUDE_OPUS_48_MODEL_ID,\n"
80
- " DEEPSEEK_V4_PRO_MODEL_ID,\n"
81
- " GLM_52_MODEL_ID,\n"
82
- " GPT_55_MODEL_ID,\n"
83
- " KIMI_K27_CODE_MODEL_ID,\n"
84
- " MINIMAX_M3_MODEL_ID,\n"
85
- " strip_huggingface_model_prefix,\n"
86
- ")"
87
- )
88
- new_import = (
89
- "from agent.core.model_ids import (\n"
90
- " DEEPSEEK_V4_PRO_MODEL_ID,\n"
91
- " GLM_52_MODEL_ID,\n"
92
- " GPT_55_MODEL_ID,\n"
93
- " KIMI_K27_CODE_MODEL_ID,\n"
94
- " MINIMAX_M3_MODEL_ID,\n"
95
- " TENCENT_HY3_FREE_MODEL_ID,\n"
96
- " GEMMA_4_31B_FREE_MODEL_ID,\n"
97
- " LLAMA_3_3_70B_FREE_MODEL_ID,\n"
98
- " LLAMA_3_1_8B_MODEL_ID,\n"
99
- " LAGUNA_M1_FREE_MODEL_ID,\n"
100
- " LAGUNA_S21_FREE_MODEL_ID,\n"
101
- " NEX_N2_MINI_MODEL_ID,\n"
102
- " LING_3_0_FLASH_FREE_MODEL_ID,\n"
103
- " strip_huggingface_model_prefix,\n"
104
- ")"
105
- )
106
- content = content.replace(old_import, new_import)
107
-
108
- # Update _available_models to include DeepSeek V4 Flash 0731
109
- new_func = '''def _available_models() -> list[dict[str, Any]]:
110
- models = [
111
- {"id": TENCENT_HY3_FREE_MODEL_ID, "label": "Tencent HY3:free", "recommended": True},
112
- {"id": GEMMA_4_31B_FREE_MODEL_ID, "label": "Gemma 4 31B:free", "recommended": True},
113
- {"id": LLAMA_3_3_70B_FREE_MODEL_ID, "label": "Llama 3.3 70B:free", "recommended": True},
114
- {"id": LAGUNA_M1_FREE_MODEL_ID, "label": "Laguna M.1:free", "recommended": True},
115
- {"id": DEFAULT_GPT_MODEL_ID, "label": "Laguna S 2.1:free", "recommended": True},
116
- {"id": NEX_N2_MINI_MODEL_ID, "label": "nex-agi/nex-n2-mini", "recommended": True},
117
- {"id": LING_3_0_FLASH_FREE_MODEL_ID, "label": "inclusionai/ling-3.0-flash:free", "recommended": True},
118
- {"id": LLAMA_3_1_8B_MODEL_ID, "label": "Llama 3.1 8B"},
119
- {"id": KIMI_K27_CODE_MODEL_ID, "label": "DeepSeek V4 Pro"},
120
- {"id": MINIMAX_M3_MODEL_ID, "label": "DeepSeek V4 Flash"},
121
- {"id": DEFAULT_MODEL_ID, "label": "DeepSeek V4 Flash"},
122
- {"id": DEEPSEEK_V4_PRO_MODEL_ID, "label": "Nemotron 3 Super 120B"},
123
- {"id": "deepseek/deepseek-v4-flash-0731", "label": "DeepSeek V4 Flash 0731", "recommended": True},
124
- ]
125
- return models'''
126
-
127
- func_match = re.search(
128
- r"def _available_models\(\)\s*->\s*list\[dict\[str,\s*Any\]\]:.*?return models",
129
- content, re.DOTALL
130
- )
131
- if func_match:
132
- content = content[:func_match.start()] + new_func + content[func_match.end():]
133
- print("OK: Replaced _available_models() with DeepSeek V4 Flash 0731")
134
- else:
135
- old_func = (
136
- "def _available_models() -> list[dict[str, Any]]:\n"
137
- " models = [\n"
138
- ' {"id": CLAUDE_OPUS_48_MODEL_ID, "label": "Claude Opus 4.8"},\n'
139
- ' {"id": DEFAULT_GPT_MODEL_ID, "label": "GPT-5.5"},\n'
140
- ' {"id": KIMI_K27_CODE_MODEL_ID, "label": "Kimi K2.7 Code"},\n'
141
- ' {"id": MINIMAX_M3_MODEL_ID, "label": "MiniMax M3"},\n'
142
- ' {"id": DEFAULT_MODEL_ID, "label": "GLM 5.2", "recommended": True},\n'
143
- ' {"id": DEEPSEEK_V4_PRO_MODEL_ID, "label": "DeepSeek V4 Pro"},\n'
144
- " ]\n"
145
- " return models"
146
- )
147
- if old_func in content:
148
- content = content.replace(old_func, new_func)
149
- print("OK: String replace of _available_models()")
150
- else:
151
- print("WARN: Could not find _available_models() pattern, searching...")
152
- idx = content.find("def _available_models")
153
- if idx >= 0:
154
- print(f"Found at index {idx}, context: {content[idx:idx+400]}")
155
-
156
- content = content.replace(
157
- '"openai/gpt-oss-120b:cerebras",',
158
- '"huggingface/deepseek-ai/DeepSeek-V4-Pro",'
159
- )
160
-
161
- try:
162
- ast.parse(content)
163
- print("OK: agent.py syntax OK")
164
- except SyntaxError as e:
165
- print(f"FAIL: agent.py syntax error: {e}")
166
- raise
167
-
168
- with open(AGENT_FILE, "w") as f:
169
- f.write(content)
170
-
171
- # Patch llm_params for OpenRouter routing
172
- with open(LLM_PARAMS_FILE) as f:
173
- llm_content = f.read()
174
-
175
- if "normalized_model.startswith" not in llm_content:
176
- api_key_find = "api_key = _resolve_hf_router_token(session_hf_token)"
177
- if api_key_find in llm_content:
178
- line_end = llm_content.find("\n", llm_content.find(api_key_find) + len(api_key_find)) + 1
179
- routing_insert = (
180
- ' # Route deepseek/ models to OpenRouter\n'
181
- ' if normalized_model.startswith("deepseek/"):\n'
182
- ' return {\n'
183
- ' "model": normalized_model,\n'
184
- ' "api_base": "https://openrouter.ai/api/v1",\n'
185
- ' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
186
- ' }\n\n'
187
- ' # Route openai/-prefixed models to OpenRouter\n'
188
- ' if normalized_model.startswith("openai/"):\n'
189
- ' return {\n'
190
- ' "model": normalized_model,\n'
191
- ' "api_base": "https://openrouter.ai/api/v1",\n'
192
- ' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
193
- ' }\n\n'
194
- )
195
- llm_content = llm_content[:line_end] + routing_insert + llm_content[line_end:]
196
- print("OK: Patched _resolve_llm_params with deepseek/ routing")
197
- else:
198
- print("WARN: Could not find api_key line")
199
- else:
200
- print("OK: llm_params.py already patched")
201
-
202
- try:
203
- ast.parse(llm_content)
204
- print("OK: llm_params.py syntax OK")
205
- except SyntaxError as e:
206
- print(f"FAIL: llm_params.py syntax error: {e}")
207
- raise
208
-
209
- with open(LLM_PARAMS_FILE, "w") as f:
210
- f.write(llm_content)
211
-
212
- print("OK: ALL BACKEND PATCHES APPLIED")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_sse_transport.py DELETED
@@ -1,32 +0,0 @@
1
- """Patch sse-chat-transport.ts - handle task_incomplete event."""
2
-
3
- SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
4
-
5
- def patch():
6
- import os
7
- if not os.path.exists(SSE_TRANSPORT):
8
- print(f"SKIP: {SSE_TRANSPORT} not found")
9
- return
10
-
11
- with open(SSE_TRANSPORT, "r", encoding="utf-8") as f:
12
- content = f.read()
13
-
14
- # Add task_incomplete case in createEventToChunkStream
15
- if "case 'task_incomplete'" not in content:
16
- task_incomplete_case = ''' case 'task_incomplete':
17
- sideChannel.onTaskIncomplete(
18
- (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
19
- );
20
- break;
21
- '''
22
- content = content.replace(
23
- "case 'turn_complete':",
24
- task_incomplete_case + "\n case 'turn_complete':"
25
- )
26
-
27
- with open(SSE_TRANSPORT, "w", encoding="utf-8") as f:
28
- f.write(content)
29
- print(f"OK: Patched {SSE_TRANSPORT}")
30
-
31
- if __name__ == "__main__":
32
- patch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
patch_use_agent_chat_full.py DELETED
@@ -1,121 +0,0 @@
1
- """Patch useAgentChat.ts - auto-continue feature."""
2
-
3
- USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
4
-
5
- def patch():
6
- import os
7
- if not os.path.exists(USE_AGENT_CHAT):
8
- print(f"SKIP: {USE_AGENT_CHAT} not found")
9
- return
10
-
11
- with open(USE_AGENT_CHAT, "r", encoding="utf-8") as f:
12
- content = f.read()
13
-
14
- # 1. Add useState to imports
15
- if "useState" not in content:
16
- content = content.replace(
17
- "import { useCallback, useEffect, useMemo, useRef } from 'react';",
18
- "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
19
- )
20
-
21
- # 2. Add state declarations after callbacksRef.current = { ... }
22
- state_decl = '''
23
- // Auto-continue state for free models when task incomplete
24
- const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
25
- const [showAutoContinue, setShowAutoContinue] = useState(false);
26
- const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
27
- incompletePlan: Array<{ id: string; content: string; status: string }>;
28
- } | null>(null);
29
- '''
30
-
31
- if "autoContinueTimerRef" not in content:
32
- content = content.replace(
33
- "callbacksRef.current = { onReady, onError, onSessionDead };",
34
- "callbacksRef.current = { onReady, onError, onSessionDead };" + state_decl
35
- )
36
-
37
- # 3. Add onTaskIncomplete to SideChannelCallbacks interface
38
- if "onTaskIncomplete:" not in content and "onInterrupted:" in content:
39
- content = content.replace(
40
- " onInterrupted: () => void;",
41
- " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
42
- )
43
-
44
- # 4. Add onTaskIncomplete handler in sideChannel
45
- sidechannel_handler = ''' onTaskIncomplete: (incompletePlan) => {
46
- setTaskIncompleteInfo({ incompletePlan });
47
- setShowAutoContinue(true);
48
- // Auto-start 10s timer for free models
49
- if (autoContinueTimerRef.current) {
50
- clearTimeout(autoContinueTimerRef.current);
51
- }
52
- autoContinueTimerRef.current = setTimeout(() => {
53
- startAutoContinue();
54
- }, 10000);
55
- },'''
56
-
57
- if "onTaskIncomplete: (incompletePlan)" not in content:
58
- content = content.replace(
59
- " onInterrupted: () => { /* no-op — handled by stop() caller */ },",
60
- sidechannel_handler + "\n onInterrupted: () => { /* no-op — handled by stop() caller */ },"
61
- )
62
-
63
- # 5. Add auto-continue functions before return
64
- auto_funcs = '''
65
- // -- Auto-continue for free models when task incomplete -----------------
66
- const startAutoContinue = useCallback(() => {
67
- setShowAutoContinue(false);
68
- setTaskIncompleteInfo(null);
69
- if (autoContinueTimerRef.current) {
70
- clearTimeout(autoContinueTimerRef.current);
71
- autoContinueTimerRef.current = null;
72
- }
73
- // Build continuation message from incomplete plan
74
- const incompleteItems = taskIncompleteInfo?.incompletePlan || [];
75
- const continuationText = incompleteItems.length > 0
76
- ? `Tiếp tục từ các task chưa hoàn thành:\\n${incompleteItems.map(i => `- ${i.content}`).join('\\n')}`
77
- : 'Tiếp tục nhiệm vụ.';
78
- chat.sendMessage({
79
- text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`,
80
- metadata: { createdAt: new Date().toISOString() },
81
- });
82
- }, [taskIncompleteInfo, chat]);
83
-
84
- const cancelAutoContinue = useCallback(() => {
85
- setShowAutoContinue(false);
86
- setTaskIncompleteInfo(null);
87
- if (autoContinueTimerRef.current) {
88
- clearTimeout(autoContinueTimerRef.current);
89
- autoContinueTimerRef.current = null;
90
- }
91
- }, []);
92
-
93
- // Cleanup timer on unmount
94
- useEffect(() => {
95
- return () => {
96
- if (autoContinueTimerRef.current) {
97
- clearTimeout(autoContinueTimerRef.current);
98
- }
99
- };
100
- }, []);
101
- '''
102
-
103
- if "startAutoContinue" not in content:
104
- content = content.replace(
105
- '\n return {\n messages: chat.messages,',
106
- auto_funcs + '\n return {\n messages: chat.messages,'
107
- )
108
-
109
- # 6. Update return to include new values
110
- if "showAutoContinue," not in content:
111
- content = content.replace(
112
- "refreshMessages,\n };",
113
- "refreshMessages,\n showAutoContinue,\n taskIncompleteInfo,\n startAutoContinue,\n cancelAutoContinue,\n };"
114
- )
115
-
116
- with open(USE_AGENT_CHAT, "w", encoding="utf-8") as f:
117
- f.write(content)
118
- print(f"OK: Complete patch applied to {USE_AGENT_CHAT}")
119
-
120
- if __name__ == "__main__":
121
- patch()