@yeaft/webchat-agent 1.0.123 → 1.0.125

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.
@@ -0,0 +1,123 @@
1
+ const DEFAULT_POLL_INTERVAL_MS = 2_000;
2
+ const DEFAULT_LEASE_MS = 60_000;
3
+
4
+ export class WorkItemWatcher {
5
+ constructor(options) {
6
+ this.store = options.store;
7
+ this.controller = options.controller;
8
+ this.runner = options.runner;
9
+ this.ownerBootId = options.ownerBootId;
10
+ this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : () => {};
11
+ this.pollIntervalMs = Number(options.pollIntervalMs) > 0
12
+ ? Number(options.pollIntervalMs)
13
+ : DEFAULT_POLL_INTERVAL_MS;
14
+ this.leaseMs = Number(options.leaseMs) > 0 ? Number(options.leaseMs) : DEFAULT_LEASE_MS;
15
+ this.timer = null;
16
+ this.ticking = false;
17
+ this.activeRuns = new Map();
18
+ }
19
+
20
+ status() {
21
+ return {
22
+ enabled: !!this.timer,
23
+ activeRuns: this.activeRuns.size,
24
+ ownerBootId: this.ownerBootId,
25
+ };
26
+ }
27
+
28
+ start() {
29
+ if (this.timer) return;
30
+ this.timer = setInterval(() => { this.tick().catch(() => {}); }, this.pollIntervalMs);
31
+ this.timer.unref?.();
32
+ this.tick().catch(() => {});
33
+ }
34
+
35
+ async stop() {
36
+ if (this.timer) clearInterval(this.timer);
37
+ this.timer = null;
38
+ const active = Array.from(this.activeRuns.values());
39
+ for (const entry of active) {
40
+ this.store.interruptRun(
41
+ entry.runId,
42
+ this.ownerBootId,
43
+ entry.leaseEpoch,
44
+ 'Work Center watcher stopped',
45
+ );
46
+ entry.abortController.abort('watcher_stopped');
47
+ }
48
+ await Promise.allSettled(active.map(entry => entry.promise));
49
+ }
50
+
51
+ abortInvalidWorkItemRuns(workItemId) {
52
+ for (const entry of this.activeRuns.values()) {
53
+ if (entry.workItemId !== workItemId) continue;
54
+ if (!this.store.isActiveRun(entry.runId, this.ownerBootId, entry.leaseEpoch)) {
55
+ entry.abortController.abort('work_item_state_changed');
56
+ }
57
+ }
58
+ }
59
+
60
+ async tick() {
61
+ // V1 deliberately serializes WorkItem Runs per Agent. Without a dedicated
62
+ // per-WorkItem workspace manager, concurrent writers could mutate the same
63
+ // project directory. Parallel execution can be added only with exclusive
64
+ // workspace leases.
65
+ if (this.ticking || this.activeRuns.size > 0 || !this.runner) return;
66
+ this.ticking = true;
67
+ try {
68
+ const claim = this.store.claimReadyAction(this.ownerBootId, this.leaseMs);
69
+ if (!claim) return;
70
+ const abortController = new AbortController();
71
+ const key = claim.run.id;
72
+ const renewEvery = Math.max(1_000, Math.floor(this.leaseMs / 3));
73
+ const renewal = setInterval(() => {
74
+ const ok = this.store.renewLease(key, this.ownerBootId, claim.run.leaseEpoch, this.leaseMs);
75
+ if (!ok) abortController.abort('work_item_lease_lost');
76
+ }, renewEvery);
77
+ renewal.unref?.();
78
+
79
+ const promise = this.#execute(claim, abortController.signal)
80
+ .finally(() => {
81
+ clearInterval(renewal);
82
+ this.activeRuns.delete(key);
83
+ });
84
+ this.activeRuns.set(key, {
85
+ promise,
86
+ abortController,
87
+ workItemId: claim.workItem.id,
88
+ runId: claim.run.id,
89
+ leaseEpoch: claim.run.leaseEpoch,
90
+ });
91
+ this.onEvent({ type: 'run.started', workItem: this.store.getWorkItemDetail(claim.workItem.id) });
92
+ } finally {
93
+ this.ticking = false;
94
+ }
95
+ }
96
+
97
+ async #execute(claim, signal) {
98
+ let result;
99
+ try {
100
+ result = await this.runner.run({ ...claim, signal, ownerBootId: this.ownerBootId });
101
+ } catch (err) {
102
+ if (signal.aborted) return;
103
+ result = {
104
+ outcome: err?.retryable === false ? 'failed' : 'retryable',
105
+ summary: '',
106
+ evidence: [],
107
+ error: err?.message || String(err),
108
+ };
109
+ }
110
+ if (signal.aborted) return;
111
+ try {
112
+ const workItem = this.controller.submit(
113
+ claim.run.id,
114
+ this.ownerBootId,
115
+ claim.run.leaseEpoch,
116
+ result,
117
+ );
118
+ this.onEvent({ type: 'run.finished', workItem });
119
+ } catch (err) {
120
+ if (!/stale|cancelled|already finished/i.test(err?.message || '')) throw err;
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,99 @@
1
+ const SOFTWARE_CHANGE_STEPS = Object.freeze([
2
+ { type: 'triage', requiredRole: 'omni' },
3
+ { type: 'implement', requiredRole: 'linus' },
4
+ { type: 'review', requiredRole: 'martin' },
5
+ { type: 'deliver', requiredRole: 'linus' },
6
+ ]);
7
+
8
+ export const WORKFLOW_TEMPLATES = Object.freeze({
9
+ 'software-change': SOFTWARE_CHANGE_STEPS,
10
+ });
11
+
12
+ export const RUN_OUTCOMES = Object.freeze([
13
+ 'completed',
14
+ 'waiting',
15
+ 'retryable',
16
+ 'failed',
17
+ ]);
18
+
19
+ export function getWorkflowSteps(template = 'software-change') {
20
+ const steps = WORKFLOW_TEMPLATES[template];
21
+ if (!steps) throw new Error(`Unsupported Work Center workflow: ${template}`);
22
+ return steps;
23
+ }
24
+
25
+ export function getStep(template, type) {
26
+ return getWorkflowSteps(template).find(step => step.type === type) || null;
27
+ }
28
+
29
+ export function getNextStep(template, type, result = {}) {
30
+ const steps = getWorkflowSteps(template);
31
+ const index = steps.findIndex(step => step.type === type);
32
+ if (index === -1) throw new Error(`Action type ${type} is not in workflow ${template}`);
33
+ if (type === 'review') {
34
+ if (result.reviewDecision === 'changes_requested') {
35
+ return steps.find(step => step.type === 'implement') || null;
36
+ }
37
+ if (result.reviewDecision !== 'approved') {
38
+ throw new Error('Completed review requires approved or changes_requested');
39
+ }
40
+ }
41
+ return steps[index + 1] || null;
42
+ }
43
+
44
+ function renderContext(context = []) {
45
+ if (!Array.isArray(context) || context.length === 0) return '';
46
+ const blocks = context.map(entry => {
47
+ const evidence = Array.isArray(entry.evidence) && entry.evidence.length > 0
48
+ ? `\nEvidence:\n${entry.evidence.map(item => {
49
+ const status = item.status ? ` [${item.status}]` : '';
50
+ const ref = item.ref ? ` (${item.ref})` : '';
51
+ return `- ${item.kind}: ${item.label}${status}${ref}`;
52
+ }).join('\n')}`
53
+ : '';
54
+ const decision = entry.reviewDecision ? `\nReview decision: ${entry.reviewDecision}` : '';
55
+ const waitingReason = entry.waitingReason ? `\nWaiting reason: ${entry.waitingReason}` : '';
56
+ const answer = entry.answer ? `\nUser answer: ${entry.answer}` : '';
57
+ return `### ${entry.type} (${entry.role || 'unknown role'})\n${entry.summary || '(no summary)'}${decision}${waitingReason}${answer}${evidence}`;
58
+ });
59
+ return `\n\nPrior Action results:\n${blocks.join('\n\n')}`;
60
+ }
61
+
62
+ export function actionInstruction(step, workItem, context = []) {
63
+ const criteria = (workItem.acceptanceCriteria || []).map(item => `- ${item}`).join('\n') || '- No explicit criteria';
64
+ const common = `WorkItem: ${workItem.title}\nGoal: ${workItem.goal}\nAcceptance criteria:\n${criteria}${renderContext(context)}`;
65
+ switch (step.type) {
66
+ case 'triage':
67
+ return `${common}\n\nAnalyze the request, verify scope and risks, and make the contract executable. Do not implement yet. If the goal or acceptance criteria need refinement, submit a contractPatch.`;
68
+ case 'implement':
69
+ return `${common}\n\nImplement the smallest correct change in the supplied work directory. Add and run relevant tests. Use the prior triage/review findings. Do not approve your own work.`;
70
+ case 'review':
71
+ return `${common}\n\nReview the implementation and evidence independently. Return approved or changes_requested with concrete findings.`;
72
+ case 'deliver':
73
+ return `${common}\n\nDeliver the approved change using the repository release policy. Verify the final remote state and provide evidence.`;
74
+ default:
75
+ return common;
76
+ }
77
+ }
78
+
79
+ export function initialActionFor(workItem) {
80
+ const step = getWorkflowSteps(workItem.workflowTemplate)[0];
81
+ return {
82
+ ...step,
83
+ context: [],
84
+ instruction: actionInstruction(step, workItem, []),
85
+ maxAttempts: 2,
86
+ };
87
+ }
88
+
89
+ export function actionContextFromRuns(runs = []) {
90
+ return runs
91
+ .filter(run => run && run.summary)
92
+ .map(run => ({
93
+ type: run.actionType || 'action',
94
+ role: run.roleSnapshot?.id || run.vpSnapshot?.id || null,
95
+ summary: run.summary,
96
+ evidence: Array.isArray(run.evidence) ? run.evidence : [],
97
+ reviewDecision: run.reviewDecision || null,
98
+ }));
99
+ }