@yeaft/webchat-agent 1.0.256 → 1.0.258
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 +21 -17
- 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/action-identity.js +16 -8
- package/yeaft/work-center/bridge.js +2 -1
- package/yeaft/work-center/controller.js +24 -0
- package/yeaft/work-center/service.js +7 -0
- package/yeaft/work-center/store.js +113 -10
|
Binary file
|
package/package.json
CHANGED
|
@@ -8,23 +8,31 @@ export function eventMatchesActionGeneration(event, action) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function currentActionInputEventIds(events, action) {
|
|
11
|
-
const
|
|
12
|
-
.filter(event => event?.
|
|
11
|
+
const actionEvents = (Array.isArray(events) ? events : [])
|
|
12
|
+
.filter(event => event?.actionId === action?.id);
|
|
13
|
+
const inputEvents = new Map(actionEvents
|
|
14
|
+
.filter(event => event.type === 'action.input_added')
|
|
13
15
|
.map(event => [String(event.id), event]));
|
|
14
16
|
const valid = new Set();
|
|
17
|
+
const superseded = new Set();
|
|
15
18
|
for (const event of inputEvents.values()) {
|
|
16
19
|
if (eventMatchesActionGeneration(event, action)) valid.add(String(event.id));
|
|
17
20
|
}
|
|
18
|
-
for (const event of
|
|
19
|
-
if (event?.type !== 'action.input_rebound' || event.actionId !== action?.id
|
|
20
|
-
|| !eventMatchesActionGeneration(event, action)) continue;
|
|
21
|
+
for (const event of actionEvents) {
|
|
21
22
|
const sourceEventIds = [
|
|
22
23
|
event.data?.sourceEventId,
|
|
23
24
|
...(Array.isArray(event.data?.sourceEventIds) ? event.data.sourceEventIds : []),
|
|
24
|
-
].filter(value => value != null);
|
|
25
|
+
].filter(value => value != null).map(String);
|
|
26
|
+
if (event.type === 'action.input_superseded') {
|
|
27
|
+
for (const eventId of sourceEventIds) {
|
|
28
|
+
superseded.add(eventId);
|
|
29
|
+
valid.delete(eventId);
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (event.type !== 'action.input_rebound' || !eventMatchesActionGeneration(event, action)) continue;
|
|
25
34
|
for (const eventId of sourceEventIds) {
|
|
26
|
-
|
|
27
|
-
if (inputEvents.has(key)) valid.add(key);
|
|
35
|
+
if (inputEvents.has(eventId) && !superseded.has(eventId)) valid.add(eventId);
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
return valid;
|
|
@@ -20,7 +20,7 @@ let shutdownPromise = null;
|
|
|
20
20
|
let serviceFactory = null;
|
|
21
21
|
|
|
22
22
|
const BROWSER_DETAIL_OPS = new Set([
|
|
23
|
-
'get', 'create', 'update', 'start', 'cancel', 'action_input', 'retry_action', 'guide', 'retry',
|
|
23
|
+
'get', 'create', 'update', 'start', 'cancel', 'resume', 'action_input', 'retry_action', 'guide', 'retry',
|
|
24
24
|
]);
|
|
25
25
|
const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
|
|
26
26
|
// `files` is an internal server-to-Agent field. The browser relay rejects any
|
|
@@ -34,6 +34,7 @@ const BROWSER_FILE_FIELDS = Object.freeze({
|
|
|
34
34
|
],
|
|
35
35
|
action_input: ['id', 'text', 'actionId', 'revision', 'generation', 'files'],
|
|
36
36
|
retry_action: ['id', 'actionId', 'revision', 'generation'],
|
|
37
|
+
resume: ['id', 'revision'],
|
|
37
38
|
delete: ['id', 'revision'],
|
|
38
39
|
guide: ['id', 'guidance', 'actionId', 'revision', 'generation', 'files'],
|
|
39
40
|
get_action_messages: ['id', 'actionId', 'generation', 'cursor', 'limit'],
|
|
@@ -173,6 +173,30 @@ export class WorkflowController {
|
|
|
173
173
|
return this.store.getWorkItemDetail(id);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
resume(id, input = {}) {
|
|
177
|
+
const revision = Number(input.revision);
|
|
178
|
+
if (!Number.isInteger(revision) || revision < 1) {
|
|
179
|
+
throw new Error('revision is required to resume a WorkItem');
|
|
180
|
+
}
|
|
181
|
+
const detail = this.store.resumeWorkItemAtomic(id, revision, workItem => {
|
|
182
|
+
const action = initialActionFor(workItem);
|
|
183
|
+
if (workItem.reuseMemory === false) return action;
|
|
184
|
+
const context = this.store.getReusableContext(workItem.workDir, workItem.id);
|
|
185
|
+
return {
|
|
186
|
+
...action,
|
|
187
|
+
context,
|
|
188
|
+
instruction: actionInstruction(
|
|
189
|
+
action,
|
|
190
|
+
workItem,
|
|
191
|
+
context,
|
|
192
|
+
renderSessionContextSnapshot(workItem.sessionContext),
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
197
|
+
return detail;
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
guide(id, input = {}) {
|
|
177
201
|
const guidance = typeof input.guidance === 'string' ? input.guidance.trim().slice(0, 8_000) : '';
|
|
178
202
|
const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
|
|
@@ -307,6 +307,13 @@ export class WorkCenterService {
|
|
|
307
307
|
this.#emit({ type: 'work_item.cancelled', workItem: detail });
|
|
308
308
|
return detail;
|
|
309
309
|
}
|
|
310
|
+
case 'resume': {
|
|
311
|
+
const id = requiredString(payload.id, 'id');
|
|
312
|
+
const detail = this.controller.resume(id, { revision: payload.revision });
|
|
313
|
+
this.watcher.abortInvalidWorkItemRuns(id);
|
|
314
|
+
this.#emit({ type: 'work_item.resumed', workItem: detail });
|
|
315
|
+
return detail;
|
|
316
|
+
}
|
|
310
317
|
case 'delete': {
|
|
311
318
|
const id = requiredString(payload.id, 'id');
|
|
312
319
|
const deleted = this.store.deleteWorkItemAtomic(id, Number(payload.revision));
|
|
@@ -8,7 +8,7 @@ import { currentActionInputEventIds, runMatchesActionIdentity } from './action-i
|
|
|
8
8
|
import { canonicalActionInstruction, withoutActionInputContext } from './workflow.js';
|
|
9
9
|
|
|
10
10
|
const SCHEMA_VERSION = 22;
|
|
11
|
-
const
|
|
11
|
+
const UNFINISHED_ACTION_STATUSES = "'ready','running','waiting','failed'";
|
|
12
12
|
const MAX_REUSABLE_CONTEXT_ITEMS = 12;
|
|
13
13
|
const MAX_RUN_RESPONSE_CHARS = 65_536;
|
|
14
14
|
|
|
@@ -2643,16 +2643,32 @@ export class WorkItemStore {
|
|
|
2643
2643
|
}
|
|
2644
2644
|
|
|
2645
2645
|
#invalidateExecution(workItem, actionStatus, runStatus, reason, now) {
|
|
2646
|
-
const
|
|
2647
|
-
AND status IN (${
|
|
2648
|
-
this.#assertNoIntegrationReservation(
|
|
2649
|
-
this.#supersedePendingActionInputs(
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2646
|
+
const unfinishedActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
2647
|
+
AND status IN (${UNFINISHED_ACTION_STATUSES})`).all(workItem.id).map(mapAction);
|
|
2648
|
+
this.#assertNoIntegrationReservation(unfinishedActions, now);
|
|
2649
|
+
this.#supersedePendingActionInputs(unfinishedActions, reason, now);
|
|
2650
|
+
const actionIds = unfinishedActions.map(action => action.id);
|
|
2651
|
+
if (actionIds.length === 0) return;
|
|
2652
|
+
const placeholders = actionIds.map(() => '?').join(',');
|
|
2653
|
+
this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, error = ?, accepting_input = 0
|
|
2654
|
+
WHERE work_item_id = ? AND action_id IN (${placeholders}) AND status = 'running'`).run(
|
|
2655
|
+
runStatus,
|
|
2656
|
+
now,
|
|
2657
|
+
reason,
|
|
2658
|
+
workItem.id,
|
|
2659
|
+
...actionIds,
|
|
2660
|
+
);
|
|
2661
|
+
const changed = this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL,
|
|
2662
|
+
lease_epoch = lease_epoch + 1, updated_at = ? WHERE work_item_id = ?
|
|
2663
|
+
AND id IN (${placeholders}) AND status IN (${UNFINISHED_ACTION_STATUSES})`).run(
|
|
2664
|
+
actionStatus,
|
|
2665
|
+
now,
|
|
2666
|
+
workItem.id,
|
|
2667
|
+
...actionIds,
|
|
2668
|
+
);
|
|
2669
|
+
if (Number(changed.changes) !== actionIds.length) {
|
|
2670
|
+
throw new Error('Work Center execution changed while it was invalidated');
|
|
2653
2671
|
}
|
|
2654
|
-
this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL, updated_at = ?
|
|
2655
|
-
WHERE work_item_id = ? AND status IN (${OPEN_ACTION_STATUSES})`).run(actionStatus, now, workItem.id);
|
|
2656
2672
|
}
|
|
2657
2673
|
|
|
2658
2674
|
updateWorkItemAtomic(id, patch, makeInitialAction) {
|
|
@@ -2732,6 +2748,93 @@ export class WorkItemStore {
|
|
|
2732
2748
|
});
|
|
2733
2749
|
}
|
|
2734
2750
|
|
|
2751
|
+
resumeWorkItemAtomic(id, expectedRevision, makeInitialAction) {
|
|
2752
|
+
return withTransaction(this.db, () => {
|
|
2753
|
+
const workItem = this.getWorkItem(id);
|
|
2754
|
+
if (!workItem) return null;
|
|
2755
|
+
if (!Number.isInteger(expectedRevision) || workItem.revision !== expectedRevision) {
|
|
2756
|
+
throw new Error('WorkItem changed before it was resumed; refresh and try again');
|
|
2757
|
+
}
|
|
2758
|
+
if (workItem.status !== 'cancelled') {
|
|
2759
|
+
throw new Error(`WorkItem in ${workItem.status} cannot be resumed`);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
const now = this.now();
|
|
2763
|
+
const cancelledActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
2764
|
+
AND status = 'cancelled' ORDER BY sequence`).all(id).map(mapAction);
|
|
2765
|
+
this.#assertNoIntegrationReservation(cancelledActions, now);
|
|
2766
|
+
this.#supersedePendingActionInputs(
|
|
2767
|
+
cancelledActions,
|
|
2768
|
+
'WorkItem resume started a new Action generation',
|
|
2769
|
+
now,
|
|
2770
|
+
);
|
|
2771
|
+
const resumedActions = [];
|
|
2772
|
+
for (const action of cancelledActions) {
|
|
2773
|
+
const context = carryCurrentActionInputContext(this.db, action);
|
|
2774
|
+
const candidate = {
|
|
2775
|
+
...action,
|
|
2776
|
+
context,
|
|
2777
|
+
status: 'ready',
|
|
2778
|
+
attempt: 0,
|
|
2779
|
+
currentRunId: null,
|
|
2780
|
+
resultRunId: null,
|
|
2781
|
+
workspace: null,
|
|
2782
|
+
generation: action.generation + 1,
|
|
2783
|
+
};
|
|
2784
|
+
candidate.instruction = canonicalActionInstruction(workItem, candidate, context);
|
|
2785
|
+
candidate.specHash = actionSpecHash(candidate);
|
|
2786
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
|
|
2787
|
+
current_run_id = NULL, lease_epoch = lease_epoch + 1, generation = generation + 1,
|
|
2788
|
+
context = ?, instruction = ?, spec_hash = ?, identity_history = ?, result_run_id = NULL,
|
|
2789
|
+
workspace = NULL, updated_at = ? WHERE id = ? AND work_item_id = ?
|
|
2790
|
+
AND status = 'cancelled' AND generation = ? AND spec_hash = ?`).run(
|
|
2791
|
+
stringify(context),
|
|
2792
|
+
candidate.instruction,
|
|
2793
|
+
candidate.specHash,
|
|
2794
|
+
stringify(actionIdentityHistory(action, candidate.generation, candidate.specHash)),
|
|
2795
|
+
now,
|
|
2796
|
+
action.id,
|
|
2797
|
+
id,
|
|
2798
|
+
action.generation,
|
|
2799
|
+
action.specHash,
|
|
2800
|
+
);
|
|
2801
|
+
if (Number(changed.changes) !== 1) {
|
|
2802
|
+
throw new Error('WorkItem Action changed before it was resumed');
|
|
2803
|
+
}
|
|
2804
|
+
resumedActions.push(this.getAction(action.id));
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
if (resumedActions.length === 0) {
|
|
2808
|
+
const initialAction = makeInitialAction(workItem);
|
|
2809
|
+
resumedActions.push(this.#insertAction(id, {
|
|
2810
|
+
...initialAction,
|
|
2811
|
+
contractRevision: workItem.revision,
|
|
2812
|
+
}, this.#nextSequence(id), now));
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
const currentAction = resumedActions
|
|
2816
|
+
.find(action => action.dependsOnStageIds.length === 0) || resumedActions[0];
|
|
2817
|
+
const changedWorkItem = this.db.prepare(`UPDATE work_items SET status = 'ready',
|
|
2818
|
+
current_action_id = ?, current_run_id = NULL, updated_at = ?
|
|
2819
|
+
WHERE id = ? AND status = 'cancelled' AND revision = ?`).run(
|
|
2820
|
+
currentAction.id,
|
|
2821
|
+
now,
|
|
2822
|
+
id,
|
|
2823
|
+
expectedRevision,
|
|
2824
|
+
);
|
|
2825
|
+
if (Number(changedWorkItem.changes) !== 1) {
|
|
2826
|
+
throw new Error('WorkItem changed before it was resumed; refresh and try again');
|
|
2827
|
+
}
|
|
2828
|
+
this.appendEvent(id, 'work_item.resumed', {
|
|
2829
|
+
resumedActionIds: resumedActions.map(action => action.id),
|
|
2830
|
+
}, {
|
|
2831
|
+
actionId: currentAction.id,
|
|
2832
|
+
actionGeneration: currentAction.generation,
|
|
2833
|
+
});
|
|
2834
|
+
return this.getWorkItemDetail(id);
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2735
2838
|
startWorkItemAtomic(id, makeInitialAction) {
|
|
2736
2839
|
return withTransaction(this.db, () => {
|
|
2737
2840
|
const workItem = this.getWorkItem(id);
|