@yeaft/webchat-agent 1.0.531 → 1.0.532

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.
@@ -17,6 +17,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import { sessionMessageQuotePrompt } from '../session-message-quote.js';
19
19
  import { buildWorkItemAttachmentContext } from './attachments.js';
20
+ import { WorkCenterResourceAdapter } from './resource-control.js';
20
21
  import { withUsageAccounting } from '../llm/usage-accounting.js';
21
22
  import {
22
23
  commitActionWorktree,
@@ -51,20 +52,7 @@ import {
51
52
  renderMainlineContextSnapshot,
52
53
  } from './mainline-projection.js';
53
54
 
54
- const WORK_ITEM_TOOL_NAMES = Object.freeze([
55
- 'FileRead',
56
- 'FileWrite',
57
- 'FileEdit',
58
- 'ApplyPatch',
59
- 'Glob',
60
- 'Grep',
61
- 'ListDir',
62
- 'Bash',
63
- 'WebSearch',
64
- 'WebFetch',
65
- 'ViewImage',
66
- 'Skill',
67
- ]);
55
+ import { WORK_ITEM_TOOL_NAMES, workItemBuiltinToolNames } from './capabilities.js';
68
56
  const WORK_ITEM_TOOL_ALLOWLIST = new Set(WORK_ITEM_TOOL_NAMES);
69
57
  const DEFAULT_PROGRESS_INTERVAL_MS = 200;
70
58
  const ACTION_INPUT_QUOTE_MAX_BYTES = 8 * 1024;
@@ -272,7 +260,7 @@ function assertToolInput(toolName, input, workDir, attachmentFiles) {
272
260
 
273
261
  export function workItemToolPolicySnapshot(workDir, attachmentRefs = [], extraToolNames = []) {
274
262
  const hasAttachments = attachmentRefs.length > 0;
275
- const builtInTools = WORK_ITEM_TOOL_NAMES.filter(name => !hasAttachments || name !== 'Bash');
263
+ const builtInTools = workItemBuiltinToolNames(hasAttachments);
276
264
  return {
277
265
  policyVersion: 1,
278
266
  allowedToolNames: [...builtInTools, ...extraToolNames],
@@ -427,7 +415,7 @@ export function createSubmitWorkItemPlanTool({
427
415
  properties: {
428
416
  summary: { type: 'string', minLength: 1, maxLength: 2_000 },
429
417
  evidence: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'string', minLength: 1, maxLength: 1_000 } },
430
- acceptanceChecks: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['criterion', 'status', 'evidence'], properties: { criterion: { type: 'string' }, status: { type: 'string', enum: ['passed', 'deferred', 'not_applicable'] }, evidence: { type: 'string', minLength: 1, maxLength: 1_000 } } } },
418
+ acceptanceChecks: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['criterion', 'status', 'evidence'], properties: { criterion: { type: 'string' }, status: { type: 'string', enum: ['passed', 'failed', 'deferred', 'not_applicable'] }, evidence: { type: 'string', minLength: 1, maxLength: 1_000 } } } },
431
419
  contractPatch: { type: 'object', additionalProperties: false, required: ['title', 'goal', 'acceptanceCriteria'], properties: { title: { type: 'string', minLength: 1, maxLength: 200 }, goal: { type: 'string', minLength: 1, maxLength: 8_000 }, acceptanceCriteria: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1, maxLength: 2_000 } } } },
432
420
  workItemType: { type: 'string', minLength: 1, maxLength: 64 },
433
421
  actions: { type: 'array', minItems: 1, maxItems: 8, items: { type: 'object', additionalProperties: false, required: ['id', 'name', 'type', 'objective', 'approach', 'expectedOutcome', 'candidateVpIds', 'assignmentReason', 'dependsOnActionIds', 'workspaceMode'], properties: {
@@ -483,7 +471,7 @@ function terminalPlanningFields(options = {}) {
483
471
  return {
484
472
  summary: { type: 'string', minLength: 1, maxLength: 2_000 },
485
473
  evidence: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'string', minLength: 1, maxLength: 1_000 } },
486
- acceptanceChecks: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['criterion', 'status', 'evidence'], properties: { criterion: { type: 'string' }, status: { type: 'string', enum: ['passed', 'deferred', 'not_applicable'] }, evidence: { type: 'string', minLength: 1, maxLength: 1_000 } } } },
474
+ acceptanceChecks: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['criterion', 'status', 'evidence'], properties: { criterion: { type: 'string' }, status: { type: 'string', enum: ['passed', 'failed', 'deferred', 'not_applicable'] }, evidence: { type: 'string', minLength: 1, maxLength: 1_000 } } } },
487
475
  ...(options.review === true ? {
488
476
  reviewDecision: { type: 'string', const: 'changes_requested' },
489
477
  } : {}),
