zames_pro 2.70.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 +6 -0
- package/README.md +8 -0
- package/dist/agent-loop.js +71 -1
- package/dist/config.js +18 -0
- package/dist/i18n/config.js +8 -0
- package/dist/index.js +33 -1
- 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/package.json +1 -1
- package/scripts/self-smoke.mjs +45 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.71.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **subagent:** add the Task tool, enable subagents by default and instruct the model when to delegate
|
|
15
|
+
|
|
10
16
|
## [2.70.0] - 2026-10-08
|
|
11
17
|
|
|
12
18
|
### Added
|
package/README.md
CHANGED
|
@@ -100,6 +100,10 @@ Key pieces:
|
|
|
100
100
|
- **Project context** — reads `AGENTS.md`, `MEMORY.md`, skills (`SKILL.md`) and
|
|
101
101
|
custom commands from the repo and `~/.zames`, the same idea as Codex / Claude
|
|
102
102
|
Code.
|
|
103
|
+
- **Subagents** — the `Task` tool delegates a self-contained sub-task to an
|
|
104
|
+
agent in its own separate chat (isolated context), which returns only a
|
|
105
|
+
report, so heavy research does not flood the main context. On by default;
|
|
106
|
+
tune with `browser.subagents` / `browser.maxSubagents`.
|
|
103
107
|
- **MCP support** — plug in external tool servers (e.g. `@playwright/mcp`).
|
|
104
108
|
- **Scheduling** — `/loop`, `/cron` and `/jobs` repeat tasks on a timer.
|
|
105
109
|
- **Bilingual UI** — Russian / English (`/config lang`).
|
|
@@ -573,6 +577,10 @@ zames не вызывает API модели. Он запускает headless C
|
|
|
573
577
|
- **Контекст проекта** — читает `AGENTS.md`, `MEMORY.md`, навыки (`SKILL.md`) и
|
|
574
578
|
пользовательские команды из репозитория и `~/.zames`, как в Codex / Claude
|
|
575
579
|
Code.
|
|
580
|
+
- **Субагенты** — инструмент `Task` поручает самодостаточную подзадачу агенту
|
|
581
|
+
в отдельном чате (изолированный контекст), который возвращает только отчёт,
|
|
582
|
+
так что тяжёлая разведка не засоряет основной контекст. Включены по
|
|
583
|
+
умолчанию; настраиваются через `browser.subagents` / `browser.maxSubagents`.
|
|
576
584
|
- **Поддержка MCP** — подключение внешних серверов инструментов (например,
|
|
577
585
|
`@playwright/mcp`).
|
|
578
586
|
- **Планировщик** — `/loop`, `/cron` и `/jobs` повторяют задачи по таймеру.
|
package/dist/agent-loop.js
CHANGED
|
@@ -7,7 +7,7 @@ import { substituteAttachmentMarkers } from './commands.js';
|
|
|
7
7
|
import { normText } from './browser.js';
|
|
8
8
|
import { loadHooks, runPreToolUse, runPostToolUse, } from './hooks.js';
|
|
9
9
|
import { loadPermissions, decidePermission, } from './permissions.js';
|
|
10
|
-
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, onAskPermission = undefined, selfImprovement = false, }) {
|
|
10
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, onAskPermission = undefined, selfImprovement = false, onSubagent = null, }) {
|
|
11
11
|
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
12
12
|
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
13
13
|
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
@@ -705,6 +705,76 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
705
705
|
continue;
|
|
706
706
|
}
|
|
707
707
|
for (const call of callsToRun) {
|
|
708
|
+
// `Task` is a SEAM, not an ordinary tool: it is handled by the caller's
|
|
709
|
+
// onSubagent callback (a separate chat with its own context). It is not
|
|
710
|
+
// in `tools` when subagents are disabled, so it is matched by name here
|
|
711
|
+
// BEFORE the unknown-tool branch. With no callback we answer honestly
|
|
712
|
+
// instead of pretending the tool exists.
|
|
713
|
+
if (call.tool === 'Task') {
|
|
714
|
+
const rawPrompt = typeof call.args.prompt === 'string' ? call.args.prompt.trim() : '';
|
|
715
|
+
const rawType = String(call.args.subagent_type ?? '').toLowerCase();
|
|
716
|
+
const subType = rawType === 'explore' ? 'explore' : 'general';
|
|
717
|
+
const rawDesc = typeof call.args.description === 'string' ? call.args.description : '';
|
|
718
|
+
if (!onSubagent || !rawPrompt) {
|
|
719
|
+
const err = !onSubagent
|
|
720
|
+
? 'Subagents are not available in this run. Do the work yourself with the other tools.'
|
|
721
|
+
: 'Task requires a non-empty `prompt` argument.';
|
|
722
|
+
safeToolResult(err);
|
|
723
|
+
transcript?.log('tool_error', { tool: 'Task', error: err });
|
|
724
|
+
results.push({ tool: 'Task', result: err });
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
syncToolAbort();
|
|
728
|
+
safeToolCall('Task', call.args);
|
|
729
|
+
transcript?.log('subagent_start', {
|
|
730
|
+
type: subType,
|
|
731
|
+
description: rawDesc,
|
|
732
|
+
prompt: rawPrompt,
|
|
733
|
+
});
|
|
734
|
+
const poll = setInterval(syncToolAbort, 100);
|
|
735
|
+
if (typeof poll.unref === 'function')
|
|
736
|
+
poll.unref();
|
|
737
|
+
const subStart = Date.now();
|
|
738
|
+
let subResult;
|
|
739
|
+
try {
|
|
740
|
+
subResult = await onSubagent({
|
|
741
|
+
prompt: rawPrompt,
|
|
742
|
+
description: rawDesc,
|
|
743
|
+
type: subType,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
subResult = {
|
|
748
|
+
ok: false,
|
|
749
|
+
text: `Subagent error: ${e.message}`,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
clearInterval(poll);
|
|
754
|
+
}
|
|
755
|
+
const subText = String(subResult.text || '').trim();
|
|
756
|
+
const subOut = subResult.ok
|
|
757
|
+
? subText || '(the subagent returned no report)'
|
|
758
|
+
: `Subagent failed: ${subText || 'unknown error'}`;
|
|
759
|
+
safeToolResult(subOut);
|
|
760
|
+
transcript?.log('subagent_done', {
|
|
761
|
+
type: subType,
|
|
762
|
+
ok: subResult.ok,
|
|
763
|
+
chars: subText.length,
|
|
764
|
+
durationMs: Date.now() - subStart,
|
|
765
|
+
});
|
|
766
|
+
results.push({ tool: 'Task', result: subOut });
|
|
767
|
+
// The subagent switched the chat underneath us and the callback has
|
|
768
|
+
// already restored the parent chat. Clear the stale-echo baseline:
|
|
769
|
+
// the capture left over from the subagent's chat would otherwise make
|
|
770
|
+
// the watchdog compare the parent's next answer against IT and fire.
|
|
771
|
+
lastRaw = '';
|
|
772
|
+
if (browser._abort || browser._stopped) {
|
|
773
|
+
transcript?.log('user_aborted');
|
|
774
|
+
return '(прервано пользователем)';
|
|
775
|
+
}
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
708
778
|
const tool = tools.find((t) => t.name === call.tool);
|
|
709
779
|
if (!tool) {
|
|
710
780
|
const err = `Unknown tool: ${call.tool}`;
|
package/dist/config.js
CHANGED
|
@@ -50,6 +50,10 @@ export const DEFAULTS = {
|
|
|
50
50
|
autoCompact: false,
|
|
51
51
|
autoCompactPct: 95,
|
|
52
52
|
resendPromptOnResume: false,
|
|
53
|
+
// Subagents on by default: the Task tool is what keeps heavy exploration
|
|
54
|
+
// out of the main context, and the main chat is restored afterwards.
|
|
55
|
+
subagents: true,
|
|
56
|
+
maxSubagents: 3,
|
|
53
57
|
deepThinking: false,
|
|
54
58
|
webSearch: true,
|
|
55
59
|
auth: {
|
|
@@ -318,6 +322,20 @@ export const CONFIG_SCHEMA = [
|
|
|
318
322
|
labelKey: 'cfg.f.browser_resendPromptOnResume',
|
|
319
323
|
groupKey: 'cfg.group.browser',
|
|
320
324
|
},
|
|
325
|
+
{
|
|
326
|
+
path: 'browser.subagents',
|
|
327
|
+
type: 'boolean',
|
|
328
|
+
labelKey: 'cfg.f.browser_subagents',
|
|
329
|
+
groupKey: 'cfg.group.browser',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
path: 'browser.maxSubagents',
|
|
333
|
+
type: 'number',
|
|
334
|
+
min: 0,
|
|
335
|
+
max: 20,
|
|
336
|
+
labelKey: 'cfg.f.browser_maxSubagents',
|
|
337
|
+
groupKey: 'cfg.group.browser',
|
|
338
|
+
},
|
|
321
339
|
{
|
|
322
340
|
path: 'browser.deepThinking',
|
|
323
341
|
type: 'boolean',
|
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/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';
|
|
@@ -1121,6 +1122,33 @@ async function main() {
|
|
|
1121
1122
|
// Write the checklist whenever the agent changes it, so a restart/resume
|
|
1122
1123
|
// brings the tasks back (they used to be in-memory only).
|
|
1123
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;
|
|
1124
1152
|
// A compact "tasks: 2/5" summary for the status line. Empty when the list
|
|
1125
1153
|
// is empty, so an idle status is not cluttered.
|
|
1126
1154
|
const todosSummary = () => {
|
|
@@ -3395,6 +3423,9 @@ async function main() {
|
|
|
3395
3423
|
undo,
|
|
3396
3424
|
todos: todoStore,
|
|
3397
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,
|
|
3398
3429
|
});
|
|
3399
3430
|
const tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
3400
3431
|
if (editor)
|
|
@@ -3452,6 +3483,7 @@ async function main() {
|
|
|
3452
3483
|
goal: sessionGoal,
|
|
3453
3484
|
todosQuery: () => todosSummary(),
|
|
3454
3485
|
selfImprovement: devMode,
|
|
3486
|
+
onSubagent: makeSubagentRunner(),
|
|
3455
3487
|
}, inputAttachments);
|
|
3456
3488
|
}
|
|
3457
3489
|
finally {
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.71.0",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/self-smoke.mjs
CHANGED
|
@@ -42,8 +42,9 @@ fs.mkdirSync(WORK, { recursive: true })
|
|
|
42
42
|
|
|
43
43
|
// --- project modules (dynamic: they read HOME at import) --------------------
|
|
44
44
|
const { DeepSeekBrowser } = await import('../src/browser.ts')
|
|
45
|
-
const { createTools } = await import('../src/tools.ts')
|
|
45
|
+
const { createTools, filterToolsForReadOnly } = await import('../src/tools.ts')
|
|
46
46
|
const { runAgentLoop } = await import('../src/agent-loop.ts')
|
|
47
|
+
const { createSubagentRunner } = await import('../src/subagent.ts')
|
|
47
48
|
const { loadConfig } = await import('../src/config.ts')
|
|
48
49
|
const { Transcript } = await import('../src/transcript.ts')
|
|
49
50
|
|
|
@@ -149,6 +150,49 @@ async function main() {
|
|
|
149
150
|
maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
|
|
150
151
|
})
|
|
151
152
|
record('follow-up answered', outC.trim().length > 0, JSON.stringify(outC.slice(0, 80)))
|
|
153
|
+
const onSubagent = createSubagentRunner({
|
|
154
|
+
browser,
|
|
155
|
+
workdir: WORK,
|
|
156
|
+
locale: 'ru',
|
|
157
|
+
runAgentLoop,
|
|
158
|
+
buildTools: (readOnly) => {
|
|
159
|
+
const base = createTools(WORK, {})
|
|
160
|
+
return readOnly ? filterToolsForReadOnly(base) : base
|
|
161
|
+
},
|
|
162
|
+
transcript,
|
|
163
|
+
maxSubagents: 2,
|
|
164
|
+
askDeadlineMs: cfg.browser.askDeadlineMs,
|
|
165
|
+
maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Scenario D — subagents (N36): the model delegates via Task; the runner
|
|
169
|
+
// opens a separate chat, returns only the report, and restores the parent
|
|
170
|
+
// chat. Requires browser.subagents (off by default).
|
|
171
|
+
const subTools = createTools(WORK, { subagents: true })
|
|
172
|
+
const outD = await runAgentLoop({
|
|
173
|
+
browser,
|
|
174
|
+
tools: subTools,
|
|
175
|
+
task:
|
|
176
|
+
'Use the Task tool with subagent_type "explore" to find which word is written in the file smoke.txt. ' +
|
|
177
|
+
'prompt the subagent in a self-contained way. Then, based on its report, call respond with that word.',
|
|
178
|
+
workdir: WORK,
|
|
179
|
+
maxIterations: 20,
|
|
180
|
+
freshChat: true,
|
|
181
|
+
sendSystemPrompt: true,
|
|
182
|
+
transcript,
|
|
183
|
+
onSubagent,
|
|
184
|
+
onToolCall: (n) => console.log(' tool: ' + n),
|
|
185
|
+
onAssistantMessage: (m) => console.log(' answer: ' + m),
|
|
186
|
+
onWarning: (m) => console.log(' warn: ' + m),
|
|
187
|
+
locale: 'ru',
|
|
188
|
+
askDeadlineMs: cfg.browser.askDeadlineMs,
|
|
189
|
+
maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
|
|
190
|
+
})
|
|
191
|
+
record(
|
|
192
|
+
'subagent: parent used the report',
|
|
193
|
+
/HELLO_SMOKE/i.test(outD),
|
|
194
|
+
JSON.stringify(outD.slice(0, 120)),
|
|
195
|
+
)
|
|
152
196
|
}
|
|
153
197
|
|
|
154
198
|
let failed = false
|