@yeaft/webchat-agent 1.0.173 → 1.0.174

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.
@@ -1,5 +1,31 @@
1
+ import { reconstructDebugRawRequest } from '../debug-trace.js';
2
+ import {
3
+ enforceActionRequestDetailBudget,
4
+ limitActionRequestDebugInput,
5
+ sanitizeDebugValue,
6
+ sanitizeDiagnosticText,
7
+ } from './debug-projection.js';
1
8
  import { normalizeActionBrief } from './workflow.js';
2
9
 
10
+ const MAX_ACTION_MESSAGE_CHARS = 16_000;
11
+ const MAX_ACTION_DIAGNOSTIC_CHARS = 8_000;
12
+ const MAX_ACTION_MESSAGES = 20;
13
+ const MAX_ACTION_REQUEST_TOOL_CALLS = 128;
14
+ const MAX_HISTORICAL_BRIEF_CHARS = 256;
15
+ export const MAX_WORK_ITEM_BROWSER_DTO_BYTES = 512 * 1024;
16
+
17
+ function jsonByteLength(value) {
18
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
19
+ }
20
+
21
+ function truncateUtf8(value, maxBytes) {
22
+ const bytes = Buffer.from(String(value || ''), 'utf8');
23
+ if (bytes.length <= maxBytes) return bytes.toString('utf8');
24
+ let end = Math.min(maxBytes, bytes.length);
25
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
26
+ return bytes.subarray(0, end).toString('utf8');
27
+ }
28
+
3
29
  function currentAction(detail) {
4
30
  if (!detail?.currentActionId || !Array.isArray(detail.actions)) return null;
5
31
  return detail.actions.find(action => action.id === detail.currentActionId) || null;
@@ -36,6 +62,70 @@ function sumExecutionStats(values) {
36
62
  }, emptyExecutionStats());
37
63
  }
38
64
 
65
+ function normalizeProjectedMessage(message) {
66
+ if (!message || typeof message !== 'object') return null;
67
+ const text = typeof message.text === 'string'
68
+ ? message.text.trim().slice(0, MAX_ACTION_MESSAGE_CHARS)
69
+ : '';
70
+ const attachments = projectAttachments(message.attachments);
71
+ if (!text && attachments.length === 0) return null;
72
+ return {
73
+ id: String(message.id || ''),
74
+ role: message.role === 'user' ? 'user' : 'assistant',
75
+ kind: message.kind === 'input' ? 'input' : 'response',
76
+ status: message.status || null,
77
+ text,
78
+ attachments,
79
+ createdAt: count(message.createdAt),
80
+ updatedAt: count(message.updatedAt || message.createdAt),
81
+ ...(message.progressRevision == null ? {} : { progressRevision: count(message.progressRevision) }),
82
+ };
83
+ }
84
+
85
+ function actionInputMessages(action, events) {
86
+ return (Array.isArray(events) ? events : [])
87
+ .filter(event => event?.actionId === action?.id
88
+ && ['action.guidance_added', 'action.input_added'].includes(event.type))
89
+ .map(event => normalizeProjectedMessage({
90
+ id: `event:${event.id}`,
91
+ role: 'user',
92
+ kind: 'input',
93
+ status: 'sent',
94
+ text: event.data?.text || event.data?.guidance || '',
95
+ attachments: event.data?.attachments,
96
+ createdAt: event.createdAt,
97
+ }))
98
+ .filter(Boolean);
99
+ }
100
+
101
+ function runResponseMessage(run) {
102
+ return normalizeProjectedMessage({
103
+ id: `run:${run.id}`,
104
+ role: 'assistant',
105
+ kind: 'response',
106
+ status: run.status || 'running',
107
+ text: typeof run.response === 'string' ? run.response : '',
108
+ createdAt: count(run.startedAt),
109
+ updatedAt: count(run.endedAt || run.startedAt),
110
+ progressRevision: count(run.progressRevision),
111
+ });
112
+ }
113
+
114
+ function actionMessages(action, runs, events) {
115
+ const matchingRuns = Array.isArray(runs)
116
+ ? runs.filter(run => run?.actionId === action?.id)
117
+ : [];
118
+ return [...actionInputMessages(action, events), ...matchingRuns
119
+ .sort((left, right) => count(left.startedAt) - count(right.startedAt))
120
+ .map(run => runResponseMessage(run))
121
+ .filter(Boolean)]
122
+ .sort((left, right) => left.createdAt - right.createdAt
123
+ || (left.role === 'user' ? -1 : 1)
124
+ || left.id.localeCompare(right.id));
125
+ }
126
+
127
+
128
+
39
129
  const MAX_FAILURE_REASON_LENGTH = 2_000;
