@yeaft/webchat-agent 1.0.531 → 1.0.533

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,135 @@
1
+ import { normalizeEvidence, normalizeOutputs } from './evidence.js';
2
+ import { runMatchesActionIdentity } from './action-identity.js';
3
+
4
+ const CHECK_STATUSES = new Set(['passed', 'failed', 'deferred', 'not_applicable']);
5
+ const INACTIVE = new Set(['closed', 'superseded', 'cancelled']);
6
+ const NEGATIVE = new Set(['failed', 'error', 'pending']);
7
+ const revision = value => Math.max(1, Number(value) || 1);
8
+
9
+ export function validGoalChecks(run, criteria) {
10
+ return Array.isArray(run?.acceptanceChecks) && run.acceptanceChecks.length === criteria.length
11
+ && run.acceptanceChecks.every((check, index) => (
12
+ check?.criterion === criteria[index] && CHECK_STATUSES.has(check.status)
13
+ && typeof check.evidence === 'string' && check.evidence.trim()
14
+ ));
15
+ }
16
+
17
+ export function hasContradictoryEvidence(run) {
18
+ return !!run?.error || run?.reviewDecision === 'changes_requested'
19
+ || [...normalizeEvidence(run?.evidence), ...normalizeOutputs(run?.outputs)]
20
+ .some(item => NEGATIVE.has(item.status));
21
+ }
22
+
23
+ /**
24
+ * Read only durable identities, never a model's progress estimate. Older records
25
+ * use Action.contractRevision when the optional execution manifest is absent.
26
+ * A completed Run needs its canonical pointer; failed/waiting Actions use their
27
+ * latest terminal attempt only to report blockers/contradictions, never proof.
28
+ */
29
+ export function currentGoalRuns(detail) {
30
+ const runs = Array.isArray(detail?.runs) ? detail.runs : [];
31
+ const result = [];
32
+ for (const action of detail?.actions || []) {
33
+ if (INACTIVE.has(action.status) || action.status === 'running'
34
+ || revision(action.contractRevision) !== revision(detail.revision)) continue;
35
+ const owned = runs.filter(run => run.actionId === action.id
36
+ && (!run.workItemId || run.workItemId === detail.id)
37
+ && (!action.workItemId || action.workItemId === detail.id)
38
+ && runMatchesActionIdentity(run, action)
39
+ && (run.executionManifest?.actionGeneration == null || revision(run.executionManifest.actionGeneration) === revision(action.generation))
40
+ && (!run.executionManifest?.actionSpecHash || run.executionManifest.actionSpecHash === action.specHash)
41
+ && revision(run.executionManifest?.contractRevision ?? action.contractRevision) === revision(detail.revision)
42
+ && revision(run.contextSnapshot?.contract?.revision ?? action.contractRevision) === revision(detail.revision)
43
+ && ['completed', 'failed', 'waiting'].includes(run.status));
44
+ const run = action.status === 'completed'
45
+ ? owned.find(candidate => candidate.id === action.resultRunId && candidate.status === 'completed')
46
+ : ['failed', 'waiting'].includes(action.status)
47
+ ? owned.sort((a, b) => Number(b.endedAt || b.startedAt) - Number(a.endedAt || a.startedAt))[0]
48
+ : null;
49
+ if (run) result.push(run);
50
+ }
51
+ return result;
52
+ }
53
+
54
+ /** Derived, restart-safe projection; no new database state or migrations. */
55
+ export function deriveGoalProgress(detail) {
56
+ const contract = Array.isArray(detail?.acceptanceCriteria) ? detail.acceptanceCriteria : [];
57
+ const runs = currentGoalRuns(detail);
58
+ const proof = runs.filter(run => run.status === 'completed' && normalizeEvidence(run.evidence).length > 0
59
+ && !hasContradictoryEvidence(run) && validGoalChecks(run, contract));
60
+ const byId = new Map((detail?.actions || []).map(action => [action.id, action]));
61
+ // Retrying, guiding, or closing an Action is not a correction. Keep historical
62
+ // negative observations until a newer current canonical Run disproves them.
63
+ // These attempts can invalidate proof but can never establish it.
64
+ const observations = (detail?.runs || []).filter(run => {
65
+ const action = byId.get(run.actionId);
66
+ return action && (!run.workItemId || run.workItemId === detail.id)
67
+ && (!action.workItemId || action.workItemId === detail.id)
68
+ && ['completed', 'failed', 'waiting', 'retryable'].includes(run.status)
69
+ && revision(run.executionManifest?.contractRevision ?? action.contractRevision) === revision(detail.revision)
70
+ && revision(run.contextSnapshot?.contract?.revision ?? action.contractRevision) === revision(detail.revision);
71
+ });
72
+ const observedAt = run => Number(run.endedAt || run.startedAt) || 0;
73
+ // Subsequent writes can invalidate earlier tests. A later read-only observation
74
+ // does not invalidate unrelated proof; after a write, re-check the affected
75
+ // contract rather than completing from a pre-change observation.
76
+ // Retiring a writer is not rollback. Include historical attempts, including
77
+ // failed/closed/superseded generations, in the freshness watermark.
78
+ const writes = (detail?.runs || []).filter(run => {
79
+ const action = byId.get(run.actionId);
80
+ return action && (!run.workItemId || run.workItemId === detail.id)
81
+ && action.workspaceMode && action.workspaceMode !== 'read'
82
+ && revision(run.executionManifest?.contractRevision ?? action.contractRevision) === revision(detail.revision);
83
+ });
84
+ // Ending after another writer is insufficient: tests may have run before its
85
+ // mutation. Without a durable ordering at equal timestamps, require a new
86
+ // observation. The writer's own end-to-end proof remains usable.
87
+ const freshProof = proof.filter(run => writes.every(writer => writer.id === run.id
88
+ || (Number(run.startedAt) || observedAt(run)) > observedAt(writer)));
89
+ const contradictions = contract.map((criterion, index) => observations.filter(run => (
90
+ run.acceptanceChecks?.[index]?.criterion === criterion
91
+ && (run.acceptanceChecks[index].status === 'failed'
92
+ || (run.acceptanceChecks[index].status !== 'not_applicable' && hasContradictoryEvidence(run)))
93
+ )));
94
+ const latestContradictionAt = contradictions.map(runs => Math.max(-Infinity, ...runs.map(observedAt)));
95
+ const criteria = contract.map((criterion, index) => {
96
+ // A correction establishes new proof; it never revives an older disproved
97
+ // Run. Overlapping/tied observations cannot establish corrective ordering.
98
+ const passing = freshProof.filter(run => run.acceptanceChecks[index].status === 'passed'
99
+ && (Number(run.startedAt) || observedAt(run)) > latestContradictionAt[index]);
100
+ const conflicts = passing.length ? [] : contradictions[index];
101
+ const evidenceRunIds = passing.map(run => run.id);
102
+ return {
103
+ criterion,
104
+ status: conflicts.length ? 'failed' : evidenceRunIds.length ? 'passed' : 'unmet',
105
+ evidenceRunIds: conflicts.length ? [] : evidenceRunIds,
106
+ ...(conflicts.length ? { conflictingRunIds: conflicts.map(run => run.id) } : {}),
107
+ };
108
+ });
109
+ const target = detail?.deliveryTarget || null;
110
+ const outputKind = { workspace_files: 'file', pull_request: 'pr', merge: 'commit' }[target];
111
+ const deliveryRunIds = freshProof.filter(run => target === 'response'
112
+ ? typeof run.summary === 'string' && run.summary.trim()
113
+ // Summaries are indivisible: do not deliver one whose applicable claims
114
+ // predate a contradiction, even after a different Run corrects it.
115
+ && run.acceptanceChecks.every((check, index) => check.status !== 'failed'
116
+ && (check.status === 'not_applicable'
117
+ || (Number(run.startedAt) || observedAt(run)) > latestContradictionAt[index]))
118
+ : outputKind && normalizeOutputs(run.outputs).some(output => output.kind === outputKind && !NEGATIVE.has(output.status)))
119
+ .map(run => run.id);
120
+ const blockers = (detail?.actions || []).filter(action => ['failed', 'waiting'].includes(action.status))
121
+ .map(action => {
122
+ const run = runs.find(candidate => candidate.actionId === action.id);
123
+ return { actionId: action.id, status: action.status, reason: run?.waitingReason || run?.error || action.brief?.objective || '' };
124
+ });
125
+ return {
126
+ contractRevision: revision(detail?.revision),
127
+ criteria,
128
+ remainingCriteria: criteria.filter(item => item.status !== 'passed').map(item => item.criterion),
129
+ completedCriteriaCount: criteria.filter(item => item.status === 'passed').length,
130
+ totalCriteriaCount: criteria.length,
131
+ evidenceRunIds: [...new Set(criteria.flatMap(item => item.evidenceRunIds))],
132
+ blockers,
133
+ delivery: { target, status: deliveryRunIds.length ? 'passed' : 'unmet', evidenceRunIds: deliveryRunIds },
134
+ };
135
+ }
@@ -162,6 +162,15 @@ function sumExecutionStats(values) {
162
162
  }, emptyExecutionStats());
