@yeaft/webchat-agent 1.0.165 → 1.0.167
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/connection/message-router.js +9 -1
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +126 -0
- package/yeaft/tools/create-work-item.js +3 -1
- package/yeaft/web-bridge.js +77 -0
- package/yeaft/work-center/assignment.js +12 -4
- package/yeaft/work-center/bridge.js +11 -2
- package/yeaft/work-center/controller.js +46 -5
- package/yeaft/work-center/projection.js +4 -0
- package/yeaft/work-center/runner.js +173 -9
- package/yeaft/work-center/service.js +5 -0
- package/yeaft/work-center/session-context.js +75 -0
- package/yeaft/work-center/store.js +381 -67
- package/yeaft/work-center/watcher.js +164 -84
- package/yeaft/work-center/workflow.js +77 -5
- package/yeaft/work-center/workspace.js +183 -0
|
@@ -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, 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';
|
|
32
|
+
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, 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
|
|
|
@@ -479,6 +479,14 @@ export async function handleMessage(msg) {
|
|
|
479
479
|
await handleYeaftLoadHistory(msg);
|
|
480
480
|
break;
|
|
481
481
|
|
|
482
|
+
case 'yeaft_search_history':
|
|
483
|
+
await handleYeaftSearchHistory(msg);
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'yeaft_load_history_window':
|
|
487
|
+
await handleYeaftLoadHistoryWindow(msg);
|
|
488
|
+
break;
|
|
489
|
+
|
|
482
490
|
case 'yeaft_load_more_history':
|
|
483
491
|
case 'unify_load_more_history':
|
|
484
492
|
await handleYeaftLoadMoreHistory(msg);
|
package/package.json
CHANGED
|
@@ -1395,6 +1395,105 @@ export class ConversationStore {
|
|
|
1395
1395
|
return Number.isFinite(seq) ? seq : null;
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
|
+
/**
|
|
1399
|
+
* Search user-visible messages inside one Session. The scan is newest-first
|
|
1400
|
+
* and stops as soon as one page plus a `hasMore` sentinel is found, so a
|
|
1401
|
+
* common recent hit does not materialize the full transcript.
|
|
1402
|
+
*
|
|
1403
|
+
* @param {string} sessionId
|
|
1404
|
+
* @param {string} query
|
|
1405
|
+
* @param {{ limit?: number, beforeSeq?: number|null }} [opts]
|
|
1406
|
+
* @returns {{ results: object[], hasMore: boolean, nextBeforeSeq: number|null }}
|
|
1407
|
+
*/
|
|
1408
|
+
searchVisibleBySession(sessionId, query, opts = {}) {
|
|
1409
|
+
const needle = typeof query === 'string' ? query.trim().toLocaleLowerCase() : '';
|
|
1410
|
+
if (!sessionId || needle.length < 2) return { results: [], hasMore: false, nextBeforeSeq: null };
|
|
1411
|
+
|
|
1412
|
+
const limit = Math.min(50, Math.max(1, Number.isFinite(opts.limit) ? Math.floor(opts.limit) : 20));
|
|
1413
|
+
const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
|
|
1414
|
+
const results = [];
|
|
1415
|
+
const seen = new Set();
|
|
1416
|
+
let hasMore = false;
|
|
1417
|
+
|
|
1418
|
+
for (const message of this.#iterateSessionRows(sessionId, { beforeSeq, desc: true })) {
|
|
1419
|
+
if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
|
|
1420
|
+
if (message.role !== 'user' && message.role !== 'assistant') continue;
|
|
1421
|
+
if (!message.id || seen.has(message.id)) continue;
|
|
1422
|
+
seen.add(message.id);
|
|
1423
|
+
|
|
1424
|
+
const text = this.#visibleSearchText(message.content);
|
|
1425
|
+
const matchIndex = text.toLocaleLowerCase().indexOf(needle);
|
|
1426
|
+
if (matchIndex < 0) continue;
|
|
1427
|
+
if (results.length >= limit) {
|
|
1428
|
+
hasMore = true;
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const seq = parseSeqFromId(message.id);
|
|
1433
|
+
if (!Number.isFinite(seq)) continue;
|
|
1434
|
+
results.push({
|
|
1435
|
+
messageId: message.id,
|
|
1436
|
+
turnId: message.turnId || message.threadId || message.id,
|
|
1437
|
+
seq,
|
|
1438
|
+
role: message.role,
|
|
1439
|
+
speakerVpId: message.speakerVpId || null,
|
|
1440
|
+
timestamp: message.ts || message.time || null,
|
|
1441
|
+
snippet: this.#searchSnippet(text, matchIndex, needle.length),
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
return {
|
|
1446
|
+
results,
|
|
1447
|
+
hasMore,
|
|
1448
|
+
nextBeforeSeq: hasMore && results.length > 0 ? results[results.length - 1].seq : null,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Load a bounded visible window around a search hit. This deliberately does
|
|
1454
|
+
* not mutate normal older-history cursors: the web client merges the window
|
|
1455
|
+
* into its cache solely to mount and focus the requested virtual-list item.
|
|
1456
|
+
*
|
|
1457
|
+
* @param {string} sessionId
|
|
1458
|
+
* @param {number} anchorSeq
|
|
1459
|
+
* @param {{ beforeTurns?: number, afterTurns?: number }} [opts]
|
|
1460
|
+
* @returns {{ messages: object[], oldestSeq: number|null, hasMoreBefore: boolean }}
|
|
1461
|
+
*/
|
|
1462
|
+
loadVisibleWindowBySession(sessionId, anchorSeq, opts = {}) {
|
|
1463
|
+
if (!sessionId || !Number.isFinite(anchorSeq)) {
|
|
1464
|
+
return { messages: [], oldestSeq: null, hasMoreBefore: false };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const beforeTurns = Math.min(10, Math.max(1, Number.isFinite(opts.beforeTurns) ? Math.floor(opts.beforeTurns) : 3));
|
|
1468
|
+
const afterTurns = Math.min(10, Math.max(1, Number.isFinite(opts.afterTurns) ? Math.floor(opts.afterTurns) : 3));
|
|
1469
|
+
const before = this.loadVisibleBySession(sessionId, anchorSeq + 1, beforeTurns + 1);
|
|
1470
|
+
const messages = before.messages.slice();
|
|
1471
|
+
const seen = new Set(messages.map(message => message?.id).filter(Boolean));
|
|
1472
|
+
let followingUserTurns = 0;
|
|
1473
|
+
|
|
1474
|
+
for (const message of this.#iterateSessionRows(sessionId, { afterSeq: anchorSeq, desc: false })) {
|
|
1475
|
+
if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
|
|
1476
|
+
if (message.role !== 'user' && message.role !== 'assistant') continue;
|
|
1477
|
+
if (message.role === 'user') {
|
|
1478
|
+
followingUserTurns += 1;
|
|
1479
|
+
if (followingUserTurns > afterTurns) break;
|
|
1480
|
+
}
|
|
1481
|
+
if (message.id && seen.has(message.id)) continue;
|
|
1482
|
+
const projected = this.#projectVisibleMessage(message);
|
|
1483
|
+
if (!projected) continue;
|
|
1484
|
+
if (projected.id) seen.add(projected.id);
|
|
1485
|
+
messages.push(projected);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
messages.sort(compareMessagesBySeq);
|
|
1489
|
+
const oldestSeq = messages.length > 0 ? parseSeqFromId(messages[0].id) : null;
|
|
1490
|
+
return {
|
|
1491
|
+
messages,
|
|
1492
|
+
oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
|
|
1493
|
+
hasMoreBefore: before.hasMore,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1398
1497
|
/**
|
|
1399
1498
|
* Count hot messages.
|
|
1400
1499
|
*
|
|
@@ -2115,6 +2214,33 @@ export class ConversationStore {
|
|
|
2115
2214
|
};
|
|
2116
2215
|
}
|
|
2117
2216
|
|
|
2217
|
+
#visibleSearchText(content) {
|
|
2218
|
+
if (typeof content === 'string') return content.replace(/\s+/g, ' ').trim();
|
|
2219
|
+
if (!Array.isArray(content)) return '';
|
|
2220
|
+
return content
|
|
2221
|
+
.filter(part => part && typeof part === 'object' && part.type === 'text')
|
|
2222
|
+
.map(part => typeof part.text === 'string' ? part.text : '')
|
|
2223
|
+
.join(' ')
|
|
2224
|
+
.replace(/\s+/g, ' ')
|
|
2225
|
+
.trim();
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
#searchSnippet(text, matchIndex, needleLength) {
|
|
2229
|
+
const radius = 90;
|
|
2230
|
+
const start = Math.max(0, matchIndex - radius);
|
|
2231
|
+
const end = Math.min(text.length, matchIndex + needleLength + radius);
|
|
2232
|
+
return `${start > 0 ? '…' : ''}${text.slice(start, end)}${end < text.length ? '…' : ''}`;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
#projectVisibleMessage(message) {
|
|
2236
|
+
if (!message || (message.role !== 'user' && message.role !== 'assistant')) return null;
|
|
2237
|
+
if (message.role !== 'assistant' || !Array.isArray(message.toolCalls) || message.toolCalls.length === 0) {
|
|
2238
|
+
return message;
|
|
2239
|
+
}
|
|
2240
|
+
const { toolCalls, ...rest } = message;
|
|
2241
|
+
return { ...rest, toolSummaryCount: toolCalls.length };
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2118
2244
|
#readSegmentRows(conversationDir, opts = {}) {
|
|
2119
2245
|
return this.#segmentStoreForConversationDir(conversationDir).readAll(opts);
|
|
2120
2246
|
}
|
|
@@ -58,7 +58,8 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
58
58
|
|
|
59
59
|
// Dynamic import avoids tools/index -> create-work-item -> bridge -> runner
|
|
60
60
|
// -> tools/index becoming a static initialization cycle.
|
|
61
|
-
const { createWorkItemFromProducer } = await import('../work-center/bridge.js');
|
|
61
|
+
const { createWorkItemFromProducer, snapshotCurrentSessionContext } = await import('../work-center/bridge.js');
|
|
62
|
+
const sessionContext = await snapshotCurrentSessionContext(sessionId);
|
|
62
63
|
const detail = await createWorkItemFromProducer({
|
|
63
64
|
title,
|
|
64
65
|
goal,
|
|
@@ -74,6 +75,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
74
75
|
createdBy: ctx.currentVpId || 'assistant',
|
|
75
76
|
},
|
|
76
77
|
linkedSessionIds: [sessionId],
|
|
78
|
+
sessionContext,
|
|
77
79
|
start: input.start !== false,
|
|
78
80
|
});
|
|
79
81
|
return JSON.stringify({
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
restoreSessionToRegistry,
|
|
51
51
|
readWorkDirRegistry,
|
|
52
52
|
migrateRegisteredWorkDirSessions,
|
|
53
|
+
resolveSessionYeaftDir,
|
|
53
54
|
} from './sessions/session-crud.js';
|
|
54
55
|
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
55
56
|
import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
|
|
@@ -5872,6 +5873,82 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
5872
5873
|
*
|
|
5873
5874
|
* @param {object} msg — { sessionId, beforeSeq, turns }
|
|
5874
5875
|
*/
|
|
5876
|
+
export async function handleYeaftSearchHistory(msg) {
|
|
5877
|
+
const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
|
|
5878
|
+
const query = typeof msg?.query === 'string' ? msg.query.trim().slice(0, 500) : '';
|
|
5879
|
+
const requestId = typeof msg?.requestId === 'string' ? msg.requestId : null;
|
|
5880
|
+
const beforeSeq = Number.isFinite(msg?.beforeSeq) ? msg.beforeSeq : null;
|
|
5881
|
+
const limit = Math.min(50, Math.max(1, Number.isFinite(msg?.limit) ? Math.floor(msg.limit) : 20));
|
|
5882
|
+
const response = {
|
|
5883
|
+
type: 'yeaft_history_search_result',
|
|
5884
|
+
requestId,
|
|
5885
|
+
sessionId: sessionId || null,
|
|
5886
|
+
query,
|
|
5887
|
+
results: [],
|
|
5888
|
+
hasMore: false,
|
|
5889
|
+
nextBeforeSeq: null,
|
|
5890
|
+
_requestClientId: msg?._requestClientId || null,
|
|
5891
|
+
};
|
|
5892
|
+
|
|
5893
|
+
if (!sessionId || query.length < 2) {
|
|
5894
|
+
sendToServer(response);
|
|
5895
|
+
return;
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
try {
|
|
5899
|
+
const defaultYeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
5900
|
+
const storeDir = resolveSessionYeaftDir(defaultYeaftDir, sessionId);
|
|
5901
|
+
const store = new ConversationStore(storeDir);
|
|
5902
|
+
const result = store.searchVisibleBySession(sessionId, query, { limit, beforeSeq });
|
|
5903
|
+
sendToServer({ ...response, ...result });
|
|
5904
|
+
} catch (err) {
|
|
5905
|
+
console.error('[Yeaft] Session history search failed:', err?.message || err);
|
|
5906
|
+
sendToServer({ ...response, error: 'search_failed' });
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
|
|
5910
|
+
export async function handleYeaftLoadHistoryWindow(msg) {
|
|
5911
|
+
const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
|
|
5912
|
+
const requestId = typeof msg?.requestId === 'string' ? msg.requestId : null;
|
|
5913
|
+
const anchorSeq = Number(msg?.anchorSeq);
|
|
5914
|
+
const anchorMessageId = typeof msg?.anchorMessageId === 'string' ? msg.anchorMessageId : null;
|
|
5915
|
+
const response = {
|
|
5916
|
+
type: 'yeaft_history_window',
|
|
5917
|
+
requestId,
|
|
5918
|
+
conversationId: ensureYeaftConversationId(),
|
|
5919
|
+
sessionId: sessionId || null,
|
|
5920
|
+
anchorMessageId,
|
|
5921
|
+
anchorSeq: Number.isFinite(anchorSeq) ? anchorSeq : null,
|
|
5922
|
+
messages: [],
|
|
5923
|
+
oldestSeq: null,
|
|
5924
|
+
hasMoreBefore: false,
|
|
5925
|
+
_requestClientId: msg?._requestClientId || null,
|
|
5926
|
+
};
|
|
5927
|
+
|
|
5928
|
+
if (!sessionId || !Number.isFinite(anchorSeq)) {
|
|
5929
|
+
sendToServer({ ...response, error: 'invalid_anchor' });
|
|
5930
|
+
return;
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
try {
|
|
5934
|
+
const defaultYeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
5935
|
+
const storeDir = resolveSessionYeaftDir(defaultYeaftDir, sessionId);
|
|
5936
|
+
const store = new ConversationStore(storeDir);
|
|
5937
|
+
const window = store.loadVisibleWindowBySession(sessionId, anchorSeq, {
|
|
5938
|
+
beforeTurns: msg?.beforeTurns,
|
|
5939
|
+
afterTurns: msg?.afterTurns,
|
|
5940
|
+
});
|
|
5941
|
+
sendToServer({
|
|
5942
|
+
...response,
|
|
5943
|
+
...window,
|
|
5944
|
+
messages: projectVisibleHistoryChunkMessages(window.messages),
|
|
5945
|
+
});
|
|
5946
|
+
} catch (err) {
|
|
5947
|
+
console.error('[Yeaft] Session history anchor load failed:', err?.message || err);
|
|
5948
|
+
sendToServer({ ...response, error: 'window_load_failed' });
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5875
5952
|
export async function handleYeaftLoadMoreHistory(msg) {
|
|
5876
5953
|
const sessionId = (msg && typeof msg.sessionId === 'string' && msg.sessionId) || null;
|
|
5877
5954
|
const perfTraceId = typeof msg?.perfTraceId === 'string' && msg.perfTraceId.trim() ? msg.perfTraceId.trim() : null;
|
|
@@ -136,13 +136,21 @@ export function resolveWorkItemModel(config, vp, rawPolicy) {
|
|
|
136
136
|
throw policyError(`Configured Work Center model is unavailable: ${model}`);
|
|
137
137
|
}
|
|
138
138
|
const effortOptions = Array.isArray(available?.effortOptions) ? available.effortOptions : [];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const effortOrder = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
140
|
+
const requestedIndex = effortOrder.indexOf(policy.effort);
|
|
141
|
+
const effort = !policy.effort || effortOptions.length === 0
|
|
142
|
+
? null
|
|
143
|
+
: effortOptions.includes(policy.effort)
|
|
144
|
+
? policy.effort
|
|
145
|
+
: effortOptions
|
|
146
|
+
.map(value => ({ value, distance: Math.abs(effortOrder.indexOf(value) - requestedIndex) }))
|
|
147
|
+
.filter(item => effortOrder.includes(item.value))
|
|
148
|
+
.sort((left, right) => left.distance - right.distance
|
|
149
|
+
|| effortOrder.indexOf(right.value) - effortOrder.indexOf(left.value))[0]?.value || null;
|
|
142
150
|
const parsed = parseModelRef(model);
|
|
143
151
|
return {
|
|
144
152
|
model,
|
|
145
|
-
effort
|
|
153
|
+
effort,
|
|
146
154
|
provider: available?.provider || parsed.providerName || null,
|
|
147
155
|
source,
|
|
148
156
|
policy,
|
|
@@ -9,6 +9,7 @@ import { projectWorkCenterEvent, projectWorkItemDetail } from './projection.js';
|
|
|
9
9
|
import { previewWorkCenterPlan } from './planner.js';
|
|
10
10
|
import { readWorkCenterSettings, writeWorkCenterSettings } from './settings.js';
|
|
11
11
|
import { defaultWorkCenterStageInstructions } from './workflow.js';
|
|
12
|
+
import { snapshotSessionContext } from './session-context.js';
|
|
12
13
|
import { join } from 'node:path';
|
|
13
14
|
|
|
14
15
|
let service = null;
|
|
@@ -22,8 +23,7 @@ const BROWSER_DETAIL_OPS = new Set(['get', 'create', 'update', 'start', 'cancel'
|
|
|
22
23
|
// client-supplied value and only emits files resolved from owned upload ids.
|
|
23
24
|
const BROWSER_FILE_FIELDS = Object.freeze({
|
|
24
25
|
create: [
|
|
25
|
-
'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', '
|
|
26
|
-
'linkedSessionIds', 'files', 'start',
|
|
26
|
+
'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'files', 'start',
|
|
27
27
|
],
|
|
28
28
|
guide: ['id', 'guidance', 'actionId', 'revision', 'files'],
|
|
29
29
|
});
|
|
@@ -100,6 +100,7 @@ async function createDefaultService() {
|
|
|
100
100
|
},
|
|
101
101
|
policyProvider: async () => readWorkCenterSettings(yeaftDir),
|
|
102
102
|
attachmentRoot: join(yeaftDir, 'work-center', 'attachments'),
|
|
103
|
+
actionWorktreeRoot: join(yeaftDir, 'work-center', 'worktrees'),
|
|
103
104
|
registry: defaultRegistry,
|
|
104
105
|
store: null,
|
|
105
106
|
});
|
|
@@ -107,6 +108,9 @@ async function createDefaultService() {
|
|
|
107
108
|
yeaftDir,
|
|
108
109
|
runner,
|
|
109
110
|
runtimeInfoProvider: getSettingsRuntime,
|
|
111
|
+
watcherOptions: {
|
|
112
|
+
concurrencyProvider: () => readWorkCenterSettings(yeaftDir).maxConcurrentActions,
|
|
113
|
+
},
|
|
110
114
|
onEvent(event) {
|
|
111
115
|
send({ type: 'work_center_event', event: projectWorkCenterEvent(event) });
|
|
112
116
|
},
|
|
@@ -140,6 +144,11 @@ export async function bootWorkCenter() {
|
|
|
140
144
|
return ensureWorkCenter();
|
|
141
145
|
}
|
|
142
146
|
|
|
147
|
+
export async function snapshotCurrentSessionContext(sessionId) {
|
|
148
|
+
const runtime = await getRuntime();
|
|
149
|
+
return snapshotSessionContext(runtime?.conversationStore, sessionId);
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
export async function createWorkItemFromProducer(payload) {
|
|
144
153
|
const workCenter = await ensureWorkCenter();
|
|
145
154
|
return workCenter.handle('create', payload, { trustedProducer: true });
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
initialActionFor,
|
|
7
7
|
RUN_OUTCOMES,
|
|
8
8
|
} from './workflow.js';
|
|
9
|
+
import { renderSessionContextSnapshot } from './session-context.js';
|
|
9
10
|
import { normalizeEvidence } from './evidence.js';
|
|
10
11
|
|
|
11
12
|
function normalizeCriteria(value) {
|
|
@@ -132,12 +133,18 @@ export class WorkflowController {
|
|
|
132
133
|
attachments: Array.isArray(input.attachments) ? input.attachments : [],
|
|
133
134
|
};
|
|
134
135
|
let firstAction = input.start !== false ? initialActionFor(draft) : null;
|
|
136
|
+
if (firstAction) {
|
|
137
|
+
firstAction = {
|
|
138
|
+
...firstAction,
|
|
139
|
+
instruction: actionInstruction(firstAction, draft, [], renderSessionContextSnapshot(draft.sessionContext)),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
135
142
|
if (firstAction && draft.reuseMemory !== false) {
|
|
136
143
|
const context = this.store.getReusableContext(draft.workDir, draft.id);
|
|
137
144
|
firstAction = {
|
|
138
145
|
...firstAction,
|
|
139
146
|
context,
|
|
140
|
-
instruction: actionInstruction(firstAction, draft, context),
|
|
147
|
+
instruction: actionInstruction(firstAction, draft, context, renderSessionContextSnapshot(draft.sessionContext)),
|
|
141
148
|
};
|
|
142
149
|
}
|
|
143
150
|
return this.store.createWorkItem(draft, firstAction);
|
|
@@ -151,7 +158,7 @@ export class WorkflowController {
|
|
|
151
158
|
return {
|
|
152
159
|
...action,
|
|
153
160
|
context,
|
|
154
|
-
instruction: actionInstruction(action, workItem, context),
|
|
161
|
+
instruction: actionInstruction(action, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
|
|
155
162
|
};
|
|
156
163
|
});
|
|
157
164
|
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
@@ -200,7 +207,7 @@ export class WorkflowController {
|
|
|
200
207
|
return {
|
|
201
208
|
...step,
|
|
202
209
|
context,
|
|
203
|
-
instruction: actionInstruction(step, workItem, context),
|
|
210
|
+
instruction: actionInstruction(step, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
|
|
204
211
|
maxAttempts: previous.maxAttempts || 2,
|
|
205
212
|
};
|
|
206
213
|
}, input.attachments);
|
|
@@ -221,6 +228,9 @@ export class WorkflowController {
|
|
|
221
228
|
assignmentPolicy: previous.assignmentPolicy,
|
|
222
229
|
modelPolicy: previous.modelPolicy,
|
|
223
230
|
requiredRole: previous.requiredRole,
|
|
231
|
+
dependsOnStageIds: previous.dependsOnStageIds,
|
|
232
|
+
workspaceMode: previous.workspaceMode,
|
|
233
|
+
changesRequestedStageId: previous.changesRequestedStageId,
|
|
224
234
|
brief: previous.brief,
|
|
225
235
|
}
|
|
226
236
|
: initialActionFor(workItem);
|
|
@@ -240,7 +250,7 @@ export class WorkflowController {
|
|
|
240
250
|
return {
|
|
241
251
|
...step,
|
|
242
252
|
context,
|
|
243
|
-
instruction: actionInstruction(step, workItem, context),
|
|
253
|
+
instruction: actionInstruction(step, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
|
|
244
254
|
maxAttempts: previous?.maxAttempts || 2,
|
|
245
255
|
};
|
|
246
256
|
});
|
|
@@ -287,6 +297,7 @@ export class WorkflowController {
|
|
|
287
297
|
workItemStatus: 'waiting',
|
|
288
298
|
keepCurrentAction: true,
|
|
289
299
|
eventType: 'action.waiting',
|
|
300
|
+
graphAdvance: workItem.workflowSnapshot?.executionMode === 'graph',
|
|
290
301
|
eventData: { reason: result.waitingReason },
|
|
291
302
|
};
|
|
292
303
|
}
|
|
@@ -298,6 +309,7 @@ export class WorkflowController {
|
|
|
298
309
|
workItemStatus: retryable ? 'ready' : 'needs_attention',
|
|
299
310
|
keepCurrentAction: true,
|
|
300
311
|
eventType: retryable ? 'action.retry_scheduled' : 'action.retry_exhausted',
|
|
312
|
+
graphAdvance: workItem.workflowSnapshot?.executionMode === 'graph',
|
|
301
313
|
eventData: {
|
|
302
314
|
attempt: action.attempt,
|
|
303
315
|
maxAttempts: action.maxAttempts,
|
|
@@ -312,6 +324,7 @@ export class WorkflowController {
|
|
|
312
324
|
workItemStatus: 'needs_attention',
|
|
313
325
|
keepCurrentAction: true,
|
|
314
326
|
eventType: 'action.failed',
|
|
327
|
+
graphAdvance: workItem.workflowSnapshot?.executionMode === 'graph',
|
|
315
328
|
eventData: { error: result.error },
|
|
316
329
|
};
|
|
317
330
|
}
|
|
@@ -332,6 +345,35 @@ export class WorkflowController {
|
|
|
332
345
|
const plannedWorkItem = generatedWorkflow
|
|
333
346
|
? { ...effectiveWorkItem, workflowSnapshot: generatedWorkflow }
|
|
334
347
|
: effectiveWorkItem;
|
|
348
|
+
const context = [...(action.context || []), contextEntry(action, result, activeRun)];
|
|
349
|
+
if (plannedWorkItem.workflowSnapshot?.executionMode === 'graph') {
|
|
350
|
+
if (action.type === 'review' && result.reviewDecision === 'changes_requested') {
|
|
351
|
+
const targetStage = plannedWorkItem.workflowSnapshot.stages
|
|
352
|
+
.find(stage => stage.id === action.changesRequestedStageId);
|
|
353
|
+
if (!targetStage) throw new Error('Work Center review return target is missing');
|
|
354
|
+
return {
|
|
355
|
+
actionStatus: 'completed', workItemStatus: 'ready', graphAdvance: true,
|
|
356
|
+
graphResetStageId: action.changesRequestedStageId,
|
|
357
|
+
graphResetAction: actionForStage(targetStage, plannedWorkItem, context),
|
|
358
|
+
eventType: 'review.changes_requested',
|
|
359
|
+
eventData: { targetStageId: action.changesRequestedStageId },
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const nextActions = generatedWorkflow
|
|
363
|
+
? generatedWorkflow.stages.slice(1).map(stage => actionForStage(stage, plannedWorkItem, context))
|
|
364
|
+
: [];
|
|
365
|
+
return {
|
|
366
|
+
actionStatus: 'completed',
|
|
367
|
+
workItemStatus: nextActions.length > 0 ? 'ready' : 'running',
|
|
368
|
+
contractPatch,
|
|
369
|
+
workflowSnapshot: generatedWorkflow,
|
|
370
|
+
nextActions,
|
|
371
|
+
graphAdvance: true,
|
|
372
|
+
eventType: 'action.completed',
|
|
373
|
+
eventData: { nextActionCount: nextActions.length, reviewDecision: result.reviewDecision },
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
335
377
|
const nextStep = getNextStep(plannedWorkItem, action.stageId || action.type, result);
|
|
336
378
|
if (!nextStep) {
|
|
337
379
|
return {
|
|
@@ -344,7 +386,6 @@ export class WorkflowController {
|
|
|
344
386
|
};
|
|
345
387
|
}
|
|
346
388
|
|
|
347
|
-
const context = [...(action.context || []), contextEntry(action, result, activeRun)];
|
|
348
389
|
return {
|
|
349
390
|
actionStatus: 'completed',
|
|
350
391
|
workItemStatus: 'ready',
|
|
@@ -100,6 +100,8 @@ function projectAction(action, runs) {
|
|
|
100
100
|
type: action.type,
|
|
101
101
|
stageId: action.stageId || action.type,
|
|
102
102
|
assignmentPolicy: projectAssignmentPolicy(action.assignmentPolicy),
|
|
103
|
+
dependsOnStageIds: Array.isArray(action.dependsOnStageIds) ? action.dependsOnStageIds : [],
|
|
104
|
+
workspaceMode: action.workspaceMode || 'shared',
|
|
103
105
|
requiredRole: action.requiredRole || '',
|
|
104
106
|
brief: normalizeActionBrief(action.brief, action.type),
|
|
105
107
|
status: action.status,
|
|
@@ -152,6 +154,7 @@ export function projectWorkItemDetail(detail) {
|
|
|
152
154
|
workflowTemplate: detail.workflowTemplate,
|
|
153
155
|
workItemType: detail.workflowSnapshot?.workItemType || null,
|
|
154
156
|
planningMode: detail.workflowSnapshot?.planningMode || 'static',
|
|
157
|
+
executionMode: detail.workflowSnapshot?.executionMode || 'linear',
|
|
155
158
|
status: detail.status,
|
|
156
159
|
currentActionId: detail.currentActionId || null,
|
|
157
160
|
executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
|
|
@@ -206,6 +209,7 @@ export function projectWorkItemSummary(detail) {
|
|
|
206
209
|
goal: detail.goal,
|
|
207
210
|
workItemType: detail.workflowSnapshot?.workItemType || null,
|
|
208
211
|
planningMode: detail.workflowSnapshot?.planningMode || 'static',
|
|
212
|
+
executionMode: detail.workflowSnapshot?.executionMode || 'linear',
|
|
209
213
|
status: detail.status,
|
|
210
214
|
currentActionId: detail.currentActionId || null,
|
|
211
215
|
executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
|