40
130
  const MAX_FAILURE_INSPECTION_LENGTH = 16_000;
41
131
  const SAFE_FAILURE_FALLBACK = 'The Action failed. Sensitive details were omitted.';
@@ -56,35 +146,71 @@ function sanitizedUrl(raw) {
56
146
  }
57
147
  }
58
148
 
149
+ function unsafeFailureText(raw) {
150
+ return PROVIDER_TOKEN_PATTERN.test(raw) || HIGH_ENTROPY_SECRET_PATTERN.test(raw)
151
+ || LOCAL_PATH_ASSIGNMENT_PATTERN.test(raw) || POSIX_ABSOLUTE_PATH_PATTERN.test(raw)
152
+ || WINDOWS_ABSOLUTE_PATH_PATTERN.test(raw);
153
+ }
154
+
155
+ function sanitizeFailureDiagnostic(value) {
156
+ const raw = typeof value === 'string'
157
+ ? value.trim().slice(0, MAX_FAILURE_INSPECTION_LENGTH)
158
+ : '';
159
+ if (!raw) return '';
160
+ if (unsafeFailureText(raw)) return SAFE_FAILURE_FALLBACK;
161
+ return sanitizeDiagnosticText(raw, MAX_ACTION_DIAGNOSTIC_CHARS);
162
+ }
163
+
59
164
  function sanitizeFailureReason(value) {
60
165
  const raw = typeof value === 'string'
61
166
  ? value.trim().slice(0, MAX_FAILURE_INSPECTION_LENGTH)
62
167
  : '';
63
168
  if (!raw) return '';
169
+ if (unsafeFailureText(raw)) return SAFE_FAILURE_FALLBACK;
64
170
  const text = raw.replace(/https?:\/\/[^\s<>'"`]+/gi, sanitizedUrl);
65
- if (CREDENTIAL_ASSIGNMENT_PATTERN.test(text) || PROVIDER_TOKEN_PATTERN.test(text)
66
- || HIGH_ENTROPY_SECRET_PATTERN.test(text)) return SAFE_FAILURE_FALLBACK;
67
- if (LOCAL_PATH_ASSIGNMENT_PATTERN.test(text) || POSIX_ABSOLUTE_PATH_PATTERN.test(text)
68
- || WINDOWS_ABSOLUTE_PATH_PATTERN.test(text)) {
69
- return SAFE_FAILURE_FALLBACK;
70
- }
171
+ if (CREDENTIAL_ASSIGNMENT_PATTERN.test(text)) return SAFE_FAILURE_FALLBACK;
71
172
  return text
72
173
  .replace(/\b(?:Bearer|Basic)\s+[^\s,;]+/gi, '[redacted credential]')
73
174
  .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
74
175
  .slice(0, MAX_FAILURE_REASON_LENGTH);
75
176
  }
76
177
 
77
- function actionExecution(action, runs) {
178
+
179
+
180
+ function actionExecution(action, runs, events, includeBody = true) {
78
181
  const matchingRuns = Array.isArray(runs)
79
182
  ? runs.filter(run => run?.actionId === action?.id)
80
183
  : [];
81
184
  if (matchingRuns.length === 0) {
185
+ const inputMessageCount = Array.isArray(events) ? events.filter(event => (
186
+ event?.actionId === action?.id
187
+ && ['action.guidance_added', 'action.input_added'].includes(event.type)
188
+ )).length : 0;
189
+ const messages = includeBody
190
+ ? (Array.isArray(action?.messages)
191
+ ? action.messages.slice(-MAX_ACTION_MESSAGES).map(normalizeProjectedMessage).filter(Boolean)
192
+ : actionInputMessages(action, events))
193
+ : [];
194
+ const messageCount = count(action?.messageCount) || (includeBody ? messages.length : inputMessageCount);
82
195
  return {
83
- ...executionStats(action),
84
- response: '',
85
- failureReason: '',
86
- progressRevision: 0,
87
- messages: [],
196
+ ...executionStats(action?.executionStats || action),
197
+ response: includeBody && typeof action?.response === 'string' ? action.response : '',
198
+ failure: includeBody && action?.failure && typeof action.failure === 'object'
199
+ ? {
200
+ error: sanitizeFailureDiagnostic(action.failure.error),
201
+ summary: sanitizeFailureDiagnostic(action.failure.summary),
202
+ failedAt: count(action.failure.failedAt),
203
+ }
204
+ : null,
205
+ progressRevision: count(action?.progressRevision),
206
+ messages,
207
+ liveMessage: includeBody ? normalizeProjectedMessage(action?.liveMessage) : null,
208
+ messageCount,
209
+ messageCursor: action?.messageCursor == null
210
+ ? (!includeBody && messageCount > 0 ? String(messageCount) : null)
211
+ : String(action.messageCursor),
212
+ failureReason: sanitizeFailureReason(action?.failureReason),
213
+
88
214
  };
89
215
  }
90
216
  const stats = sumExecutionStats(matchingRuns);
@@ -92,31 +218,38 @@ function actionExecution(action, runs) {
92
218
  count(right.progressRevision) - count(left.progressRevision)
93
219
  || count(right.startedAt) - count(left.startedAt)
94
220
  ))[0];
95
- const runsByEnd = [...matchingRuns]
96
- .sort((left, right) => count(right.endedAt || right.startedAt) - count(left.endedAt || left.startedAt));
97
- const latestRun = runsByEnd[0];
98
- const showCurrentFailure = action?.status === 'failed'
99
- || ['failed', 'retryable', 'interrupted'].includes(latestRun?.status);
100
- const latestFailure = showCurrentFailure
101
- ? runsByEnd.find(run => sanitizeFailureReason(run?.error))
221
+ const latestFailure = action?.status === 'failed'
222
+ ? [...matchingRuns]
223
+ .filter(run => run?.status === 'failed')
224
+ .sort((left, right) => count(right.endedAt || right.startedAt) - count(left.endedAt || left.startedAt))[0]
102
225
  : null;
103
- const messages = [...matchingRuns]
104
- .sort((left, right) => count(left.startedAt) - count(right.startedAt))
105
- .map((run, index) => ({
106
- id: `${action.id}:${index + 1}`,
107
- status: run.status || 'running',
108
- text: typeof run.response === 'string' ? run.response.trim().slice(0, 16_000) : '',
109
- failureReason: sanitizeFailureReason(run.error),
110
- createdAt: count(run.startedAt),
111
- updatedAt: count(run.endedAt || run.startedAt),
112
- }))
113
- .filter(message => message.text || message.failureReason);
226
+ const inputMessageCount = includeBody
227
+ ? 0
228
+ : (Array.isArray(events) ? events : []).filter(event => (
229
+ event?.actionId === action?.id
230
+ && ['action.guidance_added', 'action.input_added'].includes(event.type)
231
+ )).length;
232
+ const allMessages = includeBody ? actionMessages(action, matchingRuns, events) : [];
233
+ const totalMessageCount = includeBody ? allMessages.length : matchingRuns.length + inputMessageCount;
234
+ const messages = allMessages.slice(-MAX_ACTION_MESSAGES);
235
+ const liveMessage = includeBody ? runResponseMessage(latest) : null;
114
236
  return {
115
237
  ...stats,
116
- response: typeof latest?.response === 'string' ? latest.response : '',
238
+ response: includeBody && typeof latest?.response === 'string' ? latest.response : '',
239
+ failure: includeBody && latestFailure ? {
240
+ error: sanitizeFailureDiagnostic(latestFailure.error),
241
+ summary: sanitizeFailureDiagnostic(latestFailure.summary),
242
+ failedAt: count(latestFailure.endedAt || latestFailure.startedAt),
243
+ } : null,
117
244
  failureReason: sanitizeFailureReason(latestFailure?.error),
245
+
118
246
  progressRevision: count(latest?.progressRevision),
119
247
  messages,
248
+ liveMessage,
249
+ messageCount: totalMessageCount,
250
+ messageCursor: includeBody
251
+ ? (totalMessageCount > messages.length ? String(totalMessageCount - messages.length) : null)
252
+ : (totalMessageCount > 0 ? String(totalMessageCount) : null),
120
253
  };
121
254
  }
122
255
 
@@ -140,50 +273,169 @@ function projectAssignmentPolicy(policy) {
140
273
  };
141
274
  }
142
275
 
143
- function projectAction(action, runs) {
276
+ function projectAction(action, runs, events, includeBody = true) {
144
277
  if (!action) return null;
145
- const execution = actionExecution(action, runs);
278
+ const execution = actionExecution(action, runs, events, includeBody);
279
+ const alreadyProjected = !Array.isArray(runs) && Array.isArray(action.messages);
280
+ const brief = alreadyProjected ? (action.brief || null) : normalizeActionBrief(action.brief, action.type);
281
+ const projectedBrief = includeBody || !brief
282
+ ? brief
283
+ : Object.fromEntries(Object.entries(brief).map(([key, value]) => [
284
+ key,
285
+ truncateUtf8(value, MAX_HISTORICAL_BRIEF_CHARS),
286
+ ]));
146
287
  return {
147
288
  id: action.id,
148
289
  sequence: action.sequence,
149
290
  type: action.type,
150
291
  stageId: action.stageId || action.type,
151
- assignmentPolicy: projectAssignmentPolicy(action.assignmentPolicy),
292
+ assignmentPolicy: alreadyProjected
293
+ ? (action.assignmentPolicy || null)
294
+ : projectAssignmentPolicy(action.assignmentPolicy),
152
295
  dependsOnStageIds: Array.isArray(action.dependsOnStageIds) ? action.dependsOnStageIds : [],
153
296
  workspaceMode: action.workspaceMode || 'shared',
154
297
  requiredRole: action.requiredRole || '',
155
- brief: normalizeActionBrief(action.brief, action.type),
298
+ brief: projectedBrief,
156
299
  status: action.status,
157
300
  executionStats: executionStats(execution),
158
301
  loopCount: execution.loopCount,
159
302
  toolCount: execution.toolCount,
160
- response: execution.response,
161
- failureReason: execution.failureReason,
303
+ ...(includeBody ? {
304
+ response: execution.response,
305
+ failureReason: execution.failureReason,
306
+ } : {}),
162
307
  progressRevision: execution.progressRevision,
163
- messages: execution.messages,
308
+ messageCount: execution.messageCount,
309
+ messageCursor: execution.messageCursor,
310
+ ...(includeBody ? {
311
+ response: execution.response,
312
+ failure: execution.failure,
313
+ messages: execution.messages,
314
+ liveMessage: execution.liveMessage,
315
+ } : {}),
164
316
  };
165
317
  }
166
318
 
319
+ function bodyActionId(detail) {
320
+ const actions = Array.isArray(detail?.actions) ? detail.actions : [];
321
+ if (detail?.currentActionId && actions.some(action => action?.id === detail.currentActionId)) {
322
+ return detail.currentActionId;
323
+ }
324
+ return [...actions].sort((left, right) => (
325
+ count(right?.sequence) - count(left?.sequence)
326
+ || String(right?.id || '').localeCompare(String(left?.id || ''))
327
+ ))[0]?.id || null;
328
+ }
329
+
330
+ function stripActionBody(action, keepFailure = false) {
331
+ if (!action) return action;
332
+ const projected = { ...action };
333
+ delete projected.response;
334
+ delete projected.messages;
335
+ delete projected.liveMessage;
336
+ if (!keepFailure) delete projected.failure;
337
+ if (projected.brief) {
338
+ projected.brief = Object.fromEntries(Object.entries(projected.brief).map(([key, value]) => [
339
+ key,
340
+ truncateUtf8(value, MAX_HISTORICAL_BRIEF_CHARS),
341
+ ]));
342
+ }
343
+ return projected;
344
+ }
345
+
167
346
  function projectActionStats(detail) {
168
347
  if (!Array.isArray(detail?.actions)) return [];
348
+ const liveActionId = bodyActionId(detail);
169
349
  return detail.actions.map(action => {
170
- const projected = projectAction(action, detail.runs);
171
- return {
350
+ const projected = projectAction(
351
+ action,
352
+ detail.runs,
353
+ detail.events,
354
+ action?.id === liveActionId,
355
+ );
356
+ const stats = {
172
357
  id: projected.id,
173
358
  status: projected.status,
174
359
  executionStats: projected.executionStats,
175
360
  loopCount: projected.loopCount,
176
361
  toolCount: projected.toolCount,
177
- response: projected.response,
178
- failureReason: projected.failureReason,
179
362
  progressRevision: projected.progressRevision,
180
- messages: projected.messages
181
- .map(({ failureReason, ...message }) => message)
182
- .filter(message => message.text),
183
363
  };
364
+ if (projected.failureReason) stats.failureReason = projected.failureReason;
365
+ if (projected.id === liveActionId) {
366
+ stats.response = projected.response;
367
+ stats.failure = projected.failure;
368
+ if (projected.liveMessage) stats.liveMessage = projected.liveMessage;
369
+ }
370
+ return stats;
184
371
  });
185
372
  }
186
373
 
374
+ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
375
+ if (!value || jsonByteLength(value) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return value;
376
+ const dto = value;
377
+ const workItem = options.event === true ? dto.workItem : dto;
378
+ const actions = Array.isArray(workItem?.actions)
379
+ ? workItem.actions
380
+ : Array.isArray(workItem?.actionStats) ? workItem.actionStats : [];
381
+ const keepId = options.keepActionId || workItem?.currentActionId || actions.at(-1)?.id || null;
382
+ workItem.truncated = true;
383
+
384
+ for (let index = 0; index < actions.length; index += 1) {
385
+ if (actions[index]?.id === keepId) continue;
386
+ actions[index] = stripActionBody(actions[index]);
387
+ }
388
+ if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
389
+
390
+ const keep = actions.find(action => action?.id === keepId);
391
+ if (keep) {
392
+ delete keep.response;
393
+ delete keep.messages;
394
+ delete keep.liveMessage;
395
+ }
396
+ if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
397
+
398
+ const originalCount = actions.length;
399
+ const retained = keep ? [stripActionBody(keep, true)] : [];
400
+ if (Array.isArray(workItem.actions)) workItem.actions = retained;
401
+ else workItem.actionStats = retained;
402
+ workItem.omittedActionCount = originalCount - retained.length;
403
+ if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
404
+
405
+ if (retained[0]) {
406
+ delete retained[0].brief;
407
+ delete retained[0].failure;
408
+ }
409
+ workItem.title = truncateUtf8(workItem.title, 4 * 1024);
410
+ workItem.goal = truncateUtf8(workItem.goal, 4 * 1024);
411
+ workItem.waitingReason = truncateUtf8(workItem.waitingReason, 4 * 1024);
412
+ workItem.actionSummary = truncateUtf8(workItem.actionSummary, 4 * 1024);
413
+ if (Array.isArray(workItem.acceptanceCriteria)) workItem.acceptanceCriteria = [];
414
+ if (Array.isArray(workItem.linkedSessionIds)) workItem.linkedSessionIds = [];
415
+ if (Array.isArray(workItem.attachments)) workItem.attachments = [];
416
+ if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
417
+
418
+ const minimalWorkItem = {
419
+ id: truncateUtf8(workItem.id, 4 * 1024),
420
+ revision: count(workItem.revision),
421
+ title: truncateUtf8(workItem.title, 4 * 1024),
422
+ goal: truncateUtf8(workItem.goal, 4 * 1024),
423
+ status: truncateUtf8(workItem.status, 256),
424
+ currentActionId: truncateUtf8(workItem.currentActionId, 4 * 1024) || null,
425
+ executionStats: workItem.executionStats,
426
+ actionCount: count(workItem.actionCount),
427
+ actions: Array.isArray(workItem.actions) ? [] : undefined,
428
+ actionStats: Array.isArray(workItem.actionStats) ? [] : undefined,
429
+ truncated: true,
430
+ omittedActionCount: originalCount,
431
+ createdAt: count(workItem.createdAt),
432
+ updatedAt: count(workItem.updatedAt),
433
+ };
434
+ if (options.event === true) dto.workItem = minimalWorkItem;
435
+ else return minimalWorkItem;
436
+ return dto;
437
+ }
438
+
187
439
  function waitingReason(detail) {
188
440
  if (typeof detail?.waitingReason === 'string') return detail.waitingReason;
189
441
  if (detail?.status !== 'waiting' || !Array.isArray(detail.runs)) return '';
@@ -207,35 +459,45 @@ function workItemFailureReason(detail) {
207
459
  */
208
460
  export function projectWorkItemDetail(detail) {
209
461
  if (!detail) return null;
210
- return {
462
+ const liveActionId = bodyActionId(detail);
463
+ const projected = {
211
464
  id: detail.id,
212
465
  revision: detail.revision,
213
466
  title: detail.title,
214
467
  goal: detail.goal,
215
468
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
216
469
  workflowTemplate: detail.workflowTemplate,
217
- workItemType: detail.workflowSnapshot?.workItemType || null,
218
- planningMode: detail.workflowSnapshot?.planningMode || 'static',
219
- executionMode: detail.workflowSnapshot?.executionMode || 'linear',
470
+ workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
471
+ planningMode: detail.workflowSnapshot?.planningMode || detail.planningMode || 'static',
472
+ executionMode: detail.workflowSnapshot?.executionMode || detail.executionMode || 'linear',
220
473
  status: detail.status,
221
474
  currentActionId: detail.currentActionId || null,
222
- executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
475
+ executionStats: Array.isArray(detail.runs)
476
+ ? sumExecutionStats(detail.runs)
477
+ : executionStats(detail.executionStats),
223
478
  reuseMemory: detail.reuseMemory !== false,
224
- waitingReason: waitingReason(detail),
479
+ waitingReason: sanitizeDiagnosticText(waitingReason(detail), MAX_ACTION_DIAGNOSTIC_CHARS),
225
480
  failureReason: workItemFailureReason(detail),
481
+
226
482
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
227
483
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
228
484
  attachments: projectAttachments(detail.attachments),
229
485
  createdAt: detail.createdAt,
230
486
  updatedAt: detail.updatedAt,
231
- actionCount: Array.isArray(detail.actions) ? detail.actions.length : 0,
487
+ actionCount: Array.isArray(detail.actions) ? detail.actions.length : count(detail.actionCount),
232
488
  actionSummary: Array.isArray(detail.actions)
233
489
  ? detail.actions.map(action => action.type).filter(Boolean).join(' → ')
234
- : '',
490
+ : String(detail.actionSummary || ''),
235
491
  actions: Array.isArray(detail.actions)
236
- ? detail.actions.map(action => projectAction(action, detail.runs))
492
+ ? detail.actions.map(action => projectAction(
493
+ action,
494
+ detail.runs,
495
+ detail.events,
496
+ action?.id === liveActionId,
497
+ ))
237
498
  : [],
238
499
  };
500
+ return enforceWorkItemBrowserDtoBudget(projected, { keepActionId: liveActionId });
239
501
  }
240
502
 
241
503
  /**
@@ -250,8 +512,8 @@ export function projectWorkItemSummary(detail) {
250
512
  revision: detail.revision,
251
513
  title: detail.title,
252
514
  goal: detail.goal,
253
- workItemType: detail.workflowSnapshot?.workItemType || null,
254
- planningMode: detail.workflowSnapshot?.planningMode || 'static',
515
+ workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
516
+ planningMode: detail.workflowSnapshot?.planningMode || detail.planningMode || 'static',
255
517
  status: detail.status,
256
518
  currentActionId: detail.currentActionId || null,
257
519
  currentAction: null,
@@ -264,19 +526,22 @@ export function projectWorkItemSummary(detail) {
264
526
  };
265
527
  }
266
528
  const action = currentAction(detail);
267
- const projectedAction = action ? projectAction(action, detail.runs) : null;
529
+ const projectedAction = action ? projectAction(action, detail.runs, detail.events, false) : null;
268
530
  return {
269
531
  id: detail.id,
270
532
  revision: detail.revision,
271
533
  title: detail.title,
272
534
  goal: detail.goal,
273
- workItemType: detail.workflowSnapshot?.workItemType || null,
274
- planningMode: detail.workflowSnapshot?.planningMode || 'static',
275
- executionMode: detail.workflowSnapshot?.executionMode || 'linear',
535
+ workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
536
+ planningMode: detail.workflowSnapshot?.planningMode || detail.planningMode || 'static',
537
+ executionMode: detail.workflowSnapshot?.executionMode || detail.executionMode || 'linear',
276
538
  status: detail.status,
277
539
  currentActionId: detail.currentActionId || null,
278
- executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
540
+ executionStats: Array.isArray(detail.runs)
541
+ ? sumExecutionStats(detail.runs)
542
+ : executionStats(detail.executionStats),
279
543
  failureReason: workItemFailureReason(detail),
544
+
280
545
  currentAction: projectedAction ? {
281
546
  id: projectedAction.id,
282
547
  type: projectedAction.type,
@@ -293,11 +558,124 @@ export function projectWorkItemSummary(detail) {
293
558
  }
294
559
 
295
560
  export function projectWorkCenterEvent(event) {
296
- return {
297
- type: event?.type || 'work_item.updated',
561
+ const liveActionId = bodyActionId(event?.workItem);
562
+ return enforceWorkItemBrowserDtoBudget({
563
+ type: truncateUtf8(event?.type || 'work_item.updated', 256),
298
564
  workItem: {
299
565
  ...projectWorkItemSummary(event?.workItem),
300
566
  actionStats: projectActionStats(event?.workItem),
301
567
  },
568
+ }, { event: true, keepActionId: liveActionId });
569
+ }
570
+
571
+ function projectDebugUsage(value) {
572
+ return {
573
+ inputTokens: count(value?.inputTokens),
574
+ outputTokens: count(value?.outputTokens),
575
+ cacheReadTokens: count(value?.cacheReadTokens),
576
+ cacheWriteTokens: count(value?.cacheWriteTokens),
577
+ totalInputTokens: count(value?.totalInputTokens),
578
+ totalTokens: count(value?.totalTokens),
579
+ };
580
+ }
581
+
582
+ export function projectActionMessagePage(action, runs, events, options = {}) {
583
+ const messages = actionMessages(action, runs, events);
584
+ const requestedCursor = options.cursor == null ? messages.length : Number(options.cursor);
585
+ const end = Number.isFinite(requestedCursor)
586
+ ? Math.max(0, Math.min(messages.length, Math.floor(requestedCursor)))
587
+ : messages.length;
588
+ const limit = Math.max(1, Math.min(50, Number(options.limit) || MAX_ACTION_MESSAGES));
589
+ const start = Math.max(0, end - limit);
590
+ return {
591
+ actionId: action.id,
592
+ messages: messages.slice(start, end),
593
+ nextCursor: start > 0 ? String(start) : null,
594
+ total: messages.length,
302
595
  };
303
596
  }
597
+
598
+ export function projectActionRequestIndex(action, entries) {
599
+ return {
600
+ actionId: action.id,
601
+ requests: (Array.isArray(entries) ? entries : []).map(({ run, turn }) => ({
602
+ id: turn.turnId,
603
+ runId: run.id,
604
+ status: run.status || 'running',
605
+ model: run.modelSnapshot?.id || null,
606
+ vp: run.vpSnapshot ? {
607
+ id: run.vpSnapshot.id || null,
608
+ name: run.vpSnapshot.name || run.vpSnapshot.id || null,
609
+ } : null,
610
+ openedAt: count(turn.openedAt || run.startedAt),
611
+ closedAt: count(turn.closedAt || run.endedAt),
612
+ loopCount: count(turn.loopCount),
613
+ totalMs: count(turn.totalMs),
614
+ inputTokens: count(turn.summaryInputTokens),
615
+ outputTokens: count(turn.summaryOutputTokens),
616
+ totalTokens: count(turn.totalTokens),
617
+ })).sort((left, right) => right.openedAt - left.openedAt || right.id.localeCompare(left.id)),
618
+ };
619
+ }
620
+
621
+ export function projectActionRequestDetail(action, run, history) {
622
+ const turn = Array.isArray(history?.turns) ? history.turns[0] : null;
623
+ if (!turn) return null;
624
+ const sourceLoops = Array.isArray(history?.loops) ? history.loops : [];
625
+ const limited = limitActionRequestDebugInput(sourceLoops, turn.tools);
626
+ const tools = limited.tools;
627
+ const toolsByCall = new Map(tools.map(tool => [
628
+ `${count(tool.loopNumber)}:${tool.callId}`,
629
+ tool,
630
+ ]));
631
+ const loops = limited.loops.map(loop => ({
632
+ id: loop.loopInstanceId || `${turn.turnId}:${loop.loopNumber}`,
633
+ loopNumber: count(loop.loopNumber),
634
+ model: loop.model || run.modelSnapshot?.id || null,
635
+ systemPrompt: sanitizeDebugValue(typeof loop.systemPrompt === 'string' ? loop.systemPrompt : ''),
636
+ messages: sanitizeDebugValue(Array.isArray(loop.messages) ? loop.messages : []),
637
+ response: sanitizeDebugValue(typeof loop.response === 'string' ? loop.response : ''),
638
+ usage: projectDebugUsage(loop.usage),
639
+ latencyMs: count(loop.latencyMs),
640
+ ttfbMs: loop.ttfbMs == null ? null : count(loop.ttfbMs),
641
+ stopReason: loop.stopReason || null,
642
+ at: count(loop.at),
643
+ tools: (Array.isArray(loop.toolCalls) ? loop.toolCalls : [])
644
+ .slice(0, MAX_ACTION_REQUEST_TOOL_CALLS)
645
+ .map(call => {
646
+ const result = toolsByCall.get(`${count(loop.loopNumber)}:${call.id}`);
647
+ return {
648
+ id: call.id || null,
649
+ name: call.name || result?.name || '?',
650
+ input: sanitizeDebugValue(call.input),
651
+ output: sanitizeDebugValue(result?.toolOutput ?? null),
652
+ durationMs: count(result?.durationMs),
653
+ isError: result?.isError === true,
654
+ };
655
+ }),
656
+ rawRequest: sanitizeDebugValue(reconstructDebugRawRequest(
657
+ loop.rawRequestBase ?? loop.requestBase?.rawRequest ?? null,
658
+ loop.requestDelta || null,
659
+ )),
660
+ rawResponse: sanitizeDebugValue(loop.rawResponse ?? null),
661
+ }));
662
+ return enforceActionRequestDetailBudget({
663
+ actionId: action.id,
664
+ request: {
665
+ id: turn.turnId,
666
+ runId: run.id,
667
+ status: run.status || 'running',
668
+ model: run.modelSnapshot?.id || loops[0]?.model || null,
669
+ vp: run.vpSnapshot ? {
670
+ id: run.vpSnapshot.id || null,
671
+ name: run.vpSnapshot.name || run.vpSnapshot.id || null,
672
+ } : null,
673
+ openedAt: count(turn.openedAt || run.startedAt),
674
+ closedAt: count(turn.closedAt || run.endedAt),
675
+ loopCount: sourceLoops.length,
676
+ totalMs: count(turn.totalMs),
677
+ totalTokens: count(turn.totalTokens),
678
+ loops,
679
+ },
680
+ }, limited.omittedLoopCount);
681
+ }
@@ -3,7 +3,7 @@ import { ToolRegistry } from '../tools/registry.js';
3
3
  import { allTools } from '../tools/index.js';
4
4
  import { parsePatch } from '../tools/apply-patch.js';
5
5
  import { defaultRegistry } from '../vp/registry.js';
6
- import { NullTrace } from '../debug-trace.js';
6
+ import { createTrace } from '../debug-trace.js';
7
7
  import { isPathInsideOrEqual } from '../tools/path-safety.js';
8
8
  import { resolveWorkItemModel, selectWorkItemVp } from './assignment.js';
9
9
  import { approxTokens } from '../memory/budget.js';
@@ -490,6 +490,10 @@ export class WorkItemRunner {
490
490
  this.runtimeProvider = options.runtimeProvider;
491
491
  this.policyProvider = typeof options.policyProvider === 'function' ? options.policyProvider : null;
492
492
  this.attachmentRoot = options.attachmentRoot || null;
493
+ this.trace = options.trace || createTrace({
494
+ enabled: Boolean(options.yeaftDir),
495
+ dirPath: options.yeaftDir || null,
496
+ });
493
497
  this.actionWorktreeRoot = options.actionWorktreeRoot || null;
494
498
  this.store = options.store;
495
499
  this.registry = options.registry || defaultRegistry;
@@ -699,7 +703,7 @@ export class WorkItemRunner {
699
703
  });
700
704
  const engine = new Engine({
701
705
  adapter,
702
- trace: new NullTrace(),
706
+ trace: this.trace,
703
707
  config,
704
708
  conversationStore: null,
705
709
  memoryIndex: null,
@@ -725,6 +729,8 @@ export class WorkItemRunner {
725
729
  messages: [],
726
730
  signal,
727
731
  scenario: 'work-item',
732
+ sessionId: `work-item-${workItem.id}`,
733
+ threadId: run.id,
728
734
  vpPersona: personaFor(vp),
729
735
  workCenterInstructions: workItem?.workflowSnapshot?.globalInstructions || '',
730
736
  workDir,