163
163
  }
164
164
 
165
+ function combinedExecutionStats(detail) {
166
+ const stats = Array.isArray(detail.runs) ? sumExecutionStats(detail.runs) : executionStats(detail.executionStats);
167
+ if (detail.executionControl?.usage) {
168
+ const usage = executionStats(detail.executionControl.usage);
169
+ for (const key of Object.keys(usage)) if (!['loopCount', 'toolCount'].includes(key)) stats[key] = usage[key];
170
+ }
171
+ return stats;
172
+ }
173
+
165
174
  function actionGeneration(value) {
166
175
  return Math.max(1, count(value) || 1);
167
176
  }
@@ -765,6 +774,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
765
774
  status: truncateUtf8(workItem.status, 256),
766
775
  currentActionId: truncateUtf8(workItem.currentActionId, 4 * 1024) || null,
767
776
  executionStats: workItem.executionStats,
777
+ executionControl: workItem.executionControl,
768
778
  actionCount: count(workItem.actionCount),
769
779
  actions: Array.isArray(workItem.actions) ? [] : undefined,
770
780
  actionStats: Array.isArray(workItem.actionStats) ? [] : undefined,
@@ -896,6 +906,39 @@ function workItemFailureReason(detail) {
896
906
  * Authenticated browser detail DTO. Raw execution records stay Agent-local;
897
907
  * the browser receives only aggregate execution stats plus the explicit user-facing response.
898
908
  */
909
+ function projectGoalProgress(progress) {
910
+ if (!progress || !Array.isArray(progress.criteria)) return null;
911
+ const ids = value => Array.isArray(value) ? value.slice(0, 64).map(id => truncateUtf8(String(id), 256)) : [];
912
+ const prioritized = [...progress.criteria.filter(item => item.status !== 'passed'),
913
+ ...progress.criteria.filter(item => item.status === 'passed')];
914
+ const criteria = prioritized.slice(0, 100).map(item => ({
915
+ criterion: truncateUtf8(item.criterion || '', 1024),
916
+ status: ['passed', 'failed'].includes(item.status) ? item.status : 'unmet',
917
+ evidenceRunIds: ids(item.evidenceRunIds),
918
+ ...(item.conflictingRunIds ? { conflictingRunIds: ids(item.conflictingRunIds) } : {}),
919
+ }));
920
+ return {
921
+ contractRevision: count(progress.contractRevision), criteria,
922
+ completedCriteriaCount: count(progress.completedCriteriaCount),
923
+ totalCriteriaCount: count(progress.totalCriteriaCount),
924
+ remainingCriteriaCount: Math.max(0, count(progress.totalCriteriaCount) - count(progress.completedCriteriaCount)),
925
+ omittedCriteriaCount: Math.max(0, progress.criteria.length - criteria.length),
926
+ remainingCriteria: criteria.filter(item => item.status !== 'passed').map(item => item.criterion),
927
+ evidenceRunIds: ids(progress.evidenceRunIds),
928
+ blockers: (progress.blockers || []).slice(0, 64).map(blocker => ({
929
+ actionId: truncateUtf8(blocker.actionId || '', 256),
930
+ status: blocker.status === 'failed' ? 'failed' : 'waiting',
931
+ reason: sanitizeDiagnosticText(blocker.reason || '', MAX_ACTION_DIAGNOSTIC_CHARS),
932
+ })),
933
+ delivery: {
934
+ target: ['response', 'workspace_files', 'pull_request', 'merge'].includes(progress.delivery?.target)
935
+ ? progress.delivery.target : null,
936
+ status: progress.delivery?.status === 'passed' ? 'passed' : 'unmet',
937
+ evidenceRunIds: ids(progress.delivery?.evidenceRunIds),
938
+ },
939
+ };
940
+ }
941
+
899
942
  export function projectWorkItemDetail(detail, options = {}) {
900
943
  if (!detail) return null;
901
944
  const liveActionId = bodyActionId(detail);
@@ -949,6 +992,12 @@ export function projectWorkItemDetail(detail, options = {}) {
949
992
  runId: truncateUtf8(rawOutput?.runId || '', 256) || null,
950
993
  };
951
994
  }).filter(output => output?.kind && output.label && output.ref) : [],
