@yeaft/webchat-agent 1.0.205 → 1.0.206
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/local-runtime/web/app.bundle.js +34 -7
- 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/work-center/bridge.js +5 -1
- package/yeaft/work-center/controller.js +37 -27
- package/yeaft/work-center/mainline-projection.js +37 -7
- package/yeaft/work-center/projection.js +5 -0
- package/yeaft/work-center/service.js +24 -0
- package/yeaft/work-center/store.js +81 -8
- package/yeaft/work-center/watcher.js +7 -0
- package/yeaft/work-center/workflow.js +4 -1
|
Binary file
|
package/package.json
CHANGED
|
@@ -18,7 +18,9 @@ let shuttingDown = false;
|
|
|
18
18
|
let shutdownPromise = null;
|
|
19
19
|
let serviceFactory = null;
|
|
20
20
|
|
|
21
|
-
const BROWSER_DETAIL_OPS = new Set([
|
|
21
|
+
const BROWSER_DETAIL_OPS = new Set([
|
|
22
|
+
'get', 'create', 'update', 'start', 'cancel', 'work_item_message', 'action_input', 'retry_action', 'guide', 'retry',
|
|
23
|
+
]);
|
|
22
24
|
const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
|
|
23
25
|
// `files` is an internal server-to-Agent field. The browser relay rejects any
|
|
24
26
|
// client-supplied value and only emits files resolved from owned upload ids.
|
|
@@ -26,7 +28,9 @@ const BROWSER_FILE_FIELDS = Object.freeze({
|
|
|
26
28
|
create: [
|
|
27
29
|
'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'files', 'start',
|
|
28
30
|
],
|
|
31
|
+
work_item_message: ['id', 'text', 'revision'],
|
|
29
32
|
action_input: ['id', 'text', 'actionId', 'revision', 'generation', 'files'],
|
|
33
|
+
retry_action: ['id', 'actionId', 'revision', 'generation'],
|
|
30
34
|
guide: ['id', 'guidance', 'actionId', 'revision', 'generation', 'files'],
|
|
31
35
|
get_action_messages: ['id', 'actionId', 'cursor', 'limit'],
|
|
32
36
|
get_action_requests: ['id', 'actionId'],
|
|
@@ -189,15 +189,34 @@ export class WorkflowController {
|
|
|
189
189
|
return detail;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
message(id, input = {}) {
|
|
193
|
+
const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
|
|
194
|
+
if (!text) throw new Error('WorkItem message is required');
|
|
195
|
+
const revision = Number(input.revision);
|
|
196
|
+
if (!Number.isInteger(revision)) throw new Error('revision is required for WorkItem messages');
|
|
197
|
+
const detail = this.store.addWorkItemMessage(id, text, revision, (workItem, action) => (
|
|
198
|
+
actionInstruction(action, workItem, action.context || [], renderSessionContextSnapshot(workItem.sessionContext))
|
|
199
|
+
));
|
|
200
|
+
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
201
|
+
return detail;
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
input(id, input = {}) {
|
|
193
205
|
const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
|
|
194
206
|
const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
|
|
195
207
|
if (!text && addedAttachmentCount === 0) throw new Error('Action input or attachments are required');
|
|
196
208
|
const workItem = this.store.getWorkItem(id);
|
|
197
209
|
if (!workItem) throw new Error(`WorkItem not found: ${id}`);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
const targetAction = this.store.getAction(input.actionId);
|
|
211
|
+
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
212
|
+
const targetMatches = graphMode
|
|
213
|
+
? targetAction?.workItemId === id && targetAction.generation === input.generation
|
|
214
|
+
: workItem.currentActionId === input.actionId;
|
|
215
|
+
if (!targetMatches || workItem.revision !== input.revision) {
|
|
216
|
+
throw new Error('Action changed before input was applied; refresh and try again');
|
|
217
|
+
}
|
|
218
|
+
if (['ready', 'running'].includes(targetAction.status)) {
|
|
219
|
+
if (targetAction.status === 'running' && addedAttachmentCount > 0) {
|
|
201
220
|
throw new Error('Files cannot be added while an Action is running; send text now or wait for the next Action boundary');
|
|
202
221
|
}
|
|
203
222
|
const inputSummary = text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`;
|
|
@@ -205,20 +224,20 @@ export class WorkflowController {
|
|
|
205
224
|
actionId: input.actionId,
|
|
206
225
|
generation: input.generation,
|
|
207
226
|
revision: input.revision,
|
|
208
|
-
}, (current,
|
|
209
|
-
const context = [...(
|
|
227
|
+
}, (current, currentAction) => {
|
|
228
|
+
const context = [...(currentAction.context || []), {
|
|
210
229
|
type: 'input', role: 'user', summary: inputSummary, evidence: [],
|
|
211
230
|
}];
|
|
212
231
|
const step = {
|
|
213
|
-
type:
|
|
214
|
-
stageId:
|
|
215
|
-
assignmentPolicy:
|
|
216
|
-
modelPolicy:
|
|
217
|
-
requiredRole:
|
|
218
|
-
dependsOnStageIds:
|
|
219
|
-
workspaceMode:
|
|
220
|
-
changesRequestedStageId:
|
|
221
|
-
brief:
|
|
232
|
+
type: currentAction.type,
|
|
233
|
+
stageId: currentAction.stageId || currentAction.type,
|
|
234
|
+
assignmentPolicy: currentAction.assignmentPolicy,
|
|
235
|
+
modelPolicy: currentAction.modelPolicy,
|
|
236
|
+
requiredRole: currentAction.requiredRole,
|
|
237
|
+
dependsOnStageIds: currentAction.dependsOnStageIds,
|
|
238
|
+
workspaceMode: currentAction.workspaceMode,
|
|
239
|
+
changesRequestedStageId: currentAction.changesRequestedStageId,
|
|
240
|
+
brief: currentAction.brief,
|
|
222
241
|
};
|
|
223
242
|
return {
|
|
224
243
|
context,
|
|
@@ -226,17 +245,8 @@ export class WorkflowController {
|
|
|
226
245
|
};
|
|
227
246
|
}, input.attachments, input.addedAttachments);
|
|
228
247
|
}
|
|
229
|
-
if (!['waiting', '
|
|
230
|
-
throw new Error(`
|
|
231
|
-
}
|
|
232
|
-
const targetAction = this.store.getAction(input.actionId);
|
|
233
|
-
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
234
|
-
const targetMatches = graphMode
|
|
235
|
-
? targetAction?.workItemId === id && targetAction.generation === input.generation
|
|
236
|
-
&& ['waiting', 'failed'].includes(targetAction.status)
|
|
237
|
-
: workItem.currentActionId === input.actionId;
|
|
238
|
-
if (!targetMatches || workItem.revision !== input.revision) {
|
|
239
|
-
throw new Error('Action changed before input was applied; refresh and try again');
|
|
248
|
+
if (!['waiting', 'failed'].includes(targetAction.status)) {
|
|
249
|
+
throw new Error(`Action in ${targetAction.status} cannot accept input`);
|
|
240
250
|
}
|
|
241
251
|
return this.retry(id, {
|
|
242
252
|
answer: text,
|
|
@@ -254,8 +264,8 @@ export class WorkflowController {
|
|
|
254
264
|
const answer = typeof input.answer === 'string' ? input.answer.trim().slice(0, 8_000) : '';
|
|
255
265
|
const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
|
|
256
266
|
const detail = this.store.retryWorkItemAtomic(id, (workItem, previous, previousRun) => {
|
|
257
|
-
if (
|
|
258
|
-
throw new Error('answer or attachments are required to resume a waiting
|
|
267
|
+
if (previous?.status === 'waiting' && !answer && addedAttachmentCount === 0) {
|
|
268
|
+
throw new Error('answer or attachments are required to resume a waiting Action');
|
|
259
269
|
}
|
|
260
270
|
const step = previous
|
|
261
271
|
? {
|
|
@@ -70,9 +70,23 @@ function clamp(value, minimum, maximum) {
|
|
|
70
70
|
return Math.min(maximum, Math.max(minimum, value));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function
|
|
73
|
+
function workItemMessageView(messages) {
|
|
74
|
+
return (Array.isArray(messages) ? messages : [])
|
|
75
|
+
.filter(message => typeof message?.text === 'string' && message.text)
|
|
76
|
+
.slice()
|
|
77
|
+
.sort((left, right) => count(left.createdAt) - count(right.createdAt)
|
|
78
|
+
|| String(left.id).localeCompare(String(right.id)))
|
|
79
|
+
.map(message => ({
|
|
80
|
+
messageId: message.id,
|
|
81
|
+
text: message.text,
|
|
82
|
+
createdAt: count(message.createdAt),
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function guidanceView(events, actionId) {
|
|
74
87
|
return (Array.isArray(events) ? events : [])
|
|
75
|
-
.filter(event =>
|
|
88
|
+
.filter(event => event?.actionId === actionId
|
|
89
|
+
&& ['action.guidance_added', 'action.input_added'].includes(event.type))
|
|
76
90
|
.slice()
|
|
77
91
|
.sort((left, right) => count(left.id) - count(right.id))
|
|
78
92
|
.map(event => ({
|
|
@@ -223,6 +237,7 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
|
|
|
223
237
|
directDependencies: dependencies,
|
|
224
238
|
userContext: {
|
|
225
239
|
sessionContext: [],
|
|
240
|
+
workItemMessages: [],
|
|
226
241
|
guidance: [],
|
|
227
242
|
includedCount: 0,
|
|
228
243
|
omittedCount: 0,
|
|
@@ -249,12 +264,26 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
|
|
|
249
264
|
return false;
|
|
250
265
|
};
|
|
251
266
|
const sessionContext = normalizeSessionContextSnapshot(detail.sessionContext);
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
267
|
+
const workItemMessages = workItemMessageView(detail.messages);
|
|
268
|
+
const guidance = guidanceView(detail.events, action.id);
|
|
269
|
+
const newestFirstMessages = workItemMessages.slice().reverse();
|
|
270
|
+
for (const [index, message] of newestFirstMessages.entries()) {
|
|
271
|
+
const next = {
|
|
272
|
+
...snapshot.userContext,
|
|
273
|
+
workItemMessages: [message, ...snapshot.userContext.workItemMessages],
|
|
274
|
+
includedCount: snapshot.userContext.includedCount + 1,
|
|
275
|
+
omittedCount: 0,
|
|
276
|
+
};
|
|
277
|
+
const included = trySet('userContext', next);
|
|
278
|
+
if (index === 0 && !included) {
|
|
279
|
+
throw mainlineContextBlocked('Latest WorkItem message exceeds the Mainline prompt budget');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const otherUserEntries = [
|
|
255
283
|
...guidance.map(value => ({ kind: 'guidance', value })),
|
|
284
|
+
...sessionContext.map(value => ({ kind: 'sessionContext', value })),
|
|
256
285
|
];
|
|
257
|
-
for (const entry of
|
|
286
|
+
for (const entry of otherUserEntries) {
|
|
258
287
|
const next = {
|
|
259
288
|
...snapshot.userContext,
|
|
260
289
|
[entry.kind]: [...snapshot.userContext[entry.kind], entry.value],
|
|
@@ -263,7 +292,8 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
|
|
|
263
292
|
};
|
|
264
293
|
trySet('userContext', next);
|
|
265
294
|
}
|
|
266
|
-
snapshot.userContext.omittedCount =
|
|
295
|
+
snapshot.userContext.omittedCount = workItemMessages.length + otherUserEntries.length
|
|
296
|
+
- snapshot.userContext.includedCount;
|
|
267
297
|
|
|
268
298
|
const siblingEntries = Object.entries(projection.canonicalActionResults)
|
|
269
299
|
.filter(([actionId]) => actionId !== action.id && !dependencies.some(item => item.actionId === actionId))
|
|
@@ -610,6 +610,11 @@ export function projectWorkItemDetail(detail) {
|
|
|
610
610
|
|
|
611
611
|
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|
|
612
612
|
linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
|
|
613
|
+
messages: (Array.isArray(detail.messages) ? detail.messages : []).slice(-100).map(message => ({
|
|
614
|
+
id: String(message.id || ''),
|
|
615
|
+
text: truncateUtf8(message.text || '', MAX_ACTION_MESSAGE_CHARS),
|
|
616
|
+
createdAt: count(message.createdAt),
|
|
617
|
+
})),
|
|
613
618
|
attachments: projectAttachments(detail.attachments),
|
|
614
619
|
createdAt: detail.createdAt,
|
|
615
620
|
updatedAt: detail.updatedAt,
|
|
@@ -207,6 +207,30 @@ export class WorkCenterService {
|
|
|
207
207
|
this.#emit({ type: 'work_item.cancelled', workItem: detail });
|
|
208
208
|
return detail;
|
|
209
209
|
}
|
|
210
|
+
case 'work_item_message': {
|
|
211
|
+
const id = requiredString(payload.id, 'id');
|
|
212
|
+
const detail = this.controller.message(id, {
|
|
213
|
+
text: typeof payload.text === 'string' ? payload.text : '',
|
|
214
|
+
revision: payload.revision,
|
|
215
|
+
});
|
|
216
|
+
this.watcher.notifyWorkItemInput(id);
|
|
217
|
+
this.#emit({ type: 'work_item.message_added', workItem: detail });
|
|
218
|
+
return detail;
|
|
219
|
+
}
|
|
220
|
+
case 'retry_action': {
|
|
221
|
+
const id = requiredString(payload.id, 'id');
|
|
222
|
+
const detail = this.controller.retry(id, {
|
|
223
|
+
expected: {
|
|
224
|
+
actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
|
|
225
|
+
revision: payload.revision,
|
|
226
|
+
generation: payload.generation,
|
|
227
|
+
statuses: ['failed'],
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
this.watcher.abortInvalidWorkItemRuns(id);
|
|
231
|
+
this.#emit({ type: 'action.retried', workItem: detail });
|
|
232
|
+
return detail;
|
|
233
|
+
}
|
|
210
234
|
case 'action_input': {
|
|
211
235
|
const id = requiredString(payload.id, 'id');
|
|
212
236
|
const workItem = this.#requiredItem(id);
|
|
@@ -5,7 +5,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { normalizeEvidence } from './evidence.js';
|
|
6
6
|
import { normalizeActionCheckpoint } from './action-checkpoint.js';
|
|
7
7
|
|
|
8
|
-
const SCHEMA_VERSION =
|
|
8
|
+
const SCHEMA_VERSION = 16;
|
|
9
9
|
const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
|
|
10
10
|
const MAX_REUSABLE_CONTEXT_ITEMS = 12;
|
|
11
11
|
const MAX_RUN_RESPONSE_CHARS = 65_536;
|
|
@@ -77,6 +77,7 @@ function mapWorkItem(row) {
|
|
|
77
77
|
origin: parseJson(row.origin, null),
|
|
78
78
|
linkedSessionIds: parseJson(row.linked_session_ids, []),
|
|
79
79
|
sessionContext: parseJson(row.session_context, []),
|
|
80
|
+
messages: parseJson(row.messages, []),
|
|
80
81
|
attachments: parseJson(row.attachments, []),
|
|
81
82
|
executionStats: {
|
|
82
83
|
llmRequestCount: Math.max(0, Number(row.usage_llm_request_count) || 0),
|
|
@@ -284,6 +285,7 @@ export class WorkItemStore {
|
|
|
284
285
|
origin TEXT,
|
|
285
286
|
linked_session_ids TEXT NOT NULL DEFAULT '[]',
|
|
286
287
|
session_context TEXT NOT NULL DEFAULT '[]',
|
|
288
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
287
289
|
attachments TEXT NOT NULL DEFAULT '[]',
|
|
288
290
|
created_at INTEGER NOT NULL,
|
|
289
291
|
updated_at INTEGER NOT NULL
|
|
@@ -421,6 +423,9 @@ export class WorkItemStore {
|
|
|
421
423
|
if (!hasColumn(this.db, 'work_items', 'reuse_memory')) {
|
|
422
424
|
this.db.exec('ALTER TABLE work_items ADD COLUMN reuse_memory INTEGER NOT NULL DEFAULT 1');
|
|
423
425
|
}
|
|
426
|
+
if (!hasColumn(this.db, 'work_items', 'messages')) {
|
|
427
|
+
this.db.exec("ALTER TABLE work_items ADD COLUMN messages TEXT NOT NULL DEFAULT '[]'");
|
|
428
|
+
}
|
|
424
429
|
if (!hasColumn(this.db, 'actions', 'brief')) {
|
|
425
430
|
this.db.exec('ALTER TABLE actions ADD COLUMN brief TEXT');
|
|
426
431
|
}
|
|
@@ -555,13 +560,17 @@ export class WorkItemStore {
|
|
|
555
560
|
return withTransaction(this.db, () => {
|
|
556
561
|
const workItem = this.getWorkItem(id);
|
|
557
562
|
if (!workItem) return null;
|
|
558
|
-
|
|
563
|
+
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
564
|
+
const inputStatuses = graphMode
|
|
565
|
+
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
566
|
+
: ['ready', 'running'];
|
|
567
|
+
if (!inputStatuses.includes(workItem.status)) {
|
|
559
568
|
throw new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
|
|
560
569
|
}
|
|
561
570
|
const action = this.getAction(expected.actionId);
|
|
562
|
-
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
563
571
|
const actionMatches = graphMode
|
|
564
|
-
? action?.workItemId === id &&
|
|
572
|
+
? action?.workItemId === id && action.generation === expected.generation
|
|
573
|
+
&& ['ready', 'running'].includes(action.status)
|
|
565
574
|
: action?.id === workItem.currentActionId && ['ready', 'running'].includes(action?.status);
|
|
566
575
|
const activeRun = action?.currentRunId ? this.getRun(action.currentRunId) : null;
|
|
567
576
|
if (!actionMatches || activeRun?.acceptingInput === false || workItem.revision !== expected.revision) {
|
|
@@ -1217,6 +1226,63 @@ export class WorkItemStore {
|
|
|
1217
1226
|
return this.db.prepare(`SELECT * FROM events WHERE action_id = ? ORDER BY id`).all(actionId).map(mapEvent);
|
|
1218
1227
|
}
|
|
1219
1228
|
|
|
1229
|
+
addWorkItemMessage(id, text, expectedRevision, updateActionInstruction) {
|
|
1230
|
+
return withTransaction(this.db, () => {
|
|
1231
|
+
const workItem = this.getWorkItem(id);
|
|
1232
|
+
if (!workItem) return null;
|
|
1233
|
+
if (['done', 'cancelled'].includes(workItem.status)) {
|
|
1234
|
+
throw new Error(`WorkItem in ${workItem.status} cannot accept messages`);
|
|
1235
|
+
}
|
|
1236
|
+
if (workItem.revision !== expectedRevision) {
|
|
1237
|
+
throw new Error('WorkItem changed before the message was applied; refresh and try again');
|
|
1238
|
+
}
|
|
1239
|
+
const openActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
1240
|
+
AND status IN ('ready', 'running') ORDER BY sequence`).all(id).map(mapAction);
|
|
1241
|
+
for (const action of openActions.filter(candidate => candidate.status === 'running')) {
|
|
1242
|
+
const run = action.currentRunId ? this.getRun(action.currentRunId) : null;
|
|
1243
|
+
if (!run || run.status !== 'running' || run.acceptingInput === false) {
|
|
1244
|
+
throw new Error('A running Action closed its input window before the WorkItem message was applied; refresh and try again');
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const now = this.now();
|
|
1248
|
+
const revision = workItem.revision + 1;
|
|
1249
|
+
const message = { id: randomUUID(), text, createdAt: now };
|
|
1250
|
+
const messages = [...(workItem.messages || []), message].slice(-100);
|
|
1251
|
+
this.db.prepare(`UPDATE work_items SET messages = ?, revision = ?, updated_at = ? WHERE id = ?`)
|
|
1252
|
+
.run(stringify(messages), revision, now, id);
|
|
1253
|
+
const updatedWorkItem = { ...workItem, messages, revision };
|
|
1254
|
+
for (const action of openActions) {
|
|
1255
|
+
if (action.status === 'ready') {
|
|
1256
|
+
const instruction = updateActionInstruction(updatedWorkItem, action);
|
|
1257
|
+
this.db.prepare(`UPDATE actions SET instruction = ?, spec_hash = ?, updated_at = ?
|
|
1258
|
+
WHERE id = ? AND status = 'ready'`).run(
|
|
1259
|
+
instruction,
|
|
1260
|
+
actionSpecHash({ ...action, instruction }),
|
|
1261
|
+
now,
|
|
1262
|
+
action.id,
|
|
1263
|
+
);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const run = this.getRun(action.currentRunId);
|
|
1267
|
+
const eventId = this.appendEvent(id, 'work_item.message_applied', { message }, {
|
|
1268
|
+
actionId: action.id,
|
|
1269
|
+
runId: action.currentRunId,
|
|
1270
|
+
});
|
|
1271
|
+
this.db.prepare(`INSERT INTO pending_action_inputs
|
|
1272
|
+
(event_id, work_item_id, action_id, run_id, text, attachments, consumed_at)
|
|
1273
|
+
VALUES (?, ?, ?, ?, ?, '[]', NULL)`).run(
|
|
1274
|
+
eventId,
|
|
1275
|
+
id,
|
|
1276
|
+
action.id,
|
|
1277
|
+
action.currentRunId,
|
|
1278
|
+
`WorkItem-level message: ${text}`,
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
this.appendEvent(id, 'work_item.message_added', { message });
|
|
1282
|
+
return this.getWorkItemDetail(id);
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1220
1286
|
getReusableContext(workDir, excludeWorkItemId = null) {
|
|
1221
1287
|
const workspaceKey = canonicalWorkspaceKey(workDir);
|
|
1222
1288
|
if (!workspaceKey) return [];
|
|
@@ -1432,17 +1498,24 @@ export class WorkItemStore {
|
|
|
1432
1498
|
return withTransaction(this.db, () => {
|
|
1433
1499
|
const workItem = this.getWorkItem(id);
|
|
1434
1500
|
if (!workItem) return null;
|
|
1435
|
-
|
|
1501
|
+
const graphMode = isGraphWorkItem(workItem);
|
|
1502
|
+
const retryableWorkItemStatuses = graphMode
|
|
1503
|
+
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
1504
|
+
: ['waiting', 'needs_attention'];
|
|
1505
|
+
if (!retryableWorkItemStatuses.includes(workItem.status)) {
|
|
1436
1506
|
throw new Error(`WorkItem in ${workItem.status} does not need retry`);
|
|
1437
1507
|
}
|
|
1438
|
-
const graphMode = isGraphWorkItem(workItem);
|
|
1439
1508
|
let previous = workItem.currentActionId ? this.getAction(workItem.currentActionId) : null;
|
|
1440
1509
|
if (options.expected) {
|
|
1441
1510
|
const expectedAction = this.getAction(options.expected.actionId);
|
|
1511
|
+
const allowedExpectedStatuses = Array.isArray(options.expected.statuses)
|
|
1512
|
+
? options.expected.statuses
|
|
1513
|
+
: ['waiting', 'failed'];
|
|
1442
1514
|
const expectedMatches = graphMode
|
|
1443
1515
|
? expectedAction?.workItemId === id && expectedAction.generation === options.expected.generation
|
|
1444
|
-
&&
|
|
1445
|
-
: workItem.currentActionId === options.expected.actionId
|
|
1516
|
+
&& allowedExpectedStatuses.includes(expectedAction.status)
|
|
1517
|
+
: workItem.currentActionId === options.expected.actionId
|
|
1518
|
+
&& allowedExpectedStatuses.includes(expectedAction?.status);
|
|
1446
1519
|
if (!expectedMatches || workItem.revision !== options.expected.revision) {
|
|
1447
1520
|
throw new Error('Action changed before input was applied; refresh and try again');
|
|
1448
1521
|
}
|
|
@@ -95,6 +95,13 @@ export class WorkItemWatcher {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
notifyWorkItemInput(workItemId) {
|
|
99
|
+
for (const entry of this.activeRuns.values()) {
|
|
100
|
+
if (entry.workItemId !== workItemId) continue;
|
|
101
|
+
try { entry.wakeForPendingUserMessage?.(); } catch {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
#recoverExpiredRuns() {
|
|
99
106
|
const recovered = this.store.recoverInterruptedRuns?.(this.ownerBootId) || 0;
|
|
100
107
|
if (recovered > 0) {
|
|
@@ -675,7 +675,10 @@ function renderContext(context = []) {
|
|
|
675
675
|
|
|
676
676
|
export function actionInstruction(stage, workItem, context = [], sessionContextBlock = renderSessionContextSnapshot(workItem?.sessionContext)) {
|
|
677
677
|
const criteria = (workItem.acceptanceCriteria || []).map(item => `- ${item}`).join('\n') || '- No explicit criteria';
|
|
678
|
-
const
|
|
678
|
+
const workItemMessages = Array.isArray(workItem.messages) && workItem.messages.length > 0
|
|
679
|
+
? `\n\nWorkItem-level user messages (apply to every unfinished Action):\n${workItem.messages.map(message => `- ${message.text}`).join('\n')}`
|
|
680
|
+
: '';
|
|
681
|
+
const common = `WorkItem: ${workItem.title}\nGoal: ${workItem.goal}\nAcceptance criteria:\n${criteria}${sessionContextBlock}${workItemMessages}${renderContext(context)}`;
|
|
679
682
|
const policy = stage.instruction || defaultWorkCenterStageInstruction(stage.type);
|
|
680
683
|
const brief = normalizeActionBrief(stage.brief || stage, stage.type);
|
|
681
684
|
const contract = `Action type: ${stage.type}\nWhat to do:\n${brief.objective}\n\nHow to do it:\n${brief.approach}\n\nExpected result:\n${brief.expectedOutcome}`;
|