@yeaft/webchat-agent 1.0.129 → 1.0.130
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
|
@@ -69,12 +69,29 @@ export class WorkflowController {
|
|
|
69
69
|
workflowTemplate: input.workflowTemplate || 'software-change',
|
|
70
70
|
acceptanceCriteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
|
|
71
71
|
};
|
|
72
|
-
|
|
72
|
+
let firstAction = input.start !== false ? initialActionFor(draft) : null;
|
|
73
|
+
if (firstAction && draft.reuseMemory !== false) {
|
|
74
|
+
const context = this.store.getReusableContext(draft.workDir, draft.id);
|
|
75
|
+
firstAction = {
|
|
76
|
+
...firstAction,
|
|
77
|
+
context,
|
|
78
|
+
instruction: actionInstruction(firstAction, draft, context),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
73
81
|
return this.store.createWorkItem(draft, firstAction);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
start(id) {
|
|
77
|
-
const detail = this.store.startWorkItemAtomic(id,
|
|
85
|
+
const detail = this.store.startWorkItemAtomic(id, workItem => {
|
|
86
|
+
const action = initialActionFor(workItem);
|
|
87
|
+
if (workItem.reuseMemory === false) return action;
|
|
88
|
+
const context = this.store.getReusableContext(workItem.workDir, workItem.id);
|
|
89
|
+
return {
|
|
90
|
+
...action,
|
|
91
|
+
context,
|
|
92
|
+
instruction: actionInstruction(action, workItem, context),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
78
95
|
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
79
96
|
return detail;
|
|
80
97
|
}
|
|
@@ -91,6 +108,35 @@ export class WorkflowController {
|
|
|
91
108
|
return this.store.getWorkItemDetail(id);
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
guide(id, input = {}) {
|
|
112
|
+
const guidance = typeof input.guidance === 'string' ? input.guidance.trim().slice(0, 8_000) : '';
|
|
113
|
+
if (!guidance) throw new Error('guidance is required');
|
|
114
|
+
const expected = {
|
|
115
|
+
actionId: typeof input.actionId === 'string' ? input.actionId : '',
|
|
116
|
+
revision: Number(input.revision),
|
|
117
|
+
};
|
|
118
|
+
if (!expected.actionId || !Number.isInteger(expected.revision)) {
|
|
119
|
+
throw new Error('actionId and revision are required for guidance');
|
|
120
|
+
}
|
|
121
|
+
const detail = this.store.addActionGuidance(id, guidance, expected, (workItem, previous) => {
|
|
122
|
+
const context = [...(previous.context || []), {
|
|
123
|
+
type: 'guidance',
|
|
124
|
+
role: 'user',
|
|
125
|
+
summary: guidance,
|
|
126
|
+
evidence: [],
|
|
127
|
+
}];
|
|
128
|
+
const step = { type: previous.type, requiredRole: previous.requiredRole };
|
|
129
|
+
return {
|
|
130
|
+
...step,
|
|
131
|
+
context,
|
|
132
|
+
instruction: actionInstruction(step, workItem, context),
|
|
133
|
+
maxAttempts: previous.maxAttempts || 2,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
137
|
+
return detail;
|
|
138
|
+
}
|
|
139
|
+
|
|
94
140
|
retry(id, input = {}) {
|
|
95
141
|
const answer = typeof input.answer === 'string' ? input.answer.trim().slice(0, 8_000) : '';
|
|
96
142
|
const detail = this.store.retryWorkItemAtomic(id, (workItem, previous, previousRun) => {
|
|
@@ -54,6 +54,21 @@ function canonicalWorkDir(workDir) {
|
|
|
54
54
|
return realpathSync(workDir);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export function resolveWorkItemWorkDir(workItem, defaultWorkDir) {
|
|
58
|
+
if (typeof workItem?.workspaceKey === 'string' && workItem.workspaceKey) {
|
|
59
|
+
const expected = path.resolve(workItem.workspaceKey);
|
|
60
|
+
const actual = canonicalWorkDir(expected);
|
|
61
|
+
if (actual !== expected) {
|
|
62
|
+
throw new Error('WorkItem canonical workspace identity changed; update its workDir before retrying');
|
|
63
|
+
}
|
|
64
|
+
return expected;
|
|
65
|
+
}
|
|
66
|
+
if (typeof workItem?.workDir === 'string' && workItem.workDir.trim()) {
|
|
67
|
+
throw new Error('WorkItem has no canonical workspace identity; update its workDir before retrying');
|
|
68
|
+
}
|
|
69
|
+
return canonicalWorkDir(path.resolve(defaultWorkDir || process.cwd()));
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
function assertPathInside(toolName, workDir, value) {
|
|
58
73
|
const resolved = path.resolve(workDir, value);
|
|
59
74
|
if (!isPathInsideOrEqual(workDir, resolved)) {
|
|
@@ -203,8 +218,7 @@ export class WorkItemRunner {
|
|
|
203
218
|
|
|
204
219
|
async run({ workItem, action, run, signal, ownerBootId }) {
|
|
205
220
|
const runtime = await this.runtimeProvider();
|
|
206
|
-
const
|
|
207
|
-
const workDir = canonicalWorkDir(path.resolve(rawWorkDir));
|
|
221
|
+
const workDir = resolveWorkItemWorkDir(workItem, runtime.defaultWorkDir);
|
|
208
222
|
const vp = copyVp(this.registry.getVp(action.requiredRole));
|
|
209
223
|
if (!vp) {
|
|
210
224
|
const error = new Error(`Required Work Center role is unavailable: ${action.requiredRole}`);
|
|
@@ -260,7 +274,6 @@ export class WorkItemRunner {
|
|
|
260
274
|
});
|
|
261
275
|
|
|
262
276
|
let text = '';
|
|
263
|
-
const toolEvidence = [];
|
|
264
277
|
try {
|
|
265
278
|
for await (const event of engine.query({
|
|
266
279
|
prompt: `${action.instruction}${completionContract(action)}`,
|
|
@@ -275,18 +288,10 @@ export class WorkItemRunner {
|
|
|
275
288
|
if (typeof event?.text === 'string') text += event.text;
|
|
276
289
|
else if (typeof event?.delta === 'string') text += event.delta;
|
|
277
290
|
else if (typeof event?.content === 'string' && event.type === 'assistant') text += event.content;
|
|
278
|
-
if (event?.type === 'tool_end') {
|
|
279
|
-
toolEvidence.push({
|
|
280
|
-
tool: event.name,
|
|
281
|
-
isError: !!event.isError,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
291
|
}
|
|
285
292
|
} finally {
|
|
286
293
|
try { engine.abort?.('work_item_run_finished'); } catch {}
|
|
287
294
|
}
|
|
288
|
-
|
|
289
|
-
result.evidence = [...result.evidence, ...toolEvidence].slice(-100);
|
|
290
|
-
return result;
|
|
295
|
+
return parseStructuredResult(text, action.type);
|
|
291
296
|
}
|
|
292
297
|
}
|
|
@@ -47,6 +47,7 @@ export class WorkCenterService {
|
|
|
47
47
|
: [],
|
|
48
48
|
workflowTemplate: payload.workflowTemplate || 'software-change',
|
|
49
49
|
workDir: typeof payload.workDir === 'string' ? payload.workDir.trim() : '',
|
|
50
|
+
reuseMemory: payload.reuseMemory !== false,
|
|
50
51
|
origin: payload.origin && typeof payload.origin === 'object'
|
|
51
52
|
? {
|
|
52
53
|
sessionId: typeof payload.origin.sessionId === 'string' ? payload.origin.sessionId : null,
|
|
@@ -82,6 +83,17 @@ export class WorkCenterService {
|
|
|
82
83
|
this.#emit({ type: 'work_item.cancelled', workItem: detail });
|
|
83
84
|
return detail;
|
|
84
85
|
}
|
|
86
|
+
case 'guide': {
|
|
87
|
+
const id = requiredString(payload.id, 'id');
|
|
88
|
+
const detail = this.controller.guide(id, {
|
|
89
|
+
guidance: typeof payload.guidance === 'string' ? payload.guidance : '',
|
|
90
|
+
actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
|
|
91
|
+
revision: payload.revision,
|
|
92
|
+
});
|
|
93
|
+
this.watcher.abortInvalidWorkItemRuns(id);
|
|
94
|
+
this.#emit({ type: 'action.guidance_added', workItem: detail });
|
|
95
|
+
return detail;
|
|
96
|
+
}
|
|
85
97
|
case 'retry': {
|
|
86
98
|
const detail = this.controller.retry(requiredString(payload.id, 'id'), {
|
|
87
99
|
answer: typeof payload.answer === 'string' ? payload.answer : '',
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import { dirname } from 'node:path';
|
|
2
|
+
import { mkdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { normalizeEvidence } from './evidence.js';
|
|
6
6
|
|
|
7
|
-
const SCHEMA_VERSION =
|
|
7
|
+
const SCHEMA_VERSION = 3;
|
|
8
8
|
const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
|
|
9
|
+
const MAX_REUSABLE_CONTEXT_ITEMS = 12;
|
|
10
|
+
|
|
11
|
+
function canonicalWorkspaceKey(workDir) {
|
|
12
|
+
if (typeof workDir !== 'string' || !workDir.trim()) return '';
|
|
13
|
+
try { return realpathSync(resolve(workDir.trim())); } catch { return ''; }
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
function parseJson(value, fallback) {
|
|
11
17
|
if (typeof value !== 'string' || !value) return fallback;
|
|
@@ -29,6 +35,8 @@ function mapWorkItem(row) {
|
|
|
29
35
|
currentActionId: row.current_action_id || null,
|
|
30
36
|
currentRunId: row.current_run_id || null,
|
|
31
37
|
workDir: row.work_dir || '',
|
|
38
|
+
workspaceKey: row.workspace_key || '',
|
|
39
|
+
reuseMemory: row.reuse_memory !== 0,
|
|
32
40
|
origin: parseJson(row.origin, null),
|
|
33
41
|
linkedSessionIds: parseJson(row.linked_session_ids, []),
|
|
34
42
|
createdAt: row.created_at,
|
|
@@ -142,6 +150,8 @@ export class WorkItemStore {
|
|
|
142
150
|
current_action_id TEXT,
|
|
143
151
|
current_run_id TEXT,
|
|
144
152
|
work_dir TEXT NOT NULL DEFAULT '',
|
|
153
|
+
workspace_key TEXT NOT NULL DEFAULT '',
|
|
154
|
+
reuse_memory INTEGER NOT NULL DEFAULT 1,
|
|
145
155
|
origin TEXT,
|
|
146
156
|
linked_session_ids TEXT NOT NULL DEFAULT '[]',
|
|
147
157
|
created_at INTEGER NOT NULL,
|
|
@@ -202,7 +212,20 @@ export class WorkItemStore {
|
|
|
202
212
|
`);
|
|
203
213
|
|
|
204
214
|
// The feature shipped first as an unmerged PR, but keep the store tolerant
|
|
205
|
-
// of
|
|
215
|
+
// of databases created by review builds.
|
|
216
|
+
if (!hasColumn(this.db, 'work_items', 'workspace_key')) {
|
|
217
|
+
withTransaction(this.db, () => {
|
|
218
|
+
this.db.exec("ALTER TABLE work_items ADD COLUMN workspace_key TEXT NOT NULL DEFAULT ''");
|
|
219
|
+
const update = this.db.prepare('UPDATE work_items SET workspace_key = ? WHERE id = ?');
|
|
220
|
+
for (const row of this.db.prepare("SELECT id, work_dir FROM work_items WHERE work_dir != ''").all()) {
|
|
221
|
+
const workspaceKey = canonicalWorkspaceKey(row.work_dir);
|
|
222
|
+
if (workspaceKey) update.run(workspaceKey, row.id);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (!hasColumn(this.db, 'work_items', 'reuse_memory')) {
|
|
227
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN reuse_memory INTEGER NOT NULL DEFAULT 1');
|
|
228
|
+
}
|
|
206
229
|
if (!hasColumn(this.db, 'actions', 'context')) {
|
|
207
230
|
this.db.exec("ALTER TABLE actions ADD COLUMN context TEXT NOT NULL DEFAULT '[]'");
|
|
208
231
|
}
|
|
@@ -244,11 +267,12 @@ export class WorkItemStore {
|
|
|
244
267
|
return withTransaction(this.db, () => {
|
|
245
268
|
const now = this.now();
|
|
246
269
|
const id = input.id || randomUUID();
|
|
270
|
+
const workspaceKey = canonicalWorkspaceKey(input.workDir);
|
|
247
271
|
this.db.prepare(`INSERT INTO work_items
|
|
248
272
|
(id, revision, title, goal, acceptance_criteria, workflow_template, status,
|
|
249
|
-
current_action_id, current_run_id, work_dir, origin, linked_session_ids,
|
|
273
|
+
current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
|
|
250
274
|
created_at, updated_at)
|
|
251
|
-
VALUES (?, 1, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?)`).run(
|
|
275
|
+
VALUES (?, 1, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
252
276
|
id,
|
|
253
277
|
input.title,
|
|
254
278
|
input.goal,
|
|
@@ -256,6 +280,8 @@ export class WorkItemStore {
|
|
|
256
280
|
input.workflowTemplate || 'software-change',
|
|
257
281
|
firstAction ? 'ready' : 'draft',
|
|
258
282
|
input.workDir || '',
|
|
283
|
+
workspaceKey,
|
|
284
|
+
input.reuseMemory === false ? 0 : 1,
|
|
259
285
|
stringify(input.origin || null),
|
|
260
286
|
stringify(input.linkedSessionIds || []),
|
|
261
287
|
now,
|
|
@@ -366,6 +392,64 @@ export class WorkItemStore {
|
|
|
366
392
|
};
|
|
367
393
|
}
|
|
368
394
|
|
|
395
|
+
getReusableContext(workDir, excludeWorkItemId = null) {
|
|
396
|
+
const workspaceKey = canonicalWorkspaceKey(workDir);
|
|
397
|
+
if (!workspaceKey) return [];
|
|
398
|
+
const rows = this.db.prepare(`SELECT r.*, a.type AS action_type, a.required_role,
|
|
399
|
+
w.title AS source_title
|
|
400
|
+
FROM runs r
|
|
401
|
+
JOIN actions a ON a.id = r.action_id
|
|
402
|
+
JOIN work_items w ON w.id = r.work_item_id
|
|
403
|
+
WHERE w.workspace_key = ? AND w.reuse_memory = 1 AND w.id != ? AND w.status = 'done'
|
|
404
|
+
AND r.status = 'completed' AND length(trim(COALESCE(r.summary, ''))) > 0
|
|
405
|
+
ORDER BY r.ended_at DESC, r.started_at DESC
|
|
406
|
+
LIMIT ?`).all(workspaceKey, excludeWorkItemId || '', MAX_REUSABLE_CONTEXT_ITEMS);
|
|
407
|
+
return rows.reverse().map(row => {
|
|
408
|
+
const run = mapRun(row);
|
|
409
|
+
return {
|
|
410
|
+
type: row.action_type,
|
|
411
|
+
role: row.required_role,
|
|
412
|
+
summary: run.summary,
|
|
413
|
+
evidence: run.evidence,
|
|
414
|
+
reviewDecision: run.reviewDecision,
|
|
415
|
+
sourceTitle: row.source_title,
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
addActionGuidance(id, guidance, expected, makeAction) {
|
|
421
|
+
return withTransaction(this.db, () => {
|
|
422
|
+
const workItem = this.getWorkItem(id);
|
|
423
|
+
if (!workItem) return null;
|
|
424
|
+
if (workItem.currentActionId !== expected.actionId || workItem.revision !== expected.revision) {
|
|
425
|
+
throw new Error('Action changed before guidance was applied; refresh and try again');
|
|
426
|
+
}
|
|
427
|
+
if (!['ready', 'running'].includes(workItem.status)) {
|
|
428
|
+
throw new Error(`WorkItem in ${workItem.status} cannot accept Action guidance`);
|
|
429
|
+
}
|
|
430
|
+
const previous = workItem.currentActionId ? this.getAction(workItem.currentActionId) : null;
|
|
431
|
+
if (!previous || !['ready', 'running'].includes(previous.status)) {
|
|
432
|
+
throw new Error('WorkItem has no active Action for guidance');
|
|
433
|
+
}
|
|
434
|
+
const now = this.now();
|
|
435
|
+
this.#invalidateExecution(
|
|
436
|
+
workItem,
|
|
437
|
+
'superseded',
|
|
438
|
+
'superseded',
|
|
439
|
+
'Action restarted after user guidance',
|
|
440
|
+
now,
|
|
441
|
+
);
|
|
442
|
+
const action = this.#insertAction(id, {
|
|
443
|
+
...makeAction(workItem, previous),
|
|
444
|
+
contractRevision: workItem.revision,
|
|
445
|
+
}, this.#nextSequence(id), now);
|
|
446
|
+
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
447
|
+
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(action.id, now, id);
|
|
448
|
+
this.appendEvent(id, 'action.guidance_added', { guidance }, { actionId: action.id });
|
|
449
|
+
return this.getWorkItemDetail(id);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
369
453
|
#invalidateExecution(workItem, actionStatus, runStatus, reason, now) {
|
|
370
454
|
if (workItem.currentRunId) {
|
|
371
455
|
this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, error = ?
|
|
@@ -403,11 +487,12 @@ export class WorkItemStore {
|
|
|
403
487
|
);
|
|
404
488
|
}
|
|
405
489
|
this.db.prepare(`UPDATE work_items SET title = ?, goal = ?, acceptance_criteria = ?,
|
|
406
|
-
work_dir = ?, revision = ?, updated_at = ? WHERE id = ?`).run(
|
|
490
|
+
work_dir = ?, workspace_key = ?, revision = ?, updated_at = ? WHERE id = ?`).run(
|
|
407
491
|
next.title,
|
|
408
492
|
next.goal,
|
|
409
493
|
stringify(next.acceptanceCriteria),
|
|
410
494
|
next.workDir,
|
|
495
|
+
canonicalWorkspaceKey(next.workDir),
|
|
411
496
|
revision,
|
|
412
497
|
now,
|
|
413
498
|
id,
|
|
@@ -54,9 +54,10 @@ function renderContext(context = []) {
|
|
|
54
54
|
const decision = entry.reviewDecision ? `\nReview decision: ${entry.reviewDecision}` : '';
|
|
55
55
|
const waitingReason = entry.waitingReason ? `\nWaiting reason: ${entry.waitingReason}` : '';
|
|
56
56
|
const answer = entry.answer ? `\nUser answer: ${entry.answer}` : '';
|
|
57
|
-
|
|
57
|
+
const source = entry.sourceTitle ? ` from ${entry.sourceTitle}` : '';
|
|
58
|
+
return `### ${entry.type}${source} (${entry.role || 'unknown role'})\n${entry.summary || '(no summary)'}${decision}${waitingReason}${answer}${evidence}`;
|
|
58
59
|
});
|
|
59
|
-
return `\n\
|
|
60
|
+
return `\n\nReusable Work Center context and prior Action results:\n${blocks.join('\n\n')}`;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function actionInstruction(step, workItem, context = []) {
|