@yeaft/webchat-agent 0.1.936 → 0.1.937

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.936",
3
+ "version": "0.1.937",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/engine.js CHANGED
@@ -42,7 +42,7 @@ import { countTurns } from './turn-utils.js';
42
42
  import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
43
43
  import { resolveThinking } from './router/thinking.js';
44
44
  import { approxTokens } from './memory/budget.js';
45
- import { truncateToolResultIfNeeded } from './tools/registry.js';
45
+ import { COLLAB_TOOL_POLICY, truncateToolResultIfNeeded } from './tools/registry.js';
46
46
  import {
47
47
  TOOL_BATCH_SIZE,
48
48
  TURN_SUMMARY_THRESHOLD,
@@ -551,9 +551,9 @@ export class Engine {
551
551
  *
552
552
  * @returns {import('./llm/adapter.js').UnifiedToolDef[]}
553
553
  */
554
- #getToolDefs() {
554
+ #getToolDefs(collabToolPolicy = null) {
555
555
  if (this.#toolRegistry) {
556
- return this.#toolRegistry.getToolDefs(this.#config?.language || 'en');
556
+ return this.#toolRegistry.getToolDefs(this.#config?.language || 'en', { collabToolPolicy });
557
557
  }
558
558
  // Legacy path: no mode filtering
559
559
  const defs = [];
@@ -1339,7 +1339,7 @@ export class Engine {
1339
1339
  * string-prompt shape (no regression for existing callers).
1340
1340
  * @yields {EngineEvent}
1341
1341
  */
1342
- async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null } = {}) {
1342
+ async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1343
1343
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1344
1344
  yield {
1345
1345
  type: 'error',
@@ -1365,6 +1365,9 @@ export class Engine {
1365
1365
  const parsed = parseEffortPrefix(prompt);
1366
1366
  const effectivePrompt = parsed.cleanedPrompt;
1367
1367
  const effectiveUserEffort = normalizeEffort(userEffort) || parsed.effort || null;
1368
+ const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1369
+ ? collabToolPolicy
1370
+ : null;
1368
1371
 
1369
1372
  // ─── task-325a: engine-owned AbortController ─────────────
1370
1373
  // We create our own controller for this query run so `engine.abort()`
@@ -1399,7 +1402,7 @@ export class Engine {
1399
1402
 
1400
1403
  try {
1401
1404
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1402
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages });
1405
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy });
1403
1406
  } finally {
1404
1407
  if (signal) {
1405
1408
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -1419,7 +1422,11 @@ export class Engine {
1419
1422
  * in a try/finally without indenting the whole loop.
1420
1423
  * @private
1421
1424
  */
1422
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null }) {
1425
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null }) {
1426
+
1427
+ const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1428
+ ? collabToolPolicy
1429
+ : null;
1423
1430
 
1424
1431
  // ─── Pre-query: FTS5 Memory Recall + AMS snapshot ─────
1425
1432
  // Memory has a SINGLE render outlet now (DESIGN-PROMPT §3 ③):
@@ -1685,7 +1692,7 @@ export class Engine {
1685
1692
  };
1686
1693
  }
1687
1694
 
1688
- const toolDefs = this.#getToolDefs();
1695
+ const toolDefs = this.#getToolDefs(effectiveCollabToolPolicy);
1689
1696
  let turnNumber = 0;
1690
1697
  let continueTurns = 0; // auto-continue counter
1691
1698
  let toolLoopTurns = 0; // task-327b: tool-use turns for long-loop auto-bump
@@ -2408,7 +2415,7 @@ export class Engine {
2408
2415
 
2409
2416
  // Resolve tool: prefer ToolRegistry, fallback to legacy #tools Map
2410
2417
  const hasTool = this.#toolRegistry
2411
- ? this.#toolRegistry.has(tc.name)
2418
+ ? this.#toolRegistry.isAllowed(tc.name, { collabToolPolicy: effectiveCollabToolPolicy })
2412
2419
  : this.#tools.has(tc.name);
2413
2420
 
2414
2421
  if (!hasTool) {
@@ -11,6 +11,27 @@
11
11
 
12
12
  import { formatSize } from '../archive/tool-results.js';
13
13
 
14
+ /**
15
+ * Collaboration tools are mutually exclusive per Yeaft group shape:
16
+ * single-VP groups use sub-agents; multi-VP groups use VP-to-VP forwarding.
17
+ * Keep the policy names close to the tool registry so LLM exposure and
18
+ * execution gating use the same source of truth.
19
+ */
20
+ export const COLLAB_TOOL_POLICY = Object.freeze({
21
+ SINGLE_VP: 'single-vp',
22
+ MULTI_VP: 'multi-vp',
23
+ });
24
+
25
+ export const SUB_AGENT_TOOL_NAMES = Object.freeze([
26
+ 'SpawnAgent',
27
+ 'PromptAgent',
28
+ 'WaitAgent',
29
+ 'CloseAgent',
30
+ 'ListAgents',
31
+ ]);
32
+
33
+ export const FORWARD_TOOL_NAMES = Object.freeze(['RouteForward']);
34
+
14
35
  /**
15
36
  * Per-tool-result hard cap.
16
37
  *
@@ -208,6 +229,20 @@ function runWithTimeout(promise, timeoutMs, toolName) {
208
229
  });
209
230
  }
210
231
 
232
+ export function normalizeCollabToolPolicy(policy) {
233
+ if (policy === COLLAB_TOOL_POLICY.SINGLE_VP || policy === COLLAB_TOOL_POLICY.MULTI_VP) {
234
+ return policy;
235
+ }
236
+ return null;
237
+ }
238
+
239
+ export function isToolHiddenByCollabPolicy(toolName, policy) {
240
+ const normalized = normalizeCollabToolPolicy(policy);
241
+ if (!normalized) return false;
242
+ if (normalized === COLLAB_TOOL_POLICY.SINGLE_VP) return FORWARD_TOOL_NAMES.includes(toolName);
243
+ return SUB_AGENT_TOOL_NAMES.includes(toolName);
244
+ }
245
+
211
246
  export class ToolRegistry {
212
247
  /** @type {Map<string, import('./types.js').ToolDef>} */
213
248
  #tools = new Map();
@@ -293,17 +328,36 @@ export class ToolRegistry {
293
328
 
294
329
  /**
295
330
  * Get tool definitions for the LLM adapter.
296
- * Returns all registered tools mode filtering was removed in task-297.
331
+ * Returns all registered tools unless a collaboration policy hides one of
332
+ * the mutually-exclusive orchestration tool families.
297
333
  * @param {string} [language='en']
334
+ * @param {{ collabToolPolicy?: string }} [opts]
298
335
  * @returns {{ name: string, description: string, parameters: object }[]}
299
336
  */
300
- getToolDefs(language = 'en') {
337
+ getToolDefs(language = 'en', opts = {}) {
301
338
  const lang = normalizeLanguage(language);
302
- return this.getAllTools().map(t => ({
303
- name: t.name,
304
- description: localizeVisibleText(t.description, lang, t.name),
305
- parameters: localizeParameters(t.parameters, lang, t.name),
306
- }));
339
+ const collabToolPolicy = normalizeCollabToolPolicy(opts?.collabToolPolicy);
340
+ return this.getAllTools()
341
+ .filter(t => !isToolHiddenByCollabPolicy(t.name, collabToolPolicy))
342
+ .map(t => ({
343
+ name: t.name,
344
+ description: localizeVisibleText(t.description, lang, t.name),
345
+ parameters: localizeParameters(t.parameters, lang, t.name),
346
+ }));
347
+ }
348
+
349
+ /**
350
+ * Check whether a tool may be called under the current collaboration policy.
351
+ * Unknown / absent policy keeps the historical behavior: all registered
352
+ * tools remain callable.
353
+ * @param {string} name
354
+ * @param {{ collabToolPolicy?: string }} [opts]
355
+ * @returns {boolean}
356
+ */
357
+ isAllowed(name, opts = {}) {
358
+ const tool = this.#tools.get(name);
359
+ if (!tool) return false;
360
+ return !isToolHiddenByCollabPolicy(tool.name, opts?.collabToolPolicy);
307
361
  }
308
362
 
309
363
  /**
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { join } from 'node:path';
22
+ import { COLLAB_TOOL_POLICY } from './tools/registry.js';
22
23
  import { existsSync } from 'node:fs';
23
24
  import { randomUUID } from 'node:crypto';
24
25
  import { buildDreamOutputSnapshot } from './dream/output-snapshot.js';
@@ -2616,6 +2617,14 @@ async function waitForVpDrivers(_groupId, driverKeys = []) {
2616
2617
  * `route_forward` can extend `causedBy` chains correctly. Optional only
2617
2618
  * for pre-707 callers that no longer exist in production.
2618
2619
  */
2620
+ function resolveCollabToolPolicy(sessionMeta) {
2621
+ if (!sessionMeta || typeof sessionMeta !== 'object' || !Array.isArray(sessionMeta.roster)) {
2622
+ return null;
2623
+ }
2624
+ const vpCount = new Set(sessionMeta.roster.filter(v => typeof v === 'string' && v.trim())).size;
2625
+ return vpCount > 1 ? COLLAB_TOOL_POLICY.MULTI_VP : COLLAB_TOOL_POLICY.SINGLE_VP;
2626
+ }
2627
+
2619
2628
  export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope, threadId = 'main' }) {
2620
2629
  // Read the group meta once and reuse for both defaultVpId fallback and
2621
2630
  // announcement injection. Each .getMeta() reload reads + parses the
@@ -2656,6 +2665,8 @@ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope
2656
2665
  if (typeof sessionId === 'string' && sessionId.trim()) {
2657
2666
  out.sessionId = sessionId.trim();
2658
2667
  }
2668
+ const collabToolPolicy = resolveCollabToolPolicy(sessionMeta);
2669
+ if (collabToolPolicy) out.collabToolPolicy = collabToolPolicy;
2659
2670
  // task-334-group-editor: surface the group announcement to the engine so
2660
2671
  // buildWorkerPrompt can inject it as a CLAUDE.md-style shared prefix.
2661
2672
  // Empty/missing reads as '' and prompts.js skips the section.