@yeaft/webchat-agent 1.0.160 → 1.0.162

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.
@@ -29,7 +29,7 @@ import { loadMcpServers, updateMcpConfig } from '../mcp.js';
29
29
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
30
30
  import { discoverLlmModels } from '../llm-model-discovery.js';
31
31
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
32
- import { handleYeaftSessionSend, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
32
+ import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
33
33
  import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
34
34
  import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
35
35
 
@@ -282,6 +282,10 @@ export async function handleMessage(msg) {
282
282
  handleAskUserAnswer(msg);
283
283
  break;
284
284
 
285
+ case 'yeaft_ask_user_answer':
286
+ handleYeaftAskUserAnswer(msg);
287
+ break;
288
+
285
289
 
286
290
  // Port proxy
287
291
  case 'proxy_request':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.160",
3
+ "version": "1.0.162",
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/engine.js CHANGED
@@ -1185,6 +1185,7 @@ export class Engine {
1185
1185
  // Null in non-VP / test contexts — tools tolerate missing slots.
1186
1186
  getCurrentTodos: vpCtx?.getCurrentTodos || null,
1187
1187
  setCurrentTodos: vpCtx?.setCurrentTodos || null,
1188
+ askUser: vpCtx?.askUser || null,
1188
1189
  // task-707: tool-callable end-turn signal. The engine threads this
1189
1190
  // setter when constructing toolCtx so a tool (e.g. route_forward)
1190
1191
  // can mark "after this batch, end the turn — do NOT call adapter
@@ -1649,7 +1650,7 @@ export class Engine {
1649
1650
  * string-prompt shape (no regression for existing callers).
1650
1651
  * @yields {EngineEvent}
1651
1652
  */
1652
- async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1653
+ async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1653
1654
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1654
1655
  yield {
1655
1656
  type: 'error',
@@ -1717,7 +1718,7 @@ export class Engine {
1717
1718
 
1718
1719
  try {
1719
1720
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1720
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName });
1721
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName });
1721
1722
  } finally {
1722
1723
  if (signal) {
1723
1724
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -1761,7 +1762,7 @@ export class Engine {
1761
1762
  * in a try/finally without indenting the whole loop.
1762
1763
  * @private
1763
1764
  */
1764
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null, explicitSkillName = null }) {
1765
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null, explicitSkillName = null }) {
1765
1766
 
1766
1767
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1767
1768
  ? collabToolPolicy
@@ -3086,6 +3087,7 @@ export class Engine {
3086
3087
  contextWindow: currentContextWindow,
3087
3088
  getCurrentTodos,
3088
3089
  setCurrentTodos,
3090
+ askUser,
3089
3091
  workDir,
3090
3092
  currentToolCall: () => currentToolCallForAsyncTask ? { ...currentToolCallForAsyncTask } : null,
3091
3093
  requestEndTurn: (reason) => {
@@ -9,7 +9,6 @@
9
9
  */
10
10
 
11
11
  import { defineTool } from './types.js';
12
- import { randomUUID } from 'crypto';
13
12
 
14
13
  export default defineTool({
15
14
  name: 'AskUser',
@@ -57,22 +56,16 @@ Guidelines:
57
56
  },
58
57
  isConcurrencySafe: () => false,
59
58
  isReadOnly: () => true,
59
+ // This is an intentionally user-driven wait, not a stalled tool call.
60
+ timeoutMs: 0,
60
61
  async execute(input, ctx) {
61
62
  const { question, options } = input;
62
63
  if (!question) return JSON.stringify({ error: 'question is required' });
64
+ if (typeof ctx?.askUser !== 'function') {
65
+ return JSON.stringify({ error: 'Interactive user input is unavailable in this runtime.' });
66
+ }
63
67
 
64
- // Generate a unique request ID for this ask
65
- const requestId = `ask_${randomUUID().slice(0, 8)}`;
66
-
67
- // In a full web-bridge integration, this would send an ask_user event
68
- // and await the answer. For now, return a formatted prompt that the
69
- // LLM can see — the web-bridge handles the ask flow externally.
70
- return JSON.stringify({
71
- type: 'ask_user',
72
- requestId,
73
- question,
74
- ...(options ? { options } : {}),
75
- message: `Question sent to user: "${question}"${options ? ` [Options: ${options.join(', ')}]` : ''}`,
76
- });
68
+ const answers = await ctx.askUser({ question, options });
69
+ return JSON.stringify({ question, answers });
77
70
  },
78
71
  });
@@ -32,6 +32,8 @@
32
32
  * @property {number} [contextWindow] — current model's context window in
33
33
  * tokens (used by ToolRegistry.execute to cap a single tool result at a
34
34
  * fraction of the window so one runaway grep can't blow the wire).
35
+ * @property {(input: {question:string, options?:string[]}) => Promise<object>} [askUser]
36
+ * — host-provided interactive prompt. Resolves only after the user answers.
35
37
  * @property {(reason?: string|object) => void} [requestEndTurn]
36
38
  * — tool-callable signal that the current engine turn should end after
37
39
  * this batch of tool calls completes (no follow-up adapter.stream call).
@@ -79,6 +79,9 @@ const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
79
79
  const PROJECT_SKILL_TIERS = new Set(['project', 'project-claude', 'project-codex']);
80
80
  const BASE_RUNTIME_KEY = '__base__';
81
81
 
82
+ /** @type {Map<string, {resolve:Function, sessionId:string, vpId:string, threadId:string, turnId:string, signal?:AbortSignal, onAbort?:Function}>} */
83
+ const pendingUserPrompts = new Map();
84
+
82
85
  /** @type {import('./session.js').Session | null} */
83
86
  let session = null;
84
87
 
@@ -4361,6 +4364,44 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
4361
4364
  // each write a copy of the user message, and history replay
4362
4365
  // would render the user's prompt N times.
4363
4366
  userAlreadyPersisted: true,
4367
+ askUser: ({ question, options }) => new Promise((resolve, reject) => {
4368
+ const requestId = `ask_${randomUUID()}`;
4369
+ const signal = vpAbort.signal;
4370
+ // User think time is not engine silence. Pause the query watchdog until
4371
+ // the card is answered; the normal tool/LLM budget resumes afterward.
4372
+ if (queryTimer) {
4373
+ clearTimeout(queryTimer);
4374
+ queryTimer = null;
4375
+ }
4376
+ const onAbort = () => {
4377
+ pendingUserPrompts.delete(requestId);
4378
+ reject(new Error('aborted'));
4379
+ };
4380
+ pendingUserPrompts.set(requestId, {
4381
+ resolve: answers => {
4382
+ resetQueryTimer();
4383
+ resolve(answers);
4384
+ },
4385
+ sessionId,
4386
+ vpId,
4387
+ threadId,
4388
+ turnId,
4389
+ signal,
4390
+ onAbort,
4391
+ });
4392
+ signal.addEventListener('abort', onAbort, { once: true });
4393
+ sendSessionEvent({
4394
+ type: 'ask_user_question',
4395
+ requestId,
4396
+ questions: [{
4397
+ question,
4398
+ options: Array.isArray(options)
4399
+ ? options.map(label => ({ label, description: '' }))
4400
+ : [],
4401
+ multiSelect: false,
4402
+ }],
4403
+ }, envelope);
4404
+ }),
4364
4405
  threadId,
4365
4406
  vpTurnId: turnId,
4366
4407
  drainPendingUserMessages: () => {
@@ -5387,6 +5428,24 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
5387
5428
  });
5388
5429
  }
5389
5430
 
5431
+ /** Resolve a pending Yeaft AskUser prompt from the web UI. */
5432
+ export function handleYeaftAskUserAnswer(msg) {
5433
+ const requestId = typeof msg?.requestId === 'string' ? msg.requestId : '';
5434
+ const pending = pendingUserPrompts.get(requestId);
5435
+ if (!pending) return false;
5436
+ if (msg.sessionId && msg.sessionId !== pending.sessionId) return false;
5437
+ if (msg.vpId && msg.vpId !== pending.vpId) return false;
5438
+ if (msg.turnId && msg.turnId !== pending.turnId) return false;
5439
+ if (msg.threadId && msg.threadId !== pending.threadId) return false;
5440
+
5441
+ pendingUserPrompts.delete(requestId);
5442
+ if (pending.signal && pending.onAbort) {
5443
+ try { pending.signal.removeEventListener('abort', pending.onAbort); } catch { /* ignore */ }
5444
+ }
5445
+ pending.resolve(msg.answers || {});
5446
+ return true;
5447
+ }
5448
+
5390
5449
  export async function handleYeaftSessionSend(msg) {
5391
5450
  return runYeaftSessionSend(msg);
5392
5451
  }
@@ -6207,6 +6266,15 @@ export const __testHooks = {
6207
6266
  vpAborts.clear();
6208
6267
  vpInboxes.clear();
6209
6268
  },
6269
+ seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test' } = {}) {
6270
+ let resolved;
6271
+ const promise = new Promise(resolve => { resolved = resolve; });
6272
+ pendingUserPrompts.set(requestId, { resolve: resolved, sessionId, vpId, threadId, turnId });
6273
+ return promise;
6274
+ },
6275
+ resetPendingUserPrompts() {
6276
+ pendingUserPrompts.clear();
6277
+ },
6210
6278
  resetVpStatusBroker() {
6211
6279
  if (vpStatusBroker) vpStatusBroker.reset();
6212
6280
  },