@yeaft/webchat-agent 1.0.366 → 1.0.368

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.
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { isDynamicWorkItem } from './execution-mode.js';
2
3
  import {
3
4
  currentActionInputEventIds,
4
5
  eventMatchesActionGeneration,
@@ -281,6 +282,7 @@ export function buildMainlineProjection(detail) {
281
282
  const actions = (Array.isArray(detail.actions) ? detail.actions : []).slice().sort(stableActionOrder);
282
283
  const runs = Array.isArray(detail.runs) ? detail.runs : [];
283
284
  const activeActions = actions.filter(action => action.status !== 'superseded');
285
+ const dynamic = isDynamicWorkItem(detail);
284
286
  const completedStageIds = new Set(activeActions
285
287
  .filter(action => action.status === 'completed')
286
288
  .map(action => action.stageId));
@@ -292,10 +294,11 @@ export function buildMainlineProjection(detail) {
292
294
  generation: Math.max(1, count(action.generation) || 1),
293
295
  specHash: action.specHash || '',
294
296
  status: action.status,
295
- dependsOnStageIds: [...new Set(action.dependsOnStageIds || [])].sort(),
297
+ dependsOnStageIds: dynamic ? [] : [...new Set(action.dependsOnStageIds || [])].sort(),
298
+ sourceActionIds: dynamic ? [...new Set(action.sourceActionIds || [])].sort() : [],
296
299
  }));
297
300
  const frontier = nodes.filter(node => !CLOSED_ACTION_STATUSES.has(node.status)
298
- && node.dependsOnStageIds.every(stageId => completedStageIds.has(stageId)))
301
+ && (dynamic || node.dependsOnStageIds.every(stageId => completedStageIds.has(stageId))))
299
302
  .map(node => node.id);
300
303
  const canonicalActionResults = {};
301
304
  for (const action of activeActions) {
@@ -321,7 +324,9 @@ export function buildMainlineProjection(detail) {
321
324
  goal: detail.goal || '',
322
325
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
323
326
  },
324
- graph: { planRevision: count(detail.planRevision), nodes, frontier },
327
+ ...(dynamic
328
+ ? { actionJournal: { revision: count(detail.planRevision), entries: nodes, runnableActionIds: frontier } }
329
+ : { graph: { planRevision: count(detail.planRevision), nodes, frontier } }),
325
330
  canonicalActionResults,
326
331
  planConflicts: (Array.isArray(detail.planConflicts) ? detail.planConflicts : [])
327
332
  .slice()
@@ -343,14 +348,17 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
343
348
  throw mainlineContextBlocked(`Mainline fixed prompt content exceeds 64 KiB (${reservedBytes} rendered UTF-8 bytes)`);
344
349
  }
345
350
  const projection = buildMainlineProjection(detail);
