@yeaft/webchat-agent 1.0.411 → 1.0.413
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 +22 -5
- 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 +46 -2
- package/yeaft/llm/adapter.js +23 -0
- package/yeaft/llm/anthropic.js +3 -0
- package/yeaft/llm/openai-responses.js +3 -0
- package/yeaft/tools/create-work-item.js +1 -1
- package/yeaft/web-bridge.js +7 -3
- package/yeaft/work-center/bridge.js +3 -2
- package/yeaft/work-center/completion-contract.js +6 -0
- package/yeaft/work-center/controller.js +2 -1
- package/yeaft/work-center/coordinator.js +45 -14
- package/yeaft/work-center/durable-model.js +45 -1
- package/yeaft/work-center/dynamic-coordination.js +34 -0
- package/yeaft/work-center/evidence.js +235 -0
- package/yeaft/work-center/mainline-projection.js +4 -1
- package/yeaft/work-center/projection.js +45 -4
- package/yeaft/work-center/runner.js +82 -7
- package/yeaft/work-center/service.js +6 -0
- package/yeaft/work-center/store.js +162 -19
- package/yeaft/work-center/workflow.js +6 -0
|
@@ -2,7 +2,8 @@ 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 { normalizeEvidence } from './evidence.js';
|
|
5
|
+
import { normalizeEvidence, normalizeOutputs } from './evidence.js';
|
|
6
|
+
import { normalizeContractPatch } from './completion-contract.js';
|
|
6
7
|
import { normalizeActionCheckpoint } from './action-checkpoint.js';
|
|
7
8
|
import { currentActionInputEventIds, runMatchesActionIdentity } from './action-identity.js';
|
|
8
9
|
import { isDynamicWorkItem, usesLegacyGraph } from './execution-mode.js';
|
|
@@ -108,6 +109,7 @@ function mapWorkItem(row) {
|
|
|
108
109
|
ledgerRevision: Math.max(0, Number(row.ledger_revision) || 0),
|
|
109
110
|
coordinationMode: row.coordination_mode || 'legacy',
|
|
110
111
|
finalResult: parseJson(row.final_result, null),
|
|
112
|
+
deliveryTarget: row.delivery_target || null,
|
|
111
113
|
title: row.title,
|
|
112
114
|
goal: row.goal,
|
|
113
115
|
acceptanceCriteria: parseJson(row.acceptance_criteria, []),
|
|
@@ -211,6 +213,7 @@ function mapAction(row) {
|
|
|
211
213
|
modelPolicy: parseJson(row.model_policy, null),
|
|
212
214
|
dependsOnStageIds: parseJson(row.depends_on_stage_ids, []),
|
|
213
215
|
sourceActionIds: parseJson(row.source_action_ids, []),
|
|
216
|
+
creationSource: row.creation_source || 'legacy',
|
|
214
217
|
workspaceMode: row.workspace_mode || 'shared',
|
|
215
218
|
changesRequestedStageId: row.changes_requested_stage_id || null,
|
|
216
219
|
workspace: parseJson(row.workspace, null),
|
|
@@ -229,6 +232,8 @@ function mapAction(row) {
|
|
|
229
232
|
currentRunId: row.current_run_id || null,
|
|
230
233
|
leaseEpoch: row.lease_epoch,
|
|
231
234
|
replacesActionId: row.replaces_action_id || null,
|
|
235
|
+
closeReason: row.close_reason || null,
|
|
236
|
+
closedAt: row.closed_at || null,
|
|
232
237
|
createdAt: row.created_at,
|
|
233
238
|
updatedAt: row.updated_at,
|
|
234
239
|
};
|
|
@@ -271,6 +276,7 @@ function mapRun(row) {
|
|
|
271
276
|
response: row.response || '',
|
|
272
277
|
summary: row.summary || '',
|
|
273
278
|
evidence: normalizeEvidence(parseJson(row.evidence, [])),
|
|
279
|
+
outputs: normalizeOutputs(parseJson(row.outputs, [])),
|
|
274
280
|
acceptanceChecks: parseJson(row.acceptance_checks, []),
|
|
275
281
|
waitingReason: row.waiting_reason || null,
|
|
276
282
|
error: row.error || null,
|
|
@@ -830,6 +836,7 @@ export class WorkItemStore {
|
|
|
830
836
|
ledger_revision INTEGER NOT NULL DEFAULT 0,
|
|
831
837
|
coordination_mode TEXT NOT NULL DEFAULT 'legacy',
|
|
832
838
|
final_result TEXT,
|
|
839
|
+
delivery_target TEXT,
|
|
833
840
|
title TEXT NOT NULL,
|
|
834
841
|
goal TEXT NOT NULL,
|
|
835
842
|
acceptance_criteria TEXT NOT NULL,
|
|
@@ -861,6 +868,7 @@ export class WorkItemStore {
|
|
|
861
868
|
model_policy TEXT,
|
|
862
869
|
depends_on_stage_ids TEXT NOT NULL DEFAULT '[]',
|
|
863
870
|
source_action_ids TEXT NOT NULL DEFAULT '[]',
|
|
871
|
+
creation_source TEXT NOT NULL DEFAULT 'legacy',
|
|
864
872
|
workspace_mode TEXT NOT NULL DEFAULT 'shared',
|
|
865
873
|
changes_requested_stage_id TEXT,
|
|
866
874
|
workspace TEXT,
|
|
@@ -878,6 +886,8 @@ export class WorkItemStore {
|
|
|
878
886
|
current_run_id TEXT,
|
|
879
887
|
lease_epoch INTEGER NOT NULL DEFAULT 0,
|
|
880
888
|
replaces_action_id TEXT REFERENCES actions(id) ON DELETE SET NULL,
|
|
889
|
+
close_reason TEXT,
|
|
890
|
+
closed_at INTEGER,
|
|
881
891
|
created_at INTEGER NOT NULL,
|
|
882
892
|
updated_at INTEGER NOT NULL,
|
|
883
893
|
UNIQUE(work_item_id, sequence)
|
|
@@ -900,6 +910,7 @@ export class WorkItemStore {
|
|
|
900
910
|
execution_manifest TEXT,
|
|
901
911
|
summary TEXT,
|
|
902
912
|
evidence TEXT NOT NULL DEFAULT '[]',
|
|
913
|
+
outputs TEXT NOT NULL DEFAULT '[]',
|
|
903
914
|
acceptance_checks TEXT NOT NULL DEFAULT '[]',
|
|
904
915
|
waiting_reason TEXT,
|
|
905
916
|
error TEXT,
|
|
@@ -1106,9 +1117,15 @@ export class WorkItemStore {
|
|
|
1106
1117
|
if (!hasColumn(this.db, 'work_items', 'final_result')) {
|
|
1107
1118
|
this.db.exec('ALTER TABLE work_items ADD COLUMN final_result TEXT');
|
|
1108
1119
|
}
|
|
1120
|
+
if (!hasColumn(this.db, 'work_items', 'delivery_target')) {
|
|
1121
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN delivery_target TEXT');
|
|
1122
|
+
}
|
|
1109
1123
|
if (!hasColumn(this.db, 'actions', 'source_action_ids')) {
|
|
1110
1124
|
this.db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
|
|
1111
1125
|
}
|
|
1126
|
+
if (!hasColumn(this.db, 'actions', 'creation_source')) {
|
|
1127
|
+
this.db.exec("ALTER TABLE actions ADD COLUMN creation_source TEXT NOT NULL DEFAULT 'legacy'");
|
|
1128
|
+
}
|
|
1112
1129
|
if (!hasColumn(this.db, 'actions', 'generation')) {
|
|
1113
1130
|
this.db.exec('ALTER TABLE actions ADD COLUMN generation INTEGER NOT NULL DEFAULT 1');
|
|
1114
1131
|
}
|
|
@@ -1131,6 +1148,15 @@ export class WorkItemStore {
|
|
|
1131
1148
|
if (!hasColumn(this.db, 'actions', 'replaces_action_id')) {
|
|
1132
1149
|
this.db.exec('ALTER TABLE actions ADD COLUMN replaces_action_id TEXT REFERENCES actions(id) ON DELETE SET NULL');
|
|
1133
1150
|
}
|
|
1151
|
+
if (!hasColumn(this.db, 'actions', 'close_reason')) {
|
|
1152
|
+
this.db.exec('ALTER TABLE actions ADD COLUMN close_reason TEXT');
|
|
1153
|
+
}
|
|
1154
|
+
if (!hasColumn(this.db, 'actions', 'closed_at')) {
|
|
1155
|
+
this.db.exec('ALTER TABLE actions ADD COLUMN closed_at INTEGER');
|
|
1156
|
+
}
|
|
1157
|
+
if (!hasColumn(this.db, 'runs', 'outputs')) {
|
|
1158
|
+
this.db.exec("ALTER TABLE runs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]'");
|
|
1159
|
+
}
|
|
1134
1160
|
if (!hasColumn(this.db, 'runs', 'action_generation')) {
|
|
1135
1161
|
this.db.exec('ALTER TABLE runs ADD COLUMN action_generation INTEGER NOT NULL DEFAULT 1');
|
|
1136
1162
|
}
|
|
@@ -2296,14 +2322,15 @@ export class WorkItemStore {
|
|
|
2296
2322
|
const id = input.id || randomUUID();
|
|
2297
2323
|
const workspaceKey = canonicalWorkspaceKey(input.workDir);
|
|
2298
2324
|
this.db.prepare(`INSERT INTO work_items
|
|
2299
|
-
(id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result,
|
|
2325
|
+
(id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result, delivery_target,
|
|
2300
2326
|
title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
2301
2327
|
current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
|
|
2302
2328
|
session_context, attachments, created_at, updated_at)
|
|
2303
|
-
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2329
|
+
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2304
2330
|
id,
|
|
2305
2331
|
Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
|
|
2306
2332
|
input.coordinationMode || 'legacy',
|
|
2333
|
+
input.deliveryTarget || null,
|
|
2307
2334
|
input.title,
|
|
2308
2335
|
input.goal,
|
|
2309
2336
|
stringify(input.acceptanceCriteria || []),
|
|
@@ -2330,15 +2357,26 @@ export class WorkItemStore {
|
|
|
2330
2357
|
});
|
|
2331
2358
|
}
|
|
2332
2359
|
|
|
2333
|
-
#insertAction(workItemId, input, sequence, now = this.now()) {
|
|
2360
|
+
#insertAction(workItemId, input, sequence, now = this.now(), options = {}) {
|
|
2361
|
+
const workItem = this.getWorkItem(workItemId);
|
|
2334
2362
|
const stageId = input.stageId || input.type;
|
|
2335
|
-
const activeStage = usesLegacyGraph(
|
|
2363
|
+
const activeStage = usesLegacyGraph(workItem)
|
|
2336
2364
|
? this.db.prepare(`SELECT id FROM actions WHERE work_item_id = ? AND stage_id = ?
|
|
2337
2365
|
AND status NOT IN ('superseded', 'cancelled') LIMIT 1`).get(workItemId, stageId)
|
|
2338
2366
|
: null;
|
|
2339
2367
|
if (activeStage) {
|
|
2340
2368
|
throw new Error(`Work Center Action stage identity is already active: ${stageId}`);
|
|
2341
2369
|
}
|
|
2370
|
+
const creationSource = input.creationSource || 'legacy';
|
|
2371
|
+
if (input.type === 'create_vp'
|
|
2372
|
+
&& (!isDynamicWorkItem(workItem)
|
|
2373
|
+
|| creationSource !== 'dynamic_coordinator'
|
|
2374
|
+
|| options.dynamicCoordinator !== true)) {
|
|
2375
|
+
throw new Error('create_vp Actions can only be persisted by the dynamic WorkItem Coordinator');
|
|
2376
|
+
}
|
|
2377
|
+
if (input.type === 'create_vp' && input.workspaceMode === 'read') {
|
|
2378
|
+
throw new Error('create_vp Actions cannot use read workspace mode because VP creation mutates Agent-global state');
|
|
2379
|
+
}
|
|
2342
2380
|
const action = {
|
|
2343
2381
|
id: input.id || randomUUID(),
|
|
2344
2382
|
workItemId,
|
|
@@ -2349,6 +2387,7 @@ export class WorkItemStore {
|
|
|
2349
2387
|
modelPolicy: input.modelPolicy || null,
|
|
2350
2388
|
dependsOnStageIds: Array.isArray(input.dependsOnStageIds) ? input.dependsOnStageIds : [],
|
|
2351
2389
|
sourceActionIds: Array.isArray(input.sourceActionIds) ? input.sourceActionIds : [],
|
|
2390
|
+
creationSource,
|
|
2352
2391
|
workspaceMode: input.workspaceMode || 'shared',
|
|
2353
2392
|
changesRequestedStageId: input.changesRequestedStageId || null,
|
|
2354
2393
|
workspace: input.workspace || null,
|
|
@@ -2373,10 +2412,10 @@ export class WorkItemStore {
|
|
|
2373
2412
|
action.identityHistory = actionIdentityHistory(action);
|
|
2374
2413
|
this.db.prepare(`INSERT INTO actions
|
|
2375
2414
|
(id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
|
|
2376
|
-
depends_on_stage_ids, source_action_ids, workspace_mode, changes_requested_stage_id, workspace,
|
|
2415
|
+
depends_on_stage_ids, source_action_ids, creation_source, workspace_mode, changes_requested_stage_id, workspace,
|
|
2377
2416
|
instruction, brief, context, contract_revision, generation, spec_hash, identity_history, result_run_id,
|
|
2378
2417
|
status, attempt, max_attempts, current_run_id, lease_epoch, replaces_action_id, created_at, updated_at)
|
|
2379
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
|
|
2418
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
|
|
2380
2419
|
action.id,
|
|
2381
2420
|
workItemId,
|
|
2382
2421
|
action.sequence,
|
|
@@ -2387,6 +2426,7 @@ export class WorkItemStore {
|
|
|
2387
2426
|
stringify(action.modelPolicy),
|
|
2388
2427
|
stringify(action.dependsOnStageIds),
|
|
2389
2428
|
stringify(action.sourceActionIds),
|
|
2429
|
+
action.creationSource,
|
|
2390
2430
|
action.workspaceMode,
|
|
2391
2431
|
action.changesRequestedStageId,
|
|
2392
2432
|
stringify(action.workspace),
|
|
@@ -3150,10 +3190,11 @@ export class WorkItemStore {
|
|
|
3150
3190
|
coordinatorRevision: detail.coordinatorRevision,
|
|
3151
3191
|
status: detail.status,
|
|
3152
3192
|
actionFence: coordinatorActionFence(
|
|
3153
|
-
(detail.actions || []).filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status)),
|
|
3193
|
+
(detail.actions || []).filter(action => !['completed', 'closed', 'superseded', 'cancelled'].includes(action.status)),
|
|
3154
3194
|
),
|
|
3155
3195
|
recovery: assistant.recovery ? { ...assistant.recovery } : null,
|
|
3156
3196
|
automatic: assistant.automatic === true,
|
|
3197
|
+
userOriginated: assistant.userOriginated === true,
|
|
3157
3198
|
claim: {
|
|
3158
3199
|
mailboxId: claim.mailboxId,
|
|
3159
3200
|
ownerBootId: claim.ownerBootId,
|
|
@@ -3215,9 +3256,10 @@ export class WorkItemStore {
|
|
|
3215
3256
|
coordinatorRevision,
|
|
3216
3257
|
status: workItem.status,
|
|
3217
3258
|
actionFence: coordinatorActionFence(
|
|
3218
|
-
(detail.actions || []).filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status)),
|
|
3259
|
+
(detail.actions || []).filter(action => !['completed', 'closed', 'superseded', 'cancelled'].includes(action.status)),
|
|
3219
3260
|
),
|
|
3220
3261
|
automatic: true,
|
|
3262
|
+
userOriginated: false,
|
|
3221
3263
|
claim: {
|
|
3222
3264
|
mailboxId: mailbox.id,
|
|
3223
3265
|
ownerBootId: expected.ownerBootId,
|
|
@@ -3285,7 +3327,7 @@ export class WorkItemStore {
|
|
|
3285
3327
|
isImage: attachment.isImage === true,
|
|
3286
3328
|
}));
|
|
3287
3329
|
const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
3288
|
-
AND status NOT IN ('completed', 'superseded', 'cancelled') ORDER BY sequence`).all(id).map(mapAction);
|
|
3330
|
+
AND status NOT IN ('completed', 'closed', 'superseded', 'cancelled') ORDER BY sequence`).all(id).map(mapAction);
|
|
3289
3331
|
this.#assertNoIntegrationReservation(activeActions, now);
|
|
3290
3332
|
let recovery = options.recovery && typeof options.recovery === 'object'
|
|
3291
3333
|
? { ...options.recovery } : null;
|
|
@@ -3325,6 +3367,7 @@ export class WorkItemStore {
|
|
|
3325
3367
|
const assistantMessage = {
|
|
3326
3368
|
id: randomUUID(), turnId, role: 'assistant', text: '', status: 'thinking',
|
|
3327
3369
|
createdAt: now, updatedAt: now, decision: null,
|
|
3370
|
+
userOriginated: userMessage !== null,
|
|
3328
3371
|
...(recovery ? { recovery: { ...recovery } } : {}),
|
|
3329
3372
|
};
|
|
3330
3373
|
if (userMessage) {
|
|
@@ -3380,6 +3423,8 @@ export class WorkItemStore {
|
|
|
3380
3423
|
status: workItem.status,
|
|
3381
3424
|
actionFence: coordinatorActionFence(activeActions),
|
|
3382
3425
|
recovery: recovery ? { ...recovery } : null,
|
|
3426
|
+
userOriginated: userMessage !== null,
|
|
3427
|
+
claim: null,
|
|
3383
3428
|
},
|
|
3384
3429
|
};
|
|
3385
3430
|
});
|
|
@@ -3411,7 +3456,7 @@ export class WorkItemStore {
|
|
|
3411
3456
|
const decision = result?.decision || {};
|
|
3412
3457
|
const now = this.now();
|
|
3413
3458
|
const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
3414
|
-
AND status NOT IN ('completed', 'superseded', 'cancelled') ORDER BY sequence`).all(workItem.id).map(mapAction);
|
|
3459
|
+
AND status NOT IN ('completed', 'closed', 'superseded', 'cancelled') ORDER BY sequence`).all(workItem.id).map(mapAction);
|
|
3415
3460
|
if (coordinatorActionFence(activeActions) !== expected.actionFence) {
|
|
3416
3461
|
throw new Error('WorkItem Actions changed while the Coordinator was responding; send the message again');
|
|
3417
3462
|
}
|
|
@@ -3421,6 +3466,9 @@ export class WorkItemStore {
|
|
|
3421
3466
|
turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
|
|
3422
3467
|
});
|
|
3423
3468
|
}
|
|
3469
|
+
if (decision.contractPatch?.deliveryTarget) {
|
|
3470
|
+
throw new Error('Only a dynamic user-originated Coordinator turn can confirm the WorkItem delivery target');
|
|
3471
|
+
}
|
|
3424
3472
|
|
|
3425
3473
|
const graphMode = usesLegacyGraph(workItem);
|
|
3426
3474
|
if (decision.kind === 'replan'
|
|
@@ -3636,10 +3684,33 @@ export class WorkItemStore {
|
|
|
3636
3684
|
turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
|
|
3637
3685
|
}) {
|
|
3638
3686
|
const decision = result?.decision || {};
|
|
3687
|
+
if (expected.automatic === true && result?.mutation?.contractPatch?.deliveryTarget) {
|
|
3688
|
+
throw new Error('Automatic Work Center Coordinator delivery target changes are forbidden');
|
|
3689
|
+
}
|
|
3690
|
+
if (decision.contractPatch?.deliveryTarget && expected.userOriginated !== true) {
|
|
3691
|
+
throw new Error('WorkItem delivery target confirmation requires a user-originated Coordinator turn');
|
|
3692
|
+
}
|
|
3693
|
+
if (decision.contractPatch?.deliveryTarget && decision.kind !== 'request_human') {
|
|
3694
|
+
throw new Error('WorkItem delivery target confirmation requires a user-originated request_human decision');
|
|
3695
|
+
}
|
|
3639
3696
|
let affectedActionIds = [];
|
|
3640
3697
|
let nextStatus = workItem.status;
|
|
3641
3698
|
let currentActionId = workItem.currentActionId;
|
|
3642
3699
|
let finalResult = null;
|
|
3700
|
+
const contractPatch = normalizeContractPatch(decision.contractPatch);
|
|
3701
|
+
if (decision.kind === 'create_actions'
|
|
3702
|
+
&& !workItem.deliveryTarget
|
|
3703
|
+
&& (result?.mutation?.createdActions || []).some(action => (
|
|
3704
|
+
action.type === 'create_vp' || action.workspaceMode !== 'read'
|
|
3705
|
+
))) {
|
|
3706
|
+
throw new Error('WorkItem delivery target must be confirmed before creating mutating or delivery Actions');
|
|
3707
|
+
}
|
|
3708
|
+
if (contractPatch) {
|
|
3709
|
+
if (contractPatch.title) workItem.title = contractPatch.title;
|
|
3710
|
+
if (contractPatch.goal) workItem.goal = contractPatch.goal;
|
|
3711
|
+
if (contractPatch.acceptanceCriteria) workItem.acceptanceCriteria = contractPatch.acceptanceCriteria;
|
|
3712
|
+
if (contractPatch.deliveryTarget) workItem.deliveryTarget = contractPatch.deliveryTarget;
|
|
3713
|
+
}
|
|
3643
3714
|
|
|
3644
3715
|
if (decision.kind === 'create_actions') {
|
|
3645
3716
|
if (activeActions.some(action => action.status === 'running')) {
|
|
@@ -3649,6 +3720,24 @@ export class WorkItemStore {
|
|
|
3649
3720
|
if (!mutation || !Array.isArray(mutation.createdActions) || mutation.createdActions.length === 0) {
|
|
3650
3721
|
throw new Error('Dynamic Coordinator Action creation is missing a validated mutation');
|
|
3651
3722
|
}
|
|
3723
|
+
for (const closure of mutation.closeActions || []) {
|
|
3724
|
+
const action = activeActions.find(candidate => candidate.id === closure.actionId);
|
|
3725
|
+
if (!action || !['waiting', 'failed'].includes(action.status)) {
|
|
3726
|
+
throw new Error('Dynamic Coordinator close target changed before apply');
|
|
3727
|
+
}
|
|
3728
|
+
this.#supersedePendingActionInputs([action], closure.reason, now);
|
|
3729
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'closed', current_run_id = NULL,
|
|
3730
|
+
close_reason = ?, closed_at = ?, lease_epoch = lease_epoch + 1, updated_at = ?
|
|
3731
|
+
WHERE id = ? AND generation = ? AND status IN ('waiting', 'failed')`).run(
|
|
3732
|
+
closure.reason, now, now, action.id, action.generation,
|
|
3733
|
+
);
|
|
3734
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator lost an Action close fence');
|
|
3735
|
+
affectedActionIds.push(action.id);
|
|
3736
|
+
this.appendEvent(workItem.id, 'action.closed', { reason: closure.reason }, {
|
|
3737
|
+
actionId: action.id,
|
|
3738
|
+
actionGeneration: action.generation,
|
|
3739
|
+
});
|
|
3740
|
+
}
|
|
3652
3741
|
for (const actionId of mutation.supersedeActionIds || []) {
|
|
3653
3742
|
const action = activeActions.find(candidate => candidate.id === actionId);
|
|
3654
3743
|
if (!action || !['ready', 'waiting', 'failed'].includes(action.status)) {
|
|
@@ -3679,8 +3768,11 @@ export class WorkItemStore {
|
|
|
3679
3768
|
const current = this.getWorkItem(workItem.id);
|
|
3680
3769
|
for (const candidate of mutation.createdActions) {
|
|
3681
3770
|
const action = this.#insertAction(workItem.id, {
|
|
3682
|
-
...candidate,
|
|
3683
|
-
|
|
3771
|
+
...candidate,
|
|
3772
|
+
creationSource: 'dynamic_coordinator',
|
|
3773
|
+
status: 'ready',
|
|
3774
|
+
contractRevision: current.revision,
|
|
3775
|
+
}, this.#nextSequence(workItem.id), now, { dynamicCoordinator: true });
|
|
3684
3776
|
affectedActionIds.push(action.id);
|
|
3685
3777
|
}
|
|
3686
3778
|
nextStatus = 'ready';
|
|
@@ -3719,7 +3811,29 @@ export class WorkItemStore {
|
|
|
3719
3811
|
nextStatus = 'waiting';
|
|
3720
3812
|
currentActionId = null;
|
|
3721
3813
|
} else if (decision.kind === 'complete') {
|
|
3722
|
-
|
|
3814
|
+
const closing = new Map((decision.closeActions || []).map(entry => [entry.actionId, entry]));
|
|
3815
|
+
if (activeActions.some(action => action.status !== 'closed' && !closing.has(action.id))) {
|
|
3816
|
+
throw new Error('Work Center cannot complete with unfinished Actions');
|
|
3817
|
+
}
|
|
3818
|
+
for (const action of activeActions) {
|
|
3819
|
+
const closure = closing.get(action.id);
|
|
3820
|
+
if (!closure) continue;
|
|
3821
|
+
if (!['waiting', 'failed'].includes(action.status)) {
|
|
3822
|
+
throw new Error('Dynamic Coordinator completion close target changed before apply');
|
|
3823
|
+
}
|
|
3824
|
+
this.#supersedePendingActionInputs([action], closure.reason, now);
|
|
3825
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'closed', current_run_id = NULL,
|
|
3826
|
+
close_reason = ?, closed_at = ?, lease_epoch = lease_epoch + 1, updated_at = ?
|
|
3827
|
+
WHERE id = ? AND generation = ? AND status IN ('waiting', 'failed')`).run(
|
|
3828
|
+
closure.reason, now, now, action.id, action.generation,
|
|
3829
|
+
);
|
|
3830
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator completion lost an Action close fence');
|
|
3831
|
+
affectedActionIds.push(action.id);
|
|
3832
|
+
this.appendEvent(workItem.id, 'action.closed', { reason: closure.reason }, {
|
|
3833
|
+
actionId: action.id,
|
|
3834
|
+
actionGeneration: action.generation,
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3723
3837
|
if (this.#hasBlockingOperation(workItem.id)) {
|
|
3724
3838
|
throw new Error('WorkItem has an unsafe blocking Operation and cannot complete');
|
|
3725
3839
|
}
|
|
@@ -3744,6 +3858,27 @@ export class WorkItemStore {
|
|
|
3744
3858
|
throw new Error(`Completion evidence Run has incomplete acceptance checks: ${runId}`);
|
|
3745
3859
|
}
|
|
3746
3860
|
}
|
|
3861
|
+
finalResult.outputs = [];
|
|
3862
|
+
const seenOutputs = new Set();
|
|
3863
|
+
for (const runId of finalResult.evidenceRunIds) {
|
|
3864
|
+
for (const output of canonicalRuns.get(runId)?.outputs || []) {
|
|
3865
|
+
const key = `${output.kind}\u0000${output.ref}`;
|
|
3866
|
+
if (seenOutputs.has(key)) continue;
|
|
3867
|
+
seenOutputs.add(key);
|
|
3868
|
+
finalResult.outputs.push({ ...output, runId });
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
const requiredOutputKind = {
|
|
3872
|
+
workspace_files: 'file',
|
|
3873
|
+
pull_request: 'pr',
|
|
3874
|
+
merge: 'commit',
|
|
3875
|
+
}[workItem.deliveryTarget];
|
|
3876
|
+
if (!requiredOutputKind) {
|
|
3877
|
+
throw new Error('WorkItem delivery target must be confirmed before completion');
|
|
3878
|
+
}
|
|
3879
|
+
if (!finalResult.outputs.some(output => output.kind === requiredOutputKind)) {
|
|
3880
|
+
throw new Error(`WorkItem completion requires a canonical ${requiredOutputKind} output for delivery target ${workItem.deliveryTarget}`);
|
|
3881
|
+
}
|
|
3747
3882
|
for (const [index, acceptanceResult] of finalResult.acceptanceResults.entries()) {
|
|
3748
3883
|
const provesCriterion = acceptanceResult.evidenceRunIds.some(runId => (
|
|
3749
3884
|
canonicalRuns.get(runId)?.acceptanceChecks?.[index]?.criterion === criteria[index]
|
|
@@ -3762,7 +3897,12 @@ export class WorkItemStore {
|
|
|
3762
3897
|
messages[assistantIndex] = {
|
|
3763
3898
|
...messages[assistantIndex], text: result.reply, status: 'completed', updatedAt: now,
|
|
3764
3899
|
...(result.speaker ? { speaker: result.speaker } : {}),
|
|
3765
|
-
decision: {
|
|
3900
|
+
decision: {
|
|
3901
|
+
kind: decision.kind,
|
|
3902
|
+
reason: decision.reason,
|
|
3903
|
+
affectedActionIds,
|
|
3904
|
+
...(decision.kind === 'request_human' ? { question: decision.question } : {}),
|
|
3905
|
+
},
|
|
3766
3906
|
};
|
|
3767
3907
|
this.#appendConversationEntry(
|
|
3768
3908
|
workItem.id, messages[assistantIndex], `coordinator:turn:${turnId}:assistant`,
|
|
@@ -3774,11 +3914,13 @@ export class WorkItemStore {
|
|
|
3774
3914
|
const current = this.getWorkItem(workItem.id);
|
|
3775
3915
|
const changed = this.db.prepare(`UPDATE work_items SET messages = ?,
|
|
3776
3916
|
coordinator_revision = coordinator_revision + 1, status = ?, current_action_id = ?,
|
|
3777
|
-
current_run_id = NULL, final_result = COALESCE(final_result, ?),
|
|
3917
|
+
current_run_id = NULL, final_result = COALESCE(final_result, ?), title = ?, goal = ?,
|
|
3918
|
+
acceptance_criteria = ?, delivery_target = ?, revision = revision + ?, updated_at = ?
|
|
3778
3919
|
WHERE id = ? AND coordinator_revision = ? AND revision = ? AND plan_revision = ?
|
|
3779
3920
|
AND ledger_revision = ? AND status NOT IN ('done', 'cancelled')`).run(
|
|
3780
3921
|
stringify(messages), nextStatus, currentActionId, finalResult ? stringify(finalResult) : null,
|
|
3781
|
-
|
|
3922
|
+
workItem.title, workItem.goal, stringify(workItem.acceptanceCriteria), workItem.deliveryTarget,
|
|
3923
|
+
contractPatch ? 1 : 0, now, workItem.id, current.coordinatorRevision, current.revision,
|
|
3782
3924
|
current.planRevision, current.ledgerRevision,
|
|
3783
3925
|
);
|
|
3784
3926
|
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator completion lost its turn fence');
|
|
@@ -4739,7 +4881,7 @@ export class WorkItemStore {
|
|
|
4739
4881
|
const ledgerIncrement = workItem.executionSchemaVersion >= 2
|
|
4740
4882
|
&& ['completed', 'failed', 'waiting'].includes(result.outcome) ? 1 : 0;
|
|
4741
4883
|
this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, response = ?, summary = ?, evidence = ?,
|
|
4742
|
-
acceptance_checks = ?, waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
4884
|
+
outputs = ?, acceptance_checks = ?, waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
4743
4885
|
loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?, output_tokens = ?,
|
|
4744
4886
|
cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?,
|
|
4745
4887
|
progress_revision = progress_revision + 1 WHERE id = ?`).run(
|
|
@@ -4748,6 +4890,7 @@ export class WorkItemStore {
|
|
|
4748
4890
|
normalizeRunResponse(result.response),
|
|
4749
4891
|
result.summary || '',
|
|
4750
4892
|
stringify(normalizeEvidence(result.evidence)),
|
|
4893
|
+
stringify(normalizeOutputs(result.outputs)),
|
|
4751
4894
|
stringify(Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : []),
|
|
4752
4895
|
result.waitingReason || null,
|
|
4753
4896
|
result.error || null,
|
|
@@ -5098,7 +5241,7 @@ export class WorkItemStore {
|
|
|
5098
5241
|
...trigger,
|
|
5099
5242
|
ledgerRevision: detail.ledgerRevision,
|
|
5100
5243
|
actionIds: (detail.actions || [])
|
|
5101
|
-
.filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status))
|
|
5244
|
+
.filter(action => !['completed', 'closed', 'superseded', 'cancelled'].includes(action.status))
|
|
5102
5245
|
.map(action => action.id),
|
|
5103
5246
|
},
|
|
5104
5247
|
}, `dynamic:reconcile:${workItemId}:${detail.ledgerRevision}`);
|
|
@@ -15,6 +15,7 @@ export const BUILT_IN_ACTION_TYPES = Object.freeze([
|
|
|
15
15
|
'operate',
|
|
16
16
|
'deliver',
|
|
17
17
|
'write',
|
|
18
|
+
'create_vp',
|
|
18
19
|
'custom',
|
|
19
20
|
]);
|
|
20
21
|
const STAGE_TYPES = new Set(BUILT_IN_ACTION_TYPES);
|
|
@@ -39,6 +40,7 @@ const DEFAULT_STAGE_INSTRUCTIONS = Object.freeze({
|
|
|
39
40
|
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.',
|
|
40
41
|
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.',
|
|
41
42
|
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 one persistent specialist VP only when no existing VP can execute a required capability. 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.',
|
|
42
44
|
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.',
|
|
43
45
|
});
|
|
44
46
|
|
|
@@ -56,6 +58,7 @@ const DEFAULT_ACTION_BRIEFS = Object.freeze({
|
|
|
56
58
|
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.'],
|
|
57
59
|
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.'],
|
|
58
60
|
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 missing specialist VP required by this Work Item.', '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.'],
|
|
59
62
|
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.'],
|
|
60
63
|
});
|
|
61
64
|
|
|
@@ -521,6 +524,9 @@ export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
|
|
|
521
524
|
const input = rawAction && typeof rawAction === 'object' && !Array.isArray(rawAction) ? rawAction : {};
|
|
522
525
|
const type = generatedActionType(input.type);
|
|
523
526
|
if (type === 'triage') throw new Error('AI-planned Actions cannot add another triage Action');
|
|
527
|
+
if (type === 'create_vp') {
|
|
528
|
+
throw new Error('create_vp Actions can only be created by the dynamic WorkItem Coordinator');
|
|
529
|
+
}
|
|
524
530
|
const id = canonicalActionId(input.id, `${type}-${index + 1}`);
|
|
525
531
|
if (seen.has(id)) throw new Error(`Duplicate AI-planned Action id: ${id}`);
|
|
526
532
|
if (reservedStageIds.has(id)) {
|