@yeaft/webchat-agent 0.1.936 → 0.1.938
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/conversation.js +30 -12
- package/package.json +1 -1
- package/providers/copilot.js +108 -57
- package/yeaft/engine.js +15 -8
- package/yeaft/tools/registry.js +61 -7
- package/yeaft/web-bridge.js +11 -0
package/conversation.js
CHANGED
|
@@ -457,11 +457,19 @@ export async function resumeConversation(msg) {
|
|
|
457
457
|
if (id === conversationId || (claudeSessionId && conv.claudeSessionId === claudeSessionId)) {
|
|
458
458
|
console.log(`[Resume] Cleaning up old conversation: ${id} (claudeSessionId: ${conv.claudeSessionId})`);
|
|
459
459
|
if (conv.providerOptions && !priorProviderOptions) priorProviderOptions = conv.providerOptions;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
460
|
+
let cleanupDriver = null;
|
|
461
|
+
try {
|
|
462
|
+
cleanupDriver = getProvider(conv.providerName || provider || DEFAULT_PROVIDER);
|
|
463
|
+
} catch { /* fallback to legacy cleanup below */ }
|
|
464
|
+
if (typeof cleanupDriver?.dispose === 'function') {
|
|
465
|
+
cleanupDriver.dispose(conv, 'resume cleanup');
|
|
466
|
+
} else {
|
|
467
|
+
if (conv.abortController) {
|
|
468
|
+
conv.abortController.abort();
|
|
469
|
+
}
|
|
470
|
+
if (conv.inputStream) {
|
|
471
|
+
try { conv.inputStream.done(); } catch {}
|
|
472
|
+
}
|
|
465
473
|
}
|
|
466
474
|
ctx.conversations.delete(id);
|
|
467
475
|
}
|
|
@@ -783,17 +791,23 @@ export async function handleUserInput(msg) {
|
|
|
783
791
|
|
|
784
792
|
// /clear for capable providers — reset session in-place without spawning new turn
|
|
785
793
|
if (slashCommand.type === 'slash' && slashCommand.command === '/clear') {
|
|
794
|
+
state.turnActive = true;
|
|
795
|
+
state.turnCompletedEmitted = false;
|
|
796
|
+
state.turnErrorEmitted = false;
|
|
786
797
|
if (typeof driver.clear === 'function' && driver.capabilities?.clear) {
|
|
787
798
|
try { await driver.clear(state); } catch (err) {
|
|
788
799
|
console.warn(`[${conversationId}] driver.clear failed:`, err?.message || err);
|
|
789
800
|
}
|
|
790
801
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
802
|
+
if (!state.turnCompletedEmitted) {
|
|
803
|
+
ctx.sendToServer({
|
|
804
|
+
type: 'turn_completed',
|
|
805
|
+
conversationId,
|
|
806
|
+
claudeSessionId: state.sessionId || state.claudeSessionId,
|
|
807
|
+
workDir: state.workDir
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
state.turnActive = false;
|
|
797
811
|
return;
|
|
798
812
|
}
|
|
799
813
|
|
|
@@ -812,6 +826,10 @@ export async function handleUserInput(msg) {
|
|
|
812
826
|
});
|
|
813
827
|
} finally {
|
|
814
828
|
state.turnActive = false;
|
|
829
|
+
if (state._abortKillTimer) {
|
|
830
|
+
clearTimeout(state._abortKillTimer);
|
|
831
|
+
state._abortKillTimer = null;
|
|
832
|
+
}
|
|
815
833
|
sendConversationList();
|
|
816
834
|
}
|
|
817
835
|
return;
|
|
@@ -976,7 +994,7 @@ export function handleAskUserAnswer(msg) {
|
|
|
976
994
|
if (typeof driver.respondToPermissionRequest === 'function') {
|
|
977
995
|
const ans = msg.answers || {};
|
|
978
996
|
const optionId = typeof ans === 'string' ? ans
|
|
979
|
-
: ans.optionId || ans.option || Object.values(ans)[0];
|
|
997
|
+
: ans.optionId || ans.option || ans['Copilot permission'] || Object.values(ans)[0];
|
|
980
998
|
driver.respondToPermissionRequest(state, msg.requestId, optionId);
|
|
981
999
|
return;
|
|
982
1000
|
}
|
package/package.json
CHANGED
package/providers/copilot.js
CHANGED
|
@@ -38,12 +38,7 @@ export async function start(opts) {
|
|
|
38
38
|
const conversationId = opts.conversationId;
|
|
39
39
|
// Tear down any prior entry so we don't leak children.
|
|
40
40
|
const prior = ctx.conversations.get(conversationId);
|
|
41
|
-
|
|
42
|
-
try { prior.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
43
|
-
}
|
|
44
|
-
if (prior?.acpClient) {
|
|
45
|
-
try { prior.acpClient.close('replaced'); } catch { /* noop */ }
|
|
46
|
-
}
|
|
41
|
+
dispose(prior, 'replaced');
|
|
47
42
|
|
|
48
43
|
const providerOptions = opts.providerOptions || prior?.providerOptions || {};
|
|
49
44
|
const model = providerOptions.model || DEFAULT_COPILOT_MODEL;
|
|
@@ -116,31 +111,16 @@ async function _bootAcp(state, resumeSessionId, model) {
|
|
|
116
111
|
}
|
|
117
112
|
});
|
|
118
113
|
child.on('error', (err) => {
|
|
119
|
-
|
|
120
|
-
type: 'result',
|
|
121
|
-
subtype: 'error',
|
|
122
|
-
session_id: state.sessionId,
|
|
123
|
-
is_error: true,
|
|
124
|
-
error: `copilot process error: ${err?.message || err}`,
|
|
125
|
-
});
|
|
114
|
+
_sendTurnError(state, `copilot process error: ${err?.message || err}`);
|
|
126
115
|
});
|
|
127
116
|
child.on('close', (code) => {
|
|
128
117
|
if (state.turnActive) {
|
|
129
118
|
const tail = stderrBuf.trim().slice(-2000);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
error: tail || `copilot exited mid-turn (code ${code})`,
|
|
136
|
-
});
|
|
137
|
-
ctx.sendToServer({
|
|
138
|
-
type: 'turn_completed',
|
|
139
|
-
conversationId: state.conversationId,
|
|
140
|
-
claudeSessionId: state.sessionId,
|
|
141
|
-
workDir: state.workDir,
|
|
142
|
-
});
|
|
143
|
-
state.turnActive = false;
|
|
119
|
+
_sendTurnError(state, tail || `copilot exited mid-turn (code ${code})`);
|
|
120
|
+
_completeTurn(state);
|
|
121
|
+
}
|
|
122
|
+
if (state.acpClient) {
|
|
123
|
+
try { state.acpClient.close(`copilot exited (code ${code})`); } catch { /* noop */ }
|
|
144
124
|
}
|
|
145
125
|
// Drain any in-flight permission prompts so the frontend dialog unwedges
|
|
146
126
|
// and the Promise GC roots release.
|
|
@@ -180,6 +160,7 @@ async function _bootAcp(state, resumeSessionId, model) {
|
|
|
180
160
|
state.claudeSessionId = resumeSessionId;
|
|
181
161
|
if (Array.isArray(r?.modes?.availableModes)) state.acpModes = r.modes.availableModes;
|
|
182
162
|
if (Array.isArray(r?.models?.availableModels)) cacheCopilotModelsFromAcp(r.models.availableModels);
|
|
163
|
+
_sendSessionIdUpdate(state);
|
|
183
164
|
} else {
|
|
184
165
|
if (resumeSessionId && !state.acpCapabilities.loadSession) {
|
|
185
166
|
// Surface the downgrade — silently handing back a fresh session would
|
|
@@ -198,6 +179,7 @@ async function _bootAcp(state, resumeSessionId, model) {
|
|
|
198
179
|
state.claudeSessionId = state.sessionId;
|
|
199
180
|
if (Array.isArray(r?.modes?.availableModes)) state.acpModes = r.modes.availableModes;
|
|
200
181
|
if (Array.isArray(r?.models?.availableModels)) cacheCopilotModelsFromAcp(r.models.availableModels);
|
|
182
|
+
_sendSessionIdUpdate(state);
|
|
201
183
|
}
|
|
202
184
|
|
|
203
185
|
// 3) Emit a system_init envelope so the UI populates tools / model panels.
|
|
@@ -224,14 +206,8 @@ export async function sendInput(state, prompt, opts = {}) {
|
|
|
224
206
|
try {
|
|
225
207
|
await _bootAcp(state, state.sessionId || null, state.model);
|
|
226
208
|
} catch (err) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
subtype: 'error',
|
|
230
|
-
session_id: state.sessionId,
|
|
231
|
-
is_error: true,
|
|
232
|
-
error: `copilot ACP reinit failed: ${err?.message || err}`,
|
|
233
|
-
});
|
|
234
|
-
ctx.sendToServer({ type: 'turn_completed', conversationId, claudeSessionId: state.sessionId, workDir: state.workDir });
|
|
209
|
+
_sendTurnError(state, `copilot ACP reinit failed: ${err?.message || err}`);
|
|
210
|
+
_completeTurn(state, conversationId);
|
|
235
211
|
return;
|
|
236
212
|
}
|
|
237
213
|
}
|
|
@@ -247,6 +223,8 @@ export async function sendInput(state, prompt, opts = {}) {
|
|
|
247
223
|
const abortController = new AbortController();
|
|
248
224
|
state.abortController = abortController;
|
|
249
225
|
state.turnActive = true;
|
|
226
|
+
state.turnCompletedEmitted = false;
|
|
227
|
+
state.turnErrorEmitted = false;
|
|
250
228
|
state.turnResultReceived = false;
|
|
251
229
|
|
|
252
230
|
// Build prompt content blocks. ACP ContentBlock variants: text, image,
|
|
@@ -289,21 +267,9 @@ export async function sendInput(state, prompt, opts = {}) {
|
|
|
289
267
|
error: isErr ? `copilot stop_reason=${stopReason}` : undefined,
|
|
290
268
|
});
|
|
291
269
|
} catch (err) {
|
|
292
|
-
|
|
293
|
-
type: 'result',
|
|
294
|
-
subtype: 'error',
|
|
295
|
-
session_id: state.sessionId,
|
|
296
|
-
is_error: true,
|
|
297
|
-
error: err?.message || String(err),
|
|
298
|
-
});
|
|
270
|
+
_sendTurnError(state, err?.message || String(err));
|
|
299
271
|
} finally {
|
|
300
|
-
state
|
|
301
|
-
ctx.sendToServer({
|
|
302
|
-
type: 'turn_completed',
|
|
303
|
-
conversationId,
|
|
304
|
-
claudeSessionId: state.sessionId,
|
|
305
|
-
workDir: state.workDir,
|
|
306
|
-
});
|
|
272
|
+
_completeTurn(state, conversationId);
|
|
307
273
|
}
|
|
308
274
|
}
|
|
309
275
|
|
|
@@ -330,12 +296,44 @@ export function abort(state) {
|
|
|
330
296
|
}
|
|
331
297
|
}
|
|
332
298
|
|
|
299
|
+
export function dispose(state, reason = 'disposed') {
|
|
300
|
+
if (!state) return;
|
|
301
|
+
if (state.abortController) {
|
|
302
|
+
try { state.abortController.abort(); } catch { /* noop */ }
|
|
303
|
+
state.abortController = null;
|
|
304
|
+
}
|
|
305
|
+
if (state._abortKillTimer) {
|
|
306
|
+
clearTimeout(state._abortKillTimer);
|
|
307
|
+
state._abortKillTimer = null;
|
|
308
|
+
}
|
|
309
|
+
_drainPendingPermissions(state, reason);
|
|
310
|
+
if (state.acpClient) {
|
|
311
|
+
try { state.acpClient.close(reason); } catch { /* noop */ }
|
|
312
|
+
state.acpClient = null;
|
|
313
|
+
}
|
|
314
|
+
if (state.copilotChild) {
|
|
315
|
+
try { state.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
316
|
+
state.copilotChild = null;
|
|
317
|
+
}
|
|
318
|
+
state.initialized = false;
|
|
319
|
+
state.turnActive = false;
|
|
320
|
+
}
|
|
321
|
+
|
|
333
322
|
/**
|
|
334
323
|
* /clear support: ask ACP for a brand-new session under the same
|
|
335
324
|
* conversationId. Keeps the child alive — no spawn cost.
|
|
336
325
|
*/
|
|
337
326
|
export async function clear(state) {
|
|
338
|
-
if (!state
|
|
327
|
+
if (!state) return;
|
|
328
|
+
if (!state.initialized || !state.acpClient) {
|
|
329
|
+
try {
|
|
330
|
+
await _bootAcp(state, null, state.model);
|
|
331
|
+
return;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
_sendTurnError(state, `copilot ACP reinit failed during clear: ${err?.message || err}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
339
337
|
// A fresh session invalidates any in-flight permission prompts.
|
|
340
338
|
_drainPendingPermissions(state, 'session cleared');
|
|
341
339
|
try {
|
|
@@ -345,6 +343,7 @@ export async function clear(state) {
|
|
|
345
343
|
});
|
|
346
344
|
state.sessionId = r?.sessionId || randomUUID();
|
|
347
345
|
state.claudeSessionId = state.sessionId;
|
|
346
|
+
_sendSessionIdUpdate(state);
|
|
348
347
|
sendOutput(state.conversationId, {
|
|
349
348
|
type: 'system',
|
|
350
349
|
subtype: 'init',
|
|
@@ -355,6 +354,7 @@ export async function clear(state) {
|
|
|
355
354
|
permissionMode: state.allowAllTools ? 'bypassPermissions' : 'default',
|
|
356
355
|
});
|
|
357
356
|
} catch (err) {
|
|
357
|
+
_sendTurnError(state, `copilot clear failed: ${err?.message || err}`);
|
|
358
358
|
if (ctx?.CONFIG?.debug) console.warn('[copilot] clear failed:', err?.message || err);
|
|
359
359
|
}
|
|
360
360
|
}
|
|
@@ -365,6 +365,40 @@ function sendOutput(conversationId, data) {
|
|
|
365
365
|
ctx.sendToServer({ type: 'claude_output', conversationId, data });
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
function _sendSessionIdUpdate(state) {
|
|
369
|
+
if (!state?.conversationId || !state.sessionId) return;
|
|
370
|
+
ctx.sendToServer({
|
|
371
|
+
type: 'session_id_update',
|
|
372
|
+
conversationId: state.conversationId,
|
|
373
|
+
claudeSessionId: state.sessionId,
|
|
374
|
+
workDir: state.workDir,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function _sendTurnError(state, error) {
|
|
379
|
+
if (!state || state.turnErrorEmitted) return;
|
|
380
|
+
state.turnErrorEmitted = true;
|
|
381
|
+
sendOutput(state.conversationId, {
|
|
382
|
+
type: 'result',
|
|
383
|
+
subtype: 'error',
|
|
384
|
+
session_id: state.sessionId,
|
|
385
|
+
is_error: true,
|
|
386
|
+
error,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function _completeTurn(state, conversationId = state?.conversationId) {
|
|
391
|
+
if (!state || state.turnCompletedEmitted) return;
|
|
392
|
+
state.turnCompletedEmitted = true;
|
|
393
|
+
state.turnActive = false;
|
|
394
|
+
ctx.sendToServer({
|
|
395
|
+
type: 'turn_completed',
|
|
396
|
+
conversationId,
|
|
397
|
+
claudeSessionId: state.sessionId,
|
|
398
|
+
workDir: state.workDir,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
368
402
|
function _handleAcpNotification(state, method, params) {
|
|
369
403
|
if (method === 'session/update') {
|
|
370
404
|
_handleSessionUpdate(state, params);
|
|
@@ -474,19 +508,36 @@ async function _handlePermissionRequest(state, params) {
|
|
|
474
508
|
const allow = opt.find(o => o.kind === 'allow_always' || o.kind === 'allow_once') || opt[0];
|
|
475
509
|
return { outcome: { outcome: 'selected', optionId: allow.optionId } };
|
|
476
510
|
}
|
|
477
|
-
// Otherwise route through the existing
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
// driver via `respondToPermissionRequest(state, requestId, optionId)`.
|
|
511
|
+
// Otherwise route through the existing AskUserQuestion UI. Emit the same
|
|
512
|
+
// Claude-style tool_use first, then link it with ask_user_question so the
|
|
513
|
+
// regular card renders and answer routing can stay provider-agnostic.
|
|
481
514
|
const requestId = `copilot-perm-${randomUUID()}`;
|
|
515
|
+
const question = _formatPermissionPrompt(params);
|
|
516
|
+
const questions = [{
|
|
517
|
+
header: 'Copilot permission',
|
|
518
|
+
question,
|
|
519
|
+
options: opt.map(o => ({ label: o.name || o.optionId })),
|
|
520
|
+
multiSelect: false,
|
|
521
|
+
}];
|
|
522
|
+
sendOutput(state.conversationId, {
|
|
523
|
+
type: 'assistant',
|
|
524
|
+
message: {
|
|
525
|
+
role: 'assistant',
|
|
526
|
+
content: [{
|
|
527
|
+
type: 'tool_use',
|
|
528
|
+
id: requestId,
|
|
529
|
+
name: 'AskUserQuestion',
|
|
530
|
+
input: { questions },
|
|
531
|
+
}],
|
|
532
|
+
},
|
|
533
|
+
});
|
|
482
534
|
return new Promise((resolve) => {
|
|
483
535
|
state.pendingPermissions.set(requestId, { resolve, options: opt });
|
|
484
536
|
ctx.sendToServer({
|
|
485
537
|
type: 'ask_user_question',
|
|
486
538
|
conversationId: state.conversationId,
|
|
487
539
|
requestId,
|
|
488
|
-
|
|
489
|
-
options: opt.map(o => ({ id: o.optionId, label: o.name || o.optionId, kind: o.kind })),
|
|
540
|
+
questions,
|
|
490
541
|
});
|
|
491
542
|
});
|
|
492
543
|
}
|
|
@@ -518,7 +569,7 @@ export function respondToPermissionRequest(state, requestId, optionId) {
|
|
|
518
569
|
return false;
|
|
519
570
|
}
|
|
520
571
|
state.pendingPermissions.delete(requestId);
|
|
521
|
-
const opt = slot.options.find(o => o.optionId === optionId) || slot.options[0];
|
|
572
|
+
const opt = slot.options.find(o => o.optionId === optionId || o.name === optionId) || slot.options[0];
|
|
522
573
|
slot.resolve({ outcome: { outcome: 'selected', optionId: opt?.optionId || optionId } });
|
|
523
574
|
return true;
|
|
524
575
|
}
|
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.
|
|
2418
|
+
? this.#toolRegistry.isAllowed(tc.name, { collabToolPolicy: effectiveCollabToolPolicy })
|
|
2412
2419
|
: this.#tools.has(tc.name);
|
|
2413
2420
|
|
|
2414
2421
|
if (!hasTool) {
|
package/yeaft/tools/registry.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
/**
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -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.
|