@yeaft/webchat-agent 1.0.526 → 1.0.528

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/yeaft/engine.js CHANGED
@@ -65,7 +65,7 @@ import { lookupModelLimitSync } from './llm/models-dev.js';
65
65
  import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
66
66
  import { resolveThinking } from './router/thinking.js';
67
67
  import { approxTokens, computeBudget } from './memory/budget.js';
68
- import { COLLAB_TOOL_POLICY, isToolErrorOutput, localizeVisibleText, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
68
+ import { COLLAB_TOOL_POLICY, isToolErrorOutput, toolValidationError, localizeVisibleText, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
69
69
  import { CONDITIONAL_BUILTIN_TOOL_NAMES, resolveActiveToolNames } from './tools/activation.js';
70
70
  import { discoverToolCapabilities } from './tools/discover-tools.js';
71
71
  import { agentBelongsToScope, getAgentRegistry } from './tools/agent.js';
@@ -2530,6 +2530,8 @@ export class Engine {
2530
2530
  // executions increment these counters; errors and cache reuse do not.
2531
2531
  const queryDuplicateCounts = new Map();
2532
2532
  const queryDuplicateSuppressions = new Map();
2533
+ const queryValidationFailures = new Map();
2534
+ const fileReadObservations = new Map();
2533
2535
  let duplicateReminderAwaitingResponse = false;
2534
2536
  const queryNumber = (this.#__queryCounter = (this.#__queryCounter || 0) + 1);
2535
2537
 
@@ -4281,6 +4283,7 @@ export class Engine {
4281
4283
  };
4282
4284
  return {
4283
4285
  ...toolCtx,
4286
+ fileReadObservations,
4284
4287
  currentToolCall: () => ({ ...stableToolCall }),
4285
4288
  askUser: typeof askUser === 'function'
4286
4289
  ? input => askUser(input, { ...stableToolCall })
@@ -4655,6 +4658,15 @@ export class Engine {
4655
4658
  }));
4656
4659
  }
4657
4660
  }
4661
+ const validationError = isError && !skipped ? toolValidationError(output) : null;
4662
+ if (validationError) {
4663
+ const key = `${duplicateCallKey}:${validationError}`;
4664
+ const count = (queryValidationFailures.get(key) || 0) + 1;
4665
+ queryValidationFailures.set(key, count);
4666
+ if (count === 2) pendingDupReminders.push(
4667
+ `[system note] ${tc.name} rejected the same arguments twice before execution: ${validationError.slice(0, 300)}. Correct the arguments using its schema/error hint or choose a different tool. No operation was performed; repeating unchanged arguments will not help.`,
4668
+ );
4669
+ }
4658
4670
  const toolDurationMs = readyParallelExecution?.durationMs ?? (Date.now() - toolStartTime);
4659
4671
 
4660
4672
  // feat-6af5f9f1 PR B: emit a structured `tool_exec` event for the
@@ -28,122 +28,6 @@
28
28
  import { resolveMemberId } from '../sessions/roster.js';
29
29
  import { createLoopGuard, extendCausedBy } from './loop-guard.js';
30
30
 
31
- const repoApprovals = new WeakMap();
32
- const REPO_APPROVAL_ISSUER_IDS = new Set(['martin']);
33
- export const REPO_APPROVAL_TTL_MS = 2 * 60 * 1000;
34
-
35
- function normalizeId(value) {
36
- return typeof value === 'string' ? value.trim() : '';
37
- }
38
-
39
- function normalizeSha(value) {
40
- const sha = normalizeId(value).toLowerCase();
41
- return /^[0-9a-f]{40}$/.test(sha) ? sha : '';
42
- }
43
-
44
- function normalizeApprovalRepository(value) {
45
- const repository = normalizeId(value).replace(/\.git$/i, '');
46
- const segments = repository.split('/');
47
- if (segments.length === 2) segments.unshift('github.com');
48
- if (segments.length !== 3) return '';
49
- const [host, owner, name] = segments;
50
- const safePart = part => /^[a-z0-9_.-]+$/i.test(part) && part !== '.' && part !== '..';
51
- if (!safePart(host) || !safePart(owner) || !safePart(name) || !host.includes('.')) return '';
52
- return `${host}/${owner}/${name}`.toLowerCase();
53
- }
54
-
55
- function isRepoApprovalIssuer(vpId) {
56
- return REPO_APPROVAL_ISSUER_IDS.has(normalizeId(vpId));
57
- }
58
-
59
- /**
60
- * Mint authority only inside the canonical Router path after roster identity
61
- * resolution. Keeping this function module-private prevents callers from
62
- * turning a claimed issuer string into landing authority.
63
- */
64
- function issueRepoApprovalCapability(input = {}, { now = Date.now } = {}) {
65
- const sessionId = normalizeId(input.sessionId);
66
- const issuerVpId = normalizeId(input.issuerVpId);
67
- const recipientVpId = normalizeId(input.recipientVpId);
68
- const repository = normalizeApprovalRepository(input.repository);
69
- const pr = Number(input.pr);
70
- const baseBranch = normalizeId(input.baseBranch);
71
- const baseSha = normalizeSha(input.baseSha);
72
- const reviewedHead = normalizeSha(input.reviewedHead);
73
- const reviewedSnapshot = normalizeSha(input.reviewedSnapshot);
74
- const issuedAt = Number(now());
75
- if (!sessionId || !isRepoApprovalIssuer(issuerVpId) || !recipientVpId || issuerVpId === recipientVpId
76
- || !repository || !Number.isSafeInteger(pr) || pr <= 0 || !baseBranch || !baseSha
77
- || !reviewedHead || !reviewedSnapshot || !Number.isFinite(issuedAt)) {
78
- return null;
79
- }
80
- const capability = Object.freeze(Object.create(null));
81
- repoApprovals.set(capability, {
82
- sessionId,
83
- issuerVpId,
84
- recipientVpId,
85
- repository,
86
- pr,
87
- baseBranch,
88
- baseSha,
89
- reviewedHead,
90
- reviewedSnapshot,
91
- issuedAt,
92
- expiresAt: issuedAt + REPO_APPROVAL_TTL_MS,
93
- turnId: null,
94
- });
95
- return capability;
96
- }
97
-
98
- export function isRepoApprovalCapability(capability) {
99
- return Boolean(capability && typeof capability === 'object' && repoApprovals.has(capability));
100
- }
101
-
102
- /** Bind a freshly issued capability to the exact Web execution that received it. */
103
- export function bindRepoApprovalCapability(capability, expected = {}, { now = Date.now } = {}) {
104
- if (!capability || typeof capability !== 'object') return false;
105
- const grant = repoApprovals.get(capability);
106
- const turnId = normalizeId(expected.turnId);
107
- const currentTime = Number(now());
108
- if (!grant || !turnId || !Number.isFinite(currentTime)) return false;
109
- if (currentTime > grant.expiresAt
110
- || grant.turnId
111
- || grant.sessionId !== normalizeId(expected.sessionId)
112
- || grant.recipientVpId !== normalizeId(expected.recipientVpId)) {
113
- repoApprovals.delete(capability);
114
- return false;
115
- }
116
- grant.turnId = turnId;
117
- return true;
118
- }
119
-
120
- export function revokeRepoApprovalCapability(capability) {
121
- if (!capability || typeof capability !== 'object') return false;
122
- return repoApprovals.delete(capability);
123
- }
124
-
125
- /** Consume a capability exactly once and verify its complete landing tuple. */
126
- export function consumeRepoApprovalCapability(capability, expected = {}, { now = Date.now } = {}) {
127
- if (!capability || typeof capability !== 'object') return null;
128
- const grant = repoApprovals.get(capability);
129
- repoApprovals.delete(capability);
130
- if (!grant) return null;
131
- const currentTime = Number(now());
132
- const matches = Number.isFinite(currentTime)
133
- && currentTime <= grant.expiresAt
134
- && grant.turnId
135
- && grant.turnId === normalizeId(expected.turnId)
136
- && grant.sessionId === normalizeId(expected.sessionId)
137
- && grant.recipientVpId === normalizeId(expected.recipientVpId)
138
- && grant.repository === normalizeApprovalRepository(expected.repository)
139
- && grant.pr === Number(expected.pr)
140
- && grant.baseBranch === normalizeId(expected.baseBranch)
141
- && grant.baseSha === normalizeSha(expected.baseSha)
142
- && grant.reviewedHead === normalizeSha(expected.reviewedHead)
143
- && grant.reviewedSnapshot === normalizeSha(expected.reviewedSnapshot);
144
- return matches ? Object.freeze({ ...grant }) : null;
145
- }
146
-
147
31
  function routeForwardParentFromEnvelope(envelope) {
148
32
  const msg = envelope?.msg;
149
33
  const meta = msg?.meta;
@@ -265,24 +149,6 @@ export function createRouter(deps = {}) {
265
149
  // spec's intent ("depth of forwards already taken").
266
150
  const chain = extendCausedBy(args.inboundEnvelope || null, null);
267
151
  const routeForwardParent = routeForwardParentFromEnvelope(args.inboundEnvelope);
268
- let repoApproval = null;
269
- if (args.repoApproval !== undefined) {
270
- if (targetVpId === 'all') {
271
- return { ok: false, error: 'repo_approval_requires_single_target' };
272
- }
273
- if (!isRepoApprovalIssuer(senderVpId)) {
274
- return { ok: false, error: 'repo_approval_issuer_forbidden' };
275
- }
276
- repoApproval = issueRepoApprovalCapability({
277
- ...args.repoApproval,
278
- sessionId: meta.id,
279
- issuerVpId: senderVpId,
280
- recipientVpId: targetVpId,
281
- }, { now });
282
- if (!repoApproval) {
283
- return { ok: false, error: 'invalid_repo_approval' };
284
- }
285
- }
286
152
 
287
153
  // Loop guard: for broadcast, use 'all' as the target key so one VP
288
154
  // spamming @all still gets throttled even if each cycle hits different
@@ -319,7 +185,6 @@ export function createRouter(deps = {}) {
319
185
  // of UI replay and future visible history so it doesn't render as a
320
186
  // second assistant/user block after the target VP answers.
321
187
  internal: true,
322
- ...(repoApproval ? { _repoApproval: repoApproval } : {}),
323
188
  meta: {
324
189
  synthetic: true,
325
190
  injectedBy: 'route_forward',
@@ -18,17 +18,20 @@ export function validateBudget(budget) {
18
18
  return null;
19
19
  }
20
20
 
21
- /** Defaults are safety ceilings, not targets; explicit positive limits override each field. */
22
- export function resolveSubAgentBudget(budget, persona) {
23
- return {
24
- max_tool_calls: persona === 'implementer' ? 128 : 64,
25
- wall_time_ms: 15 * 60 * 1000,
26
- ...budget,
27
- };
21
+ /** No implicit lifetime ceiling: only caller-provided positive limits apply. */
22
+ export function resolveSubAgentBudget(budget) {
23
+ return budget && typeof budget === 'object' ? { ...budget } : {};
28
24
  }
29
25
 
30
26
  export function createExecutionStats() {
31
- return { toolCalls: 0, completedCalls: 0, failedCalls: 0, repeatedResults: 0, recentCalls: [], warning: null };
27
+ return {
28
+ toolCalls: 0,
29
+ completedCalls: 0,
30
+ failedCalls: 0,
31
+ repeatedResults: 0,
32
+ recentCalls: [],
33
+ warning: null,
34
+ };
32
35
  }
33
36
 
34
37
  function fingerprint(value) {
@@ -77,8 +80,11 @@ export class SubAgentToolRegistry extends ToolRegistry {
77
80
  const elapsedMs = Date.now() - (agent.usage?.startedAt || Date.now());
78
81
  const nearTime = agent.budget?.wall_time_ms && elapsedMs >= agent.budget.wall_time_ms * 0.75;
79
82
  const updated = agent.controlRevision ? `[Parent control revision ${agent.controlRevision}] Current lifetime ceilings replace the initial preamble: ${JSON.stringify(agent.budget)}. Extra tool grants: ${JSON.stringify(agent.allowTools || [])}. Use DiscoverTools if an allowed tool is not yet visible.\n` : '';
83
+ const remainingTime = agent.budget?.wall_time_ms === undefined
84
+ ? 'wall time unlimited'
85
+ : `${Math.max(0, agent.budget.wall_time_ms - elapsedMs)}ms remaining`;
80
86
  return nearLimit || nearTime || updated ? {
81
- prompt: `${updated}[Sub-agent execution budget] ${stats.toolCalls}/${limit ?? 'unset'} tools, ${llmCalls}/${llmLimit ?? 'unset'} LLM requests used; ${Math.max(0, (agent.budget?.wall_time_ms || 0) - elapsedMs)}ms remaining. Finish the assigned result using existing evidence where possible. Investigate only essential remaining unknowns, then return a conclusion.`,
87
+ prompt: `${updated}[Sub-agent execution budget] ${stats.toolCalls}/${limit ?? 'unlimited'} tools, ${llmCalls}/${llmLimit ?? 'unlimited'} LLM requests used; ${remainingTime}. Finish the assigned result using existing evidence where possible. Investigate only essential remaining unknowns, then return a conclusion.`,
82
88
  } : null;
83
89
  }
84
90
 
@@ -115,6 +121,7 @@ export class SubAgentToolRegistry extends ToolRegistry {
115
121
  throw new Error(`${agent.toolBudgetReason}; no further tools may execute. Return findings from the available evidence.`);
116
122
  }
117
123
  stats.toolCalls += 1;
124
+ if (agent.liveness) agent.liveness.toolUseCount = stats.toolCalls;
118
125
  agent.usage ||= { tokens: 0, turns: 0, startedAt: Date.now() };
119
126
  agent.usage.toolCalls = stats.toolCalls;
120
127
  const entry = { name: tool.name, status: 'running' };
@@ -22,7 +22,8 @@
22
22
  *
23
23
  * @returns {{
24
24
  * toolUseCount: number,
25
- * tokenCount: number,
25
+ * usageTokens: number,
26
+ * outputChars: number,
26
27
  * eventCount: number,
27
28
  * lastEventAt: number,
28
29
  * lastEventType: string|null,
@@ -32,7 +33,8 @@
32
33
  export function makeLiveness() {
33
34
  return {
34
35
  toolUseCount: 0,
35
- tokenCount: 0,
36
+ usageTokens: 0,
37
+ outputChars: 0,
36
38
  eventCount: 0,
37
39
  lastEventAt: 0,
38
40
  lastEventType: null,
@@ -54,12 +56,15 @@ export function bumpLivenessFromEvent(liveness, evt) {
54
56
  liveness.lastEventAt = Date.now();
55
57
  liveness.lastEventType = evt.type || liveness.lastEventType;
56
58
  if (evt.type === 'text_delta' && typeof evt.text === 'string') {
57
- // Coarse "have we produced output" signal. Token count is not exact —
58
- // it's character-based — but it lets the parent see "yes, the model
59
- // is generating".
60
- liveness.tokenCount += evt.text.length;
61
- } else if (evt.type === 'tool_start' || evt.type === 'tool_call') {
62
- liveness.toolUseCount += 1;
59
+ // This is explicitly output volume, never represented as provider tokens.
60
+ liveness.outputChars += evt.text.length;
61
+ } else if (evt.type === 'usage') {
62
+ const cacheTokens = evt.cacheTokensAreIncludedInInput ? 0
63
+ : (evt.cacheReadTokens || 0) + (evt.cacheWriteTokens || 0);
64
+ liveness.usageTokens += (evt.inputTokens || 0) + (evt.outputTokens || 0) + cacheTokens;
65
+ } else if (evt.type === 'tool_start') {
66
+ // Keep the bounded activity trail here. Actual executions are counted at
67
+ // SubAgentToolRegistry.execute(), then copied into liveness by the runner.
63
68
  const name = evt.toolName || evt.name || (evt.tool && evt.tool.name) || null;
64
69
  if (name) {
65
70
  liveness.recentTools.push(name);
@@ -81,7 +86,8 @@ export function snapshotLiveness(liveness, now = Date.now()) {
81
86
  if (!liveness) {
82
87
  return {
83
88
  toolUseCount: 0,
84
- tokenCount: 0,
89
+ usageTokens: 0,
90
+ outputChars: 0,
85
91
  eventCount: 0,
86
92
  lastEventAt: null,
87
93
  msSinceLastEvent: null,
@@ -91,7 +97,8 @@ export function snapshotLiveness(liveness, now = Date.now()) {
91
97
  }
92
98
  return {
93
99
  toolUseCount: liveness.toolUseCount,
94
- tokenCount: liveness.tokenCount,
100
+ usageTokens: liveness.usageTokens,
101
+ outputChars: liveness.outputChars,
95
102
  eventCount: liveness.eventCount,
96
103
  lastEventAt: liveness.lastEventAt || null,
97
104
  msSinceLastEvent: liveness.lastEventAt ? Math.max(0, now - liveness.lastEventAt) : null,
@@ -134,7 +141,8 @@ export function diagnoseAgentLiveness(agent, opts = {}) {
134
141
  execution: agent?.execution ? {
135
142
  ...agent.execution,
136
143
  recentCalls: agent.execution.recentCalls.map(call => ({ ...call })),
137
- remainingToolCalls: Math.max(0, (agent.budget?.max_tool_calls || 0) - agent.execution.toolCalls),
144
+ remainingToolCalls: agent.budget?.max_tool_calls === undefined ? null
145
+ : Math.max(0, agent.budget.max_tool_calls - agent.execution.toolCalls),
138
146
  limits: { ...agent.budget },
139
147
  llmCalls: agent.usage?.llmCalls || 0,
140
148
  reportingLlmCalls: agent.usage?.reportingLlmCalls || 0,
@@ -151,7 +159,7 @@ export function diagnoseAgentLiveness(agent, opts = {}) {
151
159
  stalled: stale,
152
160
  stallThresholdMs: thresholdMs,
153
161
  diagnostic: stale
154
- ? `No sub-agent activity for ${msSinceActivity}ms; treat it as stalled instead of waiting in a loop.`
162
+ ? `No observable sub-agent event for ${msSinceActivity}ms. This is diagnostic only: the provider or tool may still be working; inspect the log before deciding whether to cancel.`
155
163
  : null,
156
164
  };
157
165
  }
@@ -17,7 +17,7 @@
17
17
  * after PromptAgent
18
18
  * - a durable output log at ~/.yeaft/sub-agents/<agentId>.log mirroring
19
19
  * every onEvent (see output-log.js)
20
- * - a liveness snapshot (toolUseCount, tokenCount, lastEventAt, …) the
20
+ * - a liveness snapshot (toolUseCount, usageTokens, outputChars, lastEventAt, …) the
21
21
  * parent reads through WaitAgent / ListAgents
22
22
  *
23
23
  * The runner is fire-and-forget: `startSubAgent(agent, deps)` schedules a
@@ -60,8 +60,8 @@ async function loadTickAgent() {
60
60
  return _tickAgent;
61
61
  }
62
62
 
63
- /** How long an idle sub-agent may wait for a follow-up before the watchdog reaps it. */
64
- const IDLE_ABANDON_MS = 5 * 60 * 1000; // 5 minutes
63
+ /** Retained idle agents have no implicit lifetime deadline. */
64
+ const IDLE_ABANDON_MS = 0;
65
65
 
66
66
  /** Cap on agent.lastResult (mid-stream preview) — keeps memory bounded. */
67
67
  const LAST_RESULT_MAX_CHARS = 8 * 1024;
@@ -137,7 +137,7 @@ export function startSubAgent(agent, deps = {}) {
137
137
  // turns must not pollute the user-facing conversation history. The
138
138
  // memory stores are shared so memory recall still works for the
139
139
  // sub-agent (matches parent VP persona memory).
140
- agent.budget = resolveSubAgentBudget(agent.budget, agent.persona);
140
+ agent.budget = resolveSubAgentBudget(agent.budget);
141
141
  agent.execution = agent.execution || createExecutionStats();
142
142
  const childRegistry = buildChildToolRegistry(deps.parentToolRegistry, { agent });
143
143
  subEngine = new Engine({
@@ -250,9 +250,8 @@ export function startSubAgent(agent, deps = {}) {
250
250
  * liveness + lastResult.
251
251
  * 3. Stash the final assistant text on agent.result, tickAgent for
252
252
  * budget enforcement, mark idle.
253
- * 4. Wait for either a new PromptAgent (status flips to running) OR
254
- * CloseAgent (status=='closed') OR the idle watchdog firing
255
- * (status=='abandoned').
253
+ * 4. Wait for PromptAgent or CloseAgent. An idle abandonment timeout is
254
+ * available only when the embedding caller explicitly configures one.
256
255
  */
257
256
  function buildWallTimeBudgetResult(agent, reason) {
258
257
  return {
@@ -311,7 +310,7 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
311
310
  wallTimeWatchdog = armWallTimeWatchdog(agent, deps);
312
311
  };
313
312
  agent.rearmWallTimeWatchdog();
314
- const idleAbandonMs = typeof deps.idleAbandonMs === 'number' && deps.idleAbandonMs > 0
313
+ const idleAbandonMs = Number.isFinite(deps.idleAbandonMs) && deps.idleAbandonMs > 0
315
314
  ? deps.idleAbandonMs : IDLE_ABANDON_MS;
316
315
 
317
316
  const wrapEvt = (evt) => ({
@@ -399,8 +398,8 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
399
398
  while (!isTerminalAgentStatus(agent.status)) {
400
399
  const queuedPrompt = dequeueNextUserPrompt();
401
400
  if (!queuedPrompt) {
402
- // No queued work — go idle and wait for PromptAgent / CloseAgent /
403
- // watchdog.
401
+ // No queued work — retain the agent for PromptAgent / CloseAgent.
402
+ // A caller-provided idleAbandonMs may opt into automatic cleanup.
404
403
  agent.status = STATUS.IDLE;
405
404
  agent.idleSince = Date.now();
406
405
  emit({ type: 'sub_agent_status', status: STATUS.IDLE });
@@ -444,7 +443,6 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
444
443
  let budgetReportText = '';
445
444
  let endedNormally = false;
446
445
  let streamError = null;
447
- const turnTokenStart = agent.liveness?.tokenCount || 0;
448
446
  const priorUsageTokens = agent.usage?.tokens || 0;
449
447
  let turnUsageTokens = 0;
450
448
  try {
@@ -594,13 +592,12 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
594
592
  try {
595
593
  const tickAgent = await loadTickAgent();
596
594
  if (typeof tickAgent === 'function') {
597
- const textTokenDelta = Math.max(0, (agent.liveness?.tokenCount || 0) - turnTokenStart);
598
- const tokenDelta = turnUsageTokens > 0 ? turnUsageTokens : textTokenDelta;
599
- // Usage events are exposed live; tickAgent adds the turn delta once.
595
+ // Provider usage is authoritative. If a provider omits usage, keep the
596
+ // count unknown/unchanged rather than disguising output characters as tokens.
600
597
  agent.usage.tokens = priorUsageTokens;
601
598
  tickResult = tickAgent(agent.id, {
602
599
  turns: 1,
603
- tokens: tokenDelta,
600
+ tokens: turnUsageTokens,
604
601
  partial_output: assistantText,
605
602
  });
606
603
  }
@@ -14,7 +14,7 @@
14
14
  * ↓ ↓
15
15
  * failed completed
16
16
  * ↓ ↓
17
- * closed abandoned (idle too long; reaped by watchdog)
17
+ * closed abandoned (only with an explicit idle timeout)
18
18
  *
19
19
  * - 'created' : registry record exists but the driver hasn't taken a
20
20
  * step yet. Transient — flips to 'running' on first tick.
@@ -26,10 +26,10 @@
26
26
  * - 'failed' : terminal — driver/adapter/stream raised; agent.error set.
27
27
  * - 'closed' : terminal — CloseAgent called (or driver finally{} reaped
28
28
  * a cleanly-finishing agent).
29
- * - 'abandoned' : terminal — idle watchdog tripped (no prompt arrived in
30
- * IDLE_ABANDON_MS). Distinct from 'closed' so the parent
31
- * can tell "the parent forgot about me" from "the parent
32
- * deliberately wrapped me up".
29
+ * - 'abandoned' : terminal — an embedding caller's explicit idle timeout
30
+ * elapsed. Distinct from 'closed' so the parent can tell
31
+ * automatic cleanup from deliberate finalization. There is
32
+ * no default idle lifetime deadline.
33
33
  */
34
34
 
35
35
  export const STATUS = Object.freeze({
@@ -55,19 +55,17 @@ export const CONDITIONAL_BUILTIN_TOOL_NAMES = new Set([
55
55
  'JsRepl',
56
56
  'NotebookEdit',
57
57
  'ImageGeneration',
58
- 'RepoWorkflow',
59
58
  ]);
60
59
 
61
60
  const HISTORY_INTENT_RE = /(?:\bhistory\b|\b(?:prior|previous) (?:chat|conversation|discussion)\b|\bprevious(?:ly)? discussed\b|\bwhat did we (?:decide|discuss|say|agree)\b|\b(?:our|the) (?:earlier|last) decision\b|历史|之前(?:的)?(?:对话|讨论|会话|决定)|过去(?:的)?会话|我们(?:之前|上次)(?:决定|讨论|说)了什么)/iu;
62
61
  const DISK_INTENT_RE = /(?:\bdisk (?:usage|space|full)\b|\bstorage (?:usage|space|full)\b|\blargest director|\benospc\b|\bno space left on device\b|磁盘(?:占用|空间|已满)|存储空间|目录占用|空间不足)/iu;
63
- const PATCH_INTENT_RE = /(?:\bapply (?:a )?patch\b|\bunified diff\b|\bpatch file\b|应用补丁|统一 diff|补丁文件)/iu;
62
+ const PATCH_INTENT_RE = /(?:\b(?:implement|refactor|fix|edit)\b|修复|重构|修改|实现|\bapply (?:a )?patch\b|\bunified diff\b|\bpatch file\b|应用补丁|统一 diff|补丁文件)/iu;
64
63
  const TASK_INTENT_RE = /(?:\bbackground (?:task|job|command|process)\b|\btask[_-][a-z0-9]+\b|\btask log\b|后台(?:任务|命令|进程)|任务日志)/iu;
65
64
  const SUB_AGENT_INTENT_RE = /(?:\bsub[ -]?agent\b|\bagent(?:s)?\b|\bparallel(?:ize| work| task| review)?\b|\bindependent(?:ly| review)?\b|\banother (?:worker|reviewer|agent)\b|\bdelegate\b|\b(?:run|start|launch|spawn) (?:the |a )?(?:task|child)\b|子 ?Agent|并行(?:处理|工作|任务|审查)?|独立(?:处理|审查)?|另一个(?:人|助手|Agent)|委派)/iu;
66
65
  const WORK_ITEM_INTENT_RE = /(?:\bwork ?center\b|\bwork ?item\b|\bdurable tracking\b|\bcross[- ]turn\b|\blong[- ]running goal\b|\bacross multiple (?:turns|sessions)\b|\buntil (?:it is|it's) finished\b|工作中心|工作项|持久(?:任务|跟踪)|跨 ?turn|跨多个会话|长期任务|持续跟踪)/iu;
67
66
  const REPL_INTENT_RE = /(?:\bjs ?repl\b|\bjavascript (?:calculation|experiment|evaluation)\b|\bcalculate\b|\bdata transform\b|JavaScript (?:计算|实验|求值)|数据转换|快速计算)/iu;
68
67
  const NOTEBOOK_INTENT_RE = /(?:\.ipynb\b|\bjupyter\b|\bnotebook (?:cell|file)\b|Jupyter|笔记本单元格)/iu;
69
68
  const IMAGE_GENERATION_INTENT_RE = /(?:\b(?:generate|make|design|draw) (?:me |us )?(?:an? |the )?(?:image|picture|logo|icon|illustration|graphic)\b|\bcreate (?:me |us )?(?:an? |the )?(?:illustration|image|picture|logo|icon|graphic)\b|生成(?:一张)?(?:图片|图像|标志|图标|插图)|创建(?:一张)?(?:插图|图片|图像|标志|图标)|画(?:一张)?(?:图|图片|图标))/iu;
70
- const REPO_WORKFLOW_INTENT_RE = /(?:\b(?:git |github )?(?:worktree|pull request|pr) (?:workflow|review|merge|landing|prepare)\b|\b(?:review|merge|land|prepare) (?:a |the )?(?:github )?(?:pull request|pr)\b|\bhead[- ]match(?:ed)? merge\b|\bmerge (?:and|\+) tag\b|\breview[- ]prep\b|\bye?aft-repo\b|仓库(?:工作流|流程)|准备(?:开发|审查|review) worktree|(?:审查|评审|合并|准备) ?(?:github )?(?:pr|pull request|拉取请求)|合并(?:并|和)?打 tag|精确 head 合并)/iu;
71
69
  const MCP_INTENT_RE = /(?:\bmcp\b|model context protocol|模型上下文协议)/iu;
72
70
 
73
71
  function messageText(message) {
@@ -159,7 +157,6 @@ export function resolveActiveToolNames({
159
157
  if (REPL_INTENT_RE.test(intentText)) active.add('JsRepl');
160
158
  if (NOTEBOOK_INTENT_RE.test(intentText)) active.add('NotebookEdit');
161
159
  if (imageGenerationConfigured && IMAGE_GENERATION_INTENT_RE.test(intentText)) active.add('ImageGeneration');
162
- if (REPO_WORKFLOW_INTENT_RE.test(intentText)) active.add('RepoWorkflow');
163
160
 
164
161
  for (const name of matchedMcpTools(intentText, toolNames)) active.add(name);
165
162
 
@@ -123,7 +123,7 @@ export function validateSpec(input) {
123
123
  task: task || mission,
124
124
  expected_output: expected_output || null,
125
125
  persona: persona || null,
126
- budget: resolveSubAgentBudget(budget, persona),
126
+ budget: resolveSubAgentBudget(budget),
127
127
  },
128
128
  };
129
129
  }
@@ -222,9 +222,10 @@ export default defineTool({
222
222
  en: `Create a sub-agent to work on an independent task in parallel.
223
223
 
224
224
  Sub-agents run in their own context and can be given a concrete mission
225
- with an optional expected_output schema. Default safety ceilings: 64 actual tool
226
- executions (128 for implementer) and 15 minutes; budget overrides each field.
227
- max_tokens is checked during provider usage; max_turns counts query turns, not tools.
225
+ with an optional expected_output schema. By default there is no lifetime tool,
226
+ LLM, token, turn, or wall-time ceiling: the agent may finish its assigned work.
227
+ Explicit budget fields are absolute lifetime limits. max_tokens uses provider
228
+ usage; max_turns counts query turns, not tools.
228
229
  Pick a preset persona to pre-wire a tool subset and model tier:
229
230
  - explorer : fast, read-only scout (Read/Grep/Glob/ListDir)
230
231
  - implementer: builder with full work tools (primary model)
@@ -235,29 +236,20 @@ Guidelines:
235
236
  - Give a clear, focused mission — what "done" looks like
236
237
  - Use expected_output when the return shape matters
237
238
  - Delegate one clear result with the workspace/base and completion evidence. Let the child choose its steps; do simple work directly. Split unrelated goals, not individual reads.
238
- - Usually omit budget: defaults are safety ceilings, not targets. Do not impose a tiny tool limit on a multi-file review. Tool exhaustion reserves one tool-free handoff within the remaining time/token limits; unfinished work stays budget_exceeded.
239
+ - Usually omit budget so the child can finish. Add a budget only when the task actually needs a hard lifetime ceiling; tool exhaustion reserves one tool-free handoff and unfinished work stays budget_exceeded.
239
240
  - Persona tools are defaults, not task boundaries: reviewer has GitRead; grant Bash or write tools explicitly via allow_tools only when needed. Bash is not a read-only sandbox; isolate concurrent writable tasks.
240
241
  - Use UpdateAgent to adjust a live child's time/tool/LLM ceilings or extra grants after inspecting evidence; counters and context are retained. Do not extend stalled work blindly or respawn the same exhausted mission automatically.
241
242
 
242
- Async orchestration:
243
- 1. SpawnAgent — starts the sub-agent as a background task and returns immediately.
244
- 2. Continue — keep working in the parent VP; do not block just to poll.
245
- 3. ListAgents — non-blocking status check when you need progress/liveness.
246
- 4. PromptAgent — optional follow-up if the sub-agent is idle and needs guidance.
247
- After queueing it, call WaitAgent in the same parent turn and collect the
248
- reply before ending. If a bounded wait times out, wait again with a larger
249
- bound unless the agent is stale/stalled.
250
- 5. CloseAgent — stop or finalize a sub-agent when it is no longer needed.
251
-
252
- Completion/failure is delivered through sub-agent notifications on later parent
253
- turns, so do not call WaitAgent merely to poll a newly spawned running agent.
254
- After PromptAgent follow-up, however, the answer is required to complete that
255
- workflow; use bounded WaitAgent calls and inspect liveness instead of blind loops.`,
243
+ Orchestration: SpawnAgent returns immediately, so continue parent work; do not call WaitAgent merely to poll a newly spawned running agent.
244
+ Use ListAgents for a non-blocking status check; completion/failure also arrives by
245
+ notification. PromptAgent is for follow-up guidance. After queueing it, call WaitAgent in the same parent turn and collect that reply before ending.
246
+ A stale diagnostic is evidence to inspect, not proof the child is dead. CloseAgent
247
+ stops or finalizes work no longer needed.`,
256
248
  zh: `创建一个子 Agent 并行处理独立任务。
257
249
 
258
250
  子 Agent 在独立上下文中运行,可给定具体 mission 和可选的 expected_output schema。
259
- 默认安全上限:64 次实际工具执行(implementer 为 128 次)、15 分钟;budget 可逐项覆盖。
260
- max_tokens 在 provider usage 到达时检查;max_turns 是 query turn 数,不是工具调用数。
251
+ 默认不设工具执行、LLM 请求、token、turn 或总耗时上限,让 Agent 完成已分配工作。
252
+ 显式 budget 字段是累计生命周期硬上限;max_tokens 使用 provider usage,max_turns 统计 query turn 而非工具调用。
261
253
  选择预设 persona 来预配置工具子集和模型层级:
262
254
  - explorer : 快速只读侦察(Read/Grep/Glob/ListDir)
263
255
  - implementer: 具备完整工作工具的构建者(主模型)
@@ -268,22 +260,11 @@ max_tokens 在 provider usage 到达时检查;max_turns 是 query turn 数,
268
260
  - 给出清晰聚焦的 mission——"完成"是什么样子
269
261
  - 当返回结构重要时使用 expected_output
270
262
  - 一次只委派一个明确结果,提供工作目录/基线和完成证据,让子 Agent 自主选择步骤;简单工作直接做。拆分不相关目标,不要拆成逐个读取任务。
271
- - 通常省略 budget:默认值是安全上限,不是执行目标。不要给多文件 review 人为设置极小的工具额度。工具耗尽后会在剩余时间/token预算内留一次无工具交付机会,未完成仍返回 budget_exceeded。
263
+ - 通常省略 budget,让子 Agent 完成工作;仅当任务确实需要硬性累计上限时才设置。不要给多文件 review 人为设置极小额度。工具额度耗尽后保留一次无工具交付机会,未完成仍返回 budget_exceeded。
272
264
  - Persona 是默认工具集,不是任务死边界:reviewer 有 GitRead;按需用 allow_tools 显式授予 Bash/写工具。Bash 并非只读沙箱;并行写任务应隔离 workspace。
273
265
  - 检查已有证据后,用 UpdateAgent 原地调整活跃子任务的时间/工具/LLM 上限或额外授权,保留计数与上下文;不要盲目扩额停滞任务,也不要自动重启同一个耗尽任务。
274
266
 
275
- 异步编排流程:
276
- 1. SpawnAgent — 启动子 Agent 作为后台任务并立即返回。
277
- 2. Continue — 父 VP 继续工作;不要仅仅为了轮询而阻塞。
278
- 3. ListAgents — 需要进度信息时的非阻塞状态检查。
279
- 4. PromptAgent — 子 Agent 空闲且需要指导时,可选发送后续提示。
280
- 排队后必须在同一个父级 turn 调用 WaitAgent 并拿到回复再结束;有界等待超时后,除非 Agent
281
- 已 stale/stalled,否则使用更大的有界 timeout 再次等待。
282
- 5. CloseAgent — 不再需要时停止或结束子 Agent。
283
-
284
- 完成或失败会通过之后父级 turn 的 notification 送达,因此不要为了轮询刚创建且仍运行的
285
- Agent 调用 WaitAgent。但 PromptAgent 后续工作必须拿到答案才算完成;使用有界 WaitAgent 调用并检查
286
- liveness,不要盲目循环。`
267
+ 异步编排:SpawnAgent 立即返回;父 VP 继续工作,不要为了轮询刚创建且仍运行的 Agent 调用 WaitAgent。需要非阻塞状态时用 ListAgents,完成/失败也会通过 notification 送达。不再需要的工作用 CloseAgent 停止或结束。PromptAgent — 子 Agent 空闲且需要指导时发送;排队后必须在同一个父级 turn 调用 WaitAgent 并拿到回复再结束。超时且非 stale/stalled 时扩大有界 timeout 再等;stale 只是诊断,应先检查日志再决定是否取消。`
287
268
  },
288
269
  parameters: {
289
270
  type: 'object',
@@ -340,17 +321,17 @@ liveness,不要盲目循环。`
340
321
  zh: '可选实际 Engine 模型请求上限(含重试),不同于 query turn;另保留并单独统计一次无工具报告请求。',
341
322
  } },
342
323
  max_tool_calls: { type: 'integer', minimum: 1, description: {
343
- en: 'Actual tool execution ceiling; default 64, or 128 for implementer. Includes parallel and discovered tools.',
344
- zh: '实际工具执行上限;默认 64,implementer 为 128;包括并行及发现的工具。',
324
+ en: 'Optional actual tool execution ceiling, including parallel and discovered tools; no default limit is applied',
325
+ zh: '可选实际工具执行上限,包括并行及发现的工具;默认不设限制',
345
326
  } },
346
327
  wall_time_ms: { type: 'number', description: {
347
- en: 'Elapsed-time ceiling in milliseconds; default 900000 (15 minutes)',
348
- zh: '耗时上限(毫秒);默认 900000(15 分钟)',
328
+ en: 'Optional elapsed-time ceiling in milliseconds; no default limit is applied',
329
+ zh: '可选耗时上限(毫秒);默认不设限制',
349
330
  } },
350
331
  },
351
332
  description: {
352
- en: 'Override default tool/time safety ceilings; token/turn limits are optional. A cutoff returns { status: "budget_exceeded", partial_output, reason }, not successful completion.',
353
- zh: '覆盖默认工具/时间安全上限;token/turn 限制可选。截止时返回 { status: "budget_exceeded", partial_output, reason },不代表任务成功。',
333
+ en: 'Optional absolute lifetime ceilings. Omit budget to allow task completion. A cutoff returns { status: "budget_exceeded", partial_output, reason }, not successful completion.',
334
+ zh: '可选累计生命周期硬上限。省略 budget 即允许任务完成。截止时返回 { status: "budget_exceeded", partial_output, reason },不代表任务成功。',
354
335
  },
355
336
  },
356
337
  allow_tools: {