@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
|
@@ -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 = 15;
|
|
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;
|
|
@@ -195,6 +195,7 @@ function mapRun(row) {
|
|
|
195
195
|
totalTokens: Math.max(0, Number(row.total_tokens) || 0),
|
|
196
196
|
progressRevision: Math.max(0, Number(row.progress_revision) || 0),
|
|
197
197
|
checkpoint: normalizeActionCheckpoint(parseJson(row.checkpoint, null)),
|
|
198
|
+
acceptingInput: row.accepting_input !== 0,
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -350,7 +351,8 @@ export class WorkItemStore {
|
|
|
350
351
|
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
351
352
|
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
352
353
|
progress_revision INTEGER NOT NULL DEFAULT 0,
|
|
353
|
-
checkpoint TEXT
|
|
354
|
+
checkpoint TEXT,
|
|
355
|
+
accepting_input INTEGER NOT NULL DEFAULT 1
|
|
354
356
|
);
|
|
355
357
|
CREATE TABLE IF NOT EXISTS events (
|
|
356
358
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -361,6 +363,15 @@ export class WorkItemStore {
|
|
|
361
363
|
data TEXT NOT NULL,
|
|
362
364
|
created_at INTEGER NOT NULL
|
|
363
365
|
);
|
|
366
|
+
CREATE TABLE IF NOT EXISTS pending_action_inputs (
|
|
367
|
+
event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE,
|
|
368
|
+
work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE CASCADE,
|
|
369
|
+
action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
|
|
370
|
+
run_id TEXT,
|
|
371
|
+
text TEXT NOT NULL,
|
|
372
|
+
attachments TEXT NOT NULL DEFAULT '[]',
|
|
373
|
+
consumed_at INTEGER
|
|
374
|
+
);
|
|
364
375
|
CREATE TABLE IF NOT EXISTS plan_audits (
|
|
365
376
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
366
377
|
work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE CASCADE,
|
|
@@ -391,6 +402,8 @@ export class WorkItemStore {
|
|
|
391
402
|
CREATE INDEX IF NOT EXISTS idx_actions_ready ON actions(status, updated_at, sequence);
|
|
392
403
|
CREATE INDEX IF NOT EXISTS idx_runs_active ON runs(status, expires_at);
|
|
393
404
|
CREATE INDEX IF NOT EXISTS idx_events_work_item ON events(work_item_id, id);
|
|
405
|
+
CREATE INDEX IF NOT EXISTS idx_pending_action_inputs_action
|
|
406
|
+
ON pending_action_inputs(action_id, consumed_at, event_id);
|
|
394
407
|
`);
|
|
395
408
|
|
|
396
409
|
// The feature shipped first as an unmerged PR, but keep the store tolerant
|
|
@@ -459,6 +472,9 @@ export class WorkItemStore {
|
|
|
459
472
|
if (!hasColumn(this.db, 'runs', 'checkpoint')) {
|
|
460
473
|
this.db.exec('ALTER TABLE runs ADD COLUMN checkpoint TEXT');
|
|
461
474
|
}
|
|
475
|
+
if (!hasColumn(this.db, 'runs', 'accepting_input')) {
|
|
476
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN accepting_input INTEGER NOT NULL DEFAULT 1');
|
|
477
|
+
}
|
|
462
478
|
if (!hasColumn(this.db, 'work_items', 'workflow_snapshot')) {
|
|
463
479
|
this.db.exec('ALTER TABLE work_items ADD COLUMN workflow_snapshot TEXT');
|
|
464
480
|
}
|
|
@@ -535,6 +551,110 @@ export class WorkItemStore {
|
|
|
535
551
|
return Number(result.lastInsertRowid);
|
|
536
552
|
}
|
|
537
553
|
|
|
554
|
+
addActionInput(id, input, expected, updateReadyAction, attachments = null, addedAttachments = []) {
|
|
555
|
+
return withTransaction(this.db, () => {
|
|
556
|
+
const workItem = this.getWorkItem(id);
|
|
557
|
+
if (!workItem) return null;
|
|
558
|
+
if (!['ready', 'running'].includes(workItem.status)) {
|
|
559
|
+
throw new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
|
|
560
|
+
}
|
|
561
|
+
const action = this.getAction(expected.actionId);
|
|
562
|
+
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
563
|
+
const actionMatches = graphMode
|
|
564
|
+
? action?.workItemId === id && ['ready', 'running'].includes(action.status)
|
|
565
|
+
: action?.id === workItem.currentActionId && ['ready', 'running'].includes(action?.status);
|
|
566
|
+
const activeRun = action?.currentRunId ? this.getRun(action.currentRunId) : null;
|
|
567
|
+
if (!actionMatches || activeRun?.acceptingInput === false || workItem.revision !== expected.revision) {
|
|
568
|
+
throw new Error('Action changed before input was applied; refresh and try again');
|
|
569
|
+
}
|
|
570
|
+
const now = this.now();
|
|
571
|
+
const revision = workItem.revision + 1;
|
|
572
|
+
const updated = updateReadyAction(workItem, action);
|
|
573
|
+
const changedAction = this.db.prepare(`UPDATE actions SET context = ?, instruction = ?, updated_at = ?
|
|
574
|
+
WHERE id = ? AND status = ? AND current_run_id IS ?`).run(
|
|
575
|
+
stringify(updated.context || []),
|
|
576
|
+
updated.instruction || action.instruction,
|
|
577
|
+
now,
|
|
578
|
+
action.id,
|
|
579
|
+
action.status,
|
|
580
|
+
action.currentRunId,
|
|
581
|
+
);
|
|
582
|
+
if (Number(changedAction.changes) !== 1) {
|
|
583
|
+
throw new Error('Action changed before input was applied; refresh and try again');
|
|
584
|
+
}
|
|
585
|
+
this.db.prepare(`UPDATE work_items SET attachments = ?, revision = ?, updated_at = ?
|
|
586
|
+
WHERE id = ?`).run(
|
|
587
|
+
stringify(Array.isArray(attachments) ? attachments : workItem.attachments),
|
|
588
|
+
revision,
|
|
589
|
+
now,
|
|
590
|
+
id,
|
|
591
|
+
);
|
|
592
|
+
const projectedAttachments = (Array.isArray(addedAttachments) ? addedAttachments : []).map(attachment => ({
|
|
593
|
+
id: attachment.id,
|
|
594
|
+
name: attachment.name,
|
|
595
|
+
mimeType: attachment.mimeType,
|
|
596
|
+
size: Math.max(0, Number(attachment.size) || 0),
|
|
597
|
+
isImage: attachment.isImage === true,
|
|
598
|
+
}));
|
|
599
|
+
const eventId = this.appendEvent(id, 'action.input_added', {
|
|
600
|
+
text: input,
|
|
601
|
+
attachments: projectedAttachments,
|
|
602
|
+
}, { actionId: action.id, runId: action.currentRunId });
|
|
603
|
+
if (action.status === 'running') {
|
|
604
|
+
this.db.prepare(`INSERT INTO pending_action_inputs
|
|
605
|
+
(event_id, work_item_id, action_id, run_id, text, attachments, consumed_at)
|
|
606
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)`).run(
|
|
607
|
+
eventId,
|
|
608
|
+
id,
|
|
609
|
+
action.id,
|
|
610
|
+
action.currentRunId,
|
|
611
|
+
input,
|
|
612
|
+
stringify(projectedAttachments),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
return this.getWorkItemDetail(id);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
listPendingActionInputs(actionId, runId, ownerBootId, leaseEpoch) {
|
|
620
|
+
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
621
|
+
if (!active || active.action_id !== actionId) return [];
|
|
622
|
+
return this.db.prepare(`SELECT * FROM pending_action_inputs
|
|
623
|
+
WHERE action_id = ? AND consumed_at IS NULL ORDER BY event_id`).all(actionId).map(row => ({
|
|
624
|
+
id: String(row.event_id),
|
|
625
|
+
text: row.text || '',
|
|
626
|
+
attachments: parseJson(row.attachments, []),
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
acknowledgeActionInput(eventId, actionId, runId, ownerBootId, leaseEpoch) {
|
|
631
|
+
return withTransaction(this.db, () => {
|
|
632
|
+
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
633
|
+
if (!active || active.action_id !== actionId) return false;
|
|
634
|
+
const result = this.db.prepare(`UPDATE pending_action_inputs SET consumed_at = ?
|
|
635
|
+
WHERE event_id = ? AND action_id = ? AND consumed_at IS NULL`).run(
|
|
636
|
+
this.now(), Number(eventId), actionId,
|
|
637
|
+
);
|
|
638
|
+
return Number(result.changes) === 1;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
appendRunLoop(runId, ownerBootId, leaseEpoch, loop = {}) {
|
|
643
|
+
return withTransaction(this.db, () => {
|
|
644
|
+
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
645
|
+
if (!active) return null;
|
|
646
|
+
const response = normalizeRunResponse(loop.response).trim();
|
|
647
|
+
if (response) {
|
|
648
|
+
this.appendEvent(active.work_item_id, 'run.loop_output', {
|
|
649
|
+
loopNumber: Math.max(0, Number(loop.loopNumber) || 0),
|
|
650
|
+
response,
|
|
651
|
+
stopReason: loop.stopReason || null,
|
|
652
|
+
}, { actionId: active.action_id, runId });
|
|
653
|
+
}
|
|
654
|
+
return this.getWorkItemDetail(active.work_item_id);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
538
658
|
createPlanConflict(workItemId, input = {}) {
|
|
539
659
|
const id = input.id || randomUUID();
|
|
540
660
|
const now = this.now();
|
|
@@ -1001,6 +1121,10 @@ export class WorkItemStore {
|
|
|
1001
1121
|
return graphExecutionState(detail, detail.actions);
|
|
1002
1122
|
}
|
|
1003
1123
|
|
|
1124
|
+
listActionEvents(actionId) {
|
|
1125
|
+
return this.db.prepare(`SELECT * FROM events WHERE action_id = ? ORDER BY id`).all(actionId).map(mapEvent);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1004
1128
|
getReusableContext(workDir, excludeWorkItemId = null) {
|
|
1005
1129
|
const workspaceKey = canonicalWorkspaceKey(workDir);
|
|
1006
1130
|
if (!workspaceKey) return [];
|
|
@@ -1544,12 +1668,29 @@ export class WorkItemStore {
|
|
|
1544
1668
|
};
|
|
1545
1669
|
}
|
|
1546
1670
|
|
|
1671
|
+
closeRunInput(runId, ownerBootId, leaseEpoch) {
|
|
1672
|
+
return withTransaction(this.db, () => {
|
|
1673
|
+
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
1674
|
+
if (!active) return false;
|
|
1675
|
+
const pendingInput = this.db.prepare(`SELECT event_id FROM pending_action_inputs
|
|
1676
|
+
WHERE action_id = ? AND consumed_at IS NULL LIMIT 1`).get(active.action_id);
|
|
1677
|
+
if (pendingInput) return false;
|
|
1678
|
+
const changed = this.db.prepare(`UPDATE runs SET accepting_input = 0
|
|
1679
|
+
WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'
|
|
1680
|
+
AND accepting_input = 1`).run(runId, ownerBootId, leaseEpoch);
|
|
1681
|
+
return Number(changed.changes) === 1;
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1547
1685
|
finalizeRun(runId, ownerBootId, leaseEpoch, result, makeTransition) {
|
|
1548
1686
|
return withTransaction(this.db, () => {
|
|
1549
1687
|
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
1550
|
-
if (!active) return null;
|
|
1688
|
+
if (!active || Number(active.accepting_input) !== 0) return null;
|
|
1551
1689
|
const action = this.getAction(active.action_id);
|
|
1552
1690
|
const workItem = this.getWorkItem(active.work_item_id);
|
|
1691
|
+
const pendingInput = this.db.prepare(`SELECT event_id FROM pending_action_inputs
|
|
1692
|
+
WHERE action_id = ? AND consumed_at IS NULL LIMIT 1`).get(action.id);
|
|
1693
|
+
if (pendingInput) throw new Error('Run has unconsumed Action input and cannot finish yet');
|
|
1553
1694
|
const priorRuns = this.db.prepare(`SELECT * FROM runs
|
|
1554
1695
|
WHERE work_item_id = ? AND id != ? AND status != 'running'
|
|
1555
1696
|
ORDER BY started_at ASC`).all(workItem.id, runId).map(mapRun);
|
|
@@ -1727,14 +1868,15 @@ export class WorkItemStore {
|
|
|
1727
1868
|
currentActionId = blocked?.id || runnable?.id || null;
|
|
1728
1869
|
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
1729
1870
|
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
|
|
1730
|
-
WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')
|
|
1731
|
-
|
|
1871
|
+
WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')
|
|
1872
|
+
AND revision = ?`).run(
|
|
1873
|
+
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, nextWorkItem.revision,
|
|
1732
1874
|
);
|
|
1733
1875
|
} else {
|
|
1734
1876
|
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
1735
1877
|
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ? WHERE id = ? AND current_run_id = ?
|
|
1736
|
-
AND current_action_id = ? AND status = 'running'
|
|
1737
|
-
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, runId, action.id,
|
|
1878
|
+
AND current_action_id = ? AND status = 'running' AND revision = ?`).run(
|
|
1879
|
+
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, runId, action.id, nextWorkItem.revision,
|
|
1738
1880
|
);
|
|
1739
1881
|
}
|
|
1740
1882
|
if (Number(changedWorkItem.changes) !== 1) {
|
|
@@ -88,6 +88,13 @@ export class WorkItemWatcher {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
notifyActionInput(workItemId, actionId) {
|
|
92
|
+
for (const entry of this.activeRuns.values()) {
|
|
93
|
+
if (entry.workItemId !== workItemId || entry.actionId !== actionId) continue;
|
|
94
|
+
try { entry.wakeForPendingUserMessage?.(); } catch {}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
#recoverExpiredRuns() {
|
|
92
99
|
const recovered = this.store.recoverInterruptedRuns?.(this.ownerBootId) || 0;
|
|
93
100
|
if (recovered > 0) {
|
|
@@ -172,11 +179,15 @@ export class WorkItemWatcher {
|
|
|
172
179
|
readFinalProgress: null,
|
|
173
180
|
interrupted: false,
|
|
174
181
|
workItemId: claim.workItem.id,
|
|
182
|
+
actionId: claim.action.id,
|
|
175
183
|
runId: claim.run.id,
|
|
176
184
|
leaseEpoch: claim.run.leaseEpoch,
|
|
185
|
+
wakeForPendingUserMessage: null,
|
|
177
186
|
};
|
|
178
187
|
entry.promise = this.#execute(claim, abortController.signal, readProgress => {
|
|
179
188
|
entry.readFinalProgress = readProgress;
|
|
189
|
+
}, wake => {
|
|
190
|
+
entry.wakeForPendingUserMessage = wake;
|
|
180
191
|
}).finally(() => {
|
|
181
192
|
clearInterval(renewal);
|
|
182
193
|
this.activeRuns.delete(key);
|
|
@@ -186,7 +197,7 @@ export class WorkItemWatcher {
|
|
|
186
197
|
this.onEvent({ type: 'run.started', workItem: this.store.getWorkItemDetail(claim.workItem.id) });
|
|
187
198
|
}
|
|
188
199
|
|
|
189
|
-
async #execute(claim, signal, registerProgressReader) {
|
|
200
|
+
async #execute(claim, signal, registerProgressReader, registerInputWake) {
|
|
190
201
|
try {
|
|
191
202
|
let result;
|
|
192
203
|
try {
|
|
@@ -195,6 +206,7 @@ export class WorkItemWatcher {
|
|
|
195
206
|
signal,
|
|
196
207
|
ownerBootId: this.ownerBootId,
|
|
197
208
|
registerProgressReader,
|
|
209
|
+
registerInputWake,
|
|
198
210
|
onProgress: progress => {
|
|
199
211
|
const detail = this.store.updateRunProgress(
|
|
200
212
|
claim.run.id,
|