@yeaft/webchat-agent 1.0.557 → 1.0.558

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.557",
3
+ "version": "1.0.558",
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",
@@ -21,7 +21,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
21
21
  properties: {
22
22
  title: {
23
23
  type: 'string',
24
- description: { en: 'Short work item title', zh: '简短的工作项标题' },
24
+ description: { en: 'Optional explicit title; otherwise the Coordinator generates one', zh: '可选的显式标题;省略时由 Coordinator 生成' },
25
25
  },
26
26
  goal: {
27
27
  type: 'string',
@@ -45,7 +45,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
45
45
  description: { en: 'Start Coordinator execution immediately (default true)', zh: '是否立即启动 Coordinator 执行(默认 true)' },
46
46
  },
47
47
  },
48
- required: ['title', 'goal'],
48
+ required: ['goal'],
49
49
  },
50
50
  isConcurrencySafe: () => false,
51
51
  isReadOnly: () => false,
@@ -57,14 +57,14 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
57
57
  if (!sessionId) throw new Error('CreateWorkItem requires an active Session');
58
58
  const title = typeof input?.title === 'string' ? input.title.trim() : '';
59
59
  const goal = typeof input?.goal === 'string' ? input.goal.trim() : '';
60
- if (!title || !goal) throw new Error('title and goal are required');
60
+ if (!goal) throw new Error('goal is required');
61
61
 
62
62
  // Dynamic import avoids tools/index -> create-work-item -> bridge -> runner
63
63
  // -> tools/index becoming a static initialization cycle.
64
64
  const { createWorkItemFromProducer, snapshotCurrentSessionContext } = await import('../work-center/bridge.js');
65
65
  const sessionContext = await snapshotCurrentSessionContext(sessionId);