346
- const dependencyIds = new Set(action.dependsOnStageIds || []);
347
- const actionByStage = new Map((detail.actions || []).filter(candidate => candidate.status !== 'superseded')
348
- .map(candidate => [candidate.stageId, candidate]));
349
- const dependencies = [...dependencyIds].sort().map(stageId => {
350
- const dependency = actionByStage.get(stageId);
351
- if (!dependency) return { stageId, actionId: null, result: null };
351
+ const dynamic = isDynamicWorkItem(detail);
352
+ const dependencyIds = new Set(dynamic ? action.sourceActionIds || [] : action.dependsOnStageIds || []);
353
+ const actionByReference = new Map((detail.actions || []).filter(candidate => candidate.status !== 'superseded')
354
+ .map(candidate => [dynamic ? candidate.id : candidate.stageId, candidate]));
355
+ const dependencies = [...dependencyIds].sort().map(reference => {
356
+ const dependency = actionByReference.get(reference);
357
+ if (!dependency) return dynamic
358
+ ? { sourceActionId: reference, actionId: null, result: null }
359
+ : { stageId: reference, actionId: null, result: null };
352
360
  return {
353
- stageId,
361
+ ...(dynamic ? { sourceActionId: reference } : { stageId: reference }),
354
362
  actionId: dependency.id,
355
363
  generation: dependency.generation || 1,
356
364
  specHash: dependency.specHash || '',
@@ -377,14 +385,18 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
377
385
  policyInstruction: detail.workflowSnapshot?.actionInstructions?.[action.type]
378
386
  || detail.workflowSnapshot?.actionInstructions?.custom
379
387
  || '',
380
- dependsOnStageIds: [...dependencyIds].sort(),
388
+ ...(dynamic
389
+ ? { sourceActionIds: [...dependencyIds].sort() }
390
+ : { dependsOnStageIds: [...dependencyIds].sort() }),
381
391
  workspaceMode: action.workspaceMode || 'shared',
382
392
  changesRequestedStageId: action.changesRequestedStageId || null,
383
393
  },
384
394
  },
385
- graph: projection.graph,
395
+ ...(dynamic
396
+ ? { actionJournal: projection.actionJournal }
397
+ : { graph: projection.graph }),
386
398
  canonicalCompletedResultsIndex: resultIndex,
387
- directDependencies: dependencies,
399
+ ...(dynamic ? { sourceResults: dependencies } : { directDependencies: dependencies }),
388
400
  userContext: {
389
401
  sessionContext: [],
390
402
  workItemMessages: [],
@@ -605,6 +605,7 @@ function projectAction(action, runs, events, includeBody = true) {
605
605
  ? (action.assignmentPolicy || null)
606
606
  : projectAssignmentPolicy(action.assignmentPolicy),
607
607
  dependsOnStageIds: Array.isArray(action.dependsOnStageIds) ? action.dependsOnStageIds : [],
608
+ sourceActionIds: Array.isArray(action.sourceActionIds) ? action.sourceActionIds : [],
608
609
  workspaceMode: action.workspaceMode || 'shared',
609
610
  requiredRole: action.requiredRole || '',
610
611
  generation: Math.max(1, count(action.generation) || 1),
@@ -797,17 +798,20 @@ function projectCanonicalEvidence(value) {
797
798
  }
798
799
 
799
800
  function projectMainlineBrowser(detail) {
800
- if (!detail?.id || detail.executionSchemaVersion !== 2) return null;
801
+ if (!detail?.id || Number(detail.executionSchemaVersion) < 2) return null;
801
802
  const mainline = buildMainlineProjection(detail);
803
+ const actionSet = mainline.actionJournal || mainline.graph;
804
+ const nodes = actionSet.entries || actionSet.nodes || [];
805
+ const frontier = actionSet.runnableActionIds || actionSet.frontier || [];
802
806
  const actionById = new Map((detail.actions || []).map(action => [action.id, action]));
803
807
  const activeActionIds = Array.isArray(detail.activeActionIds)
804
808
  ? detail.activeActionIds
805
- : mainline.graph.nodes.filter(node => ['ready', 'running'].includes(node.status)).map(node => node.id);
809
+ : nodes.filter(node => ['ready', 'running'].includes(node.status)).map(node => node.id);
806
810
  const attentionActionIds = Array.isArray(detail.attentionActionIds)
807
811
  ? detail.attentionActionIds
808
- : mainline.graph.nodes.filter(node => ['waiting', 'failed'].includes(node.status)).map(node => node.id);
812
+ : nodes.filter(node => ['waiting', 'failed'].includes(node.status)).map(node => node.id);
809
813
  const counts = Object.fromEntries(['completed', 'running', 'ready', 'waiting', 'failed']
810
- .map(status => [status, mainline.graph.nodes.filter(node => node.status === status).length]));
814
+ .map(status => [status, nodes.filter(node => node.status === status).length]));
811
815
  return {
812
816
  contract: {
813
817
  title: truncateUtf8(mainline.contract.title, 8_000),
@@ -816,15 +820,15 @@ function projectMainlineBrowser(detail) {
816
820
  .map(criterion => truncateUtf8(criterion, 4_000)),
817
821
  },
818
822
  progress: {
819
- lifecycle: detail.lifecycle || (counts.completed === mainline.graph.nodes.length ? 'done' : 'active'),
823
+ lifecycle: detail.lifecycle || (counts.completed === nodes.length ? 'done' : 'active'),
820
824
  attentionState: detail.attentionState || (counts.waiting && counts.failed ? 'mixed'
821
825
  : counts.waiting ? 'waiting' : counts.failed ? 'failed' : 'none'),
822
826
  activeActionIds: [...activeActionIds],
823
827
  attentionActionIds: [...attentionActionIds],
824
- frontierActionIds: [...mainline.graph.frontier],
828
+ frontierActionIds: [...frontier],
825
829
  counts,
826
830
  },
827
- actions: mainline.graph.nodes.map(node => {
831
+ actions: nodes.map(node => {
828
832
  const action = actionById.get(node.id) || {};
829
833
  const result = mainline.canonicalActionResults[node.id];
830
834
  return {
@@ -839,7 +843,7 @@ function projectMainlineBrowser(detail) {
839
843
  truncateUtf8(value, MAX_CURRENT_BRIEF_BYTES),
840
844
  ]))
841
845
  : null,
842
- dependencies: [...node.dependsOnStageIds],
846
+ dependencies: [...(node.sourceActionIds?.length ? node.sourceActionIds : node.dependsOnStageIds || [])],
843
847
  canonicalResult: result ? {
844
848
  status: result.status,
845
849
  summary: sanitizeMainlineDiagnostic(result.summary, MAX_ACTION_DIAGNOSTIC_CHARS),
@@ -895,6 +899,22 @@ export function projectWorkItemDetail(detail, options = {}) {
895
899
  planRevision: count(detail.planRevision),
896
900
  ledgerRevision: count(detail.ledgerRevision),
897
901
  coordinatorRevision: count(detail.coordinatorRevision),
902
+ coordinationMode: detail.coordinationMode || 'legacy',
903
+ finalResult: detail.finalResult && typeof detail.finalResult === 'object' ? {
904
+ summary: truncateUtf8(detail.finalResult.summary || '', MAX_ACTION_MESSAGE_CHARS),
905
+ acceptanceResults: Array.isArray(detail.finalResult.acceptanceResults)
906
+ ? detail.finalResult.acceptanceResults.slice(0, 24).map(result => ({
907
+ criterion: truncateUtf8(result?.criterion || '', MAX_ACTION_MESSAGE_CHARS),
908
+ status: result?.status === 'passed' ? 'passed' : null,
909
+ evidenceRunIds: Array.isArray(result?.evidenceRunIds)
910
+ ? result.evidenceRunIds.map(String).slice(0, 24) : [],
911
+ })) : [],
912
+ evidenceRunIds: Array.isArray(detail.finalResult.evidenceRunIds)
913
+ ? detail.finalResult.evidenceRunIds.map(String).slice(0, 64) : [],
914
+ residualRisks: Array.isArray(detail.finalResult.residualRisks)
915
+ ? detail.finalResult.residualRisks
916
+ .map(risk => truncateUtf8(risk, MAX_ACTION_MESSAGE_CHARS)).slice(0, 24) : [],
917
+ } : null,
898
918
  title: detail.title,
899
919
  goal: detail.goal,
900
920
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
@@ -931,7 +951,8 @@ export function projectWorkItemDetail(detail, options = {}) {
931
951
  status: ['thinking', 'completed', 'failed'].includes(message.status) ? message.status : 'completed',
932
952
  error: truncateUtf8(message.error || '', MAX_ACTION_DIAGNOSTIC_CHARS) || null,
933
953
  decision: message.decision && typeof message.decision === 'object' ? {
934
- kind: ['answer', 'guide_actions', 'replan', 'request_human'].includes(message.decision.kind)
954
+ kind: ['answer', 'create_actions', 'guide_actions', 'replan', 'request_human', 'complete']
955
+ .includes(message.decision.kind)
935
956
  ? message.decision.kind : null,
936
957
  reason: truncateUtf8(message.decision.reason || '', MAX_ACTION_DIAGNOSTIC_CHARS),
937
958
  changedContract: message.decision.changedContract === true,
@@ -981,6 +1002,7 @@ export function projectWorkItemSummary(detail) {
981
1002
  planRevision: count(detail.planRevision),
982
1003
  ledgerRevision: count(detail.ledgerRevision),
983
1004
  coordinatorRevision: count(detail.coordinatorRevision),
1005
+ coordinationMode: detail.coordinationMode || 'legacy',
984
1006
  title: detail.title,
985
1007
  goal: detail.goal,
986
1008
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
@@ -1017,6 +1039,7 @@ export function projectWorkItemSummary(detail) {
1017
1039
  planRevision: count(detail.planRevision),
1018
1040
  ledgerRevision: count(detail.ledgerRevision),
1019
1041
  coordinatorRevision: count(detail.coordinatorRevision),
1042
+ coordinationMode: detail.coordinationMode || 'legacy',
1020
1043
  title: detail.title,
1021
1044
  goal: detail.goal,
1022
1045
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
@@ -34,6 +34,7 @@ import { MCPManager } from '../mcp.js';
34
34
  import { buildMcpFlattenedTools } from '../tools/mcp-tools.js';
35
35
  import { recallWorkspaceSessionContext } from './workspace-context.js';
36
36
  import { applyGeneratedPlan, BUILT_IN_ACTION_TYPES } from './workflow.js';
37
+ import { isDynamicWorkItem, usesMainlineContext } from './execution-mode.js';
37
38
  import {
38
39
  applyAdditivePlanProposal,
39
40
  applyReplanMutation,
@@ -946,7 +947,9 @@ export class WorkItemRunner {
946
947
  action: finalizeOwnedIntegration(this.store, action, run, ownerBootId),
947
948
  };
948
949
  }
949
- const dependencies = this.store.listActionDependencies(workItem.id, action.dependsOnStageIds || []);
950
+ const dependencies = isDynamicWorkItem(workItem)
951
+ ? this.store.listActionSources(workItem.id, action.sourceActionIds || [])
952
+ : this.store.listActionDependencies(workItem.id, action.dependsOnStageIds || []);
950
953
  if (dependencies.length > 0 && dependencies.every(dependency => (
951
954
  dependency.workspaceMode === 'shared' && !dependency.workspace?.isolated
952
955
  ))) {
@@ -1021,9 +1024,8 @@ export class WorkItemRunner {
1021
1024
 
1022
1025
  async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader, registerInputWake }) {
1023
1026
  const runtime = await this.runtimeProvider();
1024
- const currentSettings = workItem?.workflowSnapshot?.planningMode === 'ai' && this.policyProvider
1025
- ? await this.policyProvider()
1026
- : null;
1027
+ const currentSettings = ['ai', 'coordinator'].includes(workItem?.workflowSnapshot?.planningMode)
1028
+ && this.policyProvider ? await this.policyProvider() : null;
1027
1029
  const currentModelPolicy = currentSettings?.actionModelPolicies?.[action.type]
1028
1030
  || currentSettings?.actionModelPolicies?.custom
1029
1031
  || currentSettings?.modelPolicy
@@ -1036,15 +1038,16 @@ export class WorkItemRunner {
1036
1038
  ? resolveWorkItemWorkDir({ workspaceKey: action.workspace.path }, runtime.defaultWorkDir)
1037
1039
  : workspaceDir;
1038
1040
  const priorRuns = this.store.listCompletedRuns(workItem.id);
1039
- const v2Execution = Number(workItem.executionSchemaVersion) === 2;
1040
- const dependencyContext = v2Execution
1041
- ? []
1042
- : this.store.listActionDependencies?.(workItem.id, action.dependsOnStageIds || []) || [];
1041
+ const mainlineExecution = usesMainlineContext(workItem);
1042
+ const dependencyContext = isDynamicWorkItem(workItem)
1043
+ ? this.store.listActionSources?.(workItem.id, action.sourceActionIds || []) || []
1044
+ : mainlineExecution ? []
1045
+ : this.store.listActionDependencies?.(workItem.id, action.dependsOnStageIds || []) || [];
1043
1046
  const dependencyBlock = dependencyContext.length === 0 ? '' : `\n\nCompleted dependency results:\n${dependencyContext.map(dependency => {
1044
1047
  const evidence = dependency.evidence?.length
1045
1048
  ? `\nEvidence: ${dependency.evidence.map(item => item.label).join('; ')}`
1046
1049
  : '';
1047
- return `### ${dependency.stageId} (${dependency.vpId || 'unknown VP'})\n${dependency.summary || '(no summary)'}${evidence}`;
1050
+ return `### ${isDynamicWorkItem(workItem) ? dependency.id : dependency.stageId} (${dependency.vpId || 'unknown VP'})\n${dependency.summary || '(no summary)'}${evidence}`;
1048
1051
  }).join('\n\n')}`;
1049
1052
  const resumeBlock = renderActionResumeBlock(this.store.getActionResumeContext?.(action.id, run.id));
1050
1053
  const assignment = executionAction.assignmentPolicy
@@ -1084,7 +1087,7 @@ export class WorkItemRunner {
1084
1087
  const attachmentFileById = new Map(attachmentContext.files.map(file => [file.id, file]));
1085
1088
  const fixedPromptSuffix = `${resumeBlock}${attachmentContext.promptBlock}${completionContract(executionAction, workItem)}`;
1086
1089
  const reservedPromptBytes = Buffer.byteLength(fixedPromptSuffix, 'utf8');
1087
- const mainline = v2Execution
1090
+ const mainline = mainlineExecution
1088
1091
  ? buildMainlineContextSnapshot(
1089
1092
  this.store.getWorkItemDetail(workItem.id),
1090
1093
  executionAction,
@@ -1212,7 +1215,9 @@ export class WorkItemRunner {
1212
1215
  executionManifest: mainline ? {
1213
1216
  schemaVersion: 2,
1214
1217
  ledgerRevision: mainline.contextSnapshot.ledgerRevision,
1215
- planRevision: mainline.contextSnapshot.graph.planRevision,
1218
+ planRevision: isDynamicWorkItem(workItem)
1219
+ ? mainline.contextSnapshot.actionJournal.revision
1220
+ : mainline.contextSnapshot.graph.planRevision,
1216
1221
  contractRevision: mainline.contextSnapshot.contract.revision,
1217
1222
  actionGeneration: mainline.contextSnapshot.action.generation,
1218
1223
  actionSpecHash: mainline.contextSnapshot.action.specHash,
@@ -1336,11 +1341,11 @@ export class WorkItemRunner {
1336
1341
  }
1337
1342
  };
1338
1343
  try {
1339
- const prompt = v2Execution
1344
+ const prompt = mainlineExecution
1340
1345
  ? `${renderMainlineContextSnapshot(mainline.contextSnapshot)}${fixedPromptSuffix}`
1341
1346
  : `${executionAction.instruction}${dependencyBlock}${resumeBlock}${attachmentContext.promptBlock}${workspaceSessionBlock}${memoryBlock}${completionContract(executionAction, workItem)}`;
1342
1347
  const promptBytes = Buffer.byteLength(prompt, 'utf8');
1343
- if (v2Execution && promptBytes > MAINLINE_CONTEXT_HARD_LIMIT_BYTES) {
1348
+ if (mainlineExecution && promptBytes > MAINLINE_CONTEXT_HARD_LIMIT_BYTES) {
1344
1349
  throw new Error(`Work Center Mainline prompt exceeds 64 KiB (${promptBytes} rendered UTF-8 bytes)`);
1345
1350
  }
1346
1351
  const promptParts = attachmentContext.promptParts.length > 0