@yeaft/webchat-agent 1.0.286 → 1.0.288
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/engine.js +108 -21
- package/yeaft/sub-agent/runner.js +95 -78
- package/yeaft/tools/agent.js +8 -0
- package/yeaft/web-bridge.js +32 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.288"}
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -81,6 +81,9 @@ const MAX_CONTINUE_TURNS = 3;
|
|
|
81
81
|
/** Bound the best-effort post-turn AMS LLM call independently of the user turn. */
|
|
82
82
|
const AMS_ADJUST_TIMEOUT_MS = 30_000;
|
|
83
83
|
|
|
84
|
+
/** Maximum silence while a visible turn waits for a result-producing task. */
|
|
85
|
+
const DEFAULT_ASYNC_TASK_WAIT_TIMEOUT_MS = 120_000;
|
|
86
|
+
|
|
84
87
|
// ─── LLM retry policy defaults ──────────────────────────────────
|
|
85
88
|
// Hard-coded floor / ceiling for retry behaviour. The engine reads the
|
|
86
89
|
// effective policy from `config.llmRetry` so users can dial these via
|
|
@@ -629,6 +632,7 @@ export class Engine {
|
|
|
629
632
|
* onUnregister?: (taskId:string, engine:Engine) => void,
|
|
630
633
|
* onConsumed?: (taskId:string, engine:Engine) => void,
|
|
631
634
|
* onUndelivered?: (taskId:string, delivery:object, engine:Engine) => void,
|
|
635
|
+
* onDeferred?: (taskId:string, engine:Engine) => void,
|
|
632
636
|
* } | null}
|
|
633
637
|
*/
|
|
634
638
|
#asyncTaskCoordinator = null;
|
|
@@ -3209,14 +3213,17 @@ export class Engine {
|
|
|
3209
3213
|
if ((stopReason !== 'tool_use' || toolCalls.length === 0)
|
|
3210
3214
|
&& this.#pendingAsyncTaskIds.size > 0
|
|
3211
3215
|
&& !signal?.aborted) {
|
|
3216
|
+
const asyncTaskWaitTimeoutMs = this.#asyncTaskWaitTimeoutMs();
|
|
3217
|
+
const deferredTaskIds = [];
|
|
3212
3218
|
// Drop into a wait loop. The loop wakes on (a) any task
|
|
3213
3219
|
// terminal event delivered via `notifyAsyncTaskCompleted`, (b)
|
|
3214
3220
|
// a fresh user append (which is honored as a higher priority
|
|
3215
3221
|
// user input), or (c) abort. On wake we re-check: if either
|
|
3216
3222
|
// queue has content, drain + splice + continue the outer loop.
|
|
3217
3223
|
// If both queues are empty AND we still have pending tasks AND
|
|
3218
|
-
// we're not aborted,
|
|
3219
|
-
//
|
|
3224
|
+
// we're not aborted, wait only until the oldest tracked task has
|
|
3225
|
+
// been silent for the bounded window. Stale ownership is then
|
|
3226
|
+
// released so a later terminal event uses the rescue-turn path.
|
|
3220
3227
|
yield {
|
|
3221
3228
|
type: 'async_task_wait_start',
|
|
3222
3229
|
turnId: queryTurnId,
|
|
@@ -3237,7 +3244,10 @@ export class Engine {
|
|
|
3237
3244
|
&& this.#pendingUserMessages.length === 0
|
|
3238
3245
|
&& !this.#externalUserWakePending) {
|
|
3239
3246
|
if (this.#pendingAsyncTaskIds.size === 0) break;
|
|
3240
|
-
|
|
3247
|
+
const waitMs = this.#nextAsyncTaskWaitMs(asyncTaskWaitTimeoutMs);
|
|
3248
|
+
if (await this.#waitForAsyncWake(signal, waitMs) === 'timeout') {
|
|
3249
|
+
deferredTaskIds.push(...this.#deferExpiredAsyncTasks(asyncTaskWaitTimeoutMs));
|
|
3250
|
+
}
|
|
3241
3251
|
}
|
|
3242
3252
|
yield {
|
|
3243
3253
|
type: 'async_task_wait_end',
|
|
@@ -3246,6 +3256,8 @@ export class Engine {
|
|
|
3246
3256
|
threadId,
|
|
3247
3257
|
aborted: Boolean(signal?.aborted),
|
|
3248
3258
|
remainingTaskIds: Array.from(this.#pendingAsyncTaskIds),
|
|
3259
|
+
timedOut: deferredTaskIds.length > 0,
|
|
3260
|
+
deferredTaskIds,
|
|
3249
3261
|
};
|
|
3250
3262
|
if (signal?.aborted) {
|
|
3251
3263
|
yield { type: 'aborted', reason: this.#abortReason || 'external', turnNumber, threadId };
|
|
@@ -4186,6 +4198,69 @@ export class Engine {
|
|
|
4186
4198
|
try { this.#asyncTaskCoordinator?.onRegister?.(taskId, this); } catch { /* coord must not throw into tools */ }
|
|
4187
4199
|
}
|
|
4188
4200
|
|
|
4201
|
+
#asyncTaskWaitTimeoutMs() {
|
|
4202
|
+
const configured = Number(this.#config?.asyncTaskWaitTimeoutMs);
|
|
4203
|
+
if (!Number.isFinite(configured)) return DEFAULT_ASYNC_TASK_WAIT_TIMEOUT_MS;
|
|
4204
|
+
return Math.max(1, Math.min(60 * 60_000, Math.floor(configured)));
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
#asyncTaskLastActivityAt(taskId) {
|
|
4208
|
+
if (!this.#taskManager || typeof this.#taskManager.getTask !== 'function') return null;
|
|
4209
|
+
const sessionId = this.#sessionId || 'default';
|
|
4210
|
+
let task = null;
|
|
4211
|
+
try { task = this.#taskManager.getTask(sessionId, taskId); } catch { return null; }
|
|
4212
|
+
if (!task || task.status !== 'running') return 0;
|
|
4213
|
+
const updatedAt = Date.parse(task.updatedAt || task.startedAt || task.createdAt || '');
|
|
4214
|
+
return Number.isFinite(updatedAt) ? updatedAt : 0;
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
#nextAsyncTaskWaitMs(timeoutMs) {
|
|
4218
|
+
const now = Date.now();
|
|
4219
|
+
let next = timeoutMs;
|
|
4220
|
+
for (const taskId of this.#pendingAsyncTaskIds) {
|
|
4221
|
+
const lastActivityAt = this.#asyncTaskLastActivityAt(taskId);
|
|
4222
|
+
if (lastActivityAt === null) continue;
|
|
4223
|
+
next = Math.min(next, Math.max(1, lastActivityAt + timeoutMs - now));
|
|
4224
|
+
}
|
|
4225
|
+
return Math.max(1, next);
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
/**
|
|
4229
|
+
* Release stale same-turn ownership without stopping the underlying tasks.
|
|
4230
|
+
* Active sub-agents refresh TaskManager.updatedAt from their event stream, so
|
|
4231
|
+
* the timeout measures silence rather than total runtime. A later terminal
|
|
4232
|
+
* event misses the owner map and uses the bridge rescue path.
|
|
4233
|
+
* @param {number} timeoutMs
|
|
4234
|
+
* @returns {string[]}
|
|
4235
|
+
*/
|
|
4236
|
+
#deferExpiredAsyncTasks(timeoutMs) {
|
|
4237
|
+
if (this.#pendingAsyncTaskIds.size === 0) return [];
|
|
4238
|
+
const now = Date.now();
|
|
4239
|
+
const taskIds = Array.from(this.#pendingAsyncTaskIds).filter((taskId) => {
|
|
4240
|
+
const lastActivityAt = this.#asyncTaskLastActivityAt(taskId);
|
|
4241
|
+
return lastActivityAt === null || now - lastActivityAt >= timeoutMs;
|
|
4242
|
+
});
|
|
4243
|
+
for (const taskId of taskIds) {
|
|
4244
|
+
// Keep ownership visible while the coordinator decides whether this is a
|
|
4245
|
+
// real defer. If a terminal event already won and removed the task, the
|
|
4246
|
+
// coordinator can reject a stale timeout callback instead of scheduling
|
|
4247
|
+
// a duplicate rescue.
|
|
4248
|
+
try {
|
|
4249
|
+
if (typeof this.#asyncTaskCoordinator?.onDeferred === 'function') {
|
|
4250
|
+
this.#asyncTaskCoordinator.onDeferred(taskId, this);
|
|
4251
|
+
} else {
|
|
4252
|
+
this.#asyncTaskCoordinator?.onUnregister?.(taskId, this);
|
|
4253
|
+
}
|
|
4254
|
+
} catch { /* best-effort */ }
|
|
4255
|
+
this.#pendingAsyncTaskIds.delete(taskId);
|
|
4256
|
+
this.#asyncTaskToolMeta.delete(taskId);
|
|
4257
|
+
}
|
|
4258
|
+
if (taskIds.length > 0) {
|
|
4259
|
+
console.warn(`[Engine] async task wait silent for ${timeoutMs}ms; deferring ${taskIds.join(', ')}`);
|
|
4260
|
+
}
|
|
4261
|
+
return taskIds;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4189
4264
|
#wakeAsyncTaskWaiters() {
|
|
4190
4265
|
if (this.#asyncTaskWaiters.length === 0) return;
|
|
4191
4266
|
const waiters = this.#asyncTaskWaiters.splice(0);
|
|
@@ -4196,30 +4271,42 @@ export class Engine {
|
|
|
4196
4271
|
|
|
4197
4272
|
/**
|
|
4198
4273
|
* Wait for *any* of: an async task terminal event, a fresh user append,
|
|
4199
|
-
*
|
|
4200
|
-
*
|
|
4201
|
-
* concurrent tasks all wake the same waiter once, then the loop drains
|
|
4202
|
-
* everything in one iteration and decides whether to keep waiting.
|
|
4274
|
+
* signal abort, or the current silence budget. The loop re-evaluates all
|
|
4275
|
+
* queues and task activity on every wake.
|
|
4203
4276
|
* @param {AbortSignal|null|undefined} signal
|
|
4204
|
-
* @
|
|
4277
|
+
* @param {number} timeoutMs
|
|
4278
|
+
* @returns {Promise<'wake'|'timeout'>}
|
|
4205
4279
|
*/
|
|
4206
|
-
#waitForAsyncWake(signal) {
|
|
4280
|
+
#waitForAsyncWake(signal, timeoutMs) {
|
|
4207
4281
|
return new Promise((resolve) => {
|
|
4282
|
+
let settled = false;
|
|
4283
|
+
let timer = null;
|
|
4284
|
+
let onAbort = null;
|
|
4208
4285
|
// Fast paths — anything already pending releases instantly. This is
|
|
4209
4286
|
// the common case when a task finished between adapter loops.
|
|
4210
|
-
if (this.#pendingTaskResultMessages.length > 0) return resolve();
|
|
4211
|
-
if (this.#pendingTaskResultUpdates.length > 0) return resolve();
|
|
4212
|
-
if (this.#pendingUserMessages.length > 0) return resolve();
|
|
4213
|
-
if (this.#externalUserWakePending) return resolve();
|
|
4214
|
-
if (signal?.aborted) return resolve();
|
|
4215
|
-
|
|
4287
|
+
if (this.#pendingTaskResultMessages.length > 0) return resolve('wake');
|
|
4288
|
+
if (this.#pendingTaskResultUpdates.length > 0) return resolve('wake');
|
|
4289
|
+
if (this.#pendingUserMessages.length > 0) return resolve('wake');
|
|
4290
|
+
if (this.#externalUserWakePending) return resolve('wake');
|
|
4291
|
+
if (signal?.aborted) return resolve('wake');
|
|
4292
|
+
const finish = (reason) => {
|
|
4293
|
+
if (settled) return;
|
|
4294
|
+
settled = true;
|
|
4295
|
+
if (timer) clearTimeout(timer);
|
|
4296
|
+
if (signal && onAbort) {
|
|
4297
|
+
try { signal.removeEventListener('abort', onAbort); } catch { /* ignore */ }
|
|
4298
|
+
}
|
|
4299
|
+
const idx = this.#asyncTaskWaiters.indexOf(wake);
|
|
4300
|
+
if (idx >= 0) this.#asyncTaskWaiters.splice(idx, 1);
|
|
4301
|
+
resolve(reason);
|
|
4302
|
+
};
|
|
4303
|
+
const wake = () => finish('wake');
|
|
4304
|
+
this.#asyncTaskWaiters.push(wake);
|
|
4305
|
+
timer = setTimeout(() => finish('timeout'), Math.max(1, Number(timeoutMs) || 1));
|
|
4306
|
+
timer.unref?.();
|
|
4216
4307
|
if (signal && typeof signal.addEventListener === 'function') {
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
// Splice the resolver out of the wait queue so it can't double-fire.
|
|
4220
|
-
const idx = this.#asyncTaskWaiters.indexOf(resolve);
|
|
4221
|
-
if (idx >= 0) this.#asyncTaskWaiters.splice(idx, 1);
|
|
4222
|
-
try { resolve(); } catch { /* ignore */ }
|
|
4308
|
+
onAbort = () => {
|
|
4309
|
+
try { finish('wake'); } catch { /* ignore */ }
|
|
4223
4310
|
};
|
|
4224
4311
|
try { signal.addEventListener('abort', onAbort, { once: true }); } catch { /* old runtimes */ }
|
|
4225
4312
|
}
|
|
@@ -137,88 +137,105 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
137
137
|
if (agent.__driverStarted) return; // idempotent
|
|
138
138
|
agent.__driverStarted = true;
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
agent.liveness = agent.liveness || makeLiveness();
|
|
170
|
-
agent.parentVpId = deps.parentVpId || null;
|
|
171
|
-
agent.parentSessionId = deps.parentSessionId || null;
|
|
172
|
-
agent.parentThreadId = deps.parentThreadId || 'main';
|
|
173
|
-
agent.outputLog = createOutputLog(agent.id, deps.subAgentLogDir);
|
|
174
|
-
agent.outputFile = agent.outputLog.path;
|
|
175
|
-
if (agent.taskId && deps.taskManager && agent.parentSessionId) {
|
|
176
|
-
try { deps.taskManager.setTaskLogPath(agent.parentSessionId, agent.taskId, agent.outputFile); } catch { /* ignore */ }
|
|
177
|
-
}
|
|
178
|
-
agent.outputLog.write({ type: 'sub_agent_spawned', agentId: agent.id, agentName: agent.name, mission: agent.mission || agent.task || '' });
|
|
179
|
-
|
|
180
|
-
// Compose the system-prompt-overlay we want injected.
|
|
181
|
-
const preamble = buildSpawnedPreamble({
|
|
182
|
-
parentName: deps.parentName || 'parent',
|
|
183
|
-
parentVpId: deps.parentVpId || null,
|
|
184
|
-
agentName: agent.name,
|
|
185
|
-
mission: agent.mission || agent.task || '',
|
|
186
|
-
language: deps.language || deps.config?.language || 'en',
|
|
187
|
-
});
|
|
140
|
+
let subEngine = null;
|
|
141
|
+
let outputLog = null;
|
|
142
|
+
try {
|
|
143
|
+
// Build sub-engine wired to the parent's adapter/stores/config but with
|
|
144
|
+
// a restricted toolset. We DO NOT pass a conversationStore: sub-agent
|
|
145
|
+
// turns must not pollute the user-facing conversation history. The
|
|
146
|
+
// memory stores are shared so memory recall still works for the
|
|
147
|
+
// sub-agent (matches parent VP persona memory).
|
|
148
|
+
const childRegistry = buildChildToolRegistry(deps.parentToolRegistry);
|
|
149
|
+
subEngine = new Engine({
|
|
150
|
+
adapter: deps.adapter,
|
|
151
|
+
trace: deps.trace,
|
|
152
|
+
config: { ...deps.config, _readOnly: true },
|
|
153
|
+
conversationStore: null,
|
|
154
|
+
memoryStore: deps.memoryStore || null,
|
|
155
|
+
memoryShardStore: deps.memoryShardStore || null,
|
|
156
|
+
toolRegistry: childRegistry,
|
|
157
|
+
skillManager: deps.skillManager || null,
|
|
158
|
+
mcpManager: deps.mcpManager || null,
|
|
159
|
+
yeaftDir: deps.yeaftDir || null,
|
|
160
|
+
toolStats: deps.toolStats || null,
|
|
161
|
+
taskManager: deps.taskManager || null,
|
|
162
|
+
});
|
|
163
|
+
// Same-turn result plumbing: inherit the parent's coordinator so any
|
|
164
|
+
// result-producing child task launched from this sub-agent uses the shared
|
|
165
|
+
// owner map. Persistent shell tasks remain status-only and do not register.
|
|
166
|
+
if (deps.asyncTaskCoordinator && typeof subEngine.setAsyncTaskCoordinator === 'function') {
|
|
167
|
+
subEngine.setAsyncTaskCoordinator(deps.asyncTaskCoordinator);
|
|
168
|
+
}
|
|
188
169
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
170
|
+
agent.subEngine = subEngine;
|
|
171
|
+
agent.engineMessages = agent.engineMessages || [];
|
|
172
|
+
agent.liveness = agent.liveness || makeLiveness();
|
|
173
|
+
agent.parentVpId = deps.parentVpId || null;
|
|
174
|
+
agent.parentSessionId = deps.parentSessionId || null;
|
|
175
|
+
agent.parentThreadId = deps.parentThreadId || 'main';
|
|
176
|
+
outputLog = createOutputLog(agent.id, deps.subAgentLogDir);
|
|
177
|
+
agent.outputLog = outputLog;
|
|
178
|
+
agent.outputFile = outputLog.path;
|
|
179
|
+
if (agent.taskId && deps.taskManager && agent.parentSessionId) {
|
|
180
|
+
try { deps.taskManager.setTaskLogPath(agent.parentSessionId, agent.taskId, agent.outputFile); } catch { /* ignore */ }
|
|
181
|
+
}
|
|
182
|
+
outputLog.write({ type: 'sub_agent_spawned', agentId: agent.id, agentName: agent.name, mission: agent.mission || agent.task || '' });
|
|
183
|
+
|
|
184
|
+
// Compose the system-prompt-overlay we want injected.
|
|
185
|
+
const preamble = buildSpawnedPreamble({
|
|
186
|
+
parentName: deps.parentName || 'parent',
|
|
187
|
+
parentVpId: deps.parentVpId || null,
|
|
188
|
+
agentName: agent.name,
|
|
189
|
+
mission: agent.mission || agent.task || '',
|
|
190
|
+
language: deps.language ?? deps.config?.language ?? 'en',
|
|
191
|
+
});
|
|
205
192
|
|
|
206
|
-
|
|
193
|
+
const baseVpPersona =
|
|
194
|
+
deps.parentVpPersona && typeof deps.parentVpPersona === 'object'
|
|
195
|
+
? { ...deps.parentVpPersona }
|
|
196
|
+
: {};
|
|
197
|
+
baseVpPersona.persona =
|
|
198
|
+
[(baseVpPersona.persona || '').trim(), preamble.trim()]
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.join('\n\n');
|
|
201
|
+
if (!baseVpPersona.displayName || !String(baseVpPersona.displayName).trim()) {
|
|
202
|
+
baseVpPersona.displayName = `${deps.parentName || 'Parent'}/${agent.name || 'sub-agent'}`;
|
|
203
|
+
}
|
|
204
|
+
baseVpPersona.subAgent = {
|
|
205
|
+
parentVpId: deps.parentVpId || null,
|
|
206
|
+
agentId: agent.id,
|
|
207
|
+
agentName: agent.name,
|
|
208
|
+
};
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
210
|
+
agent.subVpPersona = baseVpPersona;
|
|
211
|
+
|
|
212
|
+
// Background driver — pumps queued user messages through engine.query
|
|
213
|
+
// turn by turn until the agent reaches a terminal state.
|
|
214
|
+
driveSubAgent(agent, subEngine, baseVpPersona, deps).catch((err) => {
|
|
215
|
+
// The driver normally handles its own failures (stream try/catch +
|
|
216
|
+
// terminal transition). This .catch covers genuinely unexpected
|
|
217
|
+
// throws between turns (e.g. inside dequeueNextUserPrompt) so we
|
|
218
|
+
// never leave a zombie record without a terminal status.
|
|
219
|
+
if (isTerminalAgentStatus(agent.status)) return;
|
|
220
|
+
transitionTerminal(agent, STATUS.FAILED, {
|
|
221
|
+
error: err && err.message ? err.message : String(err),
|
|
222
|
+
diagnostic: 'driver_error',
|
|
223
|
+
deps,
|
|
224
|
+
});
|
|
220
225
|
});
|
|
221
|
-
})
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// Startup is transactional. Nothing owns these resources until the driver
|
|
228
|
+
// promise has been scheduled; a synchronous failure must leave the record
|
|
229
|
+
// restartable and release every partially-created handle.
|
|
230
|
+
try { outputLog?.close(); } catch { /* best-effort rollback */ }
|
|
231
|
+
try { subEngine?.retireAsyncTasks?.('sub_agent_startup_failed', { rescue: false }); } catch { /* best-effort rollback */ }
|
|
232
|
+
agent.outputLog = null;
|
|
233
|
+
agent.outputFile = null;
|
|
234
|
+
agent.subEngine = null;
|
|
235
|
+
agent.subVpPersona = null;
|
|
236
|
+
agent.__driverStarted = false;
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
222
239
|
}
|
|
223
240
|
|
|
224
241
|
/**
|
package/yeaft/tools/agent.js
CHANGED
|
@@ -440,6 +440,14 @@ use it as the default workflow or call it repeatedly in a loop.`,
|
|
|
440
440
|
}
|
|
441
441
|
startSubAgent(agent, deps);
|
|
442
442
|
} catch (err) {
|
|
443
|
+
if (agent.taskId && ctx?.taskManager?.completeTask) {
|
|
444
|
+
try {
|
|
445
|
+
ctx.taskManager.completeTask(callerScope.sessionId || ctx?.sessionId || 'default', agent.taskId, {
|
|
446
|
+
status: 'failed',
|
|
447
|
+
error: err && err.message ? err.message : String(err),
|
|
448
|
+
});
|
|
449
|
+
} catch { /* task terminal delivery is best-effort */ }
|
|
450
|
+
}
|
|
443
451
|
agent.status = STATUS.FAILED;
|
|
444
452
|
agent.error = err && err.message ? err.message : String(err);
|
|
445
453
|
agent.diagnostics.push({ type: 'spawn_error', error: agent.error, at: Date.now() });
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -76,7 +76,7 @@ import { getAgentRegistry, agentBelongsToScope } from './tools/agent.js';
|
|
|
76
76
|
import { isPromptableAgentStatus } from './sub-agent/status.js';
|
|
77
77
|
import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
78
78
|
import { recordAgentSessionCreated, recordAgentTurn } from '../metrics.js';
|
|
79
|
-
import { TASK_RESULT_DELIVERY, taskResultDeliveryFor } from './tasks/store.js';
|
|
79
|
+
import { TASK_RESULT_DELIVERY, isTerminalTaskStatus, taskResultDeliveryFor } from './tasks/store.js';
|
|
80
80
|
|
|
81
81
|
const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
|
|
82
82
|
const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
|
|
@@ -487,6 +487,7 @@ function retireCachedVpEngine(key, {
|
|
|
487
487
|
* onUnregister: (taskId: string, engine: import('./engine.js').Engine) => void,
|
|
488
488
|
* onConsumed: (taskId: string, engine: import('./engine.js').Engine) => void,
|
|
489
489
|
* onUndelivered: (taskId: string, delivery: object, engine: import('./engine.js').Engine) => void,
|
|
490
|
+
* onDeferred: (taskId: string, engine: import('./engine.js').Engine) => void,
|
|
490
491
|
* }}
|
|
491
492
|
*/
|
|
492
493
|
function buildAsyncTaskCoordinator() {
|
|
@@ -519,6 +520,22 @@ function buildAsyncTaskCoordinator() {
|
|
|
519
520
|
taskStatus: delivery?.taskStatus,
|
|
520
521
|
});
|
|
521
522
|
},
|
|
523
|
+
onDeferred(taskId, engine) {
|
|
524
|
+
if (!engine?.ownsPendingAsyncTask?.(taskId)) return;
|
|
525
|
+
if (!deleteOwnerIfMatch(taskId, engine)) return;
|
|
526
|
+
const sessionId = engine?.sessionId || null;
|
|
527
|
+
const task = sessionId && session?.taskManager?.getTask?.(sessionId, taskId);
|
|
528
|
+
if (!task || !isTerminalTaskStatus(task.status)) return;
|
|
529
|
+
scheduleTaskResultRescue({
|
|
530
|
+
taskId,
|
|
531
|
+
sessionId: task.sessionId || sessionId,
|
|
532
|
+
vpId: task.ownerVpId || engine?.vpId || null,
|
|
533
|
+
threadId: task.source?.threadId || task.runtime?.threadId || engine?.currentThreadId || 'main',
|
|
534
|
+
content: formatTaskResultForVp(task),
|
|
535
|
+
taskKind: task.kind,
|
|
536
|
+
taskStatus: task.status,
|
|
537
|
+
});
|
|
538
|
+
},
|
|
522
539
|
};
|
|
523
540
|
}
|
|
524
541
|
/**
|
|
@@ -4024,6 +4041,12 @@ function handleEngineEvent(event, hctx) {
|
|
|
4024
4041
|
break;
|
|
4025
4042
|
|
|
4026
4043
|
case 'async_task_wait_end':
|
|
4044
|
+
if (event.timedOut) {
|
|
4045
|
+
console.warn(
|
|
4046
|
+
'[Yeaft] same-turn async task wait timed out; continuing the VP turn and deferring task results:',
|
|
4047
|
+
Array.isArray(event.deferredTaskIds) ? event.deferredTaskIds : [],
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4027
4050
|
if (!event.aborted && typeof hctx.resetQueryTimer === 'function') hctx.resetQueryTimer();
|
|
4028
4051
|
sendSessionEvent({
|
|
4029
4052
|
type: 'vp_async_task_wait_end',
|
|
@@ -4032,6 +4055,8 @@ function handleEngineEvent(event, hctx) {
|
|
|
4032
4055
|
loopNumber: event.loopNumber,
|
|
4033
4056
|
aborted: Boolean(event.aborted),
|
|
4034
4057
|
remainingTaskIds: Array.isArray(event.remainingTaskIds) ? event.remainingTaskIds : [],
|
|
4058
|
+
timedOut: Boolean(event.timedOut),
|
|
4059
|
+
deferredTaskIds: Array.isArray(event.deferredTaskIds) ? event.deferredTaskIds : [],
|
|
4035
4060
|
ts: Date.now(),
|
|
4036
4061
|
}, envelope);
|
|
4037
4062
|
break;
|
|
@@ -7303,4 +7328,10 @@ export const __testHooks = {
|
|
|
7303
7328
|
queuedTurnIds() {
|
|
7304
7329
|
return Array.from(vpInboxes.values()).flatMap((inbox) => Array.isArray(inbox) ? inbox.map((entry) => entry.turnId) : []);
|
|
7305
7330
|
},
|
|
7331
|
+
asyncTaskCoordinatorForTest() {
|
|
7332
|
+
return buildAsyncTaskCoordinator();
|
|
7333
|
+
},
|
|
7334
|
+
asyncTaskOwnerForTest(taskId) {
|
|
7335
|
+
return asyncTaskOwners.get(taskId) || null;
|
|
7336
|
+
},
|
|
7306
7337
|
};
|