@yeaft/webchat-agent 1.0.365 → 1.0.367
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 +3 -0
- package/local-runtime/server/handlers/client-conversation.js +4 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +97 -97
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +223 -14
- package/yeaft/tools/create-work-item.js +8 -8
- package/yeaft/web-bridge.js +71 -7
- package/yeaft/work-center/controller.js +12 -2
- package/yeaft/work-center/coordinator.js +156 -26
- package/yeaft/work-center/durable-model.js +51 -1
- package/yeaft/work-center/dynamic-coordination.js +243 -0
- package/yeaft/work-center/execution-mode.js +16 -0
- package/yeaft/work-center/mainline-projection.js +25 -13
- package/yeaft/work-center/projection.js +32 -9
- package/yeaft/work-center/runner.js +18 -13
- package/yeaft/work-center/service.js +86 -13
- package/yeaft/work-center/store.js +535 -60
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
} from '../llm/adapter.js';
|
|
10
10
|
import { resolveWorkItemModel, selectWorkItemVp } from './assignment.js';
|
|
11
11
|
import { normalizeContractPatch } from './completion-contract.js';
|
|
12
|
+
import { prepareDynamicActionMutation } from './dynamic-coordination.js';
|
|
13
|
+
import { isDynamicWorkItem } from './execution-mode.js';
|
|
12
14
|
import { applyCoordinatorReplan } from './plan-mutation.js';
|
|
13
15
|
import { buildWorkItemAttachmentContext } from './attachments.js';
|
|
14
16
|
import { sanitizeDiagnosticText } from './debug-projection.js';
|
|
@@ -124,17 +126,25 @@ function coordinatorStageReferences(detail) {
|
|
|
124
126
|
};
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
function boundedAction(action, result, stageReferences, compact = false) {
|
|
129
|
+
function boundedAction(action, result, stageReferences, compact = false, dynamic = false) {
|
|
128
130
|
const brief = action?.brief && typeof action.brief === 'object' ? action.brief : null;
|
|
129
131
|
return {
|
|
130
|
-
|
|
132
|
+
...(dynamic
|
|
133
|
+
? {
|
|
134
|
+
actionId: truncateUtf8(action?.id, 256),
|
|
135
|
+
sourceActionIds: (Array.isArray(action?.sourceActionIds) ? action.sourceActionIds : [])
|
|
136
|
+
.slice(0, 8).map(value => truncateUtf8(value, 256)).filter(Boolean),
|
|
137
|
+
}
|
|
138
|
+
: { stageId: stageReferences.project(action?.stageId) }),
|
|
131
139
|
type: truncateUtf8(action?.type, 64),
|
|
132
140
|
status: truncateUtf8(action?.status, 64),
|
|
133
141
|
generation: Math.max(1, Number(action?.generation) || 1),
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
...(dynamic ? {} : {
|
|
143
|
+
dependencies: (Array.isArray(action?.dependsOnStageIds) ? action.dependsOnStageIds : [])
|
|
144
|
+
.slice(0, 8)
|
|
145
|
+
.map(value => stageReferences.project(value))
|
|
146
|
+
.filter(Boolean),
|
|
147
|
+
}),
|
|
138
148
|
workspaceMode: truncateUtf8(action?.workspaceMode, 64),
|
|
139
149
|
...(!compact && brief ? {
|
|
140
150
|
brief: {
|
|
@@ -146,7 +156,15 @@ function boundedAction(action, result, stageReferences, compact = false) {
|
|
|
146
156
|
result: result ? {
|
|
147
157
|
status: truncateUtf8(result.status, 64),
|
|
148
158
|
summary: truncateUtf8(result.summary, compact ? 256 : 768),
|
|
149
|
-
...(!compact ? {
|
|
159
|
+
...(!compact ? {
|
|
160
|
+
evidence: boundedEvidence(result.evidence),
|
|
161
|
+
acceptanceChecks: (Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : [])
|
|
162
|
+
.slice(0, 24).map(check => ({
|
|
163
|
+
criterion: truncateUtf8(check?.criterion, 512),
|
|
164
|
+
status: truncateUtf8(check?.status, 64),
|
|
165
|
+
evidence: truncateUtf8(check?.evidence, 1_000),
|
|
166
|
+
})),
|
|
167
|
+
} : {}),
|
|
150
168
|
waitingReason: truncateUtf8(result.waitingReason, 384) || null,
|
|
151
169
|
error: truncateUtf8(result.error, 384) || null,
|
|
152
170
|
reviewDecision: truncateUtf8(result.reviewDecision, 64) || null,
|
|
@@ -188,11 +206,42 @@ Decision rules:
|
|
|
188
206
|
- Stage ids in the snapshot may be bounded aliases. Echo them exactly; the runtime resolves them to durable identities.
|
|
189
207
|
- Never return destructive cancellation. Tell the user to use the explicit cancel control instead.`;
|
|
190
208
|
|
|
191
|
-
|
|
209
|
+
const DYNAMIC_COORDINATOR_SYSTEM_PROMPT = `You are the persistent Work Center Coordinator. You own progress toward one durable WorkItem.
|
|
210
|
+
|
|
211
|
+
Observe the current contract, Action journal, canonical Run evidence, and conversation. Decide only the next justified step; never predict or emit a complete workflow graph.
|
|
212
|
+
|
|
213
|
+
Return exactly one JSON object and no surrounding prose:
|
|
214
|
+
{
|
|
215
|
+
"reply": "natural user-facing response",
|
|
216
|
+
"decision": {
|
|
217
|
+
"kind": "answer|create_actions|guide_actions|request_human|complete",
|
|
218
|
+
"reason": "short audit reason",
|
|
219
|
+
"question": null,
|
|
220
|
+
"workItemType": null,
|
|
221
|
+
"contractPatch": null,
|
|
222
|
+
"supersedeActionIds": [],
|
|
223
|
+
"guidance": [],
|
|
224
|
+
"actions": [],
|
|
225
|
+
"completion": null
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
Rules:
|
|
230
|
+
- answer: explain state only. Never use it for an automatic advance trigger.
|
|
231
|
+
- create_actions: create 1..8 currently runnable Actions. Every Action needs type, objective, approach, expectedOutcome, capability, candidateVpIds, assignmentReason, sourceActionIds, workspaceMode, and optional maxAttempts/separateFromActionTypes. sourceActionIds are context/audit references, never scheduling dependencies. Do not include dependsOnActionIds, dependsOnStageIds, stages, or a graph.
|
|
232
|
+
- guide_actions: target 1..8 unfinished non-running Actions by durable actionId.
|
|
233
|
+
- request_human: only when external information or a user decision is genuinely required.
|
|
234
|
+
- complete: only when every acceptance criterion has canonical completed Run evidence and there are no unfinished Actions. Include summary, ordered acceptanceResults with evidenceRunIds, evidenceRunIds, and residualRisks.
|
|
235
|
+
- Preserve completed Action history. Never claim tests, review, merge, release, or external effects without canonical Run evidence.
|
|
236
|
+
- Action templates are reusable capabilities, not a prescribed workflow. Create the smallest useful Action boundary, not tool-call-sized work.
|
|
237
|
+
- Never return destructive cancellation. The user owns the explicit cancel control.`;
|
|
238
|
+
|
|
239
|
+
function coordinatorSystemPrompt(language, detail) {
|
|
192
240
|
const userLanguage = coordinatorLanguage(language) === 'zh'
|
|
193
241
|
? 'Simplified Chinese (zh-CN)'
|
|
194
242
|
: 'English';
|
|
195
|
-
|
|
243
|
+
const base = isDynamicWorkItem(detail) ? DYNAMIC_COORDINATOR_SYSTEM_PROMPT : COORDINATOR_SYSTEM_PROMPT;
|
|
244
|
+
return `${base}
|
|
196
245
|
|
|
197
246
|
User-facing language: ${userLanguage}. Write reply and question in that language. Keep JSON property names and decision enum values in English. Never expose deterministic validator errors as the user-facing reply; explain the underlying issue plainly.`;
|
|
198
247
|
}
|
|
@@ -327,19 +376,22 @@ function normalizeGuidance(value, detail) {
|
|
|
327
376
|
if (!Array.isArray(value) || value.length < 1 || value.length > 8) {
|
|
328
377
|
throw new Error('Work Center Coordinator guidance requires between 1 and 8 targets');
|
|
329
378
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
.
|
|
379
|
+
const dynamic = isDynamicWorkItem(detail);
|
|
380
|
+
const active = (detail.actions || [])
|
|
381
|
+
.filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status));
|
|
382
|
+
const activeByReference = new Map(active.map(action => [dynamic ? action.id : action.stageId, action]));
|
|
333
383
|
const stageReferences = coordinatorStageReferences(detail);
|
|
334
384
|
const seen = new Set();
|
|
335
385
|
return value.map(entry => {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
386
|
+
const reference = dynamic
|
|
387
|
+
? (typeof entry?.actionId === 'string' ? entry.actionId.trim() : '')
|
|
388
|
+
: stageReferences.resolve(entry?.stageId);
|
|
389
|
+
if (!reference || seen.has(reference) || !activeByReference.has(reference)) {
|
|
390
|
+
throw new Error(`Work Center Coordinator guidance references an invalid unfinished Action: ${reference || '(missing)'}`);
|
|
339
391
|
}
|
|
340
|
-
seen.add(
|
|
392
|
+
seen.add(reference);
|
|
341
393
|
return {
|
|
342
|
-
stageId,
|
|
394
|
+
...(dynamic ? { actionId: reference } : { stageId: reference }),
|
|
343
395
|
instruction: cleanText(entry?.instruction, COORDINATOR_MAX_INSTRUCTION_CHARS, 'guidance instruction'),
|
|
344
396
|
};
|
|
345
397
|
});
|
|
@@ -368,9 +420,14 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
368
420
|
const source = parsed?.decision && typeof parsed.decision === 'object' && !Array.isArray(parsed.decision)
|
|
369
421
|
? parsed.decision
|
|
370
422
|
: {};
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
423
|
+
const dynamic = isDynamicWorkItem(detail);
|
|
424
|
+
const allowedKinds = dynamic
|
|
425
|
+
? (options.automatic === true
|
|
426
|
+
? ['create_actions', 'guide_actions', 'request_human', 'complete']
|
|
427
|
+
: ['answer', 'create_actions', 'guide_actions', 'request_human', 'complete'])
|
|
428
|
+
: (options.recovery === true
|
|
429
|
+
? ['guide_actions', 'replan', 'request_human']
|
|
430
|
+
: ['answer', 'guide_actions', 'replan']);
|
|
374
431
|
const kind = allowedKinds.includes(source.kind) ? source.kind : '';
|
|
375
432
|
if (!kind) throw new Error('Work Center Coordinator decision kind is invalid');
|
|
376
433
|
const reason = cleanText(source.reason, 2_000, 'decision reason');
|
|
@@ -411,6 +468,41 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
411
468
|
},
|
|
412
469
|
};
|
|
413
470
|
}
|
|
471
|
+
if (dynamic && kind === 'complete') {
|
|
472
|
+
return {
|
|
473
|
+
reply,
|
|
474
|
+
decision: {
|
|
475
|
+
kind,
|
|
476
|
+
reason,
|
|
477
|
+
completion: source.completion,
|
|
478
|
+
contractPatch: null,
|
|
479
|
+
guidance: [],
|
|
480
|
+
actions: [],
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (dynamic && kind === 'create_actions') {
|
|
485
|
+
const contractPatch = normalizeContractPatch(source.contractPatch);
|
|
486
|
+
const decision = {
|
|
487
|
+
kind,
|
|
488
|
+
reason,
|
|
489
|
+
workItemType: source.workItemType,
|
|
490
|
+
contractPatch,
|
|
491
|
+
supersedeActionIds: source.supersedeActionIds,
|
|
492
|
+
guidance: [],
|
|
493
|
+
actions: source.actions,
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
reply,
|
|
497
|
+
decision,
|
|
498
|
+
mutation: prepareDynamicActionMutation({
|
|
499
|
+
workItem: detail,
|
|
500
|
+
actions: detail.actions || [],
|
|
501
|
+
decision,
|
|
502
|
+
availableVpIds: options.availableVpIds,
|
|
503
|
+
}),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
414
506
|
const contractPatch = normalizeContractPatch(source.contractPatch);
|
|
415
507
|
if (!Array.isArray(source.actions)) {
|
|
416
508
|
throw new Error('Work Center Coordinator replan requires the complete unfinished Action graph');
|
|
@@ -496,6 +588,7 @@ function coordinatorSnapshot(detail) {
|
|
|
496
588
|
throw new Error('WorkItem contract cannot be represented within the Coordinator snapshot budget');
|
|
497
589
|
}
|
|
498
590
|
|
|
591
|
+
const dynamic = isDynamicWorkItem(detail);
|
|
499
592
|
const currentActions = (Array.isArray(detail.actions) ? detail.actions : [])
|
|
500
593
|
.filter(action => !['superseded', 'cancelled'].includes(action.status));
|
|
501
594
|
const stageReferences = coordinatorStageReferences(detail);
|
|
@@ -510,20 +603,24 @@ function coordinatorSnapshot(detail) {
|
|
|
510
603
|
canonicalRunByAction.get(action.id),
|
|
511
604
|
stageReferences,
|
|
512
605
|
action.status === 'completed',
|
|
606
|
+
dynamic,
|
|
513
607
|
));
|
|
514
608
|
let actions = boundedJsonArray(projectedActions, COORDINATOR_MAX_ACTIONS_BYTES);
|
|
515
|
-
|
|
516
|
-
|
|
609
|
+
const identity = action => `${dynamic ? action.actionId : action.stageId}:${action.generation}`;
|
|
610
|
+
const expectedIdentity = action => `${dynamic ? action.id : stageReferences.project(action.stageId)}:${action.generation}`;
|
|
611
|
+
let includedActionIdentities = new Set(actions.map(identity));
|
|
612
|
+
if (unfinished.some(action => !includedActionIdentities.has(expectedIdentity(action)))) {
|
|
517
613
|
projectedActions = selected.map(action => boundedAction(
|
|
518
614
|
action,
|
|
519
615
|
canonicalRunByAction.get(action.id),
|
|
520
616
|
stageReferences,
|
|
521
617
|
true,
|
|
618
|
+
dynamic,
|
|
522
619
|
));
|
|
523
620
|
actions = boundedJsonArray(projectedActions, COORDINATOR_MAX_ACTIONS_BYTES);
|
|
524
|
-
includedActionIdentities = new Set(actions.map(
|
|
621
|
+
includedActionIdentities = new Set(actions.map(identity));
|
|
525
622
|
}
|
|
526
|
-
if (unfinished.some(action => !includedActionIdentities.has(
|
|
623
|
+
if (unfinished.some(action => !includedActionIdentities.has(expectedIdentity(action)))) {
|
|
527
624
|
throw new Error('Active Actions cannot be represented within the Coordinator snapshot budget');
|
|
528
625
|
}
|
|
529
626
|
|
|
@@ -604,6 +701,34 @@ export class WorkItemCoordinator {
|
|
|
604
701
|
});
|
|
605
702
|
}
|
|
606
703
|
|
|
704
|
+
advance(mailboxId, options = {}) {
|
|
705
|
+
if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
|
|
706
|
+
const claim = this.store.claimCoordinatorMailbox(options.workItemId, this.ownerBootId, this.claimLeaseMs);
|
|
707
|
+
if (!claim) return null;
|
|
708
|
+
if (claim.id !== mailboxId) {
|
|
709
|
+
this.store.releaseCoordinatorMailboxClaim(
|
|
710
|
+
claim.id, this.ownerBootId, Number(claim.claim_epoch),
|
|
711
|
+
);
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
const started = this.store.beginDynamicCoordinatorTurn(mailboxId, {
|
|
715
|
+
ownerBootId: this.ownerBootId,
|
|
716
|
+
claimEpoch: Number(claim.claim_epoch),
|
|
717
|
+
});
|
|
718
|
+
if (!started) {
|
|
719
|
+
this.store.releaseCoordinatorMailboxClaim(
|
|
720
|
+
claim.id, this.ownerBootId, Number(claim.claim_epoch),
|
|
721
|
+
);
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
options.onUpdate?.('coordinator.advance_started', started.detail);
|
|
725
|
+
const trigger = started.detail.messages?.find(message => message.turnId === started.turnId)?.trigger;
|
|
726
|
+
const text = `Automatic WorkItem advance trigger: ${JSON.stringify(trigger || { kind: claim.kind })}. `
|
|
727
|
+
+ 'Observe the current durable state and choose the next justified Actions, a genuine human request, '
|
|
728
|
+
+ 'or evidence-backed completion. Do not stop merely because the previous Action ended.';
|
|
729
|
+
return this.#scheduleTurn(started, { text, recovery: false, options });
|
|
730
|
+
}
|
|
731
|
+
|
|
607
732
|
recover(id, options = {}) {
|
|
608
733
|
if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
|
|
609
734
|
const detail = this.store.getWorkItemDetail(id);
|
|
@@ -746,7 +871,7 @@ export class WorkItemCoordinator {
|
|
|
746
871
|
: latestMessage;
|
|
747
872
|
const requestBody = {
|
|
748
873
|
model: resolved.model,
|
|
749
|
-
system: coordinatorSystemPrompt(language),
|
|
874
|
+
system: coordinatorSystemPrompt(language, started.detail),
|
|
750
875
|
messages: [{ role: 'user', content }],
|
|
751
876
|
maxTokens: Math.min(
|
|
752
877
|
resolveMaxOutputTokens(resolved.model, runtime.config),
|
|
@@ -799,8 +924,11 @@ export class WorkItemCoordinator {
|
|
|
799
924
|
}
|
|
800
925
|
normalized = normalizeCoordinatorResponse(result?.text, started.detail, {
|
|
801
926
|
recovery,
|
|
927
|
+
automatic: started.fence.automatic === true,
|
|
802
928
|
recoveryActionId: started.fence.recovery?.actionId || null,
|
|
929
|
+
availableVpIds: vps.map(vp => vp.id),
|
|
803
930
|
});
|
|
931
|
+
mutation = normalized.mutation || null;
|
|
804
932
|
if (normalized.decision.kind === 'replan') {
|
|
805
933
|
finalizedCriteria(started.detail, normalized.decision.contractPatch);
|
|
806
934
|
mutation = applyCoordinatorReplan({
|
|
@@ -873,7 +1001,9 @@ export class WorkItemCoordinator {
|
|
|
873
1001
|
attemptCount,
|
|
874
1002
|
}, started.fence);
|
|
875
1003
|
if (!detail) return this.store.getWorkItemDetail(started.detail.id);
|
|
876
|
-
options.onUpdate?.(
|
|
1004
|
+
options.onUpdate?.(started.fence.automatic === true
|
|
1005
|
+
? 'coordinator.advance_completed'
|
|
1006
|
+
: recovery ? 'coordinator.recovery_completed' : 'coordinator.turn_completed', detail);
|
|
877
1007
|
return detail;
|
|
878
1008
|
} catch (error) {
|
|
879
1009
|
if (providerTurn?.status === 'responded') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
export const WORK_CENTER_SCHEMA_VERSION =
|
|
3
|
+
export const WORK_CENTER_SCHEMA_VERSION = 37;
|
|
4
4
|
|
|
5
5
|
const MIGRATIONS = [
|
|
6
6
|
['23-conversation-stream', migrateConversationStream],
|
|
@@ -16,6 +16,8 @@ const MIGRATIONS = [
|
|
|
16
16
|
['33-coordinator-provider-turns', migrateCoordinatorProviderTurns],
|
|
17
17
|
['34-engine-turn-status-repair', repairEngineTurnStatusContract],
|
|
18
18
|
['35-coordinator-provider-claims', migrateCoordinatorProviderClaims],
|
|
19
|
+
['36-dynamic-coordination', migrateDynamicCoordination],
|
|
20
|
+
['37-run-acceptance-checks', migrateRunAcceptanceChecks],
|
|
19
21
|
];
|
|
20
22
|
|
|
21
23
|
const MIGRATION_ALIASES = new Map([
|
|
@@ -478,6 +480,54 @@ function migrateCoordinatorProviderClaims(db) {
|
|
|
478
480
|
`);
|
|
479
481
|
}
|
|
480
482
|
|
|
483
|
+
function migrateDynamicCoordination(db) {
|
|
484
|
+
if (!hasColumn(db, 'work_items', 'coordination_mode')) {
|
|
485
|
+
db.exec("ALTER TABLE work_items ADD COLUMN coordination_mode TEXT NOT NULL DEFAULT 'legacy'");
|
|
486
|
+
}
|
|
487
|
+
if (!hasColumn(db, 'work_items', 'final_result')) {
|
|
488
|
+
db.exec('ALTER TABLE work_items ADD COLUMN final_result TEXT');
|
|
489
|
+
}
|
|
490
|
+
if (!hasColumn(db, 'actions', 'source_action_ids')) {
|
|
491
|
+
db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
|
|
492
|
+
}
|
|
493
|
+
db.exec(`
|
|
494
|
+
CREATE INDEX IF NOT EXISTS idx_work_items_dynamic_status
|
|
495
|
+
ON work_items(coordination_mode, status, updated_at);
|
|
496
|
+
CREATE TRIGGER IF NOT EXISTS trg_work_item_final_result_immutable
|
|
497
|
+
BEFORE UPDATE OF final_result ON work_items
|
|
498
|
+
WHEN OLD.final_result IS NOT NULL AND NEW.final_result IS NOT OLD.final_result
|
|
499
|
+
BEGIN
|
|
500
|
+
SELECT RAISE(ABORT, 'WorkItem final result is immutable');
|
|
501
|
+
END;
|
|
502
|
+
`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function migrateRunAcceptanceChecks(db) {
|
|
506
|
+
if (!hasColumn(db, 'runs', 'acceptance_checks')) {
|
|
507
|
+
db.exec("ALTER TABLE runs ADD COLUMN acceptance_checks TEXT NOT NULL DEFAULT '[]'");
|
|
508
|
+
}
|
|
509
|
+
db.exec(`
|
|
510
|
+
DROP TRIGGER IF EXISTS trg_runs_terminal_identity_immutable;
|
|
511
|
+
CREATE TRIGGER IF NOT EXISTS trg_runs_terminal_identity_immutable
|
|
512
|
+
BEFORE UPDATE ON runs
|
|
513
|
+
WHEN OLD.terminal_status IS NOT NULL AND (
|
|
514
|
+
NEW.action_id IS NOT OLD.action_id OR NEW.work_item_id IS NOT OLD.work_item_id OR
|
|
515
|
+
NEW.owner_boot_id IS NOT OLD.owner_boot_id OR NEW.lease_epoch IS NOT OLD.lease_epoch OR
|
|
516
|
+
NEW.ordinal IS NOT OLD.ordinal OR NEW.started_at IS NOT OLD.started_at OR
|
|
517
|
+
NEW.status IS NOT OLD.status OR NEW.ended_at IS NOT OLD.ended_at OR
|
|
518
|
+
NEW.terminal_status IS NOT OLD.terminal_status OR NEW.terminal_at IS NOT OLD.terminal_at OR
|
|
519
|
+
NEW.response IS NOT OLD.response OR NEW.summary IS NOT OLD.summary OR
|
|
520
|
+
NEW.evidence IS NOT OLD.evidence OR NEW.acceptance_checks IS NOT OLD.acceptance_checks OR
|
|
521
|
+
NEW.waiting_reason IS NOT OLD.waiting_reason OR NEW.error IS NOT OLD.error OR
|
|
522
|
+
NEW.failure_kind IS NOT OLD.failure_kind OR NEW.failure_code IS NOT OLD.failure_code OR
|
|
523
|
+
NEW.review_decision IS NOT OLD.review_decision OR NEW.contract_patch IS NOT OLD.contract_patch OR
|
|
524
|
+
NEW.checkpoint IS NOT OLD.checkpoint)
|
|
525
|
+
BEGIN
|
|
526
|
+
SELECT RAISE(ABORT, 'terminal Run result is immutable');
|
|
527
|
+
END;
|
|
528
|
+
`);
|
|
529
|
+
}
|
|
530
|
+
|
|
481
531
|
function migrateReliabilityGuards(db) {
|
|
482
532
|
for (const [column, definition] of [
|
|
483
533
|
['dispatch_capability', "TEXT NOT NULL DEFAULT 'unknown'"],
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
BUILT_IN_ACTION_TYPES,
|
|
4
|
+
MAX_WORK_ITEM_ACTIONS,
|
|
5
|
+
canonicalActionId,
|
|
6
|
+
canonicalActionInstruction,
|
|
7
|
+
normalizeActionBrief,
|
|
8
|
+
normalizeWorkCenterSettings,
|
|
9
|
+
taskSpecificActionBrief,
|
|
10
|
+
} from './workflow.js';
|
|
11
|
+
import {
|
|
12
|
+
DYNAMIC_COORDINATION_MODE,
|
|
13
|
+
DYNAMIC_EXECUTION_SCHEMA_VERSION,
|
|
14
|
+
isDynamicWorkItem,
|
|
15
|
+
} from './execution-mode.js';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
DYNAMIC_COORDINATION_MODE,
|
|
19
|
+
DYNAMIC_EXECUTION_SCHEMA_VERSION,
|
|
20
|
+
isDynamicWorkItem,
|
|
21
|
+
};
|
|
22
|
+
const DYNAMIC_ACTION_LIMIT = 8;
|
|
23
|
+
const DYNAMIC_WORKSPACE_MODES = new Set(['read', 'shared', 'isolated-write', 'integrate']);
|
|
24
|
+
|
|
25
|
+
function uniqueStrings(value) {
|
|
26
|
+
if (!Array.isArray(value)) return [];
|
|
27
|
+
return [...new Set(value.map(item => String(item || '').trim()).filter(Boolean))];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requiredText(value, name, limit = 2_000) {
|
|
31
|
+
const text = typeof value === 'string' ? value.trim().slice(0, limit) : '';
|
|
32
|
+
if (!text) throw new Error(`Work Center dynamic Action ${name} is required`);
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the frozen policy catalog for a Coordinator-driven WorkItem. The legacy
|
|
38
|
+
* workflow_snapshot column remains the on-disk compatibility envelope, but new
|
|
39
|
+
* entries contain no stages, dependencies, or precomputed successors.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveDynamicActionPolicySnapshot(settings, requestedWorkItemType = null) {
|
|
42
|
+
const normalized = normalizeWorkCenterSettings(settings);
|
|
43
|
+
const requested = typeof requestedWorkItemType === 'string'
|
|
44
|
+
&& requestedWorkItemType.trim()
|
|
45
|
+
&& requestedWorkItemType.trim().toLowerCase() !== 'auto'
|
|
46
|
+
? canonicalActionId(requestedWorkItemType, '').slice(0, 64)
|
|
47
|
+
: '';
|
|
48
|
+
return {
|
|
49
|
+
version: 2,
|
|
50
|
+
id: 'coordinator-driven',
|
|
51
|
+
name: 'Coordinator driven',
|
|
52
|
+
planningMode: 'coordinator',
|
|
53
|
+
executionMode: 'dynamic',
|
|
54
|
+
workItemType: requested || null,
|
|
55
|
+
globalInstructions: normalized.globalInstructions,
|
|
56
|
+
modelPolicy: normalized.modelPolicy,
|
|
57
|
+
coordinatorModelPolicy: normalized.coordinatorModelPolicy,
|
|
58
|
+
actionModelPolicies: normalized.actionModelPolicies,
|
|
59
|
+
actionInstructions: normalized.actionInstructions,
|
|
60
|
+
actionTemplates: BUILT_IN_ACTION_TYPES.filter(type => type !== 'triage').map(type => ({ type })),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeSourceActionIds(value, actions) {
|
|
65
|
+
const ids = uniqueStrings(value);
|
|
66
|
+
const available = new Set(actions.map(action => action.id));
|
|
67
|
+
for (const id of ids) {
|
|
68
|
+
if (!available.has(id)) throw new Error(`Work Center dynamic Action references an unknown source Action: ${id}`);
|
|
69
|
+
}
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeSupersededActionIds(value, actions) {
|
|
74
|
+
const ids = uniqueStrings(value);
|
|
75
|
+
const byId = new Map(actions.map(action => [action.id, action]));
|
|
76
|
+
for (const id of ids) {
|
|
77
|
+
const action = byId.get(id);
|
|
78
|
+
if (!action || !['ready', 'waiting', 'failed'].includes(action.status)) {
|
|
79
|
+
throw new Error(`Work Center can supersede only a non-running unfinished Action: ${id}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return ids;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Normalize one just-in-time Coordinator decision into concrete durable Action
|
|
87
|
+
* records. sourceActionIds are audit/context links only and never gate dispatch.
|
|
88
|
+
*/
|
|
89
|
+
export function prepareDynamicActionMutation({
|
|
90
|
+
workItem,
|
|
91
|
+
actions,
|
|
92
|
+
decision,
|
|
93
|
+
availableVpIds = null,
|
|
94
|
+
}) {
|
|
95
|
+
if (!isDynamicWorkItem(workItem)) {
|
|
96
|
+
throw new Error('Work Center dynamic Action creation requires a Coordinator-driven WorkItem');
|
|
97
|
+
}
|
|
98
|
+
const requested = Array.isArray(decision?.actions) ? decision.actions : [];
|
|
99
|
+
if (requested.length < 1 || requested.length > DYNAMIC_ACTION_LIMIT) {
|
|
100
|
+
throw new Error('Work Center Coordinator must create between 1 and 8 currently justified Actions');
|
|
101
|
+
}
|
|
102
|
+
const historicalCount = Array.isArray(actions) ? actions.length : 0;
|
|
103
|
+
if (historicalCount + requested.length > MAX_WORK_ITEM_ACTIONS) {
|
|
104
|
+
throw new Error(`Work Center cannot exceed ${MAX_WORK_ITEM_ACTIONS} Actions`);
|
|
105
|
+
}
|
|
106
|
+
const knownVpIds = Array.isArray(availableVpIds)
|
|
107
|
+
? new Set(availableVpIds.map(value => String(value || '').trim()).filter(Boolean))
|
|
108
|
+
: null;
|
|
109
|
+
const supersedeActionIds = normalizeSupersededActionIds(decision.supersedeActionIds, actions);
|
|
110
|
+
const effectiveWorkItem = {
|
|
111
|
+
...workItem,
|
|
112
|
+
...(decision.contractPatch || {}),
|
|
113
|
+
};
|
|
114
|
+
const createdActions = requested.map((raw, index) => {
|
|
115
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
116
|
+
throw new Error('Work Center Coordinator Action specification must be an object');
|
|
117
|
+
}
|
|
118
|
+
if (Object.hasOwn(raw, 'dependsOnActionIds') || Object.hasOwn(raw, 'dependsOnStageIds')) {
|
|
119
|
+
throw new Error('Coordinator-driven Actions cannot contain dependency fields');
|
|
120
|
+
}
|
|
121
|
+
const type = canonicalActionId(raw.type, 'custom').slice(0, 64);
|
|
122
|
+
if (type === 'triage') throw new Error('Coordinator-driven WorkItems do not create triage Actions');
|
|
123
|
+
const brief = normalizeActionBrief({
|
|
124
|
+
objective: requiredText(raw.objective, 'objective'),
|
|
125
|
+
approach: requiredText(raw.approach, 'approach'),
|
|
126
|
+
expectedOutcome: requiredText(raw.expectedOutcome, 'expectedOutcome'),
|
|
127
|
+
}, type);
|
|
128
|
+
if (!taskSpecificActionBrief(brief, type)) {
|
|
129
|
+
throw new Error(`Work Center dynamic Action ${index + 1} must use a task-specific brief`);
|
|
130
|
+
}
|
|
131
|
+
const candidateVpIds = uniqueStrings(raw.candidateVpIds);
|
|
132
|
+
if (knownVpIds) {
|
|
133
|
+
const unavailable = candidateVpIds.find(vpId => !knownVpIds.has(vpId));
|
|
134
|
+
if (unavailable) throw new Error(`Work Center dynamic Action references unavailable VP "${unavailable}"`);
|
|
135
|
+
}
|
|
136
|
+
const assignmentReason = candidateVpIds.length > 0
|
|
137
|
+
? requiredText(raw.assignmentReason, 'assignmentReason', 1_000)
|
|
138
|
+
: '';
|
|
139
|
+
const id = randomUUID();
|
|
140
|
+
const workspaceMode = DYNAMIC_WORKSPACE_MODES.has(raw.workspaceMode)
|
|
141
|
+
? raw.workspaceMode
|
|
142
|
+
: 'shared';
|
|
143
|
+
const sourceActionIds = normalizeSourceActionIds(raw.sourceActionIds, actions);
|
|
144
|
+
if (workspaceMode === 'integrate' && sourceActionIds.length === 0) {
|
|
145
|
+
throw new Error('Work Center integrate Action requires sourceActionIds');
|
|
146
|
+
}
|
|
147
|
+
if (workspaceMode === 'integrate') {
|
|
148
|
+
const byId = new Map(actions.map(candidate => [candidate.id, candidate]));
|
|
149
|
+
const invalid = sourceActionIds.find(sourceId => (
|
|
150
|
+
byId.get(sourceId)?.workspaceMode !== 'isolated-write'
|
|
151
|
+
|| byId.get(sourceId)?.status !== 'completed'
|
|
152
|
+
));
|
|
153
|
+
if (invalid) {
|
|
154
|
+
throw new Error(`Work Center integrate Action source is not a completed isolated write: ${invalid}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const action = {
|
|
158
|
+
id,
|
|
159
|
+
type,
|
|
160
|
+
// stage_id is retained only as a legacy storage alias. Dynamic dispatch
|
|
161
|
+
// and Coordinator references use the durable Action id above.
|
|
162
|
+
stageId: id,
|
|
163
|
+
assignmentPolicy: candidateVpIds.length > 0 ? {
|
|
164
|
+
mode: 'planned',
|
|
165
|
+
capability: canonicalActionId(raw.capability, type),
|
|
166
|
+
candidateVpIds,
|
|
167
|
+
fixedVpId: null,
|
|
168
|
+
assignmentReason,
|
|
169
|
+
separateFromStageTypes: uniqueStrings(raw.separateFromActionTypes),
|
|
170
|
+
} : {
|
|
171
|
+
mode: 'auto',
|
|
172
|
+
capability: canonicalActionId(raw.capability, type),
|
|
173
|
+
candidateVpIds: [],
|
|
174
|
+
fixedVpId: null,
|
|
175
|
+
assignmentReason: '',
|
|
176
|
+
separateFromStageTypes: uniqueStrings(raw.separateFromActionTypes),
|
|
177
|
+
},
|
|
178
|
+
modelPolicy: workItem.workflowSnapshot?.actionModelPolicies?.[type]
|
|
179
|
+
|| workItem.workflowSnapshot?.actionModelPolicies?.custom
|
|
180
|
+
|| workItem.workflowSnapshot?.modelPolicy
|
|
181
|
+
|| null,
|
|
182
|
+
dependsOnStageIds: [],
|
|
183
|
+
sourceActionIds,
|
|
184
|
+
workspaceMode,
|
|
185
|
+
changesRequestedStageId: null,
|
|
186
|
+
requiredRole: '',
|
|
187
|
+
brief,
|
|
188
|
+
context: [],
|
|
189
|
+
maxAttempts: Math.min(Math.max(Number(raw.maxAttempts) || 2, 1), 5),
|
|
190
|
+
};
|
|
191
|
+
action.instruction = canonicalActionInstruction(effectiveWorkItem, action, []);
|
|
192
|
+
return action;
|
|
193
|
+
});
|
|
194
|
+
const workItemType = canonicalActionId(
|
|
195
|
+
decision.workItemType || workItem.workflowSnapshot?.workItemType,
|
|
196
|
+
'',
|
|
197
|
+
).slice(0, 64);
|
|
198
|
+
if (!workItemType) throw new Error('Work Center Coordinator must choose a specific WorkItem type');
|
|
199
|
+
return {
|
|
200
|
+
createdActions,
|
|
201
|
+
supersedeActionIds,
|
|
202
|
+
contractPatch: decision.contractPatch || null,
|
|
203
|
+
workItemType,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeEvidenceRunIds(value) {
|
|
208
|
+
return uniqueStrings(value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function normalizeDynamicCompletion(value, acceptanceCriteria) {
|
|
212
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
213
|
+
throw new Error('Work Center Coordinator completion is required');
|
|
214
|
+
}
|
|
215
|
+
const criteria = Array.isArray(acceptanceCriteria) ? acceptanceCriteria : [];
|
|
216
|
+
if (criteria.length === 0) throw new Error('Work Center completion requires acceptance criteria');
|
|
217
|
+
if (!Array.isArray(value.acceptanceResults) || value.acceptanceResults.length !== criteria.length) {
|
|
218
|
+
throw new Error('Work Center completion requires one ordered result for every acceptance criterion');
|
|
219
|
+
}
|
|
220
|
+
const acceptanceResults = value.acceptanceResults.map((raw, index) => {
|
|
221
|
+
const criterion = typeof raw?.criterion === 'string' ? raw.criterion.trim() : '';
|
|
222
|
+
const status = raw?.status === 'passed' ? 'passed' : '';
|
|
223
|
+
const evidenceRunIds = normalizeEvidenceRunIds(raw?.evidenceRunIds);
|
|
224
|
+
if (criterion !== criteria[index] || !status || evidenceRunIds.length === 0) {
|
|
225
|
+
throw new Error('Work Center completion requires every criterion to pass with owned Run evidence');
|
|
226
|
+
}
|
|
227
|
+
return { criterion, status, evidenceRunIds };
|
|
228
|
+
});
|
|
229
|
+
const evidenceRunIds = normalizeEvidenceRunIds(value.evidenceRunIds);
|
|
230
|
+
if (evidenceRunIds.length === 0) {
|
|
231
|
+
throw new Error('Work Center completion requires at least one evidence Run');
|
|
232
|
+
}
|
|
233
|
+
const acceptanceEvidence = new Set(acceptanceResults.flatMap(result => result.evidenceRunIds));
|
|
234
|
+
if ([...acceptanceEvidence].some(runId => !evidenceRunIds.includes(runId))) {
|
|
235
|
+
throw new Error('Work Center completion evidence must include every acceptance evidence Run');
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
summary: requiredText(value.summary, 'completion summary', 8_000),
|
|
239
|
+
acceptanceResults,
|
|
240
|
+
evidenceRunIds,
|
|
241
|
+
residualRisks: uniqueStrings(value.residualRisks).slice(0, 24),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const LEGACY_COORDINATION_MODE = 'legacy';
|
|
2
|
+
export const DYNAMIC_COORDINATION_MODE = 'dynamic';
|
|
3
|
+
export const DYNAMIC_EXECUTION_SCHEMA_VERSION = 3;
|
|
4
|
+
|
|
5
|
+
export function isDynamicWorkItem(workItem) {
|
|
6
|
+
return workItem?.coordinationMode === DYNAMIC_COORDINATION_MODE;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function usesMainlineContext(workItem) {
|
|
10
|
+
return Number(workItem?.executionSchemaVersion) >= 2;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function usesLegacyGraph(workItem) {
|
|
14
|
+
return !isDynamicWorkItem(workItem)
|
|
15
|
+
&& workItem?.workflowSnapshot?.executionMode === 'graph';
|
|
16
|
+
}
|