@yeaft/webchat-agent 1.0.171 → 1.0.172

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.171",
3
+ "version": "1.0.172",
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",
@@ -36,6 +36,44 @@ function sumExecutionStats(values) {
36
36
  }, emptyExecutionStats());
37
37
  }
38
38
 
39
+ const MAX_FAILURE_REASON_LENGTH = 2_000;
40
+ const MAX_FAILURE_INSPECTION_LENGTH = 16_000;
41
+ const SAFE_FAILURE_FALLBACK = 'The Action failed. Sensitive details were omitted.';
42
+ const CREDENTIAL_ASSIGNMENT_PATTERN = /\b(?:[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)|api[_-]?key|access[_-]?token|authorization|password|secret|token)\s*[:=]/i;
43
+ const PROVIDER_TOKEN_PATTERN = /\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{12,}|github_pat_[A-Za-z0-9_]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|AKIA[A-Z0-9]{12,})\b/i;
44
+ const HIGH_ENTROPY_SECRET_PATTERN = /\b(?=[A-Za-z0-9_./+=-]{32,}\b)(?=[A-Za-z0-9_./+=-]*[A-Za-z])(?=[A-Za-z0-9_./+=-]*\d)[A-Za-z0-9_./+=-]+\b/;
45
+ const LOCAL_PATH_ASSIGNMENT_PATTERN = /\b(?:path|cwd|file|filename|directory)\s*[:=]\s*(?:\/(?!\/)|[A-Za-z]:\\|\\\\)/i;
46
+ const POSIX_ABSOLUTE_PATH_PATTERN = /(?:^|[\s("'`])\/(?!\/)[^\r\n]*/m;
47
+ const WINDOWS_ABSOLUTE_PATH_PATTERN = /(?:^|[\s("'`])(?:[A-Za-z]:\\|\\\\)[^\r\n]*/m;
48
+
49
+ function sanitizedUrl(raw) {
50
+ try {
51
+ const url = new URL(raw);
52
+ if (!['http:', 'https:'].includes(url.protocol)) return '[redacted URL]';
53
+ return `${url.protocol}//${url.host}${url.pathname}`;
54
+ } catch {
55
+ return '[redacted URL]';
56
+ }
57
+ }
58
+
59
+ function sanitizeFailureReason(value) {
60
+ const raw = typeof value === 'string'
61
+ ? value.trim().slice(0, MAX_FAILURE_INSPECTION_LENGTH)
62
+ : '';
63
+ if (!raw) return '';
64
+ 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
+ }
71
+ return text
72
+ .replace(/\b(?:Bearer|Basic)\s+[^\s,;]+/gi, '[redacted credential]')
73
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
74
+ .slice(0, MAX_FAILURE_REASON_LENGTH);
75
+ }
76
+
39
77
  function actionExecution(action, runs) {
40
78
  const matchingRuns = Array.isArray(runs)
41
79
  ? runs.filter(run => run?.actionId === action?.id)
@@ -44,6 +82,7 @@ function actionExecution(action, runs) {
44
82
  return {
45
83
  ...executionStats(action),
46
84
  response: '',
85
+ failureReason: '',
47
86
  progressRevision: 0,
48
87
  messages: [],
49
88
  };
@@ -53,19 +92,29 @@ function actionExecution(action, runs) {
53
92
  count(right.progressRevision) - count(left.progressRevision)
54
93
  || count(right.startedAt) - count(left.startedAt)
55
94
  ))[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))
102
+ : null;
56
103
  const messages = [...matchingRuns]
57
104
  .sort((left, right) => count(left.startedAt) - count(right.startedAt))
58
105
  .map((run, index) => ({
59
106
  id: `${action.id}:${index + 1}`,
60
107
  status: run.status || 'running',
61
108
  text: typeof run.response === 'string' ? run.response.trim().slice(0, 16_000) : '',
109
+ failureReason: sanitizeFailureReason(run.error),
62
110
  createdAt: count(run.startedAt),
63
111
  updatedAt: count(run.endedAt || run.startedAt),
64
112
  }))
65
- .filter(message => message.text);
113
+ .filter(message => message.text || message.failureReason);
66
114
  return {
67
115
  ...stats,
68
116
  response: typeof latest?.response === 'string' ? latest.response : '',
117
+ failureReason: sanitizeFailureReason(latestFailure?.error),
69
118
  progressRevision: count(latest?.progressRevision),
70
119
  messages,
71
120
  };
@@ -109,6 +158,7 @@ function projectAction(action, runs) {
109
158
  loopCount: execution.loopCount,
110
159
  toolCount: execution.toolCount,
111
160
  response: execution.response,
161
+ failureReason: execution.failureReason,
112
162
  progressRevision: execution.progressRevision,
113
163
  messages: execution.messages,
114
164
  };
@@ -125,8 +175,11 @@ function projectActionStats(detail) {
125
175
  loopCount: projected.loopCount,
126
176
  toolCount: projected.toolCount,
127
177
  response: projected.response,
178
+ failureReason: projected.failureReason,
128
179
  progressRevision: projected.progressRevision,
129
- messages: projected.messages,
180
+ messages: projected.messages
181
+ .map(({ failureReason, ...message }) => message)
182
+ .filter(message => message.text),
130
183
  };
131
184
  });
132
185
  }
@@ -139,6 +192,15 @@ function waitingReason(detail) {
139
192
  ))?.waitingReason || '';
140
193
  }
