@yeaft/webchat-agent 1.0.531 → 1.0.532
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 +217 -104
- 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 +3 -2
- package/yeaft/work-center/capabilities.js +44 -0
- package/yeaft/work-center/completion-contract.js +11 -3
- package/yeaft/work-center/controller.js +8 -1
- package/yeaft/work-center/coordinator.js +42 -21
- package/yeaft/work-center/dynamic-coordination.js +51 -2
- package/yeaft/work-center/goal-state.js +135 -0
- package/yeaft/work-center/projection.js +57 -8
- package/yeaft/work-center/resource-control.js +460 -0
- package/yeaft/work-center/runner.js +10 -20
- package/yeaft/work-center/service.js +28 -7
- package/yeaft/work-center/store.js +161 -61
- package/yeaft/work-center/workflow.js +7 -3
|
@@ -2,12 +2,14 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
2
2
|
import { mkdirSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { WorkCenterResourceControl, WorkCenterResourceStopError } from './resource-control.js';
|
|
5
6
|
import { normalizeEvidence, normalizeOutputs } from './evidence.js';
|
|
6
|
-
import {
|
|
7
|
+
import { currentGoalRuns, deriveGoalProgress, hasContradictoryEvidence, validGoalChecks } from './goal-state.js';
|
|
8
|
+
import { assertCoordinatorContractAuthority, normalizeContractPatch } from './completion-contract.js';
|
|
7
9
|
import { normalizeActionCheckpoint } from './action-checkpoint.js';
|
|
8
10
|
import { currentActionInputEventIds, runMatchesActionIdentity } from './action-identity.js';
|
|
9
11
|
import { isDynamicWorkItem, usesLegacyGraph } from './execution-mode.js';
|
|
10
|
-
import { normalizeDynamicCompletion } from './dynamic-coordination.js';
|
|
12
|
+
import { normalizeDynamicCompletion, normalizeDynamicGoalRefs } from './dynamic-coordination.js';
|
|
11
13
|
import { canonicalActionInstruction, withoutActionInputContext } from './workflow.js';
|
|
12
14
|
import {
|
|
13
15
|
WORK_CENTER_SCHEMA_VERSION,
|
|
@@ -815,6 +817,7 @@ export class WorkItemStore {
|
|
|
815
817
|
this.db.exec('PRAGMA synchronous = NORMAL;');
|
|
816
818
|
this.db.exec('PRAGMA foreign_keys = ON;');
|
|
817
819
|
this.#initSchema();
|
|
820
|
+
this.resourceControl = new WorkCenterResourceControl(this);
|
|
818
821
|
this.recoverCoordinatorMailbox();
|
|
819
822
|
this.recoverCoordinatorProviderTurns();
|
|
820
823
|
this.recoverOperations();
|
|
@@ -822,6 +825,22 @@ export class WorkItemStore {
|
|
|
822
825
|
this.recoverInterruptedCoordinatorTurns();
|
|
823
826
|
}
|
|
824
827
|
|
|
828
|
+
getExecutionControl(id) { return this.resourceControl.atomic(() => this.resourceControl.snapshot(id)); }
|
|
829
|
+
isExecutionStopped(id) { return this.resourceControl.stopped(id); }
|
|
830
|
+
reserveWorkItemRequest(request) { return this.resourceControl.reserve(request); }
|
|
831
|
+
settleCoordinatorRequest(id, usage, complete = true) { return this.resourceControl.atomic(() => this.resourceControl.settleCoordinator(id, usage, complete)); }
|
|
832
|
+
settleWorkItemRequest(id, usage, complete = true) { return this.resourceControl.settle(id, usage, complete); }
|
|
833
|
+
extendExecutionBudget(id, revision, additions) { return this.resourceControl.extend(id, revision, additions); }
|
|
834
|
+
stopExecution(id, code, details) { return this.resourceControl.stop(id, code, details); }
|
|
835
|
+
canAutomaticallyCoordinate(id, { userMessage = false } = {}) {
|
|
836
|
+
const row = this.db.prepare('SELECT status FROM work_items WHERE id = ?').get(id);
|
|
837
|
+
if (!row || ['done', 'cancelled'].includes(row.status) || this.isExecutionStopped(id)) return false;
|
|
838
|
+
const control = this.getExecutionControl(id);
|
|
839
|
+
// A failed Action may be closed without retrying it. Enforce lifetime
|
|
840
|
+
// attempts at Action claim, not before a Coordinator can inspect its result.
|
|
841
|
+
return userMessage || this.now() >= control.retryAfter;
|
|
842
|
+
}
|
|
843
|
+
|
|
825
844
|
#initSchema() {
|
|
826
845
|
this.db.exec(`
|
|
827
846
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
@@ -1353,6 +1372,8 @@ export class WorkItemStore {
|
|
|
1353
1372
|
|
|
1354
1373
|
claimCoordinatorTurn(workItemId, turnId, owner, leaseMs = 60_000) {
|
|
1355
1374
|
return withTransaction(this.db, () => {
|
|
1375
|
+
const assistant = this.getWorkItem(workItemId)?.messages?.find(message => message.turnId === turnId && message.role === 'assistant');
|
|
1376
|
+
if (!this.canAutomaticallyCoordinate(workItemId, { userMessage: !assistant?.automatic && !assistant?.recovery })) return null;
|
|
1356
1377
|
const now = this.now();
|
|
1357
1378
|
const row = this.db.prepare(`SELECT * FROM coordinator_mailbox_entries
|
|
1358
1379
|
WHERE work_item_id = ? AND json_extract(payload, '$.turnId') = ?
|
|
@@ -1405,6 +1426,7 @@ export class WorkItemStore {
|
|
|
1405
1426
|
|
|
1406
1427
|
claimCoordinatorMailbox(workItemId, owner, leaseMs = 60_000) {
|
|
1407
1428
|
return withTransaction(this.db, () => {
|
|
1429
|
+
if (!this.canAutomaticallyCoordinate(workItemId)) return null;
|
|
1408
1430
|
const now = this.now();
|
|
1409
1431
|
const row = this.db.prepare(`SELECT * FROM coordinator_mailbox_entries
|
|
1410
1432
|
WHERE work_item_id = ? AND (status = 'pending'
|
|
@@ -1536,7 +1558,7 @@ export class WorkItemStore {
|
|
|
1536
1558
|
}
|
|
1537
1559
|
|
|
1538
1560
|
#activeCoordinatorMailboxClaim(coordinatorTurnId, claim = {}) {
|
|
1539
|
-
if (!claim
|
|
1561
|
+
if (!claim?.mailboxId || !claim.ownerBootId || !Number.isInteger(Number(claim.claimEpoch))) return null;
|
|
1540
1562
|
return this.db.prepare(`SELECT * FROM coordinator_mailbox_entries WHERE id = ? AND status = 'claimed'
|
|
1541
1563
|
AND claim_owner = ? AND claim_epoch = ? AND lease_expires_at > ?
|
|
1542
1564
|
AND json_extract(payload, '$.turnId') = ?`).get(
|
|
@@ -1566,10 +1588,29 @@ export class WorkItemStore {
|
|
|
1566
1588
|
return row ? this.#mapCoordinatorProviderTurn(row) : null;
|
|
1567
1589
|
}
|
|
1568
1590
|
|
|
1591
|
+
isActiveCoordinatorProviderTurn(id, claim = {}) {
|
|
1592
|
+
const turn = this.getCoordinatorProviderTurn(id);
|
|
1593
|
+
if (!turn || turn.status !== 'dispatching' || turn.claimOwner !== claim.ownerBootId
|
|
1594
|
+
|| turn.claimEpoch !== Number(claim.claimEpoch)
|
|
1595
|
+
|| !this.#activeCoordinatorMailboxClaim(turn.coordinatorTurnId, claim)
|
|
1596
|
+
|| this.isExecutionStopped(turn.workItemId)) return false;
|
|
1597
|
+
const item = this.getWorkItem(turn.workItemId);
|
|
1598
|
+
return !!item && !['done', 'cancelled'].includes(item.status);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1569
1601
|
dispatchCoordinatorProviderTurn(id, claim = {}) {
|
|
1570
1602
|
return withTransaction(this.db, () => {
|
|
1571
1603
|
const existing = this.getCoordinatorProviderTurn(id);
|
|
1572
|
-
if (!existing || !
|
|
1604
|
+
if (!existing || !['prepared', 'dispatching'].includes(existing.status)
|
|
1605
|
+
|| !this.#activeCoordinatorMailboxClaim(existing.coordinatorTurnId, claim)) return null;
|
|
1606
|
+
const reservation = this.reserveWorkItemRequest({
|
|
1607
|
+
id: existing.status === 'prepared' ? id : `${id}:retry:${randomUUID()}`, workItemId: existing.workItemId,
|
|
1608
|
+
kind: 'coordinator', request: existing.requestBody });
|
|
1609
|
+
if (!reservation.allowed) return null;
|
|
1610
|
+
if (existing.status === 'dispatching') {
|
|
1611
|
+
for (const earlier of this.resourceControl.coordinatorRequestIds(id).slice(1)) this.settleWorkItemRequest(earlier, null, false);
|
|
1612
|
+
return existing;
|
|
1613
|
+
}
|
|
1573
1614
|
const now = this.now();
|
|
1574
1615
|
const changed = this.db.prepare(`UPDATE coordinator_provider_turns SET status = 'dispatching',
|
|
1575
1616
|
dispatched_at = ?, updated_at = ? WHERE id = ? AND status = 'prepared'
|
|
@@ -1593,6 +1634,7 @@ export class WorkItemStore {
|
|
|
1593
1634
|
stringify(response), responseHash, now, now, id, requestHash,
|
|
1594
1635
|
claim.ownerBootId, Number(claim.claimEpoch),
|
|
1595
1636
|
);
|
|
1637
|
+
if (Number(changed.changes) === 1) this.resourceControl.settleCoordinator(id, response?.usage);
|
|
1596
1638
|
return Number(changed.changes) === 1 ? this.getCoordinatorProviderTurn(id) : null;
|
|
1597
1639
|
});
|
|
1598
1640
|
}
|
|
@@ -2133,6 +2175,7 @@ export class WorkItemStore {
|
|
|
2133
2175
|
const turn = this.getEngineTurn(turnId);
|
|
2134
2176
|
if (!turn || turn.ownerBootId !== ownerBootId || turn.leaseEpoch !== leaseEpoch) return null;
|
|
2135
2177
|
if (!this.#activeRunRow(turn.runId, ownerBootId, leaseEpoch, true)) return null;
|
|
2178
|
+
if (this.isExecutionStopped(turn.workItemId)) return null;
|
|
2136
2179
|
if (turn.status === 'dispatching') return turn;
|
|
2137
2180
|
if (turn.status !== 'prepared') return null;
|
|
2138
2181
|
const now = this.now();
|
|
@@ -3114,7 +3157,7 @@ export class WorkItemStore {
|
|
|
3114
3157
|
}
|
|
3115
3158
|
return workItems.map(workItem => {
|
|
3116
3159
|
const actions = actionsByWorkItem.get(workItem.id) || [];
|
|
3117
|
-
return graphExecutionState({ ...workItem, actions, runs: runsByWorkItem.get(workItem.id) || [] }, actions);
|
|
3160
|
+
return graphExecutionState({ ...workItem, executionControl: this.getExecutionControl(workItem.id), actions, runs: runsByWorkItem.get(workItem.id) || [] }, actions);
|
|
3118
3161
|
});
|
|
3119
3162
|
}
|
|
3120
3163
|
|
|
@@ -3123,11 +3166,13 @@ export class WorkItemStore {
|
|
|
3123
3166
|
if (!workItem) return null;
|
|
3124
3167
|
const detail = {
|
|
3125
3168
|
...workItem,
|
|
3169
|
+
executionControl: this.getExecutionControl(id),
|
|
3126
3170
|
actions: this.db.prepare('SELECT * FROM actions WHERE work_item_id = ? ORDER BY sequence').all(id).map(mapAction),
|
|
3127
3171
|
runs: this.db.prepare('SELECT * FROM runs WHERE work_item_id = ? ORDER BY started_at DESC').all(id).map(mapRun),
|
|
3128
3172
|
planConflicts: this.listPlanConflicts(id),
|
|
3129
3173
|
events: this.db.prepare('SELECT * FROM events WHERE work_item_id = ? ORDER BY id DESC LIMIT 500').all(id).map(mapEvent),
|
|
3130
3174
|
};
|
|
3175
|
+
detail.goalProgress = deriveGoalProgress(detail);
|
|
3131
3176
|
return graphExecutionState(detail, detail.actions);
|
|
3132
3177
|
}
|
|
3133
3178
|
|
|
@@ -3212,7 +3257,7 @@ export class WorkItemStore {
|
|
|
3212
3257
|
);
|
|
3213
3258
|
if (!mailbox) return null;
|
|
3214
3259
|
const workItem = this.getWorkItem(mailbox.work_item_id);
|
|
3215
|
-
if (!isDynamicWorkItem(workItem) ||
|
|
3260
|
+
if (!isDynamicWorkItem(workItem) || !this.canAutomaticallyCoordinate(workItem.id)) return null;
|
|
3216
3261
|
const latest = (workItem.messages || []).at(-1);
|
|
3217
3262
|
if (latest?.role === 'assistant' && latest.status === 'thinking') return null;
|
|
3218
3263
|
const now = this.now();
|
|
@@ -3304,6 +3349,7 @@ export class WorkItemStore {
|
|
|
3304
3349
|
}
|
|
3305
3350
|
const workItem = this.getWorkItem(id);
|
|
3306
3351
|
if (!workItem) return null;
|
|
3352
|
+
if (this.isExecutionStopped(id)) throw new WorkCenterResourceStopError(this.getExecutionControl(id).stopReason);
|
|
3307
3353
|
if (['done', 'cancelled'].includes(workItem.status)) {
|
|
3308
3354
|
throw new Error(`WorkItem in ${workItem.status} cannot accept Coordinator messages`);
|
|
3309
3355
|
}
|
|
@@ -3436,6 +3482,7 @@ export class WorkItemStore {
|
|
|
3436
3482
|
if (!this.#activeCoordinatorMailboxClaim(turnId, claim)) return null;
|
|
3437
3483
|
const workItem = this.getWorkItem(expected.workItemId);
|
|
3438
3484
|
if (!workItem) return null;
|
|
3485
|
+
if (this.isExecutionStopped(workItem.id)) throw new WorkCenterResourceStopError(this.getExecutionControl(workItem.id).stopReason);
|
|
3439
3486
|
if (workItem.revision !== expected.revision
|
|
3440
3487
|
|| workItem.planRevision !== expected.planRevision
|
|
3441
3488
|
|| workItem.ledgerRevision !== expected.ledgerRevision
|
|
@@ -3454,6 +3501,13 @@ export class WorkItemStore {
|
|
|
3454
3501
|
}
|
|
3455
3502
|
const recovery = coordinatorRecoveryIdentity(persistedRecovery);
|
|
3456
3503
|
const decision = result?.decision || {};
|
|
3504
|
+
const userOriginated = messages[assistantIndex]?.userOriginated === true
|
|
3505
|
+
&& messages[assistantIndex]?.automatic !== true;
|
|
3506
|
+
assertCoordinatorContractAuthority(decision.contractPatch, userOriginated);
|
|
3507
|
+
assertCoordinatorContractAuthority(result?.mutation?.contractPatch, userOriginated);
|
|
3508
|
+
if (expected.userOriginated === true && !userOriginated) {
|
|
3509
|
+
throw new Error('Coordinator user-originated authority does not match the persisted turn');
|
|
3510
|
+
}
|
|
3457
3511
|
const now = this.now();
|
|
3458
3512
|
const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
3459
3513
|
AND status NOT IN ('completed', 'closed', 'superseded', 'cancelled') ORDER BY sequence`).all(workItem.id).map(mapAction);
|
|
@@ -3542,17 +3596,17 @@ export class WorkItemStore {
|
|
|
3542
3596
|
type: 'coordinator-guidance', role: 'user', summary: instruction, evidence: [],
|
|
3543
3597
|
}];
|
|
3544
3598
|
const nextAction = {
|
|
3545
|
-
...action, context, generation: action.generation + 1,
|
|
3599
|
+
...action, context, generation: action.generation + 1, contractRevision: workItem.revision,
|
|
3546
3600
|
};
|
|
3547
3601
|
nextAction.instruction = canonicalActionInstruction(workItem, nextAction, context);
|
|
3548
3602
|
const specHash = actionSpecHash(nextAction);
|
|
3549
3603
|
const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
|
|
3550
3604
|
current_run_id = NULL, lease_epoch = lease_epoch + ?, context = ?, instruction = ?,
|
|
3551
|
-
generation = generation + 1, spec_hash = ?, identity_history = ?, result_run_id = NULL,
|
|
3605
|
+
generation = generation + 1, contract_revision = ?, spec_hash = ?, identity_history = ?, result_run_id = NULL,
|
|
3552
3606
|
workspace = NULL, updated_at = ? WHERE id = ? AND generation = ?
|
|
3553
3607
|
AND status NOT IN ('completed', 'superseded', 'cancelled')`).run(
|
|
3554
3608
|
action.status === 'running' ? 1 : 0,
|
|
3555
|
-
stringify(context), nextAction.instruction, specHash,
|
|
3609
|
+
stringify(context), nextAction.instruction, nextAction.contractRevision, specHash,
|
|
3556
3610
|
stringify(actionIdentityHistory(action, nextAction.generation, specHash)),
|
|
3557
3611
|
now, action.id, action.generation,
|
|
3558
3612
|
);
|
|
@@ -3684,20 +3738,22 @@ export class WorkItemStore {
|
|
|
3684
3738
|
turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
|
|
3685
3739
|
}) {
|
|
3686
3740
|
const decision = result?.decision || {};
|
|
3687
|
-
|
|
3688
|
-
|
|
3741
|
+
const decisionPatch = normalizeContractPatch(decision.contractPatch);
|
|
3742
|
+
const mutationPatch = normalizeContractPatch(result?.mutation?.contractPatch);
|
|
3743
|
+
if (JSON.stringify(decisionPatch) !== JSON.stringify(mutationPatch) && mutationPatch) {
|
|
3744
|
+
throw new Error('Coordinator mutation contractPatch must match the user-originated decision');
|
|
3689
3745
|
}
|
|
3690
|
-
if (
|
|
3691
|
-
throw new Error('
|
|
3746
|
+
if (decisionPatch && !['create_actions', 'request_human'].includes(decision.kind)) {
|
|
3747
|
+
throw new Error('Contract refinement requires create_actions or request_human');
|
|
3692
3748
|
}
|
|
3693
|
-
if (
|
|
3749
|
+
if (decisionPatch?.deliveryTarget && decision.kind !== 'request_human') {
|
|
3694
3750
|
throw new Error('WorkItem delivery target confirmation requires a user-originated request_human decision');
|
|
3695
3751
|
}
|
|
3696
3752
|
let affectedActionIds = [];
|
|
3697
3753
|
let nextStatus = workItem.status;
|
|
3698
3754
|
let currentActionId = workItem.currentActionId;
|
|
3699
3755
|
let finalResult = null;
|
|
3700
|
-
const contractPatch =
|
|
3756
|
+
const contractPatch = decisionPatch;
|
|
3701
3757
|
if (decision.kind === 'create_actions'
|
|
3702
3758
|
&& !workItem.deliveryTarget
|
|
3703
3759
|
&& (result?.mutation?.createdActions || []).some(action => (
|
|
@@ -3705,11 +3761,18 @@ export class WorkItemStore {
|
|
|
3705
3761
|
))) {
|
|
3706
3762
|
throw new Error('WorkItem delivery target must be confirmed before creating mutating or delivery Actions');
|
|
3707
3763
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3764
|
+
const refined = { ...workItem, ...(contractPatch || {}) };
|
|
3765
|
+
const contractChanged = ['title', 'goal', 'deliveryTarget', 'acceptanceCriteria']
|
|
3766
|
+
.some(key => JSON.stringify(refined[key]) !== JSON.stringify(workItem[key]));
|
|
3767
|
+
if (contractChanged) {
|
|
3768
|
+
this.#invalidateExecution(workItem, 'superseded', 'superseded', 'User refined the WorkItem contract', now);
|
|
3769
|
+
this.db.prepare(`UPDATE work_items SET title = ?, goal = ?, acceptance_criteria = ?,
|
|
3770
|
+
delivery_target = ?, revision = revision + 1, updated_at = ? WHERE id = ? AND revision = ?`).run(
|
|
3771
|
+
refined.title, refined.goal, stringify(refined.acceptanceCriteria), refined.deliveryTarget,
|
|
3772
|
+
now, workItem.id, workItem.revision,
|
|
3773
|
+
);
|
|
3774
|
+
workItem = this.getWorkItem(workItem.id);
|
|
3775
|
+
activeActions = [];
|
|
3713
3776
|
}
|
|
3714
3777
|
|
|
3715
3778
|
if (decision.kind === 'create_actions') {
|
|
@@ -3720,7 +3783,11 @@ export class WorkItemStore {
|
|
|
3720
3783
|
if (!mutation || !Array.isArray(mutation.createdActions) || mutation.createdActions.length === 0) {
|
|
3721
3784
|
throw new Error('Dynamic Coordinator Action creation is missing a validated mutation');
|
|
3722
3785
|
}
|
|
3723
|
-
|
|
3786
|
+
const goalDetail = this.getWorkItemDetail(workItem.id);
|
|
3787
|
+
for (const candidate of mutation.createdActions) {
|
|
3788
|
+
normalizeDynamicGoalRefs(candidate.brief?.goalRefs, goalDetail, goalDetail.actions);
|
|
3789
|
+
}
|
|
3790
|
+
for (const closure of (contractChanged ? [] : mutation.closeActions || [])) {
|
|
3724
3791
|
const action = activeActions.find(candidate => candidate.id === closure.actionId);
|
|
3725
3792
|
if (!action || !['waiting', 'failed'].includes(action.status)) {
|
|
3726
3793
|
throw new Error('Dynamic Coordinator close target changed before apply');
|
|
@@ -3738,7 +3805,7 @@ export class WorkItemStore {
|
|
|
3738
3805
|
actionGeneration: action.generation,
|
|
3739
3806
|
});
|
|
3740
3807
|
}
|
|
3741
|
-
for (const actionId of mutation.supersedeActionIds || []) {
|
|
3808
|
+
for (const actionId of (contractChanged ? [] : mutation.supersedeActionIds || [])) {
|
|
3742
3809
|
const action = activeActions.find(candidate => candidate.id === actionId);
|
|
3743
3810
|
if (!action || !['ready', 'waiting', 'failed'].includes(action.status)) {
|
|
3744
3811
|
throw new Error('Dynamic Coordinator supersede target changed before apply');
|
|
@@ -3749,18 +3816,11 @@ export class WorkItemStore {
|
|
|
3749
3816
|
AND status IN ('ready', 'waiting', 'failed')`).run(now, action.id, action.generation);
|
|
3750
3817
|
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator lost an Action fence');
|
|
3751
3818
|
}
|
|
3752
|
-
const patch = mutation.contractPatch || null;
|
|
3753
|
-
const title = patch?.title ?? workItem.title;
|
|
3754
|
-
const goal = patch?.goal ?? workItem.goal;
|
|
3755
|
-
const criteria = patch?.acceptanceCriteria ?? workItem.acceptanceCriteria;
|
|
3756
|
-
const contractChanged = title !== workItem.title || goal !== workItem.goal
|
|
3757
|
-
|| JSON.stringify(criteria) !== JSON.stringify(workItem.acceptanceCriteria);
|
|
3758
3819
|
const snapshot = { ...workItem.workflowSnapshot, workItemType: mutation.workItemType };
|
|
3759
|
-
const changedPlan = this.db.prepare(`UPDATE work_items SET
|
|
3760
|
-
acceptance_criteria = ?, workflow_snapshot = ?, revision = revision + ?,
|
|
3820
|
+
const changedPlan = this.db.prepare(`UPDATE work_items SET workflow_snapshot = ?,
|
|
3761
3821
|
plan_revision = plan_revision + 1, updated_at = ? WHERE id = ? AND revision = ?
|
|
3762
3822
|
AND plan_revision = ? AND ledger_revision = ? AND coordinator_revision = ?`).run(
|
|
3763
|
-
|
|
3823
|
+
stringify(snapshot),
|
|
3764
3824
|
now, workItem.id, workItem.revision, workItem.planRevision,
|
|
3765
3825
|
workItem.ledgerRevision, workItem.coordinatorRevision,
|
|
3766
3826
|
);
|
|
@@ -3788,14 +3848,14 @@ export class WorkItemStore {
|
|
|
3788
3848
|
const context = [...withoutActionInputContext(action.context), {
|
|
3789
3849
|
type: 'coordinator-guidance', role: 'user', summary: guidance, evidence: [],
|
|
3790
3850
|
}];
|
|
3791
|
-
const candidate = { ...action, context, generation: action.generation + 1 };
|
|
3851
|
+
const candidate = { ...action, context, generation: action.generation + 1, contractRevision: workItem.revision };
|
|
3792
3852
|
candidate.instruction = canonicalActionInstruction(workItem, candidate, context);
|
|
3793
3853
|
const specHash = actionSpecHash(candidate);
|
|
3794
3854
|
const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
|
|
3795
3855
|
current_run_id = NULL, context = ?, instruction = ?, generation = generation + 1,
|
|
3796
|
-
spec_hash = ?, identity_history = ?, result_run_id = NULL, workspace = NULL, updated_at = ?
|
|
3856
|
+
contract_revision = ?, spec_hash = ?, identity_history = ?, result_run_id = NULL, workspace = NULL, updated_at = ?
|
|
3797
3857
|
WHERE id = ? AND generation = ? AND status IN ('ready', 'waiting', 'failed')`).run(
|
|
3798
|
-
stringify(context), candidate.instruction, specHash,
|
|
3858
|
+
stringify(context), candidate.instruction, candidate.contractRevision, specHash,
|
|
3799
3859
|
stringify(actionIdentityHistory(action, candidate.generation, specHash)),
|
|
3800
3860
|
now, action.id, action.generation,
|
|
3801
3861
|
);
|
|
@@ -3838,12 +3898,10 @@ export class WorkItemStore {
|
|
|
3838
3898
|
throw new Error('WorkItem has an unsafe blocking Operation and cannot complete');
|
|
3839
3899
|
}
|
|
3840
3900
|
finalResult = normalizeDynamicCompletion(decision.completion, workItem.acceptanceCriteria);
|
|
3841
|
-
const
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
return [run.id, run];
|
|
3846
|
-
}));
|
|
3901
|
+
const detail = this.getWorkItemDetail(workItem.id);
|
|
3902
|
+
const progress = deriveGoalProgress(detail);
|
|
3903
|
+
const canonicalRuns = new Map(currentGoalRuns(detail)
|
|
3904
|
+
.filter(run => run.status === 'completed').map(run => [run.id, run]));
|
|
3847
3905
|
const criteria = Array.isArray(workItem.acceptanceCriteria) ? workItem.acceptanceCriteria : [];
|
|
3848
3906
|
for (const runId of finalResult.evidenceRunIds) {
|
|
3849
3907
|
const run = canonicalRuns.get(runId);
|
|
@@ -3851,17 +3909,18 @@ export class WorkItemStore {
|
|
|
3851
3909
|
if (run.evidence.length === 0) {
|
|
3852
3910
|
throw new Error(`Completion evidence Run has no concrete evidence: ${runId}`);
|
|
3853
3911
|
}
|
|
3854
|
-
if (!
|
|
3855
|
-
|| run.acceptanceChecks.some((check, index) => (
|
|
3856
|
-
check?.criterion !== criteria[index] || !check?.status || !String(check?.evidence || '').trim()
|
|
3857
|
-
))) {
|
|
3912
|
+
if (!validGoalChecks(run, criteria)) {
|
|
3858
3913
|
throw new Error(`Completion evidence Run has incomplete acceptance checks: ${runId}`);
|
|
3859
3914
|
}
|
|
3915
|
+
if (hasContradictoryEvidence(run)) {
|
|
3916
|
+
throw new Error(`Completion evidence Run has contradictory evidence: ${runId}`);
|
|
3917
|
+
}
|
|
3860
3918
|
}
|
|
3861
3919
|
finalResult.outputs = [];
|
|
3862
3920
|
const seenOutputs = new Set();
|
|
3863
3921
|
for (const runId of finalResult.evidenceRunIds) {
|
|
3864
3922
|
for (const output of canonicalRuns.get(runId)?.outputs || []) {
|
|
3923
|
+
if (['failed', 'error', 'pending'].includes(output.status)) continue;
|
|
3865
3924
|
const key = `${output.kind}\u0000${output.ref}`;
|
|
3866
3925
|
if (seenOutputs.has(key)) continue;
|
|
3867
3926
|
seenOutputs.add(key);
|
|
@@ -3873,16 +3932,27 @@ export class WorkItemStore {
|
|
|
3873
3932
|
pull_request: 'pr',
|
|
3874
3933
|
merge: 'commit',
|
|
3875
3934
|
}[workItem.deliveryTarget];
|
|
3876
|
-
if (!requiredOutputKind) {
|
|
3935
|
+
if (!requiredOutputKind && workItem.deliveryTarget !== 'response') {
|
|
3877
3936
|
throw new Error('WorkItem delivery target must be confirmed before completion');
|
|
3878
3937
|
}
|
|
3879
|
-
if (!finalResult.outputs.some(output => output.kind === requiredOutputKind)) {
|
|
3938
|
+
if (requiredOutputKind && !finalResult.outputs.some(output => output.kind === requiredOutputKind)) {
|
|
3880
3939
|
throw new Error(`WorkItem completion requires a canonical ${requiredOutputKind} output for delivery target ${workItem.deliveryTarget}`);
|
|
3881
3940
|
}
|
|
3941
|
+
if (!finalResult.evidenceRunIds.some(runId => progress.delivery.evidenceRunIds.includes(runId))) {
|
|
3942
|
+
throw new Error('Completion delivery requires current canonical evidence after the latest workspace change');
|
|
3943
|
+
}
|
|
3944
|
+
if (workItem.deliveryTarget === 'response') {
|
|
3945
|
+
finalResult.responses = finalResult.evidenceRunIds
|
|
3946
|
+
.filter(runId => progress.delivery.evidenceRunIds.includes(runId))
|
|
3947
|
+
.map(runId => ({ runId, summary: canonicalRuns.get(runId).summary, evidence: canonicalRuns.get(runId).evidence }));
|
|
3948
|
+
if (!finalResult.responses.length) throw new Error('Response delivery requires a canonical completed Run summary with evidence and valid checks');
|
|
3949
|
+
}
|
|
3950
|
+
if (progress.remainingCriteria.length) {
|
|
3951
|
+
throw new Error('Completion has unmet or contradictory current goal criteria');
|
|
3952
|
+
}
|
|
3882
3953
|
for (const [index, acceptanceResult] of finalResult.acceptanceResults.entries()) {
|
|
3883
3954
|
const provesCriterion = acceptanceResult.evidenceRunIds.some(runId => (
|
|
3884
|
-
|
|
3885
|
-
&& canonicalRuns.get(runId)?.acceptanceChecks?.[index]?.status === 'passed'
|
|
3955
|
+
progress.criteria[index].evidenceRunIds.includes(runId)
|
|
3886
3956
|
));
|
|
3887
3957
|
if (!provesCriterion) {
|
|
3888
3958
|
throw new Error(`Completion criterion lacks a passing canonical Run check: ${criteria[index]}`);
|
|
@@ -3915,12 +3985,12 @@ export class WorkItemStore {
|
|
|
3915
3985
|
const changed = this.db.prepare(`UPDATE work_items SET messages = ?,
|
|
3916
3986
|
coordinator_revision = coordinator_revision + 1, status = ?, current_action_id = ?,
|
|
3917
3987
|
current_run_id = NULL, final_result = COALESCE(final_result, ?), title = ?, goal = ?,
|
|
3918
|
-
acceptance_criteria = ?, delivery_target = ?,
|
|
3988
|
+
acceptance_criteria = ?, delivery_target = ?, updated_at = ?
|
|
3919
3989
|
WHERE id = ? AND coordinator_revision = ? AND revision = ? AND plan_revision = ?
|
|
3920
3990
|
AND ledger_revision = ? AND status NOT IN ('done', 'cancelled')`).run(
|
|
3921
3991
|
stringify(messages), nextStatus, currentActionId, finalResult ? stringify(finalResult) : null,
|
|
3922
3992
|
workItem.title, workItem.goal, stringify(workItem.acceptanceCriteria), workItem.deliveryTarget,
|
|
3923
|
-
|
|
3993
|
+
now, workItem.id, current.coordinatorRevision, current.revision,
|
|
3924
3994
|
current.planRevision, current.ledgerRevision,
|
|
3925
3995
|
);
|
|
3926
3996
|
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator completion lost its turn fence');
|
|
@@ -3958,7 +4028,9 @@ export class WorkItemStore {
|
|
|
3958
4028
|
messages[index],
|
|
3959
4029
|
`coordinator:turn:${turnId}:assistant`,
|
|
3960
4030
|
);
|
|
3961
|
-
|
|
4031
|
+
if (!expected.interrupted) this.resourceControl.coordinatorFailed(workItem.id, turnId);
|
|
4032
|
+
const dynamicAutomatic = isDynamicWorkItem(workItem) && expected.automatic === true
|
|
4033
|
+
&& !this.isExecutionStopped(workItem.id);
|
|
3962
4034
|
if (dynamicAutomatic) {
|
|
3963
4035
|
if (!this.releaseCoordinatorMailboxClaim(
|
|
3964
4036
|
claim.mailboxId, claim.ownerBootId, claim.claimEpoch,
|
|
@@ -4226,16 +4298,37 @@ export class WorkItemStore {
|
|
|
4226
4298
|
});
|
|
4227
4299
|
}
|
|
4228
4300
|
|
|
4229
|
-
resumeWorkItemAtomic(id, expectedRevision, makeInitialAction) {
|
|
4301
|
+
resumeWorkItemAtomic(id, expectedRevision, makeInitialAction, executionControlRevision) {
|
|
4230
4302
|
return withTransaction(this.db, () => {
|
|
4231
4303
|
const workItem = this.getWorkItem(id);
|
|
4232
4304
|
if (!workItem) return null;
|
|
4233
4305
|
if (!Number.isInteger(expectedRevision) || workItem.revision !== expectedRevision) {
|
|
4234
4306
|
throw new Error('WorkItem changed before it was resumed; refresh and try again');
|
|
4235
4307
|
}
|
|
4236
|
-
|
|
4308
|
+
const stopped = this.isExecutionStopped(id);
|
|
4309
|
+
if (workItem.status !== 'cancelled' && !stopped) {
|
|
4237
4310
|
throw new Error(`WorkItem in ${workItem.status} cannot be resumed`);
|
|
4238
4311
|
}
|
|
4312
|
+
this.resourceControl.resume(id, executionControlRevision);
|
|
4313
|
+
// Resume changes the execution epoch, never the goal contract. Retire old
|
|
4314
|
+
// Coordinator claims so recovery cannot apply pre-stop guidance later.
|
|
4315
|
+
const nowResumed = this.now();
|
|
4316
|
+
const messages = (workItem.messages || []).map(message => {
|
|
4317
|
+
if (message.role !== 'assistant' || message.status !== 'thinking') return message;
|
|
4318
|
+
const retired = { ...message, status: 'failed', error: 'Explicit user resume superseded this Coordinator turn', updatedAt: nowResumed };
|
|
4319
|
+
this.#appendConversationEntry(id, retired, `coordinator:turn:${message.turnId}:assistant`);
|
|
4320
|
+
this.db.prepare(`UPDATE coordinator_mailbox_entries SET status = 'cancelled',
|
|
4321
|
+
claim_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
4322
|
+
WHERE work_item_id = ? AND json_extract(payload, '$.turnId') = ? AND status IN ('pending', 'claimed')`)
|
|
4323
|
+
.run(nowResumed, id, message.turnId);
|
|
4324
|
+
return retired;
|
|
4325
|
+
});
|
|
4326
|
+
this.db.prepare(`UPDATE work_items SET coordinator_revision = coordinator_revision + 1,
|
|
4327
|
+
messages = ?, updated_at = ? WHERE id = ?`).run(stringify(messages), nowResumed, id);
|
|
4328
|
+
if (stopped) {
|
|
4329
|
+
this.#invalidateExecution(workItem, 'cancelled', 'cancelled', 'Explicit user resume', this.now());
|
|
4330
|
+
this.db.prepare("UPDATE work_items SET status = 'cancelled' WHERE id = ?").run(id);
|
|
4331
|
+
}
|
|
4239
4332
|
|
|
4240
4333
|
const now = this.now();
|
|
4241
4334
|
const cancelledActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
@@ -4259,7 +4352,7 @@ export class WorkItemStore {
|
|
|
4259
4352
|
}
|
|
4260
4353
|
this.enqueueCoordinatorMailbox(id, 'work_item_resumed', {
|
|
4261
4354
|
trigger: { workItemId: id, revision: expectedRevision },
|
|
4262
|
-
}, `dynamic:resume:${id}:${
|
|
4355
|
+
}, `dynamic:resume:${id}:${this.getExecutionControl(id).revision}`);
|
|
4263
4356
|
this.appendEvent(id, 'work_item.resumed', {
|
|
4264
4357
|
supersededActionIds: cancelledActions.map(action => action.id),
|
|
4265
4358
|
});
|
|
@@ -4554,7 +4647,9 @@ export class WorkItemStore {
|
|
|
4554
4647
|
)
|
|
4555
4648
|
)
|
|
4556
4649
|
ORDER BY a.updated_at ASC, a.sequence ASC`).all();
|
|
4557
|
-
const row = rows.find(candidate => !this
|
|
4650
|
+
const row = rows.find(candidate => !this.isExecutionStopped(candidate.work_item_id)
|
|
4651
|
+
&& !this.#hasBlockingOperation(candidate.work_item_id, candidate.id)
|
|
4652
|
+
&& this.resourceControl.canAttempt(mapAction(candidate)));
|
|
4558
4653
|
if (!row) return null;
|
|
4559
4654
|
const now = this.now();
|
|
4560
4655
|
let action = mapAction(row);
|
|
@@ -4654,7 +4749,7 @@ export class WorkItemStore {
|
|
|
4654
4749
|
if (!row) return null;
|
|
4655
4750
|
const workItem = this.getWorkItem(row.work_item_id);
|
|
4656
4751
|
if (usesLegacyGraph(workItem) || isDynamicWorkItem(workItem)) return row;
|
|
4657
|
-
return workItem?.status === 'running' && workItem.currentActionId === row.action_id
|
|
4752
|
+
return (workItem?.status === 'running' || this.isExecutionStopped(workItem?.id)) && workItem.currentActionId === row.action_id
|
|
4658
4753
|
&& workItem.currentRunId === row.id ? row : null;
|
|
4659
4754
|
}
|
|
4660
4755
|
|
|
@@ -4735,15 +4830,19 @@ export class WorkItemStore {
|
|
|
4735
4830
|
if (Number(actionChanged.changes) !== 1) throw new Error('Run interruption lost the Action fence');
|
|
4736
4831
|
const activeWorkItem = this.getWorkItem(active.work_item_id);
|
|
4737
4832
|
const concurrentMode = usesLegacyGraph(activeWorkItem) || isDynamicWorkItem(activeWorkItem);
|
|
4833
|
+
// Resource denial changes only admission, not the owning Run's right to
|
|
4834
|
+
// persist its final progress and release its fences during watcher stop.
|
|
4835
|
+
const stopped = this.isExecutionStopped(active.work_item_id);
|
|
4836
|
+
const nextStatus = stopped || !retryable ? 'needs_attention' : 'ready';
|
|
4738
4837
|
const itemChanged = concurrentMode
|
|
4739
4838
|
? this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
4740
|
-
updated_at = ? WHERE id = ? AND status
|
|
4741
|
-
|
|
4839
|
+
updated_at = ? WHERE id = ? AND status IN ('running', ?)`).run(
|
|
4840
|
+
nextStatus, action.id, now, active.work_item_id, stopped ? 'needs_attention' : 'running',
|
|
4742
4841
|
)
|
|
4743
4842
|
: this.db.prepare(`UPDATE work_items SET status = ?, current_run_id = NULL,
|
|
4744
|
-
updated_at = ? WHERE id = ? AND status
|
|
4843
|
+
updated_at = ? WHERE id = ? AND status IN ('running', ?) AND current_action_id = ?
|
|
4745
4844
|
AND current_run_id = ?`).run(
|
|
4746
|
-
|
|
4845
|
+
nextStatus, now, active.work_item_id, stopped ? 'needs_attention' : 'running', action.id, runId,
|
|
4747
4846
|
);
|
|
4748
4847
|
if (Number(itemChanged.changes) !== 1) throw new Error('Run interruption lost the WorkItem fence');
|
|
4749
4848
|
this.appendEvent(active.work_item_id, 'run.interrupted', { retryable, reason }, {
|
|
@@ -5186,7 +5285,7 @@ export class WorkItemStore {
|
|
|
5186
5285
|
} else {
|
|
5187
5286
|
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
5188
5287
|
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ? WHERE id = ? AND current_run_id = ?
|
|
5189
|
-
AND current_action_id = ? AND status
|
|
5288
|
+
AND current_action_id = ? AND status IN ('running', 'needs_attention') AND revision = ?`).run(
|
|
5190
5289
|
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, runId, action.id, nextWorkItem.revision,
|
|
5191
5290
|
);
|
|
5192
5291
|
}
|
|
@@ -5300,6 +5399,7 @@ export class WorkItemStore {
|
|
|
5300
5399
|
WHERE json_extract(payload, '$.turnId') = ? AND status != 'acked'`).run(now, now, turnId);
|
|
5301
5400
|
}
|
|
5302
5401
|
}
|
|
5402
|
+
this.resourceControl.coordinatorFailed(row.id, turnId);
|
|
5303
5403
|
this.appendEvent(row.id, 'coordinator.turn_interrupted', {
|
|
5304
5404
|
turnId: messages[index].turnId || null,
|
|
5305
5405
|
error: messages[index].error,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sessionMessageQuotePrompt } from '../session-message-quote.js';
|
|
2
|
+
import { isDynamicWorkItem } from './execution-mode.js';
|
|
2
3
|
import { renderSessionContextSnapshot } from './session-context.js';
|
|
3
4
|
|
|
4
5
|
export const BUILT_IN_ACTION_TYPES = Object.freeze([
|
|
@@ -40,7 +41,7 @@ const DEFAULT_STAGE_INSTRUCTIONS = Object.freeze({
|
|
|
40
41
|
operate: 'Perform the operational change with explicit preconditions, safety fences, observability, and rollback handling. Verify the live or simulated postcondition from authoritative state, avoid destructive shortcuts, and record the exact evidence needed for handoff.',
|
|
41
42
|
deliver: 'Deliver only an approved result using the repository release policy. Recheck the reviewed commit and remote state, run required final verification on the immutable delivery tree, publish only the requested artifacts, and report commit, tag, deployment, and residual-risk evidence.',
|
|
42
43
|
write: 'Produce the requested written deliverable for its intended audience. Use the available evidence, preserve required terminology and structure, avoid unsupported claims, and verify the result against every acceptance criterion.',
|
|
43
|
-
create_vp: 'Create
|
|
44
|
+
create_vp: 'Create a persistent specialist VP only when it is itself an explicitly requested deliverable. A missing skill label is not a reason to expand roles: prefer an existing VP with a task-specific brief, and request human help for unavailable tools or authorization. Author a narrow role, traits, and persona with the CreateWorkItemVp tool, then report the new VP id as evidence. Do not clone an existing VP or create a generic replacement.',
|
|
44
45
|
custom: 'Complete the Action objective using repository facts and the WorkItem contract. State the approach, handle relevant risks and boundary conditions, produce the requested artifact or change, verify the result, and return concrete evidence plus any residual uncertainty.',
|
|
45
46
|
});
|
|
46
47
|
|
|
@@ -58,7 +59,7 @@ const DEFAULT_ACTION_BRIEFS = Object.freeze({
|
|
|
58
59
|
operate: ['Perform the requested operational change safely.', 'Check preconditions, apply safety fences, preserve rollback options, and verify authoritative state.', 'A verified operational postcondition with handoff and rollback evidence.'],
|
|
59
60
|
deliver: ['Deliver the approved Work Item result using repository release policy.', 'Recheck immutable reviewed state, run final gates, and publish only the requested artifacts.', 'A traceable delivery with commit, artifact, deployment, and residual-risk evidence.'],
|
|
60
61
|
write: ['Produce the written deliverable requested by this Action.', 'Use available evidence, the intended audience, and the required terminology and structure.', 'A complete written artifact verified against the acceptance criteria.'],
|
|
61
|
-
create_vp: ['Create the
|
|
62
|
+
create_vp: ['Create the persistent specialist VP explicitly requested as this Work Item deliverable.', 'Have the assigned existing VP author a narrow persistent role and persona with the dedicated VP creation tool.', 'A new Agent-local VP whose identity and capability can be selected by later Actions.'],
|
|
62
63
|
custom: ['Complete the domain-specific objective defined for this Action.', 'Use repository facts, handle relevant risks and boundaries, and verify the produced result.', 'The requested artifact or change with concrete evidence and residual uncertainty.'],
|
|
63
64
|
});
|
|
64
65
|
|
|
@@ -774,7 +775,10 @@ export function actionInstruction(stage, workItem, context = [], sessionContextB
|
|
|
774
775
|
const policy = stage.instruction || defaultWorkCenterStageInstruction(stage.type);
|
|
775
776
|
const brief = normalizeActionBrief(stage.brief || stage, stage.type);
|
|
776
777
|
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}`;
|
|
777
|
-
|
|
778
|
+
const dynamicRules = isDynamicWorkItem(workItem)
|
|
779
|
+
? `\n\nGoal-driven execution: complete this useful unit end to end, including local tools and risk-appropriate tests. Do not invent research/design/test/review/delivery stages or create roles to work around missing skills. Keep the user contract unchanged; report real tool or authorization blockers. Delivery target: ${workItem.deliveryTarget || 'unconfirmed'}. For response delivery put the substantive result/report in summary with evidence and acceptance checks; no artificial file, PR, or packaging Action is needed.${stage.brief?.goalRefs ? `\nCurrent goal references: ${JSON.stringify(stage.brief.goalRefs)}` : ''}${stage.brief?.rationale ? `\nWhy this Action: ${stage.brief.rationale}` : ''}`
|
|
780
|
+
: '';
|
|
781
|
+
return `${common}\n\n${policy}\n\n${contract}${dynamicRules}`;
|
|
778
782
|
}
|
|
779
783
|
|
|
780
784
|
export function withoutActionInputContext(context, preserveInputIds = []) {
|