@yeaft/webchat-agent 1.0.377 → 1.0.378

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.377"}
1
+ {"version":"1.0.378"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.377",
3
+ "version": "1.0.378",
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",
package/yeaft/config.js CHANGED
@@ -351,6 +351,7 @@ function loadLegacyConfig(dir, overrides) {
351
351
  proxyUrl: overrides.proxyUrl || env.YEAFT_PROXY_URL || fileConfig.proxyUrl || 'http://localhost:6628',
352
352
  baseUrl: overrides.baseUrl || env.YEAFT_BASE_URL || fileConfig.baseUrl || null,
353
353
  adapter: overrides.adapter || env.YEAFT_ADAPTER || fileConfig.adapter || null,
354
+ imageApiUrl: overrides.imageApiUrl || env.YEAFT_IMAGE_API_URL || fileConfig.imageApiUrl || null,
354
355
  debug: overrides.debug !== undefined ? overrides.debug
355
356
  : env.YEAFT_DEBUG !== undefined ? isTruthy(env.YEAFT_DEBUG)
356
357
  : fileConfig.debug !== undefined ? fileConfig.debug : DEFAULTS.debug,
@@ -476,6 +477,7 @@ export function loadConfig(overrides = {}) {
476
477
  // General settings
477
478
  language: overrides.language || jsonConfig.language || DEFAULTS.language,
478
479
  debug: overrides.debug !== undefined ? overrides.debug : (jsonConfig.debug ?? DEFAULTS.debug),
480
+ imageApiUrl: overrides.imageApiUrl || jsonConfig.imageApiUrl || null,
479
481
  dir,
480
482
 
481
483
  // Token limits. Resolution order:
package/yeaft/engine.js CHANGED
@@ -24,7 +24,14 @@ import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
24
24
  import { getRuntimePlatformInfo } from './runtime-platform.js';
25
25
  import { LLMContextError, LLMAbortError, LLMAuthError, LLMRateLimitError, LLMServerError, LLMStreamIdleTimeoutError } from './llm/adapter.js';
26
26
  import { runMemoryPreflow, buildRelevantScopes, memoryScopeLabel } from './sessions/pre-flow.js';
27
- import { readProjectDoc, pickProjectDocFile, DEFAULT_PROJECT_DOC_MAX_BYTES } from './sessions/project-doc.js';
27
+ import {
28
+ readProjectDoc,
29
+ pickProjectDocFile,
30
+ selectProjectDocContext,
31
+ projectDocPathHintsFromToolCall,
32
+ projectDocWriteScopesNeedingReload,
33
+ DEFAULT_PROJECT_DOC_MAX_BYTES,
34
+ } from './sessions/project-doc.js';
28
35
  import { partitionMessages } from './compact/partition.js';
29
36
  import { runCompact as runCompactOrchestrator } from './compact/orchestrator.js';
30
37
  import { evaluateCompactTriggers } from './compact/triggers.js';
@@ -45,7 +52,10 @@ import { countTurns } from './turn-utils.js';
45
52
  import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
46
53
  import { resolveThinking } from './router/thinking.js';
47
54
  import { approxTokens, computeBudget } from './memory/budget.js';
48
- import { COLLAB_TOOL_POLICY, isToolErrorOutput, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
55
+ import { COLLAB_TOOL_POLICY, isToolErrorOutput, localizeVisibleText, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
56
+ import { CONDITIONAL_BUILTIN_TOOL_NAMES, resolveActiveToolNames } from './tools/activation.js';
57
+ import { discoverToolCapabilities } from './tools/discover-tools.js';
58
+ import { agentBelongsToScope, getAgentRegistry } from './tools/agent.js';
49
59
  import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
50
60
  import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
51
61
  import {
@@ -958,19 +968,22 @@ export class Engine {
958
968
  }
959
969
 
960
970
  /**
961
- * Get the list of registered tool definitions (for passing to the adapter).
962
- * Prefers ToolRegistry when available, falls back to legacy #tools Map.
963
- *
964
- * task-297: mode-based filtering was removed all registered tools are
965
- * always exposed to the LLM.
971
+ * Get the active tool definitions for one provider request. The registry keeps
972
+ * every implementation and compatibility alias loaded; only the active set is
973
+ * serialized into the request. Legacy standalone engines keep exposing their
974
+ * explicitly registered tools because they do not use the built-in registry.
966
975
  *
976
+ * @param {string|null} collabToolPolicy
977
+ * @param {Set<string>|null} activeToolNames
967
978
  * @returns {import('./llm/adapter.js').UnifiedToolDef[]}
968
979
  */
969
- #getToolDefs(collabToolPolicy = null) {
980
+ #getToolDefs(collabToolPolicy = null, activeToolNames = null) {
970
981
  if (this.#toolRegistry) {
971
- return this.#toolRegistry.getToolDefs(this.#config?.language || 'en', { collabToolPolicy });
982
+ return this.#toolRegistry.getToolDefs(this.#config?.language || 'en', {
983
+ collabToolPolicy,
984
+ activeToolNames,
985
+ });
972
986
  }
973
- // Legacy path: no mode filtering
974
987
  const defs = [];
975
988
  for (const [, tool] of this.#tools) {
976
989
  defs.push({
@@ -982,6 +995,14 @@ export class Engine {
982
995
  return defs;
983
996
  }
984
997
 
998
+ #hasScopedSubAgents({ sessionId, parentVpId, parentThreadId } = {}) {
999
+ const scope = { sessionId, parentVpId, parentThreadId };
1000
+ for (const agent of getAgentRegistry().values()) {
1001
+ if (agentBelongsToScope(agent, scope)) return true;
1002
+ }
1003
+ return false;
1004
+ }
1005
+
985
1006
  /**
986
1007
  * Load prompt-facing canonical scope content. Every durable memory scope is
987
1008
  * query-gated; summary.md remains catalog metadata for Dream triage only.
@@ -1196,7 +1217,7 @@ export class Engine {
1196
1217
  * @param {string} [args.explicitSkillName] — leading /skill:<name> command, if present
1197
1218
  * @returns {string}
1198
1219
  */
1199
- #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectInstruction, projectLabel, workCenterInstructions, projectDoc, taskCtx, activeTasks, explicitSkillName, resolvedSkillContent = null } = {}) {
1220
+ #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectInstruction, projectLabel, workCenterInstructions, projectDoc, taskCtx, activeTasks, activeToolNames = null, promptNotices = [], explicitSkillName, resolvedSkillContent = null } = {}) {
1200
1221
  // Skill selection is normally resolved once by #runQuery so the prompt and
1201
1222
  // emitted protocol events describe the exact same skills. Keep the local
1202
1223
  // fallback for internal callers that do not need selection events.
@@ -1210,10 +1231,15 @@ export class Engine {
1210
1231
  }
1211
1232
  }
1212
1233
 
1213
- // Get tool names from the appropriate source
1214
- const toolNames = this.#toolRegistry
1234
+ // Use the same active set for the prompt and provider schema. The prompt
1235
+ // only needs these names to select scoped guidance; it does not repeat the
1236
+ // catalogue because the API tool definitions are already authoritative.
1237
+ const registeredToolNames = this.#toolRegistry
1215
1238
  ? this.#toolRegistry.getToolNames()
1216
1239
  : Array.from(this.#tools.keys());
1240
+ const toolNames = activeToolNames instanceof Set
1241
+ ? registeredToolNames.filter(name => activeToolNames.has(name))
1242
+ : registeredToolNames;
1217
1243
 
1218
1244
  return buildWorkerPrompt({
1219
1245
  language: this.#config.language || 'en',
@@ -1230,6 +1256,7 @@ export class Engine {
1230
1256
  runtimePlatform: getRuntimePlatformInfo(),
1231
1257
  taskCtx,
1232
1258
  activeTasks,
1259
+ promptNotices,
1233
1260
  // Worker-shape harness is descriptive metadata for human inspection;
1234
1261
  // production prompts skip it to save tokens. Re-enable via env when
1235
1262
  // diagnosing prompt structure issues.
@@ -1336,6 +1363,7 @@ export class Engine {
1336
1363
  conversationStore: this.#conversationStore,
1337
1364
  adapter: this.#adapter,
1338
1365
  config: this.#config,
1366
+ discoverTools: vpCtx?.discoverTools,
1339
1367
  taskManager: this.#taskManager,
1340
1368
  sessionId: vpCtx?.sessionId || this.#sessionId || null,
1341
1369
  projectSessionIds: Array.isArray(vpCtx?.projectSessionIds)
@@ -2352,12 +2380,71 @@ export class Engine {
2352
2380
  envelope: inboundEnvelope || null,
2353
2381
  };
2354
2382
 
2355
- const projectDoc = this.#getProjectDocBlock(workDir);
2356
- const activeTasks = this.#taskManager
2383
+ const projectDocSource = this.#getProjectDocBlock(workDir);
2384
+ let projectDocLoadedPathHints = [];
2385
+ let projectDocContext = selectProjectDocContext(projectDocSource, {
2386
+ prompt,
2387
+ messages,
2388
+ pathHints: projectDocLoadedPathHints,
2389
+ language: this.#config.language || 'en',
2390
+ });
2391
+ let activeTaskSnapshots = this.#taskManager
2392
+ && typeof this.#taskManager.listActiveTasks === 'function'
2393
+ ? this.#taskManager.listActiveTasks(runtimeSessionId)
2394
+ : [];
2395
+ let activeTasks = this.#taskManager
2357
2396
  ? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId, {
2358
2397
  language: this.#config.language || 'en',
2359
2398
  })
2360
2399
  : '';
2400
+ const registeredToolNames = this.#toolRegistry
2401
+ ? this.#toolRegistry.getToolNames()
2402
+ : Array.from(this.#tools.keys());
2403
+ const resolveCurrentActiveToolNames = () => this.#toolRegistry
2404
+ ? resolveActiveToolNames({
2405
+ toolNames: registeredToolNames,
2406
+ prompt,
2407
+ messages,
2408
+ collabToolPolicy: effectiveCollabToolPolicy,
2409
+ activeTasks: activeTaskSnapshots,
2410
+ subAgentToolsActivated: this.#hasScopedSubAgents({
2411
+ sessionId: runtimeSessionId,
2412
+ parentVpId: queryVpId,
2413
+ parentThreadId: runtimeThreadId,
2414
+ }),
2415
+ imageGenerationConfigured: typeof this.#config?.imageApiUrl === 'string'
2416
+ && this.#config.imageApiUrl.trim().length > 0,
2417
+ })
2418
+ : null;
2419
+ const discoveredToolNames = new Set();
2420
+ const discoveryTraversals = new Map();
2421
+ const currentDiscoverableTools = () => this.#toolRegistry
2422
+ ? this.#toolRegistry.getAllTools()
2423
+ .filter(tool => CONDITIONAL_BUILTIN_TOOL_NAMES.has(tool.name) || tool.name.startsWith('mcp__'))
2424
+ : [];
2425
+ const discoveryDirectorySnapshot = (tools, language) => tools
2426
+ .map(tool => ({
2427
+ name: tool.name,
2428
+ description: localizeVisibleText(tool.description, language, tool.name),
2429
+ parameters: tool.parameters,
2430
+ }));
2431
+ const discoveryDirectoryMatches = (snapshot, liveTools, language) => {
2432
+ const liveSnapshot = discoveryDirectorySnapshot(liveTools, language);
2433
+ if (snapshot.length !== liveSnapshot.length) return false;
2434
+ const byName = new Map(snapshot.map(tool => [tool.name, tool]));
2435
+ return liveSnapshot.every(tool => {
2436
+ const prior = byName.get(tool.name);
2437
+ return prior
2438
+ && prior.description === tool.description
2439
+ && JSON.stringify(prior.parameters) === JSON.stringify(tool.parameters);
2440
+ });
2441
+ };
2442
+ const applyDiscoveredTools = (names) => {
2443
+ for (const name of names) {
2444
+ if (this.#toolRegistry?.has(name)) discoveredToolNames.add(name);
2445
+ }
2446
+ };
2447
+ let activeToolNames = resolveCurrentActiveToolNames();
2361
2448
  let resolvedSkillContent = '';
2362
2449
  let resolvedSkills = [];
2363
2450
  let skillResolutionError = null;
@@ -2385,7 +2472,8 @@ export class Engine {
2385
2472
  }
2386
2473
  }
2387
2474
 
2388
- const systemPrompt = this.#buildSystemPrompt({
2475
+ let promptNotices = [];
2476
+ const buildCurrentSystemPrompt = () => this.#buildSystemPrompt({
2389
2477
  prompt,
2390
2478
  memoryInjection,
2391
2479
  vpPersona,
@@ -2394,11 +2482,14 @@ export class Engine {
2394
2482
  projectInstruction,
2395
2483
  projectLabel,
2396
2484
  workCenterInstructions,
2397
- projectDoc,
2485
+ projectDoc: projectDocContext.text,
2398
2486
  activeTasks,
2487
+ activeToolNames,
2488
+ promptNotices,
2399
2489
  explicitSkillName,
2400
2490
  resolvedSkillContent,
2401
2491
  });
2492
+ let systemPrompt = buildCurrentSystemPrompt();
2402
2493
 
2403
2494
  // ─── HARD INVARIANT: Compact ≠ Dream (read DESIGN-COMPACT-VS-DREAM.md) ─
2404
2495
  // Compact summary (this block) ONLY lands in the messages array head as
@@ -2598,7 +2689,7 @@ export class Engine {
2598
2689
  };
2599
2690
  }
2600
2691
 
2601
- const toolDefs = this.#getToolDefs(effectiveCollabToolPolicy);
2692
+ let toolDefs = this.#getToolDefs(effectiveCollabToolPolicy, activeToolNames);
2602
2693
  let turnNumber = 0;
2603
2694
  let continueTurns = 0; // auto-continue counter
2604
2695
  let toolLoopTurns = 0; // task-327b: tool-use turns for long-loop auto-bump
@@ -2775,6 +2866,26 @@ export class Engine {
2775
2866
  }
2776
2867
  }
2777
2868
 
2869
+ // Tool availability depends on live Session state. A Bash call can start a
2870
+ // background task (or a sub-agent can appear) during the prior loop, so
2871
+ // recompute the schemas and matching guidance before every provider call.
2872
+ activeTaskSnapshots = this.#taskManager
2873
+ && typeof this.#taskManager.listActiveTasks === 'function'
2874
+ ? this.#taskManager.listActiveTasks(runtimeSessionId)
2875
+ : [];
2876
+ activeTasks = this.#taskManager
2877
+ ? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId, {
2878
+ language: this.#config.language || 'en',
2879
+ })
2880
+ : '';
2881
+ activeToolNames = resolveCurrentActiveToolNames();
2882
+ for (const name of [...discoveredToolNames]) {
2883
+ if (this.#toolRegistry?.has(name)) activeToolNames?.add(name);
2884
+ else discoveredToolNames.delete(name);
2885
+ }
2886
+ toolDefs = this.#getToolDefs(effectiveCollabToolPolicy, activeToolNames);
2887
+ systemPrompt = buildCurrentSystemPrompt();
2888
+
2778
2889
  try {
2779
2890
  // task-327b: resolve effort per-turn so the long-loop auto-bump
2780
2891
  // kicks in once toolLoopTurns crosses the threshold.
@@ -3826,6 +3937,72 @@ export class Engine {
3826
3937
  setCurrentTodos,
3827
3938
  askUser,
3828
3939
  workDir,
3940
+ discoverTools: ({ query, cursor, maxResults } = {}) => {
3941
+ if (!this.#toolRegistry) return { query: String(query || ''), tools: [], activated: 0 };
3942
+ const language = this.#config.language || 'en';
3943
+ const queryText = String(query || '');
3944
+ const traversalKey = queryText.trim();
3945
+ const liveTools = currentDiscoverableTools();
3946
+ const requestedCursor = Number(cursor);
3947
+ const hasCursor = cursor != null && cursor !== '' && Number.isInteger(requestedCursor) && requestedCursor > 0;
3948
+ let traversal = hasCursor ? discoveryTraversals.get(traversalKey) : null;
3949
+ if (hasCursor && (!traversal || !discoveryDirectoryMatches(traversal.candidates, liveTools, language))) {
3950
+ discoveryTraversals.delete(traversalKey);
3951
+ return {
3952
+ query: queryText,
3953
+ tools: [],
3954
+ next_cursor: null,
3955
+ total: liveTools.length,
3956
+ omitted_invalid: 0,
3957
+ activated: 0,
3958
+ restart_required: true,
3959
+ message: 'The hidden tool directory changed or no matching traversal exists. Restart discovery without a cursor.',
3960
+ };
3961
+ }
3962
+ if (!hasCursor) {
3963
+ traversal = {
3964
+ candidates: discoveryDirectorySnapshot(liveTools, language),
3965
+ };
3966
+ discoveryTraversals.set(traversalKey, traversal);
3967
+ } else {
3968
+ const pendingTraversal = discoveryTraversals.get(traversalKey);
3969
+ if (!pendingTraversal || pendingTraversal.nextCursor !== requestedCursor) {
3970
+ discoveryTraversals.delete(traversalKey);
3971
+ return {
3972
+ query: queryText,
3973
+ tools: [],
3974
+ next_cursor: null,
3975
+ total: liveTools.length,
3976
+ omitted_invalid: 0,
3977
+ activated: 0,
3978
+ restart_required: true,
3979
+ message: 'The discovery cursor is stale or out of sequence. Restart discovery without a cursor.',
3980
+ };
3981
+ }
3982
+ }
3983
+ const result = discoverToolCapabilities({
3984
+ query: queryText,
3985
+ candidates: traversal.candidates,
3986
+ language,
3987
+ cursor,
3988
+ maxResults,
3989
+ });
3990
+ if (result.restart_required || result.next_cursor == null) discoveryTraversals.delete(traversalKey);
3991
+ else traversal.nextCursor = result.next_cursor;
3992
+ applyDiscoveredTools(result.tools.map(tool => tool.name));
3993
+ return {
3994
+ query: queryText,
3995
+ ...result,
3996
+ activated: result.tools.length,
3997
+ message: result.restart_required
3998
+ ? (result.message || 'The hidden tool directory changed. Restart discovery without a cursor.')
3999
+ : (result.tools.length > 0
4000
+ ? (result.next_cursor == null
4001
+ ? 'This discovery page is active on the next model loop; the hidden directory is exhausted.'
4002
+ : 'This discovery page is active on the next model loop; use next_cursor if the target is not listed.')
4003
+ : 'No valid hidden registered tools remain on this directory page.'),
4004
+ };
4005
+ },
3829
4006
  currentToolCall: () => currentToolCallForAsyncTask ? { ...currentToolCallForAsyncTask } : null,
3830
4007
  requestEndTurn: (reason) => {
3831
4008
  // First call wins — preserve the kind/reason of the first tool
@@ -3921,8 +4098,17 @@ export class Engine {
3921
4098
 
3922
4099
  // Resolve tool: prefer ToolRegistry, fallback to legacy #tools Map
3923
4100
  const hasTool = this.#toolRegistry
3924
- ? this.#toolRegistry.isAllowed(tc.name, { collabToolPolicy: effectiveCollabToolPolicy })
4101
+ ? this.#toolRegistry.isAllowed(tc.name, {
4102
+ collabToolPolicy: effectiveCollabToolPolicy,
4103
+ activeToolNames,
4104
+ })
3925
4105
  : this.#tools.has(tc.name);
4106
+ const toolProjectDocPathHints = projectDocPathHintsFromToolCall(tc.name, tc.input);
4107
+ const readOnlyTool = hasTool ? isReadOnlyTool(this, tc.name, tc.input) : false;
4108
+ const missingProjectDocScopes = hasTool && !readOnlyTool
4109
+ ? projectDocWriteScopesNeedingReload(projectDocContext, toolProjectDocPathHints)
4110
+ : new Set();
4111
+ const needsProjectDocReload = missingProjectDocScopes.size > 0;
3926
4112
 
3927
4113
  if (skipped) {
3928
4114
  const source = activeToolBatchBarrier.sourceToolName || 'a preceding tool';
@@ -3945,12 +4131,42 @@ export class Engine {
3945
4131
  threadId: this.currentThreadId,
3946
4132
  };
3947
4133
  } else if (!hasTool) {
3948
- output = `Error: unknown tool "${tc.name}"`;
4134
+ const registered = this.#toolRegistry?.has(tc.name) || this.#tools.has(tc.name);
4135
+ output = registered
4136
+ ? `Error: tool "${tc.name}" is not active for this request`
4137
+ : `Error: unknown tool "${tc.name}"`;
3949
4138
  isError = true;
3950
4139
  yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
4140
+ } else if (needsProjectDocReload) {
4141
+ projectDocLoadedPathHints = [...new Set([
4142
+ ...projectDocLoadedPathHints,
4143
+ ...toolProjectDocPathHints,
4144
+ ])];
4145
+ const previouslySelectedProjectDocScopes = new Set(projectDocContext.selectedScopes);
4146
+ projectDocContext = selectProjectDocContext(projectDocSource, {
4147
+ prompt,
4148
+ messages,
4149
+ pathHints: projectDocLoadedPathHints,
4150
+ language: this.#config.language || 'en',
4151
+ forcedScopes: [...previouslySelectedProjectDocScopes, ...missingProjectDocScopes],
4152
+ });
4153
+ const zh = String(this.#config?.language || '').toLowerCase().startsWith('zh');
4154
+ const notice = zh
4155
+ ? `项目规则已针对路径 ${toolProjectDocPathHints.join(', ')} 重新加载。先复核新载入的规则,再重新提交写操作;本次调用未执行。`
4156
+ : `Project rules were reloaded for ${toolProjectDocPathHints.join(', ')}. Review the newly loaded rules before resubmitting the write; this call was not executed.`;
4157
+ promptNotices = [notice];
4158
+ systemPrompt = buildCurrentSystemPrompt();
4159
+ output = notice;
4160
+ isError = true;
4161
+ toolBatchBarrier = {
4162
+ kind: 'project_rules_reloaded',
4163
+ message: notice,
4164
+ sourceToolCallId: tc.id,
4165
+ sourceToolName: tc.name,
4166
+ };
4167
+ yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, skipped: true, threadId: this.currentThreadId };
3951
4168
  } else {
3952
4169
  duplicateKey = `${tc.name}\u001f${argsHashOf(tc.input)}`;
3953
- const readOnlyTool = isReadOnlyTool(this, tc.name, tc.input);
3954
4170
  cacheableTool = isCacheableTool(this, tc.name, tc.input);
3955
4171
  // The cache is only valid while the workspace has not changed. Clear
3956
4172
  // it before every potential mutation, including a tool that later
package/yeaft/prompts.js CHANGED
@@ -135,12 +135,10 @@ function extractExactLangSection(content, language) {
135
135
  /** Loaded templates — read once at module load time. */
136
136
  const RAW_TEMPLATES = {
137
137
  base: readTemplate('base.md'),
138
- // Phase 8 wire-up: split-out fragments for the persona-as-identity path.
139
- // `identityYeaft` ships only when NO VP persona is active. `commonRules`
140
- // ships every turn (with persona OR with Yeaft identity) — it carries
141
- // output-format, code-editing, search, and frontend rules that are
142
- // identity-independent. base.md remains as a back-compat bundle so any
143
- // external snapshotter / test that reads the file directly keeps working.
138
+ core: readTemplate('core.md'),
139
+ // base.md and the older split fragments remain packaged for external
140
+ // snapshotters and deployment compatibility. Runtime prompt assembly uses
141
+ // core.md plus scoped guidance instead of concatenating those full bundles.
144
142
  identityYeaft: readTemplate('identity-yeaft.md', { required: false }),
145
143
  commonRules: readTemplate('common-rules.md', { required: false }),
146
144
  modeUnified: readTemplate('mode-unified.md'),
@@ -195,7 +193,6 @@ const PROMPTS = {
195
193
  identity: 'No VP soul is active for this turn. Participate in the current session with grounded, evidence-based answers and preserve the user\'s context.',
196
194
  date: (d) => `Date: ${d}`,
197
195
  dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
198
- tools: (names) => `Available tools: ${names}`,
199
196
  // DESIGN-PROMPT §3 ④ — current session context block.
200
197
  activeScopeHeader: '## Current session context',
201
198
  activeScopeSessionIdLabel: 'Session ID',
@@ -215,13 +212,13 @@ const PROMPTS = {
215
212
  // AGENTS.md is the cross-tool convention (Codex / OpenAI Codex CLI).
216
213
  projectDocHeader: '[Project Doc]',
217
214
  projectDocIntro:
218
- 'The user keeps project-level instructions and context in `CLAUDE.md` or `AGENTS.md` at the session working directory. Treat the content below as authoritative project context coding conventions, task guidance, workflow rules, etc.',
215
+ 'The text below is the authoritative project context selected for this turn. It contains the stable core plus any task- or path-scoped sections that the runtime loaded on demand.',
216
+ promptNoticeHeader: '[Runtime Notice]',
219
217
  },
220
218
  zh: {
221
219
  identity: '你正在当前会话中参与协作。保持用户上下文,回答要基于证据;需要工具时使用工具,但不要把自己没有实际执行过的事说成已经执行。',
222
220
  date: (d) => `日期:${d}`,
223
221
  dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
224
- tools: (names) => `可用工具:${names}`,
225
222
  // DESIGN-PROMPT §3 ④ — 当前会话上下文。
226
223
  activeScopeHeader: '## 当前会话上下文',
227
224
  activeScopeSessionIdLabel: '会话 ID',
@@ -239,7 +236,8 @@ const PROMPTS = {
239
236
  // 项目文档块:CLAUDE.md / AGENTS.md(与 Codex 通用命名兼容)。
240
237
  projectDocHeader: '[项目文档]',
241
238
  projectDocIntro:
242
- '用户把项目级的说明和上下文记录在 session 工作目录下的 `CLAUDE.md` 或 `AGENTS.md` 中。下面的内容是权威的项目上下文 —— 编码规范、任务指导、工作流约定等,请遵循它来工作。',
239
+ '下面是当前 turn 选中的权威项目上下文,包含稳定核心以及 runtime 按任务或路径加载的范围章节。',
240
+ promptNoticeHeader: '[运行时提示]',
243
241
  },
244
242
  };
245
243
 
@@ -309,6 +307,7 @@ export function normalizePromptLanguage(language) {
309
307
  * projectLabel?: string,
310
308
  * workCenterInstructions?: string,
311
309
  * projectDoc?: string,
310
+ * promptNotices?: string[],
312
311
  * }} params
313
312
  * @returns {string}
314
313
  */
@@ -327,6 +326,7 @@ export function buildSystemPrompt({
327
326
  projectDoc = '',
328
327
  runtimePlatform,
329
328
  activeTasks = '',
329
+ promptNotices = [],
330
330
  } = {}) {
331
331
  // Normalize app locales like `zh-CN` to prompt dictionary/template keys.
332
332
  const effectiveLang = normalizePromptLanguage(language);
@@ -334,24 +334,16 @@ export function buildSystemPrompt({
334
334
 
335
335
  const parts = [];
336
336
 
337
- // ─── 1. Core Identity ──────────────────────────────────
338
- // Phase 8 wire-up: when a VP persona is active, the persona body REPLACES
339
- // the Yeaft identity block (the LLM is that VP, not Yeaft pretending). When
340
- // there is no persona, fall back to the legacy Yeaft identity bundle.
337
+ // ─── 1. Stable Core ────────────────────────────────────
338
+ // A VP persona supplies the identity layer when present. The compact core
339
+ // rules stay stable across turns and replace the old full base/common bundle.
341
340
  const personaBlock = renderVpPersona(vpPersona, lang, effectiveLang);
342
- if (personaBlock) {
343
- parts.push(personaBlock);
344
- // Common rules (output format, code editing, search, frontend) still
345
- // apply to every turn, regardless of which VP is speaking.
346
- const commonRules = getTemplate('commonRules', effectiveLang);
347
- if (commonRules) parts.push(commonRules);
348
- } else {
349
- const baseTemplate = getTemplate('base', effectiveLang);
350
- if (baseTemplate) {
351
- parts.push(baseTemplate);
352
- } else {
353
- parts.push(lang.identity);
354
- }
341
+ if (personaBlock) parts.push(personaBlock);
342
+ const coreTemplate = getTemplate('core', effectiveLang);
343
+ if (coreTemplate) {
344
+ parts.push(coreTemplate);
345
+ } else if (!personaBlock) {
346
+ parts.push(lang.identity);
355
347
  }
356
348
 
357
349
  // ─── 1.4 Project Doc (CLAUDE.md / AGENTS.md from session workDir) ───
@@ -395,6 +387,13 @@ export function buildSystemPrompt({
395
387
  parts.push(`${header}\n${intro ? `${intro}\n\n` : ''}${workCenterText}`);
396
388
  }
397
389
 
390
+ const notices = Array.isArray(promptNotices)
391
+ ? promptNotices.map(value => typeof value === 'string' ? value.trim() : '').filter(Boolean)
392
+ : [];
393
+ if (notices.length > 0) {
394
+ parts.push(`${lang.promptNoticeHeader || '[Runtime Notice]'}\n${notices.join('\n')}`);
395
+ }
396
+
398
397
  // ─── 2. Date Metadata ──────────────────────────────────
399
398
  parts.push(lang.date(new Date().toISOString().split('T')[0]));
400
399
 
@@ -414,11 +413,10 @@ export function buildSystemPrompt({
414
413
  }
415
414
 
416
415
  if (toolNames.length > 0) {
417
- parts.push(lang.tools(toolNames.join(', ')));
418
-
416
+ const guidance = renderActiveToolGuidance(toolNames, effectiveLang);
419
417
  const toolGuidanceTemplate = getTemplate('toolGuidance', effectiveLang);
420
- if (toolGuidanceTemplate) {
421
- parts.push(toolGuidanceTemplate);
418
+ if (guidance && toolGuidanceTemplate) {
419
+ parts.push(toolGuidanceTemplate.replace('{{guidance}}', guidance));
422
420
  }
423
421
  }
424
422
 
@@ -457,6 +455,59 @@ export function buildSystemPrompt({
457
455
 
458
456
  // ─── helpers ─────────────────────────────────────────────────────
459
457
 
458
+ const TOOL_GUIDANCE_GROUPS = Object.freeze([
459
+ {
460
+ tools: ['DiscoverTools'],
461
+ en: 'If the visible tools do not clearly cover the request, use `DiscoverTools` with the user goal before concluding that a capability is unavailable. If the target is absent from a page, follow `next_cursor` until found or the hidden directory is exhausted. If `restart_required` is true, restart without a cursor because the registered directory changed.',
462
+ zh: '如果可见工具不能明确覆盖请求,应先按用户目标调用 `DiscoverTools`。若当前页没有目标,应按 `next_cursor` 继续翻页,直到找到或隐藏目录耗尽后,才能判断某项能力不可用。如果 `restart_required` 为 true,说明注册目录已变化,应丢弃游标重新开始。',
463
+ },
464
+ {
465
+ tools: ['FileRead', 'FileWrite', 'FileEdit', 'Glob', 'Grep', 'ListDir', 'ApplyPatch', 'NotebookEdit'],
466
+ en: 'Read existing files before editing. Use dedicated file/search tools instead of shell search or `sed -i`; make small, reviewable edits and batch independent reads.',
467
+ zh: '编辑前先读现有文件。文件搜索和修改优先使用专用工具,不用 shell 搜索或 `sed -i`;改动保持小而可审查,独立读取应并行发出。',
468
+ },
469
+ {
470
+ tools: ['Bash'],
471
+ en: 'Use non-interactive, deterministic shell commands, set reasonable timeouts, quote paths with spaces, and do not run destructive operations without authorization.',
472
+ zh: 'Shell 命令保持非交互、确定性并设置合理 timeout;包含空格的路径要引用,未经授权不要执行破坏性操作。',
473
+ },
474
+ {
475
+ tools: ['StartPlan', 'TodoWrite'],
476
+ en: 'For non-trivial multi-step work, use `StartPlan` before execution and keep the visible `TodoWrite` checklist current; do not stop after planning unless user input genuinely blocks the first step.',
477
+ zh: '非平凡多步骤任务在执行前使用 `StartPlan`,并持续更新可见的 `TodoWrite` checklist;只有用户信息确实阻塞第一步时才在规划后停下。',
478
+ },
479
+ {
480
+ tools: ['SpawnAgent', 'PromptAgent', 'WaitAgent', 'CloseAgent', 'ListAgents'],
481
+ en: 'Delegate only independent, bounded work. Keep ownership in the parent, avoid polling loops, and close sub-agents after collecting their result.',
482
+ zh: '只委派边界清晰且独立的工作。父级保留任务所有权,不要循环轮询,取得结果后关闭子 Agent。',
483
+ },
484
+ {
485
+ tools: ['ListTasks', 'ReadTaskLog', 'CancelTask'],
486
+ en: 'Treat background tasks as live execution state, not memory facts. Inspect status or logs before retrying or cancelling work.',
487
+ zh: '后台任务是实时执行状态,不是记忆事实。重试或取消前先检查状态或日志。',
488
+ },
489
+ {
490
+ tools: ['RouteForward'],
491
+ en: 'Use `RouteForward` for explicit VP-to-VP handoff; writing an @mention in ordinary text does not dispatch another VP.',
492
+ zh: '显式 VP 转交必须使用 `RouteForward`;普通文本中的 @mention 不会调度另一个 VP。',
493
+ },
494
+ {
495
+ tools: ['CreateWorkItem'],
496
+ en: 'Use `CreateWorkItem` only for goals that need durable cross-turn coordination, recovery, review, waiting, or retry.',
497
+ zh: '只有目标需要跨 turn 持久协调、恢复、评审、等待或重试时才使用 `CreateWorkItem`。',
498
+ },
499
+ ]);
500
+
501
+ function renderActiveToolGuidance(toolNames, language) {
502
+ const active = new Set(Array.isArray(toolNames) ? toolNames : []);
503
+ const lines = [];
504
+ for (const group of TOOL_GUIDANCE_GROUPS) {
505
+ if (!group.tools.some(name => active.has(name))) continue;
506
+ lines.push(`- ${language === 'zh' ? group.zh : group.en}`);
507
+ }
508
+ return lines.join('\n');
509
+ }
510
+
460
511
  /**
461
512
  * Render the VP identity block when the engine is running on behalf of an
462
513
  * addressed VP. The `persona` body from role.md is the only soul source;