@yeaft/webchat-agent 1.0.495 → 1.0.496

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.
@@ -1 +1 @@
1
- {"version":"1.0.495"}
1
+ {"version":"1.0.496"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.495",
3
+ "version": "1.0.496",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,90 @@
1
+ /** Sub-agent-local execution policy. No Session config or parent registry is mutated. */
2
+ import { createHash } from 'node:crypto';
3
+ import { ToolRegistry, isToolErrorOutput } from '../tools/registry.js';
4
+
5
+ /** Defaults are safety ceilings, not targets; explicit positive limits override each field. */
6
+ export function resolveSubAgentBudget(budget, persona) {
7
+ return {
8
+ max_tool_calls: persona === 'implementer' ? 128 : 64,
9
+ wall_time_ms: 15 * 60 * 1000,
10
+ ...budget,
11
+ };
12
+ }
13
+
14
+ export function createExecutionStats() {
15
+ return { toolCalls: 0, completedCalls: 0, failedCalls: 0, repeatedResults: 0, recentCalls: [], warning: null };
16
+ }
17
+
18
+ function fingerprint(value) {
19
+ return createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex');
20
+ }
21
+
22
+ /**
23
+ * Structural child-only fence, including aliases and MCP hot registration. The
24
+ * synchronous reservation precedes execute(), so parallel calls cannot overshoot.
25
+ * Parent Active Tool Set checks remain independent and must not be bypassed.
26
+ */
27
+ export class SubAgentToolRegistry extends ToolRegistry {
28
+ constructor({ allows = () => true, agent = null, stopBudget = null } = {}) {
29
+ super();
30
+ this.allows = allows;
31
+ this.agent = agent;
32
+ this.stopBudget = stopBudget;
33
+ this.recentFingerprints = [];
34
+ }
35
+
36
+ register(tool) {
37
+ if (this.allows(tool)) super.register(tool);
38
+ return this;
39
+ }
40
+
41
+ async execute(name, input, ctx = {}) {
42
+ const tool = this.get(name);
43
+ if (!tool) throw new Error(`Unknown or disallowed child tool: ${name}`);
44
+ const agent = this.agent;
45
+ if (!agent) return super.execute(name, input, ctx);
46
+ // Serial dispatch can resume after a tool_start yield; never start a write
47
+ // after cancellation, even when the underlying tool ignores AbortSignal.
48
+ const signal = agent.abortController?.signal;
49
+ if (signal?.aborted) throw new Error(String(signal.reason || 'Sub-agent aborted'));
50
+ const stats = agent.execution || (agent.execution = createExecutionStats());
51
+ const limit = agent.budget?.max_tool_calls;
52
+ if (limit !== undefined && stats.toolCalls >= limit) {
53
+ const reason = `max_tool_calls (${limit}) reached; return partial evidence to the parent before extending scope`;
54
+ this.stopBudget?.(reason);
55
+ throw new Error(reason);
56
+ }
57
+ stats.toolCalls += 1;
58
+ agent.usage ||= { tokens: 0, turns: 0, startedAt: Date.now() };
59
+ agent.usage.toolCalls = stats.toolCalls;
60
+ const entry = { name: tool.name, status: 'running' };
61
+ stats.recentCalls.push(entry);
62
+ if (stats.recentCalls.length > 8) stats.recentCalls.shift();
63
+ if (limit && stats.toolCalls >= Math.ceil(limit * 0.75)) {
64
+ stats.warning = 'Tool budget nearly exhausted. Collect the current evidence; do not restart the same investigation.';
65
+ }
66
+ try {
67
+ const output = await super.execute(name, input, ctx);
68
+ const failed = tool.errorOutput === 'json-error-envelope' && isToolErrorOutput(output);
69
+ entry.status = failed ? 'error' : 'completed';
70
+ stats.completedCalls += 1;
71
+ if (failed) stats.failedCalls += 1;
72
+ // Advisory only: repeated results are not proof of semantic non-progress.
73
+ // Read ranges, cursors and changed outputs have distinct fingerprints.
74
+ try {
75
+ if (tool.isReadOnly?.(input) === true) {
76
+ const key = fingerprint([tool.name, input, output]);
77
+ if (this.recentFingerprints.includes(key)) stats.repeatedResults += 1;
78
+ this.recentFingerprints.push(key);
79
+ if (this.recentFingerprints.length > 12) this.recentFingerprints.shift();
80
+ }
81
+ } catch { /* Advisory instrumentation must never turn a successful tool into a retry. */ }
82
+ return output;
83
+ } catch (error) {
84
+ entry.status = 'error';
85
+ stats.completedCalls += 1;
86
+ stats.failedCalls += 1;
87
+ throw error;
88
+ }
89
+ }
90
+ }
@@ -131,6 +131,13 @@ export function diagnoseAgentLiveness(agent, opts = {}) {
131
131
  && msSinceActivity >= thresholdMs;
132
132
  return {
133
133
  ...liveness,
134
+ execution: agent?.execution ? {
135
+ ...agent.execution,
136
+ recentCalls: agent.execution.recentCalls.map(call => ({ ...call })),
137
+ remainingToolCalls: Math.max(0, (agent.budget?.max_tool_calls || 0) - agent.execution.toolCalls),
138
+ limits: agent.budget,
139
+ progressNote: 'Execution counts and repeated results are diagnostics, not proof of semantic progress or stalling.',
140
+ } : null,
134
141
  msSinceLastEvent: liveness.msSinceLastEvent ?? msSinceActivity,
135
142
  stale,
136
143
  stalled: stale,
@@ -36,7 +36,8 @@
36
36
  */
37
37
 
38
38
  import { Engine } from '../engine.js';
39
- import { ToolRegistry } from '../tools/registry.js';
39
+ import { SubAgentToolRegistry, resolveSubAgentBudget, createExecutionStats } from './execution-control.js';
40
+ import { getPersona } from '../personas.js';
40
41
  import { buildSpawnedPreamble } from './spawned-prompt.js';
41
42
  import { STATUS, isTerminalAgentStatus } from './status.js';
42
43
  import { createOutputLog } from './output-log.js';
@@ -83,8 +84,17 @@ const LAST_RESULT_MAX_CHARS = 8 * 1024;
83
84
  * @param {ToolRegistry|null} parentRegistry
84
85
  * @returns {ToolRegistry}
85
86
  */
86
- export function buildChildToolRegistry(parentRegistry) {
87
- const child = new ToolRegistry();
87
+ export function buildChildToolRegistry(parentRegistry, { agent = null, stopBudget = null } = {}) {
88
+ const preset = agent?.personaData || getPersona(agent?.persona);
89
+ // Implementers retain work tools; read-only roles are a structural allowlist.
90
+ // Resolve legacy template names (Read) to canonical FileRead before filtering.
91
+ const allowed = preset && preset.id !== 'implementer'
92
+ ? new Set([...preset.tools.map(name => parentRegistry?.get(name)?.name || (name === 'Read' ? 'FileRead' : name)), 'DiscoverTools'])
93
+ : null;
94
+ const child = new SubAgentToolRegistry({
95
+ agent, stopBudget,
96
+ allows: tool => !RESTRICTED_TOOLS.has(tool.name) && (!allowed || allowed.has(tool.name)),
97
+ });
88
98
  if (!parentRegistry || typeof parentRegistry.getAllTools !== 'function') {
89
99
  return child;
90
100
  }
@@ -149,7 +159,12 @@ export function startSubAgent(agent, deps = {}) {
149
159
  // turns must not pollute the user-facing conversation history. The
150
160
  // memory stores are shared so memory recall still works for the
151
161
  // sub-agent (matches parent VP persona memory).
152
- const childRegistry = buildChildToolRegistry(deps.parentToolRegistry);
162
+ agent.budget = resolveSubAgentBudget(agent.budget, agent.persona);
163
+ agent.execution = agent.execution || createExecutionStats();
164
+ const childRegistry = buildChildToolRegistry(deps.parentToolRegistry, {
165
+ agent,
166
+ stopBudget: reason => stopForBudget(agent, reason),
167
+ });
153
168
  subEngine = new Engine({
154
169
  adapter: deps.adapter,
155
170
  trace: deps.trace,
@@ -193,6 +208,9 @@ export function startSubAgent(agent, deps = {}) {
193
208
  parentVpId: deps.parentVpId || null,
194
209
  agentName: agent.name,
195
210
  mission: agent.mission || agent.task || '',
211
+ expectedOutput: agent.expected_output,
212
+ presetPrompt: (agent.personaData || getPersona(agent.persona))?.systemPrompt,
213
+ budget: agent.budget,
196
214
  language: deps.language ?? deps.config?.language ?? 'en',
197
215
  });
198
216
 
@@ -260,12 +278,21 @@ export function startSubAgent(agent, deps = {}) {
260
278
  function buildWallTimeBudgetResult(agent, reason) {
261
279
  return {
262
280
  status: 'budget_exceeded',
263
- partial_output: agent.partial_output || agent.lastResult || agent.result || '',
281
+ partial_output: agent.partial_output || agent.lastResult
282
+ || (typeof agent.result === 'string' ? agent.result : agent.result?.partial_output) || '',
264
283
  reason,
265
284
  usage: { ...(agent.usage || {}) },
266
285
  };
267
286
  }
268
287
 
288
+ function stopForBudget(agent, reason) {
289
+ if (agent.budgetStopReason || isTerminalAgentStatus(agent.status)) return;
290
+ agent.budgetStopReason = reason;
291
+ agent.result = buildWallTimeBudgetResult(agent, reason);
292
+ agent.partial_output = agent.result.partial_output || '';
293
+ agent.abortController?.abort(reason);
294
+ }
295
+
269
296
  function armWallTimeWatchdog(agent, deps) {
270
297
  const wallTimeMs = agent?.budget?.wall_time_ms;
271
298
  if (typeof wallTimeMs !== 'number' || !Number.isFinite(wallTimeMs) || wallTimeMs <= 0) {
@@ -416,10 +443,15 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
416
443
  continue;
417
444
  }
418
445
 
446
+ // Partial evidence belongs to this query, never a previous follow-up.
447
+ agent.partial_output = '';
448
+ agent.lastResult = '';
449
+ agent.result = '';
419
450
  let assistantText = '';
420
451
  let endedNormally = false;
421
452
  let streamError = null;
422
453
  const turnTokenStart = agent.liveness?.tokenCount || 0;
454
+ const priorUsageTokens = agent.usage?.tokens || 0;
423
455
  let turnUsageTokens = 0;
424
456
  try {
425
457
  const stream = subEngine.query({
@@ -461,9 +493,16 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
461
493
  // child is currently saying, not stale text from the prior
462
494
  // turn.
463
495
  agent.lastResult = capTail(assistantText, LAST_RESULT_MAX_CHARS);
496
+ agent.partial_output = agent.lastResult;
464
497
  }
465
498
  if (evt && evt.type === 'usage') {
466
- turnUsageTokens += (evt.inputTokens || 0) + (evt.outputTokens || 0);
499
+ const cacheTokens = evt.cacheTokensAreIncludedInInput ? 0
500
+ : (evt.cacheReadTokens || 0) + (evt.cacheWriteTokens || 0);
501
+ turnUsageTokens += (evt.inputTokens || 0) + (evt.outputTokens || 0) + cacheTokens;
502
+ agent.usage.tokens = priorUsageTokens + turnUsageTokens;
503
+ if (agent.budget?.max_tokens && agent.usage.tokens >= agent.budget.max_tokens) {
504
+ stopForBudget(agent, `max_tokens (${agent.budget.max_tokens}) reached`);
505
+ }
467
506
  }
468
507
  if (evt && evt.type === 'error' && evt.error) {
469
508
  streamError = evt.error.message || String(evt.error);
@@ -475,10 +514,20 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
475
514
  }
476
515
  }
477
516
  } catch (err) {
478
- transitionTerminal(agent, STATUS.FAILED, {
479
- error: err && err.message ? err.message : String(err),
480
- diagnostic: 'query_error',
481
- deps,
517
+ if (!agent.budgetStopReason) {
518
+ transitionTerminal(agent, STATUS.FAILED, {
519
+ error: err && err.message ? err.message : String(err),
520
+ diagnostic: 'query_error',
521
+ deps,
522
+ });
523
+ return;
524
+ }
525
+ }
526
+
527
+ if (agent.budgetStopReason) {
528
+ agent.result = buildWallTimeBudgetResult(agent, agent.budgetStopReason);
529
+ transitionTerminal(agent, STATUS.COMPLETED, {
530
+ error: agent.budgetStopReason, diagnostic: 'execution_budget', deps,
482
531
  });
483
532
  return;
484
533
  }
@@ -527,6 +576,8 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
527
576
  if (typeof tickAgent === 'function') {
528
577
  const textTokenDelta = Math.max(0, (agent.liveness?.tokenCount || 0) - turnTokenStart);
529
578
  const tokenDelta = turnUsageTokens > 0 ? turnUsageTokens : textTokenDelta;
579
+ // Usage events are exposed live; tickAgent adds the turn delta once.
580
+ agent.usage.tokens = priorUsageTokens;
530
581
  tickResult = tickAgent(agent.id, {
531
582
  turns: 1,
532
583
  tokens: tokenDelta,
@@ -612,14 +663,15 @@ function finalizeTerminal(agent, status, { error, deps } = {}) {
612
663
  };
613
664
  try { agent.outputLog?.write(evt); } catch { /* ignore */ }
614
665
  if (agent.taskId && deps?.taskManager && agent.parentSessionId) {
615
- const taskStatus = status === STATUS.COMPLETED ? 'succeeded'
666
+ const budgetExceeded = agent.result?.status === 'budget_exceeded';
667
+ const taskStatus = budgetExceeded ? 'failed' : status === STATUS.COMPLETED ? 'succeeded'
616
668
  : status === STATUS.CLOSED ? 'cancelled'
617
669
  : 'failed';
618
670
  try {
619
671
  deps.taskManager.completeTask(agent.parentSessionId, agent.taskId, {
620
672
  status: taskStatus,
621
- error: error || agent.error || null,
622
- summary: status === STATUS.COMPLETED
673
+ error: budgetExceeded ? agent.result.reason : (error || agent.error || null),
674
+ summary: budgetExceeded ? JSON.stringify(agent.result) : status === STATUS.COMPLETED
623
675
  ? (typeof agent.result === 'string' ? agent.result : (agent.lastResult || null))
624
676
  : null,
625
677
  });
@@ -25,8 +25,20 @@
25
25
  * @param {'en'|'zh'} [args.language='en']
26
26
  * @returns {string} preamble block (already ## headed, ready to concat)
27
27
  */
28
- export function buildSpawnedPreamble({ parentName, parentVpId, agentName, mission, language = 'en' } = {}) {
29
- const m = (mission || '').trim();
28
+ export function buildSpawnedPreamble({ parentName, parentVpId, agentName, mission, expectedOutput, presetPrompt, budget, language = 'en' } = {}) {
29
+ // Resolve template markers before embedding: the outer VP renderer treats
30
+ // markers as sections of the whole soul and would drop the parent + contract.
31
+ const locale = language === 'zh' || language === 'zh-CN' ? 'zh' : 'en';
32
+ const sections = [...(presetPrompt || '').matchAll(/<!-- lang:(\w+) -->([\s\S]*?)(?=<!-- lang:|$)/g)];
33
+ const rolePrompt = sections.length
34
+ ? (sections.find(section => section[1] === locale) || sections.find(section => section[1] === 'en'))?.[2]?.trim()
35
+ : presetPrompt;
36
+ const contract = [
37
+ rolePrompt || '',
38
+ expectedOutput ? `## expected_output\nReturn the requested structure; mark unverified facts and blockers honestly.\n${JSON.stringify(expectedOutput)}` : '',
39
+ budget ? `## Execution budget\n${JSON.stringify(budget)}\nLimits are ceilings, not targets. Stop once the mission is answered. Return partial findings before exhausting the budget; do not automatically restart the same work.` : '',
40
+ ].filter(Boolean).join('\n\n');
41
+ const m = [(mission || '').trim(), contract].filter(Boolean).join('\n\n');
30
42
  if (language === 'zh') {
31
43
  const lines = [
32
44
  '## 你是 sub-agent',
@@ -40,7 +52,9 @@ export function buildSpawnedPreamble({ parentName, parentVpId, agentName, missio
40
52
  '## 行为约束',
41
53
  '- 不要再 spawn sub-agent(你已经没有 SpawnAgent / PromptAgent / WaitAgent / CloseAgent 工具)。',
42
54
  '- 不要 route_forward 给别的 VP,不要 ask_user。',
43
- '- 完成时直接以 markdown 自由文本回复(end_turn)。建议结构:"## 结果" / "## 关键发现" / "## 遗留问题"。',
55
+ '- 每次调用必须解决一个仍未解决的问题;先检查最小定向结果,再决定是否扩展。不要重复成功读取的范围、同义搜索或仅为轮询而调用工具。',
56
+ '- 不要把子任务扩展成全项目审计;证据已足够回答时立即收敛。工具活动不等于任务进展。',
57
+ '- 完成时遵守 expected_output;未指定则返回结果、关键证据、实际验证及遗留问题。不要用计划或过程评论冒充最终结果。',
44
58
  '- 失败/不可行也要明确说出来,不要假装完成。父 VP 会读你的最终消息。',
45
59
  ];
46
60
  return lines.join('\n');
@@ -57,7 +71,9 @@ export function buildSpawnedPreamble({ parentName, parentVpId, agentName, missio
57
71
  '## Constraints',
58
72
  '- Do NOT spawn further sub-agents (SpawnAgent / PromptAgent / WaitAgent / CloseAgent are not in your toolset).',
59
73
  '- Do NOT use route_forward to other VPs. Do NOT ask_user.',
60
- '- When done, reply in free markdown (end_turn). Suggested structure: "## Result" / "## Key findings" / "## Open questions".',
74
+ '- Each call must resolve a remaining unknown. Inspect the smallest targeted result before expanding; do not repeat successful read ranges, equivalent searches, or poll tools without a concrete need.',
75
+ '- Do not expand the mission into a whole-project audit. Stop once sufficient evidence answers the mission. Tool activity is not evidence of progress.',
76
+ '- When done, follow expected_output if supplied; otherwise return result, key evidence, actual verification, and open questions. Plans and progress commentary are not final deliverables.',
61
77
  '- If the mission is infeasible or you fail, say so plainly. The parent will read your final message.',
62
78
  ];
63
79
  return lines.join('\n');
@@ -24,6 +24,7 @@ import { defineTool } from './types.js';
24
24
  import { randomUUID } from 'crypto';
25
25
  import { getPersona, listPersonaIds } from '../personas.js';
26
26
  import { startSubAgent } from '../sub-agent/runner.js';
27
+ import { resolveSubAgentBudget } from '../sub-agent/execution-control.js';
27
28
  import { STATUS, isTerminalAgentStatus } from '../sub-agent/status.js';
28
29
  import { diagnoseAgentLiveness, makeLiveness } from '../sub-agent/liveness.js';
29
30
  import { TASK_RESULT_DELIVERY } from '../tasks/store.js';
@@ -89,11 +90,12 @@ export function validateSpec(input) {
89
90
  return { ok: false, error: 'spec must be an object' };
90
91
  }
91
92
  const { name, task, mission, expected_output, persona, budget } = input;
92
- if (!name || typeof name !== 'string') {
93
+ if (!cleanString(name)) {
93
94
  return { ok: false, error: 'name is required' };
94
95
  }
95
- if (!task && !mission) {
96
- return { ok: false, error: 'task or mission is required' };
96
+ if ([task, mission].some(value => value !== undefined && !cleanString(value))
97
+ || (!cleanString(task) && !cleanString(mission))) {
98
+ return { ok: false, error: 'task or mission must be a non-empty string' };
97
99
  }
98
100
  if (persona && !getPersona(persona)) {
99
101
  return {
@@ -108,9 +110,10 @@ export function validateSpec(input) {
108
110
  if (typeof budget !== 'object' || budget === null) {
109
111
  return { ok: false, error: 'budget must be an object' };
110
112
  }
111
- for (const k of ['max_tokens', 'max_turns', 'wall_time_ms']) {
112
- if (budget[k] !== undefined && (typeof budget[k] !== 'number' || budget[k] <= 0)) {
113
- return { ok: false, error: `budget.${k} must be a positive number` };
113
+ for (const k of ['max_tokens', 'max_turns', 'wall_time_ms', 'max_tool_calls']) {
114
+ if (budget[k] !== undefined && (!Number.isFinite(budget[k]) || budget[k] <= 0
115
+ || (['max_turns', 'max_tool_calls'].includes(k) && !Number.isSafeInteger(budget[k])))) {
116
+ return { ok: false, error: `budget.${k} must be finite and positive (counts must be integers)` };
114
117
  }
115
118
  }
116
119
  }
@@ -122,7 +125,7 @@ export function validateSpec(input) {
122
125
  task: task || mission,
123
126
  expected_output: expected_output || null,
124
127
  persona: persona || null,
125
- budget: budget || null,
128
+ budget: resolveSubAgentBudget(budget, persona),
126
129
  },
127
130
  };
128
131
  }
@@ -221,8 +224,9 @@ export default defineTool({
221
224
  en: `Create a sub-agent to work on an independent task in parallel.
222
225
 
223
226
  Sub-agents run in their own context and can be given a concrete mission
224
- with an optional expected_output schema. Optional budget limits
225
- (max_tokens/max_turns/wall_time_ms) act as safety cutoffs only when supplied.
227
+ with an optional expected_output schema. Default safety ceilings: 64 actual tool
228
+ executions (128 for implementer) and 15 minutes; budget overrides each field.
229
+ max_tokens is checked during provider usage; max_turns counts query turns, not tools.
226
230
  Pick a preset persona to pre-wire a tool subset and model tier:
227
231
  - explorer : fast, read-only scout (Read/Grep/Glob/ListDir)
228
232
  - implementer: builder with full work tools (primary model)
@@ -232,7 +236,8 @@ Pick a preset persona to pre-wire a tool subset and model tier:
232
236
  Guidelines:
233
237
  - Give a clear, focused mission — what "done" looks like
234
238
  - Use expected_output when the return shape matters
235
- - Add a budget only when you need an explicit safety cutoff
239
+ - Delegate only a bounded independent result; do simple work directly. Set scope, evidence and stopping conditions in mission.
240
+ - Inspect execution counters and partial evidence before extending budgets; do not respawn the same exhausted mission automatically.
236
241
 
237
242
  Async orchestration:
238
243
  1. SpawnAgent — starts the sub-agent as a background task and returns immediately.
@@ -251,7 +256,8 @@ workflow; use bounded WaitAgent calls and inspect liveness instead of blind loop
251
256
  zh: `创建一个子 Agent 并行处理独立任务。
252
257
 
253
258
  子 Agent 在独立上下文中运行,可给定具体 mission 和可选的 expected_output schema。
254
- 可选的预算限制(max_tokens/max_turns/wall_time_ms)仅在设置后作为安全截止。
259
+ 默认安全上限:64 次实际工具执行(implementer 为 128 次)、15 分钟;budget 可逐项覆盖。
260
+ max_tokens 在 provider usage 到达时检查;max_turns 是 query turn 数,不是工具调用数。
255
261
  选择预设 persona 来预配置工具子集和模型层级:
256
262
  - explorer : 快速只读侦察(Read/Grep/Glob/ListDir)
257
263
  - implementer: 具备完整工作工具的构建者(主模型)
@@ -261,7 +267,8 @@ workflow; use bounded WaitAgent calls and inspect liveness instead of blind loop
261
267
  使用指南:
262
268
  - 给出清晰聚焦的 mission——"完成"是什么样子
263
269
  - 当返回结构重要时使用 expected_output
264
- - 仅在需要明确安全截止时添加 budget
270
+ - 只委派有界且独立的结果;简单工作直接做。在 mission 中写清范围、证据和停止条件。
271
+ - 扩大预算前检查实际执行计数和已有证据;不要自动重启同一个耗尽预算的任务。
265
272
 
266
273
  异步编排流程:
267
274
  1. SpawnAgent — 启动子 Agent 作为后台任务并立即返回。
@@ -325,15 +332,19 @@ liveness,不要盲目循环。`
325
332
  max_turns: { type: 'number', description: {
326
333
  en: 'Optional turn ceiling; no default limit is applied',
327
334
  zh: '可选 turn 上限;默认不设限制',
335
+ } },
336
+ max_tool_calls: { type: 'integer', minimum: 1, description: {
337
+ en: 'Actual tool execution ceiling; default 64, or 128 for implementer. Includes parallel and discovered tools.',
338
+ zh: '实际工具执行上限;默认 64,implementer 为 128;包括并行及发现的工具。',
328
339
  } },
329
340
  wall_time_ms: { type: 'number', description: {
330
- en: 'Optional elapsed-time ceiling in milliseconds; no default limit is applied',
331
- zh: '可选耗时上限(毫秒);默认不设限制',
341
+ en: 'Elapsed-time ceiling in milliseconds; default 900000 (15 minutes)',
342
+ zh: '耗时上限(毫秒);默认 900000(15 分钟)',
332
343
  } },
333
344
  },
334
345
  description: {
335
- en: 'Optional safety limits; no max_tokens/max_turns/wall_time_ms defaults are applied. Exceeding an explicit limit returns { status: "budget_exceeded", partial_output, reason }',
336
- zh: '可选安全限制;默认不限制 max_tokens/max_turns/wall_time_ms。超过显式限制会返回 { status: "budget_exceeded", partial_output, reason }',
346
+ en: 'Override default tool/time safety ceilings; token/turn limits are optional. A cutoff returns { status: "budget_exceeded", partial_output, reason }, not successful completion.',
347
+ zh: '覆盖默认工具/时间安全上限;token/turn 限制可选。截止时返回 { status: "budget_exceeded", partial_output, reason },不代表任务成功。',
337
348
  },
338
349
  },
339
350
  cwd: {