@@ -748,7 +736,7 @@ function completionContract(action, workItem) {
748
736
  : '';
749
737
  const acceptanceChecks = (workItem?.acceptanceCriteria || []).map(criterion => ({
750
738
  criterion,
751
- status: 'passed|deferred|not_applicable',
739
+ status: 'passed|failed|deferred|not_applicable',
752
740
  evidence: 'specific evidence reference',
753
741
  }));
754
742
  return `${toolSubmission}\n\nYou are executing one Work Center Action. Before the terminal JSON, write a concise user-facing response describing what you did and the result. Do not include raw tool output or secrets. End your response with exactly one JSON object, preferably in a json code fence:\n{
@@ -759,7 +747,7 @@ function completionContract(action, workItem) {
759
747
  "acceptanceChecks": ${JSON.stringify(acceptanceChecks)},
760
748
  "waitingReason": null,
761
749
  "error": null${reviewField}${triageField}${planField}
762
- }\nFor completed, provide at least one concrete evidence item and exactly one acceptanceChecks entry for every current acceptance criterion, in the same order, with status passed, deferred, or not_applicable and a non-empty evidence reference. Report every user-consumable file, URL, PR, or commit in outputs; evidence proves work, while outputs tell the user where the deliverable is. Triage must use its proposed criteria when submitting a contractPatch. An intermediate Action may defer criteria outside its task-specific expected result; the final deliver Action, and an approved review with no downstream work, require every criterion to pass. If a criterion is no longer applicable, ask the WorkItem Coordinator to revise the contract instead of pretending it passed. This is a deterministic submission gate, not independent proof: later verification and delivery Actions must verify the claims. A model turn ending is not completion. Use waiting when user or external input is required. Use retryable only for a transient failure. Do not start background jobs or delegate this Action.`;
750
+ }\nFor completed, provide at least one concrete evidence item and exactly one acceptanceChecks entry for every current acceptance criterion, in the same order, with status passed, failed, deferred, or not_applicable and a non-empty evidence reference. Report every user-consumable file, URL, PR, or commit in outputs; evidence proves work, while outputs tell the user where the deliverable is. Triage must use its proposed criteria when submitting a contractPatch. An intermediate Action may defer criteria outside its task-specific expected result; the final deliver Action, and an approved review with no downstream work, require every criterion to pass. If a criterion is no longer applicable, request user confirmation of any contract change instead of pretending it passed. For response delivery, provide the user-facing conclusion in summary with concrete evidence; no artificial file or PR is required. This is a deterministic submission gate, not independent proof: later verification and delivery Actions must verify the claims. A model turn ending is not completion. Use waiting when user or external input is required. Use retryable only for a transient failure. Do not start background jobs or delegate this Action.`;
763
751
  }
764
752
 
765
753
  function safeCheckpointUrl(value) {
@@ -1163,6 +1151,7 @@ export class WorkItemRunner {
1163
1151
  )
1164
1152
  : null;
1165
1153
  const isRunActive = () => !signal.aborted
1154
+ && !this.store.isExecutionStopped?.(workItem.id)
1166
1155
  && this.store.isActiveRun(run.id, ownerBootId, run.leaseEpoch);
1167
1156
  const workspaceRuntime = await this.#workspaceRuntime(workspaceDir, workDir, isRunActive);
1168
1157
  const mcpToolNames = workspaceRuntime.mcpTools.map(tool => tool.name);
@@ -1337,7 +1326,8 @@ export class WorkItemRunner {
1337
1326
  return onProgress(currentProgress());
1338
1327
  };
1339
1328
  if (typeof registerProgressReader === 'function') registerProgressReader(currentProgress);
1340
- const adapter = withUsageAccounting(runtime.adapter, usage => {
1329
+ const adapter = withUsageAccounting(
1330
+ new WorkCenterResourceAdapter(runtime.adapter, this.store, workItem.id, run.id), usage => {
1341
1331
  usageStats.inputTokens += usage.inputTokens;
1342
1332
  usageStats.outputTokens += usage.outputTokens;
1343
1333
  usageStats.cacheReadTokens += usage.cacheReadTokens;
@@ -247,13 +247,16 @@ export class WorkCenterService {
247
247
  workItemId,
248
248
  });
249
249
  const shouldStart = payload.start === undefined ? settings.startImmediately : payload.start !== false;
250
+ const goal = requiredString(payload.goal, 'goal');
251
+ const requestedCriteria = Array.isArray(payload.acceptanceCriteria)
252
+ ? payload.acceptanceCriteria.map(value => String(value).trim()).filter(Boolean) : [];
250
253
  this.controller.create({
251
254
  id: workItemId,
252
255
  title: requiredString(payload.title, 'title'),
253
- goal: requiredString(payload.goal, 'goal'),
254
- acceptanceCriteria: Array.isArray(payload.acceptanceCriteria)
255
- ? payload.acceptanceCriteria.map(value => String(value).trim()).filter(Boolean)
256
- : [],
256
+ goal,
257
+ // With no separate criteria, the user's goal itself is the minimum
258
+ // contract. Do not force a follow-up or invent broader requirements.
259
+ acceptanceCriteria: requestedCriteria.length ? requestedCriteria : [goal],
257
260
  workflowTemplate,
258
261
  workflowSnapshot,
259
262
  coordinationMode: DYNAMIC_COORDINATION_MODE,
@@ -263,7 +266,7 @@ export class WorkCenterService {
263
266
  // browser/user request. Trusted model producers may provide
264
267
  // Session provenance, but cannot grant themselves delivery rights.
265
268
  deliveryTarget: requestContext.userOriginated === true
266
- && ['workspace_files', 'pull_request', 'merge'].includes(payload.deliveryTarget)
269
+ && ['response', 'workspace_files', 'pull_request', 'merge'].includes(payload.deliveryTarget)
267
270
  ? payload.deliveryTarget : null,
268
271
  reuseMemory: payload.reuseMemory !== false,
269
272
  origin: payload.origin && typeof payload.origin === 'object'
@@ -320,9 +323,21 @@ export class WorkCenterService {
320
323
  this.#emit({ type: 'work_item.cancelled', workItem: detail });
321
324
  return detail;
322
325
  }
326
+ case 'extend_budget': {
327
+ if (requestContext.userOriginated !== true) throw new Error('Only explicit user requests can extend execution budget');
328
+ const id = requiredString(payload.id, 'id');
329
+ const detail = this.controller.extendBudget(id, payload);
330
+ this.#emit({ type: 'work_item.execution_budget_extended', workItem: detail });
331
+ return detail;
332
+ }
323
333
  case 'resume': {
334
+ if (requestContext.userOriginated !== true) throw new Error('Only explicit user requests can resume execution');
324
335
  const id = requiredString(payload.id, 'id');
325
- const detail = this.controller.resume(id, { revision: payload.revision });
336
+ if (!Number.isSafeInteger(payload.executionControlRevision)) {
337
+ throw new Error('executionControlRevision is required to resume a WorkItem');
338
+ }
339
+ const detail = this.controller.resume(id, { revision: payload.revision,
340
+ executionControlRevision: payload.executionControlRevision });
326
341
  this.watcher.abortInvalidWorkItemRuns(id);
327
342
  if (detail.coordinationMode === DYNAMIC_COORDINATION_MODE) {
328
343
  this.#queueDynamicCoordinatorWake(id);
@@ -633,7 +648,8 @@ export class WorkCenterService {
633
648
  const entry = entries.find(candidate => candidate.payload?.turnId) || entries[0];
634
649
  if (!entry) return null;
635
650
  const detail = this.store.getWorkItemDetail(workItemId);
636
- if (detail?.actions?.some(action => action.status === 'running')) return null;
651
+ if (!this.store.canAutomaticallyCoordinate(workItemId)
652
+ || detail?.actions?.some(action => action.status === 'running')) return null;
637
653
  let turn;
638
654
  try {
639
655
  turn = this.coordinator.advance(entry.id, {
@@ -673,6 +689,7 @@ export class WorkCenterService {
673
689
  this.store.recoverCoordinatorProviderTurns();
674
690
  this.store.recoverCoordinatorMailbox();
675
691
  for (const recoverable of this.store.getRecoverableCoordinatorTurns?.() || []) {
692
+ if (!this.store.canAutomaticallyCoordinate(recoverable.workItemId)) continue;
676
693
  const claim = this.store.claimCoordinatorTurn(
677
694
  recoverable.workItemId, recoverable.turnId, this.ownerBootId,
678
695
  );
@@ -717,6 +734,10 @@ export class WorkCenterService {
717
734
  let next = null;
718
735
  for (const [key, entry] of this.recoveryQueue) {
719
736
  const detail = this.store.getWorkItemDetail(entry.workItemId);
737
+ if (!this.store.canAutomaticallyCoordinate(entry.workItemId)) {
738
+ this.recoveryQueue.delete(key);
739
+ continue;
740
+ }
720
741
  const action = detail?.actions?.find(candidate => candidate.id === entry.actionId);
721
742
  if (!detail || ['done', 'cancelled'].includes(detail.status)
722
743
  || action?.status !== 'failed'