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 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` повторяют задачи по таймеру.
@@ -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',
@@ -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.
@@ -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
+ }
@@ -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.70.0",
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": {
@@ -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