995
+ responses: Array.isArray(detail.finalResult.responses)
996
+ ? detail.finalResult.responses.slice(0, 24).map(response => ({
997
+ runId: truncateUtf8(response?.runId || '', 256),
998
+ summary: truncateUtf8(response?.summary || '', 8 * 1024),
999
+ evidence: projectCanonicalEvidence(response?.evidence),
1000
+ })) : [],
952
1001
  residualRisks: Array.isArray(detail.finalResult.residualRisks)
953
1002
  ? detail.finalResult.residualRisks
954
1003
  .map(risk => truncateUtf8(risk, MAX_ACTION_MESSAGE_CHARS)).slice(0, 24) : [],
@@ -956,6 +1005,7 @@ export function projectWorkItemDetail(detail, options = {}) {
956
1005
  title: detail.title,
957
1006
  goal: detail.goal,
958
1007
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
1008
+ goalProgress: projectGoalProgress(detail.goalProgress),
959
1009
  workflowTemplate: detail.workflowTemplate,
960
1010
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
961
1011
  planningMode: detail.workflowSnapshot?.planningMode || detail.planningMode || 'static',
@@ -967,11 +1017,10 @@ export function projectWorkItemDetail(detail, options = {}) {
967
1017
  attentionActionIds: Array.isArray(detail.attentionActionIds) ? detail.attentionActionIds : undefined,
968
1018
  mainline,
969
1019
  currentActionId: detail.currentActionId || null,
970
- executionStats: Array.isArray(detail.runs)
971
- ? sumExecutionStats(detail.runs)
972
- : executionStats(detail.executionStats),
1020
+ executionControl: detail.executionControl,
1021
+ executionStats: combinedExecutionStats(detail),
973
1022
  reuseMemory: detail.reuseMemory !== false,
974
- deliveryTarget: ['workspace_files', 'pull_request', 'merge'].includes(detail.deliveryTarget)
1023
+ deliveryTarget: ['response', 'workspace_files', 'pull_request', 'merge'].includes(detail.deliveryTarget)
975
1024
  ? detail.deliveryTarget : null,
976
1025
  waitingReason: sanitizeDiagnosticText(waitingReason(detail), MAX_ACTION_DIAGNOSTIC_CHARS),
977
1026
  failureReason: workItemFailureReason(detail),
@@ -1059,7 +1108,8 @@ export function projectWorkItemSummary(detail) {
1059
1108
  ? detail.actionStats.map(action => ({ ...action })) : [],
1060
1109
  actionCount: count(detail.actionCount),
1061
1110
  completedActionCount: count(detail.completedActionCount),
1062
- executionStats: executionStats(detail.executionStats),
1111
+ executionStats: combinedExecutionStats(detail),
1112
+ executionControl: detail.executionControl,
1063
1113
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
1064
1114
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
1065
1115
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
@@ -1094,9 +1144,8 @@ export function projectWorkItemSummary(detail) {
1094
1144
  currentActionId: detail.currentActionId || null,
1095
1145
  actionCount: detail.actions.filter(item => !['superseded', 'cancelled'].includes(item?.status)).length,
1096
1146
  completedActionCount: detail.actions.filter(item => item?.status === 'completed').length,
1097
- executionStats: Array.isArray(detail.runs)
1098
- ? sumExecutionStats(detail.runs)
1099
- : executionStats(detail.executionStats),
1147
+ executionControl: detail.executionControl,
1148
+ executionStats: combinedExecutionStats(detail),
1100
1149
  failureReason: workItemFailureReason(detail),
1101
1150
 
1102
1151
  currentAction: projectCurrentActionSummary(action, projectedAction),