141
194
 
195
+ function workItemFailureReason(detail) {
196
+ if (!['needs_attention', 'cancelled'].includes(detail?.status) || !Array.isArray(detail?.runs)) return '';
197
+ const ordered = [...detail.runs].sort((left, right) => (
198
+ count(right.endedAt || right.startedAt) - count(left.endedAt || left.startedAt)
199
+ ));
200
+ const current = ordered.find(run => run?.actionId === detail.currentActionId && sanitizeFailureReason(run.error));
201
+ return sanitizeFailureReason(current?.error || ordered.find(run => sanitizeFailureReason(run?.error))?.error);
202
+ }
203
+
142
204
  /**
143
205
  * Authenticated browser detail DTO. Raw execution records stay Agent-local;
144
206
  * the browser receives only aggregate execution stats plus the explicit user-facing response.
@@ -160,6 +222,7 @@ export function projectWorkItemDetail(detail) {
160
222
  executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
161
223
  reuseMemory: detail.reuseMemory !== false,
162
224
  waitingReason: waitingReason(detail),
225
+ failureReason: workItemFailureReason(detail),
163
226
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
164
227
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
165
228
  attachments: projectAttachments(detail.attachments),
@@ -213,6 +276,7 @@ export function projectWorkItemSummary(detail) {
213
276
  status: detail.status,
214
277
  currentActionId: detail.currentActionId || null,
215
278
  executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
279
+ failureReason: workItemFailureReason(detail),
216
280
  currentAction: projectedAction ? {
217
281
  id: projectedAction.id,
218
282
  type: projectedAction.type,
@@ -327,7 +327,7 @@ function completionContract(action, workItem) {
327
327
  ? ',\n "contractPatch": { "goal": "optional refined goal", "acceptanceCriteria": ["optional refined criterion"] }'
328
328
  : '';
329
329
  const planField = action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
330
- ? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|integrate|write|custom)", "capability": "specific executor capability", "objective": "what this Action must do", "approach": "how this Action should do it", "expectedOutcome": "verifiable result this Action must produce", "dependsOnActionIds": ["earlier Action id; [] means concurrent root"], "workspaceMode": "read|isolated-write|integrate|shared", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
330
+ ? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|integrate|write|custom)", "capability": "specific executor capability", "objective": "task-specific concrete work this Action must do", "approach": "task-specific repository-aware method the executor must follow", "expectedOutcome": "task-specific verifiable result this Action must produce", "dependsOnActionIds": ["earlier Action id; [] means concurrent root"], "workspaceMode": "read|isolated-write|integrate|shared", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
331
331
  : '';
332
332
  const acceptanceChecks = (workItem?.acceptanceCriteria || []).map(criterion => ({
333
333
  criterion,
@@ -341,7 +341,7 @@ export function resolvePlanningWorkflowSnapshot(settings, requestedWorkItemType
341
341
  const typeInstruction = requestedType
342
342
  ? `The user explicitly selected workItemType "${requestedType}". Keep that exact type.`
343
343
  : 'Infer one specific workItemType from the contract.';
344
- const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReusable Action templates:\n${catalog || '(none)'}\nIf the final type matches a reusable template, return its workItemType and omit actions so Work Center can apply that frozen template. Otherwise generate the smallest reliable graph of 1 to 8 Actions. Split independent work into separate Actions and declare dependsOnActionIds. Use workspaceMode read for analysis, isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. Non-Git or dirty workspaces are serialized automatically. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. Add only Actions required by this task. Do not copy a generic workflow.`;
344
+ const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReusable Action templates:\n${catalog || '(none)'}\nIf the final type matches a reusable template, return its workItemType and omit actions so Work Center can apply that frozen template. Otherwise generate the smallest reliable graph of 1 to 8 Actions. Split independent work into separate Actions and declare dependsOnActionIds. Use workspaceMode read for analysis, isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. Non-Git or dirty workspaces are serialized automatically. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. The objective, approach, and expectedOutcome must be specific to this WorkItem and that Action: describe the concrete work, the repository-aware execution method, and the verifiable result that will guide the executor. Generic Action-type boilerplate is invalid. Add only Actions required by this task. Do not copy a generic workflow.`;
345
345
  return normalizeWorkflowDefinition({
346
346
  id: 'ai-planned',
347
347
  name: 'AI planned',
@@ -434,14 +434,20 @@ export function applyGeneratedPlan(workItem, rawPlan) {
434
434
  if (seen.has(id)) throw new Error(`Duplicate AI-planned Action id: ${id}`);
435
435
  seen.add(id);
436
436
  const objective = typeof input.objective === 'string' ? input.objective.trim().slice(0, 2_000) : '';
437
- if (!objective) throw new Error(`AI-planned Action "${id}" requires an objective`);
438
- const defaults = defaultActionBrief(type);
439
- const approach = typeof input.approach === 'string' && input.approach.trim()
440
- ? input.approach.trim().slice(0, 2_000)
441
- : defaults.approach;
442
- const expectedOutcome = typeof input.expectedOutcome === 'string' && input.expectedOutcome.trim()
437
+ if (!objective) throw new Error(`AI-planned Action "${id}" requires a task-specific objective`);
438
+ const approach = typeof input.approach === 'string' ? input.approach.trim().slice(0, 2_000) : '';
439
+ if (!approach) throw new Error(`AI-planned Action "${id}" requires a task-specific approach`);
440
+ const expectedOutcome = typeof input.expectedOutcome === 'string'
443
441
  ? input.expectedOutcome.trim().slice(0, 2_000)
444
- : defaults.expectedOutcome;
442
+ : '';
443
+ if (!expectedOutcome) {
444
+ throw new Error(`AI-planned Action "${id}" requires a task-specific expectedOutcome`);
445
+ }
446
+ const defaults = defaultActionBrief(type);
447
+ if (objective === defaults.objective || approach === defaults.approach
448
+ || expectedOutcome === defaults.expectedOutcome) {
449
+ throw new Error(`AI-planned Action "${id}" must not reuse generic Action-type brief text`);
450
+ }
445
451
  const actionInstruction = Object.hasOwn(source.actionInstructions, type)
446
452
  ? source.actionInstructions[type]
447
453
  : source.actionInstructions.custom;