@yeaft/webchat-agent 1.0.195 → 1.0.197
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/server/handlers/agent-output.js +27 -11
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +79 -84
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/engine.js +25 -4
- package/yeaft/skills.js +36 -3
- package/yeaft/web-bridge.js +89 -16
- package/yeaft/work-center/controller.js +32 -8
- package/yeaft/work-center/projection.js +35 -1
- package/yeaft/work-center/runner.js +36 -2
- package/yeaft/work-center/service.js +2 -1
- package/yeaft/work-center/store.js +149 -7
- package/yeaft/work-center/watcher.js +13 -1
|
Binary file
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -1743,7 +1743,7 @@ export class Engine {
|
|
|
1743
1743
|
* string-prompt shape (no regression for existing callers).
|
|
1744
1744
|
* @yields {EngineEvent}
|
|
1745
1745
|
*/
|
|
1746
|
-
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 } = {}) {
|
|
1746
|
+
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, closePendingUserInput = null, collabToolPolicy = null } = {}) {
|
|
1747
1747
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1748
1748
|
yield {
|
|
1749
1749
|
type: 'error',
|
|
@@ -1816,7 +1816,7 @@ export class Engine {
|
|
|
1816
1816
|
|
|
1817
1817
|
try {
|
|
1818
1818
|
this.#currentThreadId = threadId || MAIN_THREAD_ID;
|
|
1819
|
-
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 });
|
|
1819
|
+
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, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName });
|
|
1820
1820
|
} finally {
|
|
1821
1821
|
if (signal) {
|
|
1822
1822
|
try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
|
|
@@ -1850,7 +1850,7 @@ export class Engine {
|
|
|
1850
1850
|
* in a try/finally without indenting the whole loop.
|
|
1851
1851
|
* @private
|
|
1852
1852
|
*/
|
|
1853
|
-
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 }) {
|
|
1853
|
+
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, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null }) {
|
|
1854
1854
|
|
|
1855
1855
|
const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
|
|
1856
1856
|
? collabToolPolicy
|
|
@@ -3035,8 +3035,29 @@ export class Engine {
|
|
|
3035
3035
|
// into the regular end_turn path.
|
|
3036
3036
|
}
|
|
3037
3037
|
|
|
3038
|
-
// If no tool calls, we're done
|
|
3038
|
+
// If no tool calls, we're done. Callers with a durable append queue may
|
|
3039
|
+
// atomically close it here. A failed close means input won the race with
|
|
3040
|
+
// terminal completion, so drain it and keep this same Engine query alive.
|
|
3039
3041
|
if (stopReason !== 'tool_use' || toolCalls.length === 0) {
|
|
3042
|
+
if (typeof closePendingUserInput === 'function' && !closePendingUserInput()) {
|
|
3043
|
+
const appendedBeforeClose = this.#drainPendingUserMessages(drainPendingUserMessages);
|
|
3044
|
+
if (appendedBeforeClose.length === 0) {
|
|
3045
|
+
throw new Error('Could not close pending user input for terminal completion');
|
|
3046
|
+
}
|
|
3047
|
+
for (const item of appendedBeforeClose) {
|
|
3048
|
+
conversationMessages.push({ role: 'user', content: item.content });
|
|
3049
|
+
yield {
|
|
3050
|
+
type: 'user_append',
|
|
3051
|
+
turnId: queryTurnId,
|
|
3052
|
+
loopNumber: turnNumber,
|
|
3053
|
+
threadId,
|
|
3054
|
+
preview: String(item.preview || '').slice(0, 200),
|
|
3055
|
+
internal: Boolean(item.internal),
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'user_append_continue', threadId };
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3040
3061
|
if (pendingSubAgentNotifs.length > 0) {
|
|
3041
3062
|
acknowledgePendingNotifications(notifScope, pendingSubAgentNotifs.map(n => n.id));
|
|
3042
3063
|
}
|
package/yeaft/skills.js
CHANGED
|
@@ -458,6 +458,9 @@ export class SkillManager {
|
|
|
458
458
|
/** @type {Map<string, { workspaceRoot: string, relativeRoot: string }>} */
|
|
459
459
|
#secureWorkspaceByDir;
|
|
460
460
|
|
|
461
|
+
/** Hash of the effective loaded skill set, including prompt content. */
|
|
462
|
+
#snapshotHash = '';
|
|
463
|
+
|
|
461
464
|
/**
|
|
462
465
|
* @param {string | string[]} dirs — single directory (back-compat) or array of
|
|
463
466
|
* directories in priority order (lowest → highest). Falsy entries are
|
|
@@ -526,14 +529,16 @@ export class SkillManager {
|
|
|
526
529
|
* tagged with `_tier` (from the constructor's `tierByDir` map, or the dir
|
|
527
530
|
* basename as fallback) so consumers can show provenance.
|
|
528
531
|
*
|
|
529
|
-
* @returns {{ loaded: number, errors: string[] }}
|
|
532
|
+
* @returns {{ loaded: number, errors: string[], changed: boolean }}
|
|
530
533
|
*/
|
|
531
534
|
load() {
|
|
535
|
+
const previousHash = this.#snapshotHash;
|
|
532
536
|
this.#skills.clear();
|
|
533
537
|
const allErrors = [];
|
|
534
538
|
|
|
535
539
|
if (this.#skillsDirs.length === 0) {
|
|
536
|
-
|
|
540
|
+
this.#snapshotHash = this.#computeSnapshotHash();
|
|
541
|
+
return { loaded: 0, errors: [], changed: this.#snapshotHash !== previousHash };
|
|
537
542
|
}
|
|
538
543
|
|
|
539
544
|
for (const dir of this.#skillsDirs) {
|
|
@@ -554,7 +559,33 @@ export class SkillManager {
|
|
|
554
559
|
allErrors.push(...errors);
|
|
555
560
|
}
|
|
556
561
|
|
|
557
|
-
|
|
562
|
+
this.#snapshotHash = this.#computeSnapshotHash();
|
|
563
|
+
return {
|
|
564
|
+
loaded: this.#skills.size,
|
|
565
|
+
errors: allErrors,
|
|
566
|
+
changed: this.#snapshotHash !== previousHash,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
#computeSnapshotHash() {
|
|
571
|
+
const records = [...this.#skills.values()]
|
|
572
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
573
|
+
.map(skill => ({
|
|
574
|
+
name: skill.name || '',
|
|
575
|
+
description: skill.description || '',
|
|
576
|
+
trigger: skill.trigger || '',
|
|
577
|
+
mode: skill.mode || 'both',
|
|
578
|
+
category: skill.category || '',
|
|
579
|
+
platforms: skill.platforms || [],
|
|
580
|
+
keywords: skill.keywords || [],
|
|
581
|
+
content: skill.content || '',
|
|
582
|
+
source: skill._source || '',
|
|
583
|
+
path: skill._path || '',
|
|
584
|
+
tier: skill._tier || '',
|
|
585
|
+
references: skill._references || [],
|
|
586
|
+
templates: skill._templates || [],
|
|
587
|
+
}));
|
|
588
|
+
return JSON.stringify(records);
|
|
558
589
|
}
|
|
559
590
|
|
|
560
591
|
/**
|
|
@@ -746,6 +777,7 @@ export class SkillManager {
|
|
|
746
777
|
_path: filePath,
|
|
747
778
|
_tier: userTier,
|
|
748
779
|
});
|
|
780
|
+
this.#snapshotHash = this.#computeSnapshotHash();
|
|
749
781
|
|
|
750
782
|
return filename;
|
|
751
783
|
}
|
|
@@ -783,6 +815,7 @@ export class SkillManager {
|
|
|
783
815
|
}
|
|
784
816
|
|
|
785
817
|
this.#skills.delete(name);
|
|
818
|
+
this.#snapshotHash = this.#computeSnapshotHash();
|
|
786
819
|
return true;
|
|
787
820
|
}
|
|
788
821
|
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -80,6 +80,7 @@ const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
|
|
|
80
80
|
const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
|
|
81
81
|
const PROJECT_SKILL_TIERS = new Set(['project', 'project-claude', 'project-codex']);
|
|
82
82
|
const BASE_RUNTIME_KEY = '__base__';
|
|
83
|
+
const SKILL_RELOAD_INTERVAL_MS = 2_000;
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Live AskUser requests. They are Session-scoped runtime state rather than a
|
|
@@ -591,6 +592,8 @@ const projectRuntimes = new Map();
|
|
|
591
592
|
/** @type {Map<string, Promise<any>>} */
|
|
592
593
|
const baseRuntimeLoadPromises = new Map();
|
|
593
594
|
let activeRuntimeKey = BASE_RUNTIME_KEY;
|
|
595
|
+
let skillReloadTimer = null;
|
|
596
|
+
let skillReloadRunning = false;
|
|
594
597
|
|
|
595
598
|
function replaceSessionMcpTools(mcpManager) {
|
|
596
599
|
if (!session?.toolRegistry || typeof session.toolRegistry.replaceMcpTools !== 'function') {
|
|
@@ -651,6 +654,7 @@ function activateProjectRuntime(runtime) {
|
|
|
651
654
|
}
|
|
652
655
|
|
|
653
656
|
async function shutdownProjectRuntimes() {
|
|
657
|
+
stopSkillHotReload();
|
|
654
658
|
const runtimes = Array.from(projectRuntimes.values());
|
|
655
659
|
projectRuntimes.clear();
|
|
656
660
|
projectRuntimeLoadPromises.clear();
|
|
@@ -936,7 +940,6 @@ let yeaftConversationId = null;
|
|
|
936
940
|
* creates/replaces the virtual Yeaft conversation id so `/` autocomplete never
|
|
937
941
|
* falls back to built-ins while full Session metadata is still loading. */
|
|
938
942
|
let lastYeaftSlashCommandSnapshot = null;
|
|
939
|
-
let lastYeaftGeneratedSlashCommands = new Set();
|
|
940
943
|
/** @type {Map<string, Promise<any>>} */
|
|
941
944
|
const projectRuntimeLoadPromises = new Map();
|
|
942
945
|
|
|
@@ -2181,6 +2184,7 @@ export function buildMergedSkillSlashCommands(skillManagers = []) {
|
|
|
2181
2184
|
function sendSkillSlashCommandsUpdate({ conversationId, slashCommands, slashCommandDescriptions }) {
|
|
2182
2185
|
sendToServer({
|
|
2183
2186
|
type: 'slash_commands_update',
|
|
2187
|
+
commandSet: 'yeaft',
|
|
2184
2188
|
agentId: ctx.AGENT_ID || ctx.agentId || null,
|
|
2185
2189
|
conversationId,
|
|
2186
2190
|
slashCommands,
|
|
@@ -2221,21 +2225,10 @@ export function preloadYeaftSkillSlashCommands() {
|
|
|
2221
2225
|
|
|
2222
2226
|
function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
2223
2227
|
const managers = [sessionLike?.skillManager, ...extraSkillManagers].filter(Boolean);
|
|
2224
|
-
const { commands, descriptions } = buildMergedSkillSlashCommands(managers);
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|| cmd.startsWith(YEAFT_SKILL_COMMAND_PREFIX));
|
|
2229
|
-
const nonSkillCommands = (ctx.slashCommands || []).filter(cmd => !isYeaftSkillCommand(cmd));
|
|
2230
|
-
const slashCommands = [...new Set([...nonSkillCommands, ...commands])];
|
|
2231
|
-
const slashCommandDescriptions = Object.fromEntries(
|
|
2232
|
-
Object.entries(ctx.slashCommandDescriptions || {})
|
|
2233
|
-
.filter(([cmd]) => !isYeaftSkillCommand(cmd))
|
|
2234
|
-
);
|
|
2235
|
-
Object.assign(slashCommandDescriptions, descriptions);
|
|
2236
|
-
ctx.slashCommands = slashCommands;
|
|
2237
|
-
ctx.slashCommandDescriptions = slashCommandDescriptions;
|
|
2238
|
-
lastYeaftGeneratedSlashCommands = new Set(commands);
|
|
2228
|
+
const { commands: slashCommands, descriptions: slashCommandDescriptions } = buildMergedSkillSlashCommands(managers);
|
|
2229
|
+
// Yeaft owns an isolated command catalogue. Reusing ctx's Claude Chat
|
|
2230
|
+
// commands made unsupported entries such as /compact and /mcp appear in a
|
|
2231
|
+
// Session even though the Yeaft engine only parses effort and Skill prefixes.
|
|
2239
2232
|
lastYeaftSlashCommandSnapshot = { slashCommands, slashCommandDescriptions };
|
|
2240
2233
|
sendSkillSlashCommandsUpdate({
|
|
2241
2234
|
conversationId: yeaftConversationId || '__preload__',
|
|
@@ -2244,6 +2237,81 @@ function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
|
|
|
2244
2237
|
});
|
|
2245
2238
|
}
|
|
2246
2239
|
|
|
2240
|
+
function skillManagersForHotReload() {
|
|
2241
|
+
const managers = new Set();
|
|
2242
|
+
if (session?.skillManager) managers.add(session.skillManager);
|
|
2243
|
+
for (const runtime of projectRuntimes.values()) {
|
|
2244
|
+
if (runtime?.skillManager) managers.add(runtime.skillManager);
|
|
2245
|
+
}
|
|
2246
|
+
return [...managers];
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function reloadActiveSkills() {
|
|
2250
|
+
if (skillReloadRunning) return { changed: false, loaded: 0, errors: [] };
|
|
2251
|
+
skillReloadRunning = true;
|
|
2252
|
+
try {
|
|
2253
|
+
let changed = false;
|
|
2254
|
+
let loaded = 0;
|
|
2255
|
+
const errors = [];
|
|
2256
|
+
const changedManagers = new Set();
|
|
2257
|
+
const managers = skillManagersForHotReload();
|
|
2258
|
+
for (const manager of managers) {
|
|
2259
|
+
if (typeof manager?.load !== 'function') continue;
|
|
2260
|
+
const result = manager.load();
|
|
2261
|
+
if (result?.changed) {
|
|
2262
|
+
changed = true;
|
|
2263
|
+
changedManagers.add(manager);
|
|
2264
|
+
}
|
|
2265
|
+
loaded += Number(result?.loaded) || 0;
|
|
2266
|
+
errors.push(...(result?.errors || []));
|
|
2267
|
+
}
|
|
2268
|
+
if (changed) {
|
|
2269
|
+
if (session?.status && changedManagers.has(session.skillManager)) {
|
|
2270
|
+
session.status.skills = session.skillManager?.size || 0;
|
|
2271
|
+
}
|
|
2272
|
+
for (const runtime of projectRuntimes.values()) {
|
|
2273
|
+
if (runtime?.status && changedManagers.has(runtime.skillManager)) {
|
|
2274
|
+
runtime.status.skills = runtime.skillManager?.size || 0;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (activeRuntimeKey === BASE_RUNTIME_KEY) {
|
|
2278
|
+
broadcastSkillSlashCommands(session);
|
|
2279
|
+
} else {
|
|
2280
|
+
const runtime = projectRuntimes.get(activeRuntimeKey);
|
|
2281
|
+
broadcastSkillSlashCommands(session, runtime?.skillManager ? [runtime.skillManager] : []);
|
|
2282
|
+
}
|
|
2283
|
+
const activeRuntime = activeRuntimeKey === BASE_RUNTIME_KEY ? null : projectRuntimes.get(activeRuntimeKey);
|
|
2284
|
+
const activeStatus = activeRuntime ? mergedStatusForProjectRuntime(activeRuntime) : session?.status;
|
|
2285
|
+
hydrateYeaftStatusFromSession(
|
|
2286
|
+
activeStatus && session ? { ...session, status: activeStatus } : session,
|
|
2287
|
+
{ reason: 'skills_hot_reload', emitEvent: true },
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
if (errors.length > 0) {
|
|
2291
|
+
console.warn(`[Yeaft] skill hot reload completed with ${errors.length} error(s):`, errors.join('; '));
|
|
2292
|
+
}
|
|
2293
|
+
return { changed, loaded, errors };
|
|
2294
|
+
} finally {
|
|
2295
|
+
skillReloadRunning = false;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function startSkillHotReload() {
|
|
2300
|
+
if (skillReloadTimer) return;
|
|
2301
|
+
skillReloadTimer = setInterval(() => {
|
|
2302
|
+
try { reloadActiveSkills(); }
|
|
2303
|
+
catch (err) { console.warn('[Yeaft] skill hot reload failed:', err?.message || err); }
|
|
2304
|
+
}, SKILL_RELOAD_INTERVAL_MS);
|
|
2305
|
+
skillReloadTimer.unref?.();
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function stopSkillHotReload() {
|
|
2309
|
+
if (!skillReloadTimer) return;
|
|
2310
|
+
clearInterval(skillReloadTimer);
|
|
2311
|
+
skillReloadTimer = null;
|
|
2312
|
+
skillReloadRunning = false;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2247
2315
|
async function loadBaseRuntime() {
|
|
2248
2316
|
if (!session) return null;
|
|
2249
2317
|
const yeaftDir = ctx.CONFIG?.yeaftDir || session.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
@@ -2267,6 +2335,7 @@ async function loadBaseRuntime() {
|
|
|
2267
2335
|
} else {
|
|
2268
2336
|
broadcastSkillSlashCommands(session);
|
|
2269
2337
|
}
|
|
2338
|
+
startSkillHotReload();
|
|
2270
2339
|
hydrateYeaftStatusFromSession(session, { reason: 'base_runtime_skills', emitEvent: true });
|
|
2271
2340
|
|
|
2272
2341
|
if (mcpConfig.servers.length > 0) {
|
|
@@ -2341,6 +2410,7 @@ async function loadProjectRuntime(workDir) {
|
|
|
2341
2410
|
// Skill metadata is available after the filesystem scan; publish it before
|
|
2342
2411
|
// any MCP server startup so autocomplete does not wait on external processes.
|
|
2343
2412
|
activateProjectRuntime(runtime);
|
|
2413
|
+
startSkillHotReload();
|
|
2344
2414
|
if (mcpConfig.servers.length > 0) {
|
|
2345
2415
|
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
2346
2416
|
runtime.mcpStatus = mcpStatus;
|
|
@@ -6716,6 +6786,9 @@ export const __testHooks = {
|
|
|
6716
6786
|
preloadYeaftSkillSlashCommandsForTest() {
|
|
6717
6787
|
return broadcastSkillSlashCommands(session);
|
|
6718
6788
|
},
|
|
6789
|
+
reloadActiveSkillsForTest() {
|
|
6790
|
+
return reloadActiveSkills();
|
|
6791
|
+
},
|
|
6719
6792
|
loadAndBroadcastYeaftSkillSlashCommandsForTest() {
|
|
6720
6793
|
return loadAndBroadcastYeaftSkillSlashCommands();
|
|
6721
6794
|
},
|
|
@@ -249,18 +249,35 @@ export class WorkflowController {
|
|
|
249
249
|
const workItem = this.store.getWorkItem(id);
|
|
250
250
|
if (!workItem) throw new Error(`WorkItem not found: ${id}`);
|
|
251
251
|
if (['ready', 'running'].includes(workItem.status)) {
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
const activeAction = this.store.getAction(input.actionId);
|
|
253
|
+
if (activeAction?.status === 'running' && addedAttachmentCount > 0) {
|
|
254
|
+
throw new Error('Files cannot be added while an Action is running; send text now or wait for the next Action boundary');
|
|
254
255
|
}
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
const inputSummary = text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`;
|
|
257
|
+
return this.store.addActionInput(id, inputSummary, {
|
|
257
258
|
actionId: input.actionId,
|
|
258
259
|
generation: input.generation,
|
|
259
260
|
revision: input.revision,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
}, (current, action) => {
|
|
262
|
+
const context = [...(action.context || []), {
|
|
263
|
+
type: 'input', role: 'user', summary: inputSummary, evidence: [],
|
|
264
|
+
}];
|
|
265
|
+
const step = {
|
|
266
|
+
type: action.type,
|
|
267
|
+
stageId: action.stageId || action.type,
|
|
268
|
+
assignmentPolicy: action.assignmentPolicy,
|
|
269
|
+
modelPolicy: action.modelPolicy,
|
|
270
|
+
requiredRole: action.requiredRole,
|
|
271
|
+
dependsOnStageIds: action.dependsOnStageIds,
|
|
272
|
+
workspaceMode: action.workspaceMode,
|
|
273
|
+
changesRequestedStageId: action.changesRequestedStageId,
|
|
274
|
+
brief: action.brief,
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
context,
|
|
278
|
+
instruction: actionInstruction(step, current, context, renderSessionContextSnapshot(current.sessionContext)),
|
|
279
|
+
};
|
|
280
|
+
}, input.attachments, input.addedAttachments);
|
|
264
281
|
}
|
|
265
282
|
if (!['waiting', 'needs_attention'].includes(workItem.status)) {
|
|
266
283
|
throw new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
|
|
@@ -341,6 +358,13 @@ export class WorkflowController {
|
|
|
341
358
|
const activeAction = activeRun ? this.store.getAction(activeRun.actionId) : null;
|
|
342
359
|
if (!activeRun || !activeAction) throw new Error('Run is stale, cancelled, or already finished');
|
|
343
360
|
const activeWorkItem = this.store.getWorkItem(activeRun.workItemId);
|
|
361
|
+
if (activeRun.acceptingInput !== false
|
|
362
|
+
&& !this.store.closeRunInput(runId, ownerBootId, leaseEpoch)) {
|
|
363
|
+
if (!this.store.isActiveRun(runId, ownerBootId, leaseEpoch)) {
|
|
364
|
+
throw new Error('Run is stale, cancelled, expired, or already finished');
|
|
365
|
+
}
|
|
366
|
+
throw new Error('Run has unconsumed Action input and cannot finish yet');
|
|
367
|
+
}
|
|
344
368
|
const result = normalizeTerminalResult(rawResult, activeAction);
|
|
345
369
|
validateCompletedResult(result, activeAction, activeWorkItem);
|
|
346
370
|
let validatedGeneratedWorkflow = null;
|
|
@@ -113,12 +113,30 @@ function runResponseMessage(run) {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function loopOutputMessages(action, events) {
|
|
117
|
+
return (Array.isArray(events) ? events : [])
|
|
118
|
+
.filter(event => event?.actionId === action?.id && event.type === 'run.loop_output')
|
|
119
|
+
.map(event => normalizeProjectedMessage({
|
|
120
|
+
id: `event:${event.id}`,
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
kind: 'response',
|
|
123
|
+
status: 'completed',
|
|
124
|
+
text: event.data?.response || '',
|
|
125
|
+
createdAt: event.createdAt,
|
|
126
|
+
}))
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
function actionMessages(action, runs, events) {
|
|
117
131
|
const matchingRuns = Array.isArray(runs)
|
|
118
132
|
? runs.filter(run => run?.actionId === action?.id)
|
|
119
133
|
: [];
|
|
120
|
-
|
|
134
|
+
const runsWithLoopOutput = new Set((Array.isArray(events) ? events : [])
|
|
135
|
+
.filter(event => event?.actionId === action?.id && event.type === 'run.loop_output' && event.runId)
|
|
136
|
+
.map(event => event.runId));
|
|
137
|
+
return [...actionInputMessages(action, events), ...loopOutputMessages(action, events), ...matchingRuns
|
|
121
138
|
.sort((left, right) => count(left.startedAt) - count(right.startedAt))
|
|
139
|
+
.filter(run => !runsWithLoopOutput.has(run.id))
|
|
122
140
|
.map(run => runResponseMessage(run))
|
|
123
141
|
.filter(Boolean)]
|
|
124
142
|
.sort((left, right) => left.createdAt - right.createdAt
|
|
@@ -294,6 +312,18 @@ function projectAction(action, runs, events, includeBody = true) {
|
|
|
294
312
|
key,
|
|
295
313
|
truncateUtf8(value, includeBody ? MAX_CURRENT_BRIEF_BYTES : MAX_HISTORICAL_BRIEF_CHARS),
|
|
296
314
|
]));
|
|
315
|
+
const matchingRuns = Array.isArray(runs) ? runs.filter(run => run?.actionId === action.id) : [];
|
|
316
|
+
const latestRun = [...matchingRuns].sort((left, right) => (
|
|
317
|
+
count(right.startedAt) - count(left.startedAt) || count(right.progressRevision) - count(left.progressRevision)
|
|
318
|
+
))[0];
|
|
319
|
+
const assignedVp = latestRun?.vpSnapshot ? {
|
|
320
|
+
id: latestRun.vpSnapshot.id || null,
|
|
321
|
+
name: latestRun.vpSnapshot.name || latestRun.vpSnapshot.id || null,
|
|
322
|
+
} : null;
|
|
323
|
+
const contentSummary = truncateUtf8(
|
|
324
|
+
execution.response || latestRun?.summary || brief?.objective || brief?.expectedOutcome || '',
|
|
325
|
+
MAX_HISTORICAL_BRIEF_CHARS,
|
|
326
|
+
);
|
|
297
327
|
return {
|
|
298
328
|
id: action.id,
|
|
299
329
|
sequence: action.sequence,
|
|
@@ -307,6 +337,8 @@ function projectAction(action, runs, events, includeBody = true) {
|
|
|
307
337
|
requiredRole: action.requiredRole || '',
|
|
308
338
|
brief: projectedBrief,
|
|
309
339
|
status: action.status,
|
|
340
|
+
assignedVp,
|
|
341
|
+
contentSummary,
|
|
310
342
|
executionStats: executionStats(execution),
|
|
311
343
|
loopCount: execution.loopCount,
|
|
312
344
|
toolCount: execution.toolCount,
|
|
@@ -366,6 +398,8 @@ function projectActionStats(detail) {
|
|
|
366
398
|
const stats = {
|
|
367
399
|
id: projected.id,
|
|
368
400
|
status: projected.status,
|
|
401
|
+
assignedVp: projected.assignedVp,
|
|
402
|
+
contentSummary: projected.contentSummary,
|
|
369
403
|
executionStats: projected.executionStats,
|
|
370
404
|
loopCount: projected.loopCount,
|
|
371
405
|
toolCount: projected.toolCount,
|
|
@@ -783,7 +783,7 @@ export class WorkItemRunner {
|
|
|
783
783
|
}
|
|
784
784
|
}
|
|
785
785
|
|
|
786
|
-
async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader }) {
|
|
786
|
+
async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader, registerInputWake }) {
|
|
787
787
|
const runtime = await this.runtimeProvider();
|
|
788
788
|
const currentSettings = workItem?.workflowSnapshot?.planningMode === 'ai' && this.policyProvider
|
|
789
789
|
? await this.policyProvider()
|
|
@@ -840,6 +840,7 @@ export class WorkItemRunner {
|
|
|
840
840
|
reuseMemory: workItem.reuseMemory,
|
|
841
841
|
});
|
|
842
842
|
const attachmentContext = buildWorkItemAttachmentContext(workItem, { root: this.attachmentRoot });
|
|
843
|
+
const attachmentFileById = new Map(attachmentContext.files.map(file => [file.id, file]));
|
|
843
844
|
const fixedPromptSuffix = `${resumeBlock}${attachmentContext.promptBlock}${completionContract(executionAction, workItem)}`;
|
|
844
845
|
const reservedPromptBytes = Buffer.byteLength(fixedPromptSuffix, 'utf8');
|
|
845
846
|
const mainline = v2Execution
|
|
@@ -992,6 +993,29 @@ export class WorkItemRunner {
|
|
|
992
993
|
taskManager: null,
|
|
993
994
|
vpId: vp.id,
|
|
994
995
|
});
|
|
996
|
+
if (typeof registerInputWake === 'function') {
|
|
997
|
+
registerInputWake(() => engine.wakeForPendingUserMessage?.());
|
|
998
|
+
}
|
|
999
|
+
const drainPendingUserMessages = () => {
|
|
1000
|
+
const pending = this.store.listPendingActionInputs?.(
|
|
1001
|
+
action.id, run.id, ownerBootId, run.leaseEpoch,
|
|
1002
|
+
) || [];
|
|
1003
|
+
const accepted = [];
|
|
1004
|
+
for (const item of pending) {
|
|
1005
|
+
const attachmentLines = item.attachments.map(attachment => {
|
|
1006
|
+
const file = attachmentFileById.get(attachment.id);
|
|
1007
|
+
return file ? `- ${attachment.name}: ${file.ref}` : `- ${attachment.name}`;
|
|
1008
|
+
});
|
|
1009
|
+
const content = [item.text, attachmentLines.length > 0
|
|
1010
|
+
? `Additional WorkItem attachments:\n${attachmentLines.join('\n')}` : '']
|
|
1011
|
+
.filter(Boolean).join('\n\n');
|
|
1012
|
+
if (!content || !this.store.acknowledgeActionInput?.(
|
|
1013
|
+
item.id, action.id, run.id, ownerBootId, run.leaseEpoch,
|
|
1014
|
+
)) continue;
|
|
1015
|
+
accepted.push({ content, preview: item.text || '[attachments]' });
|
|
1016
|
+
}
|
|
1017
|
+
return accepted;
|
|
1018
|
+
};
|
|
995
1019
|
try {
|
|
996
1020
|
const prompt = v2Execution
|
|
997
1021
|
? `${renderMainlineContextSnapshot(mainline.contextSnapshot)}${fixedPromptSuffix}`
|
|
@@ -1015,9 +1039,19 @@ export class WorkItemRunner {
|
|
|
1015
1039
|
workCenterInstructions: workItem?.workflowSnapshot?.globalInstructions || '',
|
|
1016
1040
|
workDir,
|
|
1017
1041
|
userAlreadyPersisted: true,
|
|
1042
|
+
drainPendingUserMessages,
|
|
1043
|
+
closePendingUserInput: () => this.store.closeRunInput(
|
|
1044
|
+
run.id, ownerBootId, run.leaseEpoch,
|
|
1045
|
+
),
|
|
1018
1046
|
collabToolPolicy: 'single-vp',
|
|
1019
1047
|
})) {
|
|
1020
|
-
if (event?.type === 'loop')
|
|
1048
|
+
if (event?.type === 'loop') {
|
|
1049
|
+
loopCount += 1;
|
|
1050
|
+
this.store.appendRunLoop?.(run.id, ownerBootId, run.leaseEpoch, {
|
|
1051
|
+
...event,
|
|
1052
|
+
response: publicWorkItemResponse(event.response),
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1021
1055
|
else if (event?.type === 'tool_start') toolInputs.set(event.id, event.input);
|
|
1022
1056
|
else if (event?.type === 'tool_end') {
|
|
1023
1057
|
toolCount += 1;
|
|
@@ -93,7 +93,7 @@ export class WorkCenterService {
|
|
|
93
93
|
case 'get_action_messages': {
|
|
94
94
|
const detail = this.#requiredItem(payload.id);
|
|
95
95
|
const action = this.#requiredAction(detail, payload.actionId);
|
|
96
|
-
return projectActionMessagePage(action, detail.runs,
|
|
96
|
+
return projectActionMessagePage(action, detail.runs, this.store.listActionEvents(action.id), {
|
|
97
97
|
cursor: payload.cursor,
|
|
98
98
|
limit: payload.limit,
|
|
99
99
|
});
|
|
@@ -237,6 +237,7 @@ export class WorkCenterService {
|
|
|
237
237
|
throw error;
|
|
238
238
|
}
|
|
239
239
|
this.watcher.abortInvalidWorkItemRuns(id);
|
|
240
|
+
this.watcher.notifyActionInput(id, payload.actionId);
|
|
240
241
|
this.#emit({ type: 'action.input_added', workItem: detail });
|
|
241
242
|
return detail;
|
|
242
243
|
}
|