66
66
  const detail = await createWorkItemFromProducer({
67
- title,
67
+ ...(title ? { title } : {}),
68
68
  goal,
69
69
  acceptanceCriteria: cleanCriteria(input.acceptanceCriteria),
70
70
  workItemType: typeof input.workItemType === 'string' ? input.workItemType.trim() : 'auto',
@@ -230,6 +230,7 @@ Return exactly one JSON object and no surrounding prose:
230
230
  "decision": {
231
231
  "kind": "answer|create_actions|guide_actions|request_human|complete",
232
232
  "reason": "short audit reason",
233
+ "title": null,
233
234
  "question": null,
234
235
  "workItemType": null,
235
236
  "contractPatch": null,
@@ -242,8 +243,9 @@ Return exactly one JSON object and no surrounding prose:
242
243
  }
243
244
 
244
245
  Rules:
246
+ - When workItem.titleSource is coordinator_pending, include a concise title (prefer 6–12 words or a short Chinese phrase; at most 200 characters) in decision.title. This display label summarizes the original goal; it is not a contractPatch and must not rewrite or shorten the goal. Otherwise leave decision.title null. A missing or oversized display title is normalized by the runtime and must not change the substantive decision.
245
247
  - answer: explain state only. Never use it for an automatic advance trigger.
246
- - Never mutate title, goal, acceptanceCriteria, or deliveryTarget during automatic advance/recovery. contractPatch is allowed only for explicit user-originated refinement, never to make existing evidence pass. For an older WorkItem with no acceptance criteria, request_human to establish its completion condition before commissioning new work.
248
+ - Never mutate goal, acceptanceCriteria, or deliveryTarget during automatic advance/recovery. decision.title is the only automatic title-generation path and is allowed only while titleSource is coordinator_pending; contractPatch (including a user-specified title) is allowed only for explicit user-originated refinement, never to make existing evidence pass. For an older WorkItem with no acceptance criteria, request_human to establish its completion condition before commissioning new work.
247
249
  - create_actions: create 1..8 currently runnable Actions. Every Action needs type, objective, approach, expectedOutcome, capability, candidateVpIds, assignmentReason, sourceActionIds, workspaceMode, and optional maxAttempts/separateFromActionTypes. sourceActionIds are context/audit references, never scheduling dependencies. Do not include dependsOnActionIds, dependsOnStageIds, stages, or a graph.
248
250
  - A missing skill/capability label is not a missing execution capability. Prefer an existing VP with a task-specific brief. Missing tools, credentials, or authorization require request_human; never expand roles as a workaround. create_vp is only appropriate when creating a persistent role is itself an explicit user deliverable.
249
251
  - closeActions may accompany create_actions. Each entry is {"actionId":"failed or waiting durable Action id","reason":"why it is no longer required"}. Close only work made obsolete by replacement evidence or a clarified contract. Closed Actions remain audit history, are never acceptance evidence, and do not block completion.
@@ -290,6 +292,13 @@ function cleanText(value, limit, name) {
290
292
  return text;
291
293
  }
292
294
 
295
+ function coordinatorDisplayTitle(value, detail) {
296
+ if (detail.titleSource !== 'coordinator_pending') return null;
297
+ const proposed = typeof value === 'string' ? value.trim() : '';
298
+ const fallback = String(detail.goal || detail.title || 'Work Item').trim().replace(/\s+/g, ' ');
299
+ return (proposed || fallback || 'Work Item').slice(0, 200);
300
+ }
301
+
293
302
  function requiresDeliveryBoundaryDecision(detail, actions) {
294
303
  const requested = Array.isArray(actions) ? actions : [];
295
304
  return !detail?.deliveryTarget && requested.some(action => (
@@ -460,8 +469,9 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
460
469
  const kind = allowedKinds.includes(source.kind) ? source.kind : '';
461
470
  if (!kind) throw new Error('Work Center Coordinator decision kind is invalid');
462
471
  const reason = cleanText(source.reason, 2_000, 'decision reason');
472
+ const generatedTitle = coordinatorDisplayTitle(source.title, detail);
463
473
  if (kind === 'answer') {
464
- return { reply, decision: { kind, reason, contractPatch: null, guidance: [], actions: [] } };
474
+ return { reply, decision: { kind, reason, title: generatedTitle, contractPatch: null, guidance: [], actions: [] } };
465
475
  }
466
476
  if (kind === 'guide_actions') {
467
477
  const guidance = normalizeGuidance(source.guidance, detail);
@@ -478,6 +488,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
478
488
  decision: {
479
489
  kind,
480
490
  reason,
491
+ title: generatedTitle,
481
492
  contractPatch: null,
482
493
  guidance,
483
494
  actions: [],
@@ -491,6 +502,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
491
502
  decision: {
492
503
  kind,
493
504
  reason,
505
+ title: generatedTitle,
494
506
  question: cleanText(source.question, COORDINATOR_MAX_REPLY_CHARS, 'human question'),
495
507
  contractPatch: dynamic && options.automatic !== true ? contractPatch : null,
496
508
  guidance: [],
@@ -504,6 +516,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
504
516
  decision: {
505
517
  kind,
506
518
  reason,
519
+ title: generatedTitle,
507
520
  closeActions: normalizeDynamicActionClosures(source.closeActions, detail.actions || []),
508
521
  completion: source.completion,
509
522
  contractPatch: null,
@@ -520,6 +533,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
520
533
  const decision = {
521
534
  kind,
522
535
  reason,
536
+ title: generatedTitle,
523
537
  workItemType: source.workItemType,
524
538
  contractPatch,
525
539
  closeActions: source.closeActions,
@@ -551,6 +565,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
551
565
  decision: {
552
566
  kind,
553
567
  reason,
568
+ title: generatedTitle,
554
569
  contractPatch,
555
570
  guidance: [],
556
571
  actions: normalizeCoordinatorActionReferences(source.actions, detail),
@@ -616,6 +631,7 @@ export function coordinatorSnapshot(detail) {
616
631
  ledgerRevision: detail.ledgerRevision,
617
632
  status: truncateUtf8(detail.status, 64),
618
633
  title: truncateUtf8(detail.title, 1 * 1024),
634
+ titleSource: detail.titleSource || 'explicit',
619
635
  goal: truncateUtf8(detail.goal, 4 * 1024),
620
636
  deliveryTarget: detail.deliveryTarget || null,
621
637
  acceptanceCriteria,
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
 
3
- export const WORK_CENTER_SCHEMA_VERSION = 39;
3
+ export const WORK_CENTER_SCHEMA_VERSION = 40;
4
4
 
5
5
  const MIGRATIONS = [
6
6
  ['23-conversation-stream', migrateConversationStream],
@@ -793,18 +793,39 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
793
793
  if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
794
794
 
795
795
  const originalCount = actions.length;
796
- const retained = keep ? [stripActionBody(keep, true)] : [];
796
+ // Detail evidence and attempt links need the Action identity AND its readable
797
+ // name. Drop bulky context/dependencies before dropping their navigation
798
+ // targets. If even these stubs cannot fit, the minimal fallback below omits
799
+ // evidence with the Actions instead of leaving known sources unresolvable.
800
+ const retained = Array.isArray(workItem.runReferences) && Array.isArray(workItem.actions)
801
+ ? actions.map(action => ({
802
+ id: action.id,
803
+ sequence: action.sequence,
804
+ generation: action.generation,
805
+ type: action.type,
806
+ stageId: action.stageId,
807
+ status: action.status,
808
+ progressRevision: action.progressRevision,
809
+ messageCount: action.messageCount,
810
+ createdAt: action.createdAt,
811
+ updatedAt: action.updatedAt,
812
+ executionDurationMs: action.executionDurationMs,
813
+ executionStartedAt: action.executionStartedAt,
814
+ brief: { objective: truncateUtf8(action.brief?.objective || action.contentSummary || '', 512) },
815
+ }))
816
+ : keep ? [stripActionBody(keep, true)] : [];
797
817
  if (Array.isArray(workItem.actions)) workItem.actions = retained;
798
818
  else workItem.actionStats = retained;
799
819
  workItem.omittedActionCount = originalCount - retained.length;
800
820
  if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
801
821
 
802
- if (retained[0]) {
822
+ if (retained[0] && !Array.isArray(workItem.runReferences)) {
803
823
  delete retained[0].brief;
804
824
  delete retained[0].failure;
805
825
  }
806
826
  workItem.title = truncateUtf8(workItem.title, 4 * 1024);
807
827
  workItem.goal = truncateUtf8(workItem.goal, 4 * 1024);
828
+ if (workItem.requirement !== undefined) workItem.requirement = truncateUtf8(workItem.requirement, 4 * 1024);
808
829
  workItem.waitingReason = truncateUtf8(workItem.waitingReason, 4 * 1024);
809
830
  workItem.actionSummary = truncateUtf8(workItem.actionSummary, 4 * 1024);
810
831
  if (Array.isArray(workItem.acceptanceCriteria)) workItem.acceptanceCriteria = [];
@@ -817,6 +838,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
817
838
  revision: count(workItem.revision),
818
839
  title: truncateUtf8(workItem.title, 4 * 1024),
819
840
  goal: truncateUtf8(workItem.goal, 4 * 1024),
841
+ ...(workItem.requirement !== undefined ? { requirement: truncateUtf8(workItem.requirement, 4 * 1024) } : {}),
820
842
  status: truncateUtf8(workItem.status, 256),
821
843
  currentActionId: truncateUtf8(workItem.currentActionId, 4 * 1024) || null,
822
844
  executionStats: workItem.executionStats,
@@ -1054,6 +1076,7 @@ export function projectWorkItemDetail(detail, options = {}) {
1054
1076
  .map(risk => truncateUtf8(risk, MAX_ACTION_MESSAGE_CHARS)).slice(0, 24) : [],
1055
1077
  } : null,
1056
1078
  title: detail.title,
1079
+ requirement: detail.requirement ?? detail.goal,
1057
1080
  goal: detail.goal,
1058
1081
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
1059
1082
  goalProgress: projectGoalProgress(detail.goalProgress),
@@ -1127,6 +1150,25 @@ export function projectWorkItemDetail(detail, options = {}) {
1127
1150
  }))
1128
1151
  : [],
1129
1152
  };
1153
+ // Only identities needed by visible evidence links, never Run bodies or traces.
1154
+ // Historical evidence may belong to an older generation of the same Action.
1155
+ const evidenceRunIds = new Set([
1156
+ ...(projected.goalProgress?.evidenceRunIds || []),
1157
+ ...(projected.goalProgress?.criteria || []).flatMap(check => check.evidenceRunIds || []),
1158
+ ...(projected.goalProgress?.delivery?.evidenceRunIds || []),
1159
+ ...(projected.finalResult?.responses || []).map(response => response.runId),
1160
+ ...projected.outputs.map(output => output.runId),
1161
+ ].filter(Boolean));
1162
+ const actionIds = new Set(projected.actions.map(action => action.id));
1163
+ // The evidence collections above already have bounded browser projections.
1164
+ // Keep every retained reference resolvable; an independent count cap would
1165
+ // starve later criteria and delivery evidence. The whole DTO budget still applies.
1166
+ projected.runReferences = [...evidenceRunIds].flatMap(id => {
1167
+ const run = runById.get(id);
1168
+ if (!run || !actionIds.has(run.actionId)
1169
+ || (run.workItemId && run.workItemId !== detail.id)) return [];
1170
+ return [{ id: truncateUtf8(id, 256), actionId: truncateUtf8(run.actionId, 256) }];
1171
+ });
1130
1172
  return enforceWorkItemBrowserDtoBudget(projected, { keepActionId: liveActionId });
1131
1173
  }
1132
1174
 
@@ -248,11 +248,16 @@ export class WorkCenterService {
248
248
  });
249
249
  const shouldStart = payload.start === undefined ? settings.startImmediately : payload.start !== false;
250
250
  const goal = requiredString(payload.goal, 'goal');
251
+ const explicitTitle = payload.titleSource !== 'coordinator_pending' && typeof payload.title === 'string'
252
+ ? payload.title.trim() : '';
251
253
  const requestedCriteria = Array.isArray(payload.acceptanceCriteria)
252
254
  ? payload.acceptanceCriteria.map(value => String(value).trim()).filter(Boolean) : [];
253
255
  this.controller.create({
254
256
  id: workItemId,
255
- title: requiredString(payload.title, 'title'),
257
+ // Goal remains the original requirement. Until coordination succeeds,
258
+ // title is only a backwards-compatible display fallback.
259
+ title: explicitTitle || goal.replace(/\s+/g, ' ').slice(0, 80),
260
+ titleSource: explicitTitle ? 'explicit' : 'coordinator_pending',
256
261
  goal,
257
262
  // With no separate criteria, the user's goal itself is the minimum
258
263
  // contract. Do not force a follow-up or invent broader requirements.
@@ -113,6 +113,8 @@ function mapWorkItem(row) {
113
113
  finalResult: parseJson(row.final_result, null),
114
114
  deliveryTarget: row.delivery_target || null,
115
115
  title: row.title,
116
+ titleSource: row.title_source || 'explicit',
117
+ requirement: row.requirement ?? row.goal,
116
118
  goal: row.goal,
117
119
  acceptanceCriteria: parseJson(row.acceptance_criteria, []),
118
120
  workflowTemplate: row.workflow_template,
@@ -857,6 +859,8 @@ export class WorkItemStore {
857
859
  final_result TEXT,
858
860
  delivery_target TEXT,
859
861
  title TEXT NOT NULL,
862
+ title_source TEXT NOT NULL DEFAULT 'explicit',
863
+ requirement TEXT,
860
864
  goal TEXT NOT NULL,
861
865
  acceptance_criteria TEXT NOT NULL,
862
866
  workflow_template TEXT NOT NULL,
@@ -1139,6 +1143,15 @@ export class WorkItemStore {
1139
1143
  if (!hasColumn(this.db, 'work_items', 'delivery_target')) {
1140
1144
  this.db.exec('ALTER TABLE work_items ADD COLUMN delivery_target TEXT');
1141
1145
  }
1146
+ if (!hasColumn(this.db, 'work_items', 'title_source')) {
1147
+ // Old titles were explicit under the previous creation contract.
1148
+ this.db.exec("ALTER TABLE work_items ADD COLUMN title_source TEXT NOT NULL DEFAULT 'explicit'");
1149
+ }
1150
+ if (!hasColumn(this.db, 'work_items', 'requirement')) {
1151
+ // Old items cannot reconstruct an earlier goal; retain their current contract.
1152
+ this.db.exec('ALTER TABLE work_items ADD COLUMN requirement TEXT');
1153
+ this.db.exec('UPDATE work_items SET requirement = goal');
1154
+ }
1142
1155
  if (!hasColumn(this.db, 'actions', 'source_action_ids')) {
1143
1156
  this.db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
1144
1157
  }
@@ -2387,15 +2400,17 @@ export class WorkItemStore {
2387
2400
  const workspaceKey = canonicalWorkspaceKey(input.workDir);
2388
2401
  this.db.prepare(`INSERT INTO work_items
2389
2402
  (id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result, delivery_target,
2390
- title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
2403
+ title, title_source, requirement, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
2391
2404
  current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
2392
2405
  session_context, attachments, created_at, updated_at)
2393
- VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2406
+ VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2394
2407
  id,
2395
2408
  Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
2396
2409
  input.coordinationMode || 'legacy',
2397
2410
  input.deliveryTarget || null,
2398
2411
  input.title,
2412
+ input.titleSource === 'coordinator_pending' ? 'coordinator_pending' : 'explicit',
2413
+ input.goal,
2399
2414
  input.goal,
2400
2415
  stringify(input.acceptanceCriteria || []),
2401
2416
  input.workflowTemplate || 'software-change',
@@ -3759,6 +3774,19 @@ export class WorkItemStore {
3759
3774
  turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
3760
3775
  }) {
3761
3776
  const decision = result?.decision || {};
3777
+ const generatedTitle = workItem.titleSource === 'coordinator_pending'
3778
+ ? (typeof decision.title === 'string' && decision.title.trim()
3779
+ ? decision.title.trim().slice(0, 200)
3780
+ : String(workItem.goal || workItem.title || 'Work Item').trim().replace(/\s+/g, ' ').slice(0, 200))
3781
+ : null;
3782
+ if (generatedTitle) {
3783
+ const changedTitle = this.db.prepare(`UPDATE work_items SET title = ?, title_source = 'coordinator',
3784
+ updated_at = ? WHERE id = ? AND title_source = 'coordinator_pending'`).run(
3785
+ generatedTitle, now, workItem.id,
3786
+ );
3787
+ if (Number(changedTitle.changes) !== 1) throw new Error('Coordinator title generation lost its fence');
3788
+ workItem = this.getWorkItem(workItem.id);
3789
+ }
3762
3790
  const decisionPatch = normalizeContractPatch(decision.contractPatch);
3763
3791
  const mutationPatch = normalizeContractPatch(result?.mutation?.contractPatch);
3764
3792
  if (JSON.stringify(decisionPatch) !== JSON.stringify(mutationPatch) && mutationPatch) {
@@ -3783,17 +3811,23 @@ export class WorkItemStore {
3783
3811
  throw new Error('WorkItem delivery target must be confirmed before creating mutating or delivery Actions');
3784
3812
  }
3785
3813
  const refined = { ...workItem, ...(contractPatch || {}) };
3814
+ const hasExplicitTitle = Object.hasOwn(contractPatch || {}, 'title');
3786
3815
  const contractChanged = ['title', 'goal', 'deliveryTarget', 'acceptanceCriteria']
3787
3816
  .some(key => JSON.stringify(refined[key]) !== JSON.stringify(workItem[key]));
3788
3817
  if (contractChanged) {
3789
3818
  this.#invalidateExecution(workItem, 'superseded', 'superseded', 'User refined the WorkItem contract', now);
3790
3819
  this.db.prepare(`UPDATE work_items SET title = ?, goal = ?, acceptance_criteria = ?,
3791
- delivery_target = ?, revision = revision + 1, updated_at = ? WHERE id = ? AND revision = ?`).run(
3820
+ delivery_target = ?, title_source = CASE WHEN ? THEN 'explicit' ELSE title_source END,
3821
+ revision = revision + 1, updated_at = ? WHERE id = ? AND revision = ?`).run(
3792
3822
  refined.title, refined.goal, stringify(refined.acceptanceCriteria), refined.deliveryTarget,
3793
- now, workItem.id, workItem.revision,
3823
+ hasExplicitTitle ? 1 : 0, now, workItem.id, workItem.revision,
3794
3824
  );
3795
3825
  workItem = this.getWorkItem(workItem.id);
3796
3826
  activeActions = [];
3827
+ } else if (hasExplicitTitle && workItem.titleSource !== 'explicit') {
3828
+ this.db.prepare(`UPDATE work_items SET title_source = 'explicit', updated_at = ?
3829
+ WHERE id = ? AND revision = ?`).run(now, workItem.id, workItem.revision);
3830
+ workItem = this.getWorkItem(workItem.id);
3797
3831
  }
3798
3832
 
3799
3833
  if (decision.kind === 'create_actions') {