zames_pro 2.69.0 → 2.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +8 -0
- package/dist/agent-loop.js +71 -1
- package/dist/browser.js +114 -212
- package/dist/commands.js +14 -0
- package/dist/config.js +18 -0
- package/dist/deepseek-ui.js +225 -0
- package/dist/i18n/config.js +8 -0
- package/dist/i18n/doctor.js +20 -0
- package/dist/index.js +56 -1
- package/dist/net-capture.js +3 -2
- package/dist/run-task.js +2 -1
- package/dist/subagent.js +119 -0
- package/dist/system-prompt.js +38 -1
- package/dist/tools.js +24 -1
- package/dist/ui-health.js +71 -0
- package/package.json +1 -1
- package/scripts/self-smoke.mjs +45 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// DeepSeek UI constants — the SINGLE place where chat.deepseek.com specifics live.
|
|
2
|
+
//
|
|
3
|
+
// WHY this module exists: the agent drives the DeepSeek web UI through
|
|
4
|
+
// Playwright, so every selector, button label and protocol marker is a
|
|
5
|
+
// hardcoded assumption about a page we do not control. Keeping them scattered
|
|
6
|
+
// across browser.ts made a DeepSeek redesign a diffuse, easy-to-miss breakage
|
|
7
|
+
// (a renamed class here, a relabeled button there). Concentrating them here
|
|
8
|
+
// turns "DeepSeek changed the UI" into ONE file to audit, and lets the health
|
|
9
|
+
// check (probeUiHealth) report the exact broken capability to the operator
|
|
10
|
+
// BEFORE a task hangs on it.
|
|
11
|
+
//
|
|
12
|
+
// Priority order everywhere: prefer SEMANTIC anchors (role / aria-label /
|
|
13
|
+
// accessible name / input type) over DeepSeek's `ds-*` class names, which
|
|
14
|
+
// drift between builds. The `ds-*` selectors stay in the chains as a
|
|
15
|
+
// high-precision FIRST hit, with semantic fallbacks after them.
|
|
16
|
+
// ---------- endpoints ----------
|
|
17
|
+
export const CHAT_URL = 'https://chat.deepseek.com/';
|
|
18
|
+
export const CHAT_ID_RE = /\/chat\/s\/([a-zA-Z0-9_-]+)/;
|
|
19
|
+
// DeepSeek's chat URL shape. A direct navigation always yields the requested
|
|
20
|
+
// chat (the sidebar link click is tried first, this is the fallback).
|
|
21
|
+
export const CHAT_PATH_PREFIX = 'a/chat/s/';
|
|
22
|
+
export function chatUrl(id) {
|
|
23
|
+
return CHAT_URL + CHAT_PATH_PREFIX + id;
|
|
24
|
+
}
|
|
25
|
+
// The history endpoint returns EVERY message of a chat as JSON (the rendered
|
|
26
|
+
// page virtualizes long chats). Requested from INSIDE the page so the auth
|
|
27
|
+
// cookie and PoW handling are the browser's; see browser.fetchChatMessages.
|
|
28
|
+
export const HISTORY_MESSAGES_URL = 'https://chat.deepseek.com/api/v0/chat/history_messages';
|
|
29
|
+
// ---------- input ----------
|
|
30
|
+
export const INPUT_SELECTORS = [
|
|
31
|
+
'textarea',
|
|
32
|
+
'div[contenteditable="true"]',
|
|
33
|
+
'[role="textbox"]',
|
|
34
|
+
];
|
|
35
|
+
// ---------- send ----------
|
|
36
|
+
// IMPORTANT: you MUST NOT add the generic
|
|
37
|
+
// 'div[role="button"][class*="ds-button--primary"]' here — it matches the
|
|
38
|
+
// STOP button too, and then _isGenerating() always returns true, so an answer
|
|
39
|
+
// is never considered ready.
|
|
40
|
+
export const SEND_SELECTORS = [
|
|
41
|
+
'div[role="button"].ds-button--primary.ds-button--circle',
|
|
42
|
+
'div[role="button"].ds-button--primary.ds-button--filled',
|
|
43
|
+
'button[type="submit"]',
|
|
44
|
+
'button[aria-label*="send" i]',
|
|
45
|
+
'button[aria-label*="отправ" i]',
|
|
46
|
+
];
|
|
47
|
+
// DeepSeek's own button classes. Used as a coarse "is this a DeepSeek button?"
|
|
48
|
+
// filter in the raw-DOM fallbacks for Stop and for the send/stop icon
|
|
49
|
+
// detection. Kept as a source string because the fallback runs INSIDE
|
|
50
|
+
// page.evaluate, which cannot receive a RegExp object.
|
|
51
|
+
export const BUTTON_CLASS_RE = /ds-button--(circle|primary|filled)/i;
|
|
52
|
+
// ---------- stop ----------
|
|
53
|
+
export const STOP_SELECTORS = [
|
|
54
|
+
'div[role="button"][aria-label*="stop" i]',
|
|
55
|
+
'div[role="button"][aria-label*="останов" i]',
|
|
56
|
+
'button:has-text("Stop")',
|
|
57
|
+
'button:has-text("Остановить")',
|
|
58
|
+
'button[aria-label*="Stop" i]',
|
|
59
|
+
];
|
|
60
|
+
// Accessible name of the Stop button, for a TRUSTED Playwright click. A bare
|
|
61
|
+
// stop word (optionally with a short suffix), anchored and length-limited so a
|
|
62
|
+
// "Stop" inside rendered prose is never clicked.
|
|
63
|
+
export const STOP_NAME_RE = /^(?:stop|остановить)(?:\s+\S{0,20})?\s*[.!…]?$/i;
|
|
64
|
+
// ---------- continue ----------
|
|
65
|
+
// DeepSeek's "Continue" button. With the reasoning ("Deep thinking") toggle on
|
|
66
|
+
// the server caps the THINK phase: the reasoning stops mid-way and the UI
|
|
67
|
+
// offers a Continue button (NOT the same as a truncated turn with
|
|
68
|
+
// generation_err — there the answer FAILED; here the model paused). The agent
|
|
69
|
+
// must click it so the reasoning/answer keeps flowing.
|
|
70
|
+
//
|
|
71
|
+
// Reasoning mode uses a LONGER label («Продолжить размышление» / «Continue
|
|
72
|
+
// thinking»), not a bare «Continue», so the match is a regex: a bare continue
|
|
73
|
+
// word OR the word plus a short reasoning/answer suffix. It is anchored and
|
|
74
|
+
// length-limited, so a "Continue" inside rendered prose is never clicked.
|
|
75
|
+
export const CONTINUE_NAME_RE = /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i;
|
|
76
|
+
// ---------- messages / answers ----------
|
|
77
|
+
// The rendered assistant answer. The FIRST hit is preferred (most specific),
|
|
78
|
+
// so `ds-markdown` stays ahead of the generic `markdown` catch-all.
|
|
79
|
+
export const ANSWER_SELECTORS = [
|
|
80
|
+
'div.ds-assistant-message-main-content',
|
|
81
|
+
'div[class*="ds-assistant-message-main-content"]',
|
|
82
|
+
'div[class*="ds-markdown"]',
|
|
83
|
+
'div[class*="markdown"]',
|
|
84
|
+
];
|
|
85
|
+
// Reasoning blocks. They are NOT the answer and must never be picked up as
|
|
86
|
+
// one, otherwise the terminal would show the model's long reasoning.
|
|
87
|
+
export const THINK_CLASS_RE = /ds-think-content|thinking-content/i;
|
|
88
|
+
// Last-resort answer-only selectors for the restored-dialogue scraper. The
|
|
89
|
+
// generic `markdown` catch-all is deliberately absent here: without a message
|
|
90
|
+
// container it would also match a user bubble that happens to render markdown.
|
|
91
|
+
export const RESTORE_ANSWER_SELECTORS = [
|
|
92
|
+
'div.ds-assistant-message-main-content',
|
|
93
|
+
'div[class*="ds-assistant-message-main-content"]',
|
|
94
|
+
'div[class*="ds-markdown"]',
|
|
95
|
+
];
|
|
96
|
+
// Message-level containers for the restored dialogue (broad first).
|
|
97
|
+
export const MESSAGE_CONTAINER_SELECTORS = [
|
|
98
|
+
'[data-message-id]',
|
|
99
|
+
'[class*="ds-message"]',
|
|
100
|
+
'[class*="chat-message"]',
|
|
101
|
+
'[class*="message-item"]',
|
|
102
|
+
'[class*="_message"]',
|
|
103
|
+
];
|
|
104
|
+
// A message block is "assistant" when it carries the assistant class OR a
|
|
105
|
+
// rendered markdown wrapper (a user bubble has no rendered markdown). Used by
|
|
106
|
+
// the restored-dialogue scraper; the class names drift between builds, so the
|
|
107
|
+
// detection is content-based rather than class-prefix-only.
|
|
108
|
+
export const ASSISTANT_CLASS_RE = /ds-assistant-message|assistant-message/i;
|
|
109
|
+
export const ASSISTANT_SELECTOR = '[class*="ds-assistant-message"]';
|
|
110
|
+
export const ANSWER_MARKDOWN_SELECTOR = '[class*="ds-markdown"]';
|
|
111
|
+
// A cheap "did the page grow?" signal (_chatSignal): count message nodes and
|
|
112
|
+
// measure the LAST answer node's text length. The old code read
|
|
113
|
+
// document.body.innerText.length every tick, which serializes the WHOLE page
|
|
114
|
+
// (sidebar, history, menus) and forces a layout — expensive on a long chat.
|
|
115
|
+
export const CHAT_SIGNAL_NODES_SELECTOR = '[class*="ds-message"], [class*="chat-message"], .ds-markdown';
|
|
116
|
+
export const MARKDOWN_SELECTOR = '.ds-markdown';
|
|
117
|
+
// ---------- new chat ----------
|
|
118
|
+
// The "New chat" control is NOT a <button>/<a> on the current DeepSeek build
|
|
119
|
+
// (it is a <div>), so matching only button/a missed it. Match any
|
|
120
|
+
// clickable-ish element whose text is EXACTLY the new-chat label (an anchored
|
|
121
|
+
// regex keeps a big outer container from matching its long descendant text).
|
|
122
|
+
export const NEW_CHAT_SELECTOR = 'button, a, [role="button"], div, span';
|
|
123
|
+
export const NEW_CHAT_TEXT_RE = /^\s*(new chat|новый чат|новый диалог)\s*$/i;
|
|
124
|
+
// ---------- login ----------
|
|
125
|
+
// The login page is served on the same origin and swaps in a password field;
|
|
126
|
+
// the exact classes change, so we match loosely on input types and the known
|
|
127
|
+
// placeholder/autocomplete attributes.
|
|
128
|
+
export const PASSWORD_SELECTORS = [
|
|
129
|
+
'input[type="password"]',
|
|
130
|
+
'input[autocomplete="current-password"]',
|
|
131
|
+
'input[autocomplete="new-password"]',
|
|
132
|
+
];
|
|
133
|
+
export const LOGIN_SELECTORS = [
|
|
134
|
+
'input[type="email"]',
|
|
135
|
+
'input[type="tel"]',
|
|
136
|
+
'input[name="email"]',
|
|
137
|
+
'input[name="phone"]',
|
|
138
|
+
'input[name="username"]',
|
|
139
|
+
'input[placeholder*="email" i]',
|
|
140
|
+
'input[placeholder*="phone" i]',
|
|
141
|
+
'input[placeholder*="телефон" i]',
|
|
142
|
+
'input[placeholder*="почт" i]',
|
|
143
|
+
'input[autocomplete="username"]',
|
|
144
|
+
'input[autocomplete="email"]',
|
|
145
|
+
];
|
|
146
|
+
// Any login-ish button: a primary DeepSeek button or a plain <button>. The
|
|
147
|
+
// visible label is filtered by LOGIN_NAME_RE so "Log in with Google" is not
|
|
148
|
+
// hit by mistake.
|
|
149
|
+
export const LOGIN_BUTTON_SELECTOR = 'div[role="button"].ds-button--primary, button';
|
|
150
|
+
export const LOGIN_NAME_RE = /^\s*(log ?in|sign ?in|войти)\s*$/i;
|
|
151
|
+
// A looser label test used inside the form fallback: a Continue button can also
|
|
152
|
+
// submit the login step on some builds.
|
|
153
|
+
export const LOGIN_FORM_SUBMIT_RE = /log ?in|sign ?in|войти|continue|продолж/i;
|
|
154
|
+
export const LOGIN_SUBMIT_SELECTORS = [
|
|
155
|
+
// DeepSeek's real button: a div[role=button] with these classes and a
|
|
156
|
+
// <span class="ds-button__content">Log in</span> inside.
|
|
157
|
+
'div[role="button"].ds-button--primary.ds-button--filled',
|
|
158
|
+
'div[role="button"].ds-button--primary',
|
|
159
|
+
'button[type="submit"]',
|
|
160
|
+
'button:has-text("Log in")',
|
|
161
|
+
'button:has-text("Sign in")',
|
|
162
|
+
'button:has-text("Войти")',
|
|
163
|
+
'div[role="button"]:has-text("Log in")',
|
|
164
|
+
'div[role="button"]:has-text("Sign in")',
|
|
165
|
+
'div[role="button"]:has-text("Войти")',
|
|
166
|
+
];
|
|
167
|
+
// ---------- sidebar / chats ----------
|
|
168
|
+
export const SIDEBAR_TOGGLE_SELECTORS = [
|
|
169
|
+
'button[aria-label*="sidebar" i]',
|
|
170
|
+
'button[aria-label*="история" i]',
|
|
171
|
+
'button[aria-label*="history" i]',
|
|
172
|
+
'button[class*="sidebar-toggle"]',
|
|
173
|
+
'button[class*="sidebarToggle"]',
|
|
174
|
+
];
|
|
175
|
+
// ---------- toggles (deep thinking / web search) ----------
|
|
176
|
+
// DeepSeek exposes two toggle buttons above the input. Their labels are
|
|
177
|
+
// localized, so we match by a loose regex on the visible text and read the
|
|
178
|
+
// state from aria-pressed. `.ds-toggle-button` is the class anchor; the
|
|
179
|
+
// semantic fallback ([aria-pressed] or [role=switch]) is tried when the class
|
|
180
|
+
// is renamed, so a redesign does not silently disable the toggles.
|
|
181
|
+
export const TOGGLE_SELECTORS = [
|
|
182
|
+
'.ds-toggle-button',
|
|
183
|
+
'[role="switch"]',
|
|
184
|
+
'div[role="button"][aria-pressed]',
|
|
185
|
+
'button[aria-pressed]',
|
|
186
|
+
];
|
|
187
|
+
export const DEEP_THINKING_RE = /глубок|deep\s*think/i;
|
|
188
|
+
export const WEB_SEARCH_RE = /поиск|search/i;
|
|
189
|
+
// ---------- attachments ----------
|
|
190
|
+
export const ATTACH_SELECTORS = [
|
|
191
|
+
'div[role="button"][aria-label*="attach" i]',
|
|
192
|
+
'div[role="button"][aria-label*="влож" i]',
|
|
193
|
+
'button[aria-label*="attach" i]',
|
|
194
|
+
'button[aria-label*="влож" i]',
|
|
195
|
+
'[class*="upload"]',
|
|
196
|
+
'[class*="attach"]',
|
|
197
|
+
];
|
|
198
|
+
// ---------- service statuses / errors ----------
|
|
199
|
+
// DeepSeek UI service statuses that are NOT the model's answer. Otherwise the
|
|
200
|
+
// agent takes a status (Reading...) for an answer and breaks parsing.
|
|
201
|
+
export const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
|
|
202
|
+
// DeepSeek's answer when the rate limit is exceeded.
|
|
203
|
+
//
|
|
204
|
+
// IMPORTANT: the generic "try again later" / "повторите позже" tail is NOT
|
|
205
|
+
// part of this pattern. DeepSeek appends it to MANY toasts ("Server busy.
|
|
206
|
+
// Try again later.", "Service unavailable. Try again later."), so matching it
|
|
207
|
+
// here classified every server hiccup as a RATE LIMIT and sent the agent into
|
|
208
|
+
// a 5-minute wait. The rate limit is recognized by its specific wording only.
|
|
209
|
+
export const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|сообщени[яе] слишком част)/i;
|
|
210
|
+
// Transient server-side hiccups (DeepSeek overloaded / hiccup). Unlike the
|
|
211
|
+
// rate limit, these usually clear in seconds, so we retry quickly instead of
|
|
212
|
+
// waiting minutes.
|
|
213
|
+
export const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
|
|
214
|
+
// ---------- network protocol (SSE) ----------
|
|
215
|
+
export const GENERATION_ERR_RE = /"finish_reason":\s*"generation_err"/i;
|
|
216
|
+
export const INCOMPLETE_STATUS_RE = /"quasi_status","v":"INCOMPLETE"/i;
|
|
217
|
+
// A turn that ENDED (`quasi_status: FINISHED`) but produced NO answer text —
|
|
218
|
+
// the reasoning was generated and the model stopped WITHOUT a RESPONSE
|
|
219
|
+
// fragment. The UI shows Stopped + Continue.
|
|
220
|
+
export const FINISHED_STATUS_RE = /"quasi_status","v":"FINISHED"/i;
|
|
221
|
+
export const RESPONSE_CONTENT_RE = /"type":"RESPONSE","content":"[^"]/;
|
|
222
|
+
// Only a completion/continue turn carries a real answer. Gating the capture
|
|
223
|
+
// assignment to these URLs keeps a future endpoint (which happens to return a
|
|
224
|
+
// `content`/`text` field) from clobbering the answer and looking fresh.
|
|
225
|
+
export const ANSWER_URL_RE = /chat\/(completion|continue)/i;
|
package/dist/i18n/config.js
CHANGED
|
@@ -119,6 +119,14 @@ export const configMessages = {
|
|
|
119
119
|
ru: 'Пересылать системный промпт при возобновлении чата',
|
|
120
120
|
en: 'Resend the system prompt on chat resume',
|
|
121
121
|
},
|
|
122
|
+
'cfg.f.browser_subagents': {
|
|
123
|
+
ru: 'Субагенты: инструмент Task (отдельный чат)',
|
|
124
|
+
en: 'Subagents: the Task tool (separate chat)',
|
|
125
|
+
},
|
|
126
|
+
'cfg.f.browser_maxSubagents': {
|
|
127
|
+
ru: 'Максимум субагентов на задачу',
|
|
128
|
+
en: 'Max subagents per task',
|
|
129
|
+
},
|
|
122
130
|
'cfg.f.browser_deepThinking': {
|
|
123
131
|
ru: 'Глубокое мышление (долго; размышления не выводятся)',
|
|
124
132
|
en: 'Deep thinking (slow; reasoning is hidden)',
|
package/dist/i18n/doctor.js
CHANGED
|
@@ -38,6 +38,26 @@ export const doctorMessages = {
|
|
|
38
38
|
ru: 'UA ещё не закэширован (первый запуск перезапустится один раз)',
|
|
39
39
|
en: 'no cached UA yet (first start relaunches once)',
|
|
40
40
|
},
|
|
41
|
+
'doctor.ui_ok': {
|
|
42
|
+
ru: 'все ключевые элементы найдены',
|
|
43
|
+
en: 'all key elements found',
|
|
44
|
+
},
|
|
45
|
+
'doctor.ui_degraded': {
|
|
46
|
+
ru: 'часть элементов не найдена: {v} (агент может работать некорректно)',
|
|
47
|
+
en: 'some elements not found: {v} (the agent may misbehave)',
|
|
48
|
+
},
|
|
49
|
+
'doctor.ui_broken': {
|
|
50
|
+
ru: 'НЕ найдено критичное: {v} — DeepSeek сменил вёрстку, задачи не будут отправляться. Запусти /debug-dom.',
|
|
51
|
+
en: 'MISSING critical: {v} — DeepSeek changed the markup, tasks will not send. Run /debug-dom.',
|
|
52
|
+
},
|
|
53
|
+
'ui.probe_failed': {
|
|
54
|
+
ru: '⚠ DeepSeek UI: не найдено критичное ({v}) — возможно, сменилась вёрстка chat.deepseek.com. Задачи могут не отправляться; проверь /doctor и /debug-dom.',
|
|
55
|
+
en: '⚠ DeepSeek UI: missing critical element(s) ({v}) — chat.deepseek.com markup may have changed. Tasks may not send; check /doctor and /debug-dom.',
|
|
56
|
+
},
|
|
57
|
+
'ui.probe_degraded': {
|
|
58
|
+
ru: '⚠ DeepSeek UI: не найдено ({v}) — часть возможностей (тумблеры/новый чат) может не работать.',
|
|
59
|
+
en: '⚠ DeepSeek UI: not found ({v}) — some features (toggles/new chat) may not work.',
|
|
60
|
+
},
|
|
41
61
|
'adddir.usage': {
|
|
42
62
|
ru: 'Использование: /add-dir <path>',
|
|
43
63
|
en: 'Usage: /add-dir <path>',
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import { existsSync, statSync } from 'fs';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { theme } from './theme.js';
|
|
8
8
|
import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
|
|
9
|
-
import { createTools, mergeTools } from './tools.js';
|
|
9
|
+
import { createTools, mergeTools, filterToolsForReadOnly } from './tools.js';
|
|
10
10
|
import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
|
|
11
11
|
import { runAgentLoop } from './agent-loop.js';
|
|
12
|
+
import { createSubagentRunner } from './subagent.js';
|
|
12
13
|
import { createSpinner } from './spinner.js';
|
|
13
14
|
import { LineEditor } from './input.js';
|
|
14
15
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
@@ -997,6 +998,23 @@ async function main() {
|
|
|
997
998
|
await transcript.close();
|
|
998
999
|
process.exit(1);
|
|
999
1000
|
}
|
|
1001
|
+
// DeepSeek UI health probe. The agent drives a page we do not control, so a
|
|
1002
|
+
// redesign silently breaks selectors and the FIRST symptom is a task hanging
|
|
1003
|
+
// on "input field not found". Probe the always-present controls right after
|
|
1004
|
+
// login and warn the operator up front. Best-effort: a probe failure must
|
|
1005
|
+
// never block startup.
|
|
1006
|
+
const warnUiHealth = (h) => {
|
|
1007
|
+
if (h.missingCritical.length) {
|
|
1008
|
+
console.error(theme.warn(t('ui.probe_failed', { v: h.missingCritical.join(', ') })));
|
|
1009
|
+
}
|
|
1010
|
+
else if (h.missing.length) {
|
|
1011
|
+
console.error(theme.warn(t('ui.probe_degraded', { v: h.missing.join(', ') })));
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
warnUiHealth(await browser.probeUiHealth());
|
|
1016
|
+
}
|
|
1017
|
+
catch { }
|
|
1000
1018
|
// One-shot mode
|
|
1001
1019
|
if (task) {
|
|
1002
1020
|
const baseTools = mod.createTools(currentWorkdir, {
|
|
@@ -1104,6 +1122,33 @@ async function main() {
|
|
|
1104
1122
|
// Write the checklist whenever the agent changes it, so a restart/resume
|
|
1105
1123
|
// brings the tasks back (they used to be in-memory only).
|
|
1106
1124
|
todoStore.onChange = () => persistTodos();
|
|
1125
|
+
// Subagents (BACKLOG N36): the `Task` tool delegates a self-contained
|
|
1126
|
+
// sub-task to a nested agent loop in its OWN chat (context isolation — in
|
|
1127
|
+
// zames "context" IS the chat), then restores this chat and returns only
|
|
1128
|
+
// the report. Sequential by design (one browser, one send slot). On by
|
|
1129
|
+
// default via browser.subagents (its own budget caps the extra sends).
|
|
1130
|
+
const makeSubagentRunner = () => config.browser.subagents
|
|
1131
|
+
? createSubagentRunner({
|
|
1132
|
+
browser,
|
|
1133
|
+
workdir: currentWorkdir,
|
|
1134
|
+
locale: currentLocale,
|
|
1135
|
+
runAgentLoop: mod.runAgentLoop,
|
|
1136
|
+
buildTools: (readOnly) => {
|
|
1137
|
+
const base = mod.createTools(currentWorkdir, {
|
|
1138
|
+
undo,
|
|
1139
|
+
todos: todoStore,
|
|
1140
|
+
readOnly: false,
|
|
1141
|
+
subagents: false,
|
|
1142
|
+
});
|
|
1143
|
+
return readOnly ? filterToolsForReadOnly(base) : base;
|
|
1144
|
+
},
|
|
1145
|
+
transcript,
|
|
1146
|
+
maxSubagents: config.browser.maxSubagents,
|
|
1147
|
+
askDeadlineMs: config.browser.askDeadlineMs,
|
|
1148
|
+
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
1149
|
+
selfImprovement: devMode,
|
|
1150
|
+
})
|
|
1151
|
+
: null;
|
|
1107
1152
|
// A compact "tasks: 2/5" summary for the status line. Empty when the list
|
|
1108
1153
|
// is empty, so an idle status is not cluttered.
|
|
1109
1154
|
const todosSummary = () => {
|
|
@@ -2940,6 +2985,9 @@ async function main() {
|
|
|
2940
2985
|
const mcpStatus = mcpPool
|
|
2941
2986
|
? mcpPool.status()
|
|
2942
2987
|
: { servers: [], toolCount: 0 };
|
|
2988
|
+
// Probe the DeepSeek UI so a redesign shows up as a /doctor row instead
|
|
2989
|
+
// of a task that hangs on a missing input. Best-effort.
|
|
2990
|
+
const uiHealth = await browser.probeUiHealth().catch(() => null);
|
|
2943
2991
|
console.log(theme.system(mod.commands.renderDoctor({
|
|
2944
2992
|
nodeVersion: process.version,
|
|
2945
2993
|
platform: process.platform,
|
|
@@ -2961,6 +3009,9 @@ async function main() {
|
|
|
2961
3009
|
sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
|
|
2962
3010
|
headless: !!headless,
|
|
2963
3011
|
headlessUaCached: await headlessUACacheReady().catch(() => false),
|
|
3012
|
+
uiPresent: uiHealth?.present,
|
|
3013
|
+
uiMissing: uiHealth?.missing,
|
|
3014
|
+
uiMissingCritical: uiHealth?.missingCritical,
|
|
2964
3015
|
}, t)));
|
|
2965
3016
|
continue;
|
|
2966
3017
|
}
|
|
@@ -3372,6 +3423,9 @@ async function main() {
|
|
|
3372
3423
|
undo,
|
|
3373
3424
|
todos: todoStore,
|
|
3374
3425
|
readOnly: planMode,
|
|
3426
|
+
// `Task` is only offered when subagents are enabled AND we are not in
|
|
3427
|
+
// plan (read-only) mode — a `general` subagent could write otherwise.
|
|
3428
|
+
subagents: config.browser.subagents && !planMode,
|
|
3375
3429
|
});
|
|
3376
3430
|
const tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
3377
3431
|
if (editor)
|
|
@@ -3429,6 +3483,7 @@ async function main() {
|
|
|
3429
3483
|
goal: sessionGoal,
|
|
3430
3484
|
todosQuery: () => todosSummary(),
|
|
3431
3485
|
selfImprovement: devMode,
|
|
3486
|
+
onSubagent: makeSubagentRunner(),
|
|
3432
3487
|
}, inputAttachments);
|
|
3433
3488
|
}
|
|
3434
3489
|
finally {
|
package/dist/net-capture.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { FINISHED_STATUS_RE, RESPONSE_CONTENT_RE } from './deepseek-ui.js';
|
|
4
5
|
// Extraction of the RAW model answer text from DeepSeek's network data.
|
|
5
6
|
//
|
|
6
7
|
// Why: the answer is read from the RENDERED DOM, and DeepSeek's rendering
|
|
@@ -175,11 +176,11 @@ export function isFinishedWithoutAnswer(body) {
|
|
|
175
176
|
const b = String(body || '');
|
|
176
177
|
if (!b)
|
|
177
178
|
return false;
|
|
178
|
-
const finished =
|
|
179
|
+
const finished = FINISHED_STATUS_RE.test(b);
|
|
179
180
|
if (!finished)
|
|
180
181
|
return false;
|
|
181
182
|
// A RESPONSE fragment with actual content means the model DID answer.
|
|
182
|
-
if (
|
|
183
|
+
if (RESPONSE_CONTENT_RE.test(b))
|
|
183
184
|
return false;
|
|
184
185
|
// The turn carried reasoning (THINK) or was truncated mid-way; treat the
|
|
185
186
|
// absence of response content as "no answer produced".
|
package/dist/run-task.js
CHANGED
|
@@ -237,7 +237,7 @@ export function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
237
237
|
}
|
|
238
238
|
export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
|
|
239
239
|
const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
|
|
240
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, } = opts;
|
|
240
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, onSubagent = null, } = opts;
|
|
241
241
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
242
242
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
243
243
|
// mode (pipes) - a regular spinner + watchInput.
|
|
@@ -358,6 +358,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
358
358
|
contextLimit,
|
|
359
359
|
getTokenUsage,
|
|
360
360
|
selfImprovement,
|
|
361
|
+
onSubagent,
|
|
361
362
|
});
|
|
362
363
|
// The loop may end WITHOUT a model answer: an exhausted iteration limit
|
|
363
364
|
// or an ask() watchdog. Surface it instead of a silent stop.
|
package/dist/subagent.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Subagent runner (BACKLOG N36). A `Task` tool call from the model is handled
|
|
2
|
+
// by the agent loop as a SEAM (like onAutoCompact): it calls back into the
|
|
3
|
+
// caller, which owns the whole lifecycle. This module builds that callback.
|
|
4
|
+
//
|
|
5
|
+
// Why a SEPARATE chat: in zames "context" IS the DeepSeek chat, so context
|
|
6
|
+
// isolation can only be achieved by opening a new chat, running a nested
|
|
7
|
+
// agent loop there, and restoring the parent chat afterwards. The nested loop
|
|
8
|
+
// returns ONLY the subagent's final `respond` report; the parent never sees
|
|
9
|
+
// the subagent's raw turns, which is the whole point.
|
|
10
|
+
//
|
|
11
|
+
// This is deliberately SEQUENTIAL: one browser, one send slot. Parallel
|
|
12
|
+
// subagents would need a second page and a shared send scheduler (a change to
|
|
13
|
+
// browser.ts, which owns the live Playwright context) — out of scope here.
|
|
14
|
+
/** Hard cap on iterations for one subagent run. */
|
|
15
|
+
export const SUBAGENT_MAX_ITER = 30;
|
|
16
|
+
/**
|
|
17
|
+
* The subagent persona, prepended to the task message. It is deliberately NOT
|
|
18
|
+
* injected as a replacement system prompt: the nested runAgentLoop builds the
|
|
19
|
+
* standard system prompt itself (tool list + call format), and the model needs
|
|
20
|
+
* exactly that to work. The persona only adds role + output contract.
|
|
21
|
+
*/
|
|
22
|
+
export function subagentPersona(type) {
|
|
23
|
+
const base = 'You are a SUBAGENT launched by a main coding agent to complete ONE ' +
|
|
24
|
+
'self-contained sub-task in an ISOLATED chat. You do NOT see the main ' +
|
|
25
|
+
"conversation's history, so rely only on the instructions below and on " +
|
|
26
|
+
'the files/tools available. You cannot talk to the operator, so do not ' +
|
|
27
|
+
'ask questions. When the sub-task is done, call respond with a concise, ' +
|
|
28
|
+
'information-dense report: findings, exact file paths, relevant code ' +
|
|
29
|
+
'snippets and a clear conclusion. Do NOT call respond until the work is ' +
|
|
30
|
+
'complete.';
|
|
31
|
+
if (type === 'explore') {
|
|
32
|
+
return (base +
|
|
33
|
+
'\n\nYou are in EXPLORE (read-only) mode: you may search and read files ' +
|
|
34
|
+
'but you CANNOT modify anything or run mutating commands. Investigate ' +
|
|
35
|
+
'and report your findings.');
|
|
36
|
+
}
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the `onSubagent` callback for runAgentLoop. It enforces the per-task
|
|
41
|
+
* budget, opens a fresh chat, runs a nested loop (with the subagent's own
|
|
42
|
+
* task text and a restricted or full tool set), restores the parent chat and
|
|
43
|
+
* returns only the report.
|
|
44
|
+
*/
|
|
45
|
+
export function createSubagentRunner(opts) {
|
|
46
|
+
const { browser, workdir, locale, runAgentLoop, buildTools, transcript = null, maxSubagents, askDeadlineMs, maxAfterToolRetries, selfImprovement = false, onAskPermission, } = opts;
|
|
47
|
+
let used = 0;
|
|
48
|
+
return async (req) => {
|
|
49
|
+
if (used >= maxSubagents) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
text: 'Subagent budget exhausted (' +
|
|
53
|
+
maxSubagents +
|
|
54
|
+
' per task). Do the remaining work yourself with the other tools.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
used++;
|
|
58
|
+
const parentChatId = await browser.getCurrentChatId().catch(() => null);
|
|
59
|
+
const subTools = buildTools(req.type === 'explore');
|
|
60
|
+
const taskText = subagentPersona(req.type) + '\n\n--- SUB-TASK ---\n' + req.prompt;
|
|
61
|
+
// Save the parent's send hooks: the nested loop OVERWRITES them on
|
|
62
|
+
// browser, and leaving the nested (no-op) callbacks in place would kill
|
|
63
|
+
// the parent's spinner/status for the rest of the task.
|
|
64
|
+
const savedHooks = {
|
|
65
|
+
onSendStart: browser.onSendStart ?? null,
|
|
66
|
+
onSendPause: browser.onSendPause ?? null,
|
|
67
|
+
onSendState: browser.onSendState ?? null,
|
|
68
|
+
onNotice: browser.onNotice ?? null,
|
|
69
|
+
};
|
|
70
|
+
let report = '';
|
|
71
|
+
let failed = '';
|
|
72
|
+
try {
|
|
73
|
+
report = await runAgentLoop({
|
|
74
|
+
browser,
|
|
75
|
+
tools: subTools,
|
|
76
|
+
task: taskText,
|
|
77
|
+
workdir,
|
|
78
|
+
maxIterations: SUBAGENT_MAX_ITER,
|
|
79
|
+
// A fresh chat gives the subagent an isolated context; the nested loop
|
|
80
|
+
// opens it AND sends the standard system prompt itself.
|
|
81
|
+
freshChat: true,
|
|
82
|
+
sendSystemPrompt: true,
|
|
83
|
+
transcript,
|
|
84
|
+
locale,
|
|
85
|
+
askDeadlineMs,
|
|
86
|
+
maxAfterToolRetries,
|
|
87
|
+
selfImprovement,
|
|
88
|
+
// No recursion: the subagent cannot spawn its own subagents.
|
|
89
|
+
onSubagent: null,
|
|
90
|
+
// Do not persist the subagent chat as a session.
|
|
91
|
+
onChatReady: () => { },
|
|
92
|
+
onAskPermission: onAskPermission
|
|
93
|
+
? (info) => onAskPermission({
|
|
94
|
+
tool: info.tool,
|
|
95
|
+
reason: info.reason,
|
|
96
|
+
action: info.action,
|
|
97
|
+
})
|
|
98
|
+
: undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
failed = e.message;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
// Restore the parent chat, then the hooks, so a failure in openChat
|
|
106
|
+
// still leaves the hooks correct.
|
|
107
|
+
if (parentChatId) {
|
|
108
|
+
await browser.openChat(parentChatId).catch(() => false);
|
|
109
|
+
}
|
|
110
|
+
browser.onSendStart = savedHooks.onSendStart;
|
|
111
|
+
browser.onSendPause = savedHooks.onSendPause;
|
|
112
|
+
browser.onSendState = savedHooks.onSendState;
|
|
113
|
+
browser.onNotice = savedHooks.onNotice;
|
|
114
|
+
}
|
|
115
|
+
if (failed)
|
|
116
|
+
return { ok: false, text: failed };
|
|
117
|
+
return { ok: true, text: report };
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/system-prompt.js
CHANGED
|
@@ -21,6 +21,35 @@ Rules for that note:
|
|
|
21
21
|
- this is the ONE exception to "do not touch unrelated files". If nothing
|
|
22
22
|
worth noting, add nothing.
|
|
23
23
|
`;
|
|
24
|
+
// Guidance for the `Task` tool (subagents), injected only when that tool is in
|
|
25
|
+
// the model's set. Without it the model under-uses subagents (it does every
|
|
26
|
+
// broad search itself and floods the main context) or misuses them (delegates
|
|
27
|
+
// work that needs THIS conversation's history, which the isolated subagent
|
|
28
|
+
// cannot see). Kept short: it competes with the tool list for space.
|
|
29
|
+
const SUBAGENT_SECTION = `
|
|
30
|
+
## Task tool (subagents)
|
|
31
|
+
|
|
32
|
+
The Task tool delegates a self-contained sub-task to a subagent that works in
|
|
33
|
+
its OWN separate chat with an ISOLATED context and returns ONLY a final
|
|
34
|
+
report. The subagent does NOT see this conversation, so the prompt you pass
|
|
35
|
+
must be fully self-contained (goal, exact questions, file paths, context).
|
|
36
|
+
|
|
37
|
+
Use it to PROTECT this chat's context and to parallelize independent work:
|
|
38
|
+
|
|
39
|
+
- Broad exploration whose raw hits would flood this chat ("find every use of
|
|
40
|
+
X", "map how module Y works end to end"): delegate it with
|
|
41
|
+
subagent_type "explore" (read-only) and keep only the report.
|
|
42
|
+
- A self-contained research or verification sub-task you can specify in a
|
|
43
|
+
paragraph: delegate it and continue the main line meanwhile.
|
|
44
|
+
- Several INDEPENDENT investigations: emit several Task calls in ONE batch.
|
|
45
|
+
|
|
46
|
+
Do NOT use it for:
|
|
47
|
+
- work that depends on this conversation's history (the subagent cannot see
|
|
48
|
+
it) - pass everything needed in the prompt, or do the work yourself;
|
|
49
|
+
- a trivial single read/edit you can do directly (a subagent costs a full
|
|
50
|
+
extra chat with its own system prompt and sends);
|
|
51
|
+
- edits that must run strictly in order with the rest of this chat.
|
|
52
|
+
`;
|
|
24
53
|
/** Render the AGENTS.md / MEMORY / skills / commands blocks of the prompt. */
|
|
25
54
|
export function renderContextSection(context) {
|
|
26
55
|
if (!context)
|
|
@@ -112,6 +141,12 @@ export function renderToolSection(tools) {
|
|
|
112
141
|
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
|
|
113
142
|
const t = translate(locale);
|
|
114
143
|
const toolDescriptions = renderToolSection(tools);
|
|
144
|
+
// Emit the subagent guidance ONLY when the Task tool is actually in the set
|
|
145
|
+
// (subagents enabled and not in plan mode), so the prompt never tells the
|
|
146
|
+
// model about a tool it does not have.
|
|
147
|
+
const subagentSection = tools.some((x) => x.name === 'Task')
|
|
148
|
+
? SUBAGENT_SECTION
|
|
149
|
+
: '';
|
|
115
150
|
const gitSection = gitContext
|
|
116
151
|
? `\n## Git context\n\n${gitContext}\n`
|
|
117
152
|
: '\n## Git context\n\nNot a git repository (or git is not installed).\n';
|
|
@@ -186,7 +221,9 @@ Prefer the most specific tool; a wrong choice wastes a turn:
|
|
|
186
221
|
directory -> LS.
|
|
187
222
|
- Git operations -> the Git* tools, not Bash.
|
|
188
223
|
- Reading a file whose path you already know -> Read, not Bash cat.
|
|
189
|
-
|
|
224
|
+
- A broad search/exploration that would flood this chat -> the Task tool
|
|
225
|
+
(a subagent in its own chat) if it is available, then work from its report.
|
|
226
|
+
${subagentSection}
|
|
190
227
|
## MCP tools
|
|
191
228
|
|
|
192
229
|
Some tools are provided by external MCP servers and their names look
|
package/dist/tools.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createExtraTools } from './extraTools.js';
|
|
|
9
9
|
// user-facing. They must be in ENGLISH (like Claude Code / Codex). Only the
|
|
10
10
|
// strings the OPERATOR sees (help, spinner, service messages) are localized —
|
|
11
11
|
// those live in src/i18n.ts (CATALOG) and are picked by the locale setting.
|
|
12
|
-
export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
12
|
+
export function createTools(workdir, { undo, todos, readOnly = false, subagents = false, } = {}) {
|
|
13
13
|
const root = path.resolve(workdir);
|
|
14
14
|
const safe = (p) => safePath(root, p);
|
|
15
15
|
// Extracts text from content/content_base64. base64 is needed because the
|
|
@@ -200,11 +200,34 @@ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
|
200
200
|
parameters: { message: 'string' },
|
|
201
201
|
fn: async ({ message }) => message,
|
|
202
202
|
};
|
|
203
|
+
// `Task` is a SEAM handled by the agent loop (onSubagent), not an ordinary
|
|
204
|
+
// fn. Its body here is only a fallback for a direct unit-test call; the live
|
|
205
|
+
// path never reaches it because agent-loop intercepts the name first.
|
|
206
|
+
const taskTool = {
|
|
207
|
+
name: 'Task',
|
|
208
|
+
description: 'Delegate a self-contained sub-task to a SUBAGENT that works in its own ' +
|
|
209
|
+
'separate chat with an isolated context, then returns ONLY a final ' +
|
|
210
|
+
'report. Use it to keep heavy research out of the main context. ' +
|
|
211
|
+
'subagent_type: "explore" (read-only: search/read, cannot modify) or ' +
|
|
212
|
+
'"general" (full tools). The subagent does NOT see this conversation, so ' +
|
|
213
|
+
'`prompt` must be fully self-contained: state the goal, the exact ' +
|
|
214
|
+
'questions, and any file paths/context it needs. Returns the subagent ' +
|
|
215
|
+
'report as text.',
|
|
216
|
+
parameters: {
|
|
217
|
+
description: 'string',
|
|
218
|
+
prompt: 'string',
|
|
219
|
+
subagent_type: 'string?',
|
|
220
|
+
},
|
|
221
|
+
fn: async () => 'Subagents are not available in this run. Do the work yourself.',
|
|
222
|
+
};
|
|
223
|
+
// In plan (read-only) mode `Task` is dropped too: a `general` subagent could
|
|
224
|
+
// otherwise write through it and defeat the read-only promise.
|
|
203
225
|
const all = [
|
|
204
226
|
...baseTools,
|
|
205
227
|
...extraTools,
|
|
206
228
|
...gitTools,
|
|
207
229
|
...webTools,
|
|
230
|
+
...(subagents && !readOnly ? [taskTool] : []),
|
|
208
231
|
respondTool,
|
|
209
232
|
];
|
|
210
233
|
return readOnly ? filterToolsForReadOnly(all) : all;
|