@yeaft/webchat-agent 1.0.134 → 1.0.135

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.134",
3
+ "version": "1.0.135",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,6 +32,8 @@ function normalizeTerminalResult(result, action) {
32
32
  ? result.reviewDecision
33
33
  : null,
34
34
  contractPatch: normalizeContractPatch(result.contractPatch),
35
+ loopCount: Math.max(0, Number(result.loopCount) || 0),
36
+ toolCount: Math.max(0, Number(result.toolCount) || 0),
35
37
  };
36
38
  if (normalized.outcome === 'waiting' && !normalized.waitingReason) {
37
39
  throw new Error('waiting outcome requires waitingReason');
@@ -3,77 +3,74 @@ function currentAction(detail) {
3
3
  return detail.actions.find(action => action.id === detail.currentActionId) || null;
4
4
  }
5
5
 
6
- function projectAction(action) {
6
+ function count(value) {
7
+ return Math.max(0, Number(value) || 0);
8
+ }
9
+
10
+ function actionExecutionStats(action, runs) {
11
+ const matchingRuns = Array.isArray(runs)
12
+ ? runs.filter(run => run?.actionId === action?.id)
13
+ : [];
14
+ if (matchingRuns.length === 0) {
15
+ return {
16
+ loopCount: count(action?.loopCount),
17
+ toolCount: count(action?.toolCount),
18
+ };
19
+ }
20
+ return matchingRuns.reduce((total, run) => ({
21
+ loopCount: total.loopCount + count(run.loopCount),
22
+ toolCount: total.toolCount + count(run.toolCount),
23
+ }), { loopCount: 0, toolCount: 0 });
24
+ }
25
+
26
+ function projectAssignmentPolicy(policy) {
27
+ if (!policy || typeof policy !== 'object') return null;
28
+ return {
29
+ mode: policy.mode || null,
30
+ fixedVpId: policy.fixedVpId || null,
31
+ };
32
+ }
33
+
34
+ function projectAction(action, runs) {
7
35
  if (!action) return null;
36
+ const stats = actionExecutionStats(action, runs);
8
37
  return {
9
38
  id: action.id,
10
- workItemId: action.workItemId,
11
39
  sequence: action.sequence,
12
40
  type: action.type,
13
41
  stageId: action.stageId || action.type,
14
- assignmentPolicy: action.assignmentPolicy || null,
15
- modelPolicy: action.modelPolicy || null,
42
+ assignmentPolicy: projectAssignmentPolicy(action.assignmentPolicy),
16
43
  requiredRole: action.requiredRole || '',
17
44
  status: action.status,
18
- attempt: action.attempt,
19
- maxAttempts: action.maxAttempts,
20
- currentRunId: action.currentRunId || null,
21
- createdAt: action.createdAt,
22
- updatedAt: action.updatedAt,
45
+ loopCount: stats.loopCount,
46
+ toolCount: stats.toolCount,
23
47
  };
24
48
  }
25
49
 
26
- function projectRun(run) {
27
- if (!run) return null;
28
- return {
29
- id: run.id,
30
- actionId: run.actionId,
31
- workItemId: run.workItemId,
32
- status: run.status,
33
- startedAt: run.startedAt,
34
- expiresAt: run.expiresAt,
35
- endedAt: run.endedAt || null,
36
- summary: run.summary || '',
37
- evidence: Array.isArray(run.evidence) ? run.evidence : [],
38
- waitingReason: run.waitingReason || '',
39
- error: run.error || '',
40
- reviewDecision: run.reviewDecision || null,
41
- roleSnapshot: run.roleSnapshot ? {
42
- id: run.roleSnapshot.id,
43
- actionType: run.roleSnapshot.actionType,
44
- selectionReason: run.roleSnapshot.selectionReason,
45
- } : null,
46
- vpSnapshot: run.vpSnapshot ? {
47
- id: run.vpSnapshot.id,
48
- name: run.vpSnapshot.name || run.vpSnapshot.id,
49
- nameZh: run.vpSnapshot.nameZh || '',
50
- role: run.vpSnapshot.role || '',
51
- roleZh: run.vpSnapshot.roleZh || '',
52
- } : null,
53
- modelSnapshot: run.modelSnapshot ? {
54
- id: run.modelSnapshot.id,
55
- provider: run.modelSnapshot.provider || null,
56
- effort: run.modelSnapshot.effort || null,
57
- source: run.modelSnapshot.source || null,
58
- } : null,
59
- };
50
+ function projectActionStats(detail) {
51
+ if (!Array.isArray(detail?.actions)) return [];
52
+ return detail.actions.map(action => {
53
+ const projected = projectAction(action, detail.runs);
54
+ return {
55
+ id: projected.id,
56
+ status: projected.status,
57
+ loopCount: projected.loopCount,
58
+ toolCount: projected.toolCount,
59
+ };
60
+ });
60
61
  }
61
62
 
62
- function projectEvent(event) {
63
- if (!event) return null;
64
- return {
65
- id: event.id,
66
- workItemId: event.workItemId,
67
- actionId: event.actionId || null,
68
- runId: event.runId || null,
69
- type: event.type,
70
- createdAt: event.createdAt,
71
- };
63
+ function waitingReason(detail) {
64
+ if (typeof detail?.waitingReason === 'string') return detail.waitingReason;
65
+ if (detail?.status !== 'waiting' || !Array.isArray(detail.runs)) return '';
66
+ return detail.runs.find(run => (
67
+ run?.actionId === detail.currentActionId && typeof run.waitingReason === 'string'
68
+ ))?.waitingReason || '';
72
69
  }
73
70
 
74
71
  /**
75
- * Authenticated browser detail DTO. Execution-only snapshots, VP persona,
76
- * tool policy, prompts, event data, and local filesystem paths never cross the wire.
72
+ * Authenticated browser detail DTO. Execution records stay Agent-local; the
73
+ * browser receives only Action status and aggregate Loop/tool counts.
77
74
  */
78
75
  export function projectWorkItemDetail(detail) {
79
76
  if (!detail) return null;
@@ -86,21 +83,21 @@ export function projectWorkItemDetail(detail) {
86
83
  workflowTemplate: detail.workflowTemplate,
87
84
  status: detail.status,
88
85
  currentActionId: detail.currentActionId || null,
89
- currentRunId: detail.currentRunId || null,
90
86
  reuseMemory: detail.reuseMemory !== false,
87
+ waitingReason: waitingReason(detail),
91
88
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
92
89
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
93
90
  createdAt: detail.createdAt,
94
91
  updatedAt: detail.updatedAt,
95
- actions: Array.isArray(detail.actions) ? detail.actions.map(projectAction) : [],
96
- runs: Array.isArray(detail.runs) ? detail.runs.map(projectRun) : [],
97
- events: Array.isArray(detail.events) ? detail.events.map(projectEvent) : [],
92
+ actions: Array.isArray(detail.actions)
93
+ ? detail.actions.map(action => projectAction(action, detail.runs))
94
+ : [],
98
95
  };
99
96
  }
100
97
 
101
98
  /**
102
- * Browser event projection. This deliberately excludes local filesystem paths,
103
- * Run evidence/tool output, prompts, model snapshots, and execution errors.
99
+ * Browser list projection. This deliberately excludes local filesystem paths
100
+ * and all Run-level execution detail.
104
101
  */
105
102
  export function projectWorkItemSummary(detail) {
106
103
  if (!detail) return null;
@@ -120,6 +117,7 @@ export function projectWorkItemSummary(detail) {
120
117
  };
121
118
  }
122
119
  const action = currentAction(detail);
120
+ const projectedAction = action ? projectAction(action, detail.runs) : null;
123
121
  return {
124
122
  id: detail.id,
125
123
  revision: detail.revision,
@@ -127,12 +125,12 @@ export function projectWorkItemSummary(detail) {
127
125
  goal: detail.goal,
128
126
  status: detail.status,
129
127
  currentActionId: detail.currentActionId || null,
130
- currentAction: action ? {
131
- id: action.id,
132
- type: action.type,
133
- stageId: action.stageId || action.type,
134
- assignmentMode: action.assignmentPolicy?.mode || (action.requiredRole ? 'fixed' : null),
135
- status: action.status,
128
+ currentAction: projectedAction ? {
129
+ id: projectedAction.id,
130
+ type: projectedAction.type,
131
+ stageId: projectedAction.stageId,
132
+ assignmentMode: projectedAction.assignmentPolicy?.mode || (projectedAction.requiredRole ? 'fixed' : null),
133
+ status: projectedAction.status,
136
134
  } : null,
137
135
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
138
136
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
@@ -144,6 +142,9 @@ export function projectWorkItemSummary(detail) {
144
142
  export function projectWorkCenterEvent(event) {
145
143
  return {
146
144
  type: event?.type || 'work_item.updated',
147
- workItem: projectWorkItemSummary(event?.workItem),
145
+ workItem: {
146
+ ...projectWorkItemSummary(event?.workItem),
147
+ actionStats: projectActionStats(event?.workItem),
148
+ },
148
149
  };
149
150
  }
@@ -298,6 +298,8 @@ export class WorkItemRunner {
298
298
  });
299
299
 
300
300
  let text = '';
301
+ let loopCount = 0;
302
+ let toolCount = 0;
301
303
  try {
302
304
  for await (const event of engine.query({
303
305
  prompt: `${action.instruction}${completionContract(action)}`,
@@ -309,13 +311,22 @@ export class WorkItemRunner {
309
311
  userAlreadyPersisted: true,
310
312
  collabToolPolicy: 'single-vp',
311
313
  })) {
314
+ if (event?.type === 'loop') loopCount += 1;
315
+ else if (event?.type === 'tool_end') toolCount += 1;
312
316
  if (typeof event?.text === 'string') text += event.text;
313
317
  else if (typeof event?.delta === 'string') text += event.delta;
314
318
  else if (typeof event?.content === 'string' && event.type === 'assistant') text += event.content;
315
319
  }
320
+ } catch (error) {
321
+ error.workItemExecutionStats = { loopCount, toolCount };
322
+ throw error;
316
323
  } finally {
317
324
  try { engine.abort?.('work_item_run_finished'); } catch {}
318
325
  }
319
- return parseStructuredResult(text, action.type);
326
+ return {
327
+ ...parseStructuredResult(text, action.type),
328
+ loopCount,
329
+ toolCount,
330
+ };
320
331
  }
321
332
  }
@@ -4,7 +4,7 @@ import { dirname, resolve } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { normalizeEvidence } from './evidence.js';
6
6
 
7
- const SCHEMA_VERSION = 3;
7
+ const SCHEMA_VERSION = 4;
8
8
  const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
9
9
  const MAX_REUSABLE_CONTEXT_ITEMS = 12;
10
10
 
@@ -91,6 +91,8 @@ function mapRun(row) {
91
91
  error: row.error || null,
92
92
  reviewDecision: row.review_decision || null,
93
93
  contractPatch: parseJson(row.contract_patch, null),
94
+ loopCount: Math.max(0, Number(row.loop_count) || 0),
95
+ toolCount: Math.max(0, Number(row.tool_count) || 0),
94
96
  };
95
97
  }
96
98
 
@@ -202,7 +204,9 @@ export class WorkItemStore {
202
204
  waiting_reason TEXT,
203
205
  error TEXT,
204
206
  review_decision TEXT,
205
- contract_patch TEXT
207
+ contract_patch TEXT,
208
+ loop_count INTEGER NOT NULL DEFAULT 0,
209
+ tool_count INTEGER NOT NULL DEFAULT 0
206
210
  );
207
211
  CREATE TABLE IF NOT EXISTS events (
208
212
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -249,6 +253,12 @@ export class WorkItemStore {
249
253
  if (!hasColumn(this.db, 'runs', 'contract_patch')) {
250
254
  this.db.exec('ALTER TABLE runs ADD COLUMN contract_patch TEXT');
251
255
  }
256
+ if (!hasColumn(this.db, 'runs', 'loop_count')) {
257
+ this.db.exec('ALTER TABLE runs ADD COLUMN loop_count INTEGER NOT NULL DEFAULT 0');
258
+ }
259
+ if (!hasColumn(this.db, 'runs', 'tool_count')) {
260
+ this.db.exec('ALTER TABLE runs ADD COLUMN tool_count INTEGER NOT NULL DEFAULT 0');
261
+ }
252
262
  if (!hasColumn(this.db, 'work_items', 'workflow_snapshot')) {
253
263
  this.db.exec('ALTER TABLE work_items ADD COLUMN workflow_snapshot TEXT');
254
264
  }
@@ -780,8 +790,8 @@ export class WorkItemStore {
780
790
  }
781
791
  const now = this.now();
782
792
  this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, summary = ?, evidence = ?,
783
- waiting_reason = ?, error = ?, review_decision = ?, contract_patch = ?
784
- WHERE id = ?`).run(
793
+ waiting_reason = ?, error = ?, review_decision = ?, contract_patch = ?,
794
+ loop_count = ?, tool_count = ? WHERE id = ?`).run(
785
795
  result.outcome,
786
796
  now,
787
797
  result.summary || '',
@@ -790,6 +800,8 @@ export class WorkItemStore {
790
800
  result.error || null,
791
801
  result.reviewDecision || null,
792
802
  stringify(result.contractPatch || null),
803
+ Math.max(0, Number(result.loopCount) || 0),
804
+ Math.max(0, Number(result.toolCount) || 0),
793
805
  runId,
794
806
  );
795
807
  this.onTransitionStep?.('after_run_update');
@@ -105,6 +105,8 @@ export class WorkItemWatcher {
105
105
  summary: '',
106
106
  evidence: [],
107
107
  error: err?.message || String(err),
108
+ loopCount: err?.workItemExecutionStats?.loopCount || 0,
109
+ toolCount: err?.workItemExecutionStats?.toolCount || 0,
108
110
  };
109
111
  }
110
112
  if (signal.aborted) return;