@yeaft/webchat-agent 1.0.531 → 1.0.533
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/connection/message-router.js +85 -1
- package/index.js +21 -11
- package/local-runtime/server/handlers/agent-sync.js +8 -2
- package/local-runtime/server/handlers/client-misc.js +40 -0
- package/local-runtime/server/ws-agent.js +2 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +229 -104
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/config-api.js +30 -0
- package/yeaft/config.js +1 -0
- package/yeaft/session.js +3 -2
- package/yeaft/work-center/bridge.js +14 -3
- package/yeaft/work-center/capabilities.js +44 -0
- package/yeaft/work-center/completion-contract.js +11 -3
- package/yeaft/work-center/controller.js +8 -1
- package/yeaft/work-center/coordinator.js +42 -21
- package/yeaft/work-center/dynamic-coordination.js +51 -2
- package/yeaft/work-center/feature.js +24 -7
- package/yeaft/work-center/goal-state.js +135 -0
- package/yeaft/work-center/projection.js +57 -8
- package/yeaft/work-center/resource-control.js +460 -0
- package/yeaft/work-center/runner.js +10 -20
- package/yeaft/work-center/service.js +28 -7
- package/yeaft/work-center/store.js +161 -61
- package/yeaft/work-center/workflow.js +7 -3
|
Binary file
|
package/package.json
CHANGED
package/yeaft/config-api.js
CHANGED
|
@@ -18,6 +18,7 @@ import { normaliseBrowserRuntimeSection, validateBrowserRuntimeUpdate } from '..
|
|
|
18
18
|
import { normalizePluginConfig } from './plugins.js';
|
|
19
19
|
import { mutateAgentConfig, readAgentConfigForWrite } from './config-store.js';
|
|
20
20
|
import { isGitHubCopilotProvider, normalizeKnownProviderForRuntime, serializeKnownProviderForPersistence } from './llm/known-providers.js';
|
|
21
|
+
import { getWorkCenterFeatureState } from './work-center/feature.js';
|
|
21
22
|
|
|
22
23
|
/** Agent-owned model catalog, with the same protocol/capability resolution as runtime. */
|
|
23
24
|
function quickSendModels(config) {
|
|
@@ -393,6 +394,35 @@ export function updateTelemetrySettings(update, dir) {
|
|
|
393
394
|
}
|
|
394
395
|
}
|
|
395
396
|
|
|
397
|
+
export function getWorkCenterFeatureSettings(dir, env = process.env) {
|
|
398
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
399
|
+
const configPath = join(root, 'config.json');
|
|
400
|
+
try {
|
|
401
|
+
const json = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : {};
|
|
402
|
+
return getWorkCenterFeatureState(json, env);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return { error: `Failed to read config.json: ${error.message}` };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function updateWorkCenterFeatureSettings(update, dir, env = process.env) {
|
|
409
|
+
if (!update || typeof update !== 'object' || Array.isArray(update) || typeof update.enabled !== 'boolean'
|
|
410
|
+
|| Object.keys(update).some(key => key !== 'enabled')) return { error: 'enabled must be a boolean' };
|
|
411
|
+
const current = getWorkCenterFeatureSettings(dir, env);
|
|
412
|
+
if (current.error) return current;
|
|
413
|
+
if (current.overridden) return { ...current, error: 'Work Center is controlled by YEAFT_WORK_CENTER_ENABLED' };
|
|
414
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
415
|
+
try {
|
|
416
|
+
mutateAgentConfig(root, existing => {
|
|
417
|
+
existing.workCenter = { ...(existing.workCenter && typeof existing.workCenter === 'object' ? existing.workCenter : {}), enabled: update.enabled };
|
|
418
|
+
return existing.workCenter;
|
|
419
|
+
});
|
|
420
|
+
return { enabled: update.enabled, source: 'config', overridden: false };
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return { error: `Failed to read config.json or persist update: ${error?.message || error}` };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
396
426
|
// ─── Browser Runtime settings ───────────────────────────────────────
|
|
397
427
|
|
|
398
428
|
export function getBrowserRuntimeSettings(dir) {
|
package/yeaft/config.js
CHANGED
|
@@ -544,6 +544,7 @@ export function loadConfig(overrides = {}) {
|
|
|
544
544
|
// don't pollute the flat config namespace used by chat code.
|
|
545
545
|
yeaft: normaliseYeaftSection(jsonConfig.yeaft),
|
|
546
546
|
telemetry: normaliseTelemetrySection(jsonConfig.telemetry),
|
|
547
|
+
workCenter: { enabled: jsonConfig.workCenter?.enabled === true },
|
|
547
548
|
dream: { enabled: jsonConfig.dream?.enabled === true },
|
|
548
549
|
browserRuntime: normaliseBrowserRuntimeSection(jsonConfig.browserRuntime),
|
|
549
550
|
|
package/yeaft/session.js
CHANGED
|
@@ -141,7 +141,7 @@ export async function loadSession(options = {}) {
|
|
|
141
141
|
serverMode = false,
|
|
142
142
|
dreamEnabled,
|
|
143
143
|
managedCliReady = null,
|
|
144
|
-
workCenterEnabled
|
|
144
|
+
workCenterEnabled,
|
|
145
145
|
} = options;
|
|
146
146
|
|
|
147
147
|
// ─── 1. Determine config + store directories ─────────────
|
|
@@ -173,6 +173,7 @@ export async function loadSession(options = {}) {
|
|
|
173
173
|
|
|
174
174
|
// ─── 2. Load config ───────────────────────────────────
|
|
175
175
|
const config = loadConfig(overrides);
|
|
176
|
+
const effectiveWorkCenterEnabled = workCenterEnabled ?? isWorkCenterEnabled(process.env, config);
|
|
176
177
|
// fix/dream-cadence-and-ui-trigger: tag config so the dream scheduler
|
|
177
178
|
// can decide whether to keep its interval timer alive (server) or
|
|
178
179
|
// unref it (CLI / tests). Non-persisted — set per-session by caller.
|
|
@@ -412,7 +413,7 @@ export async function loadSession(options = {}) {
|
|
|
412
413
|
// ─── 8. Build tool registry ────────────────────────────
|
|
413
414
|
const taskManager = new TaskManager({ yeaftDir });
|
|
414
415
|
const toolRegistry = createFullRegistry();
|
|
415
|
-
if (!
|
|
416
|
+
if (!effectiveWorkCenterEnabled) toolRegistry.unregister('CreateWorkItem');
|
|
416
417
|
|
|
417
418
|
// Register any extra tools from caller
|
|
418
419
|
for (const tool of extraTools) {
|
|
@@ -18,9 +18,10 @@ let initPromise = null;
|
|
|
18
18
|
let shuttingDown = false;
|
|
19
19
|
let shutdownPromise = null;
|
|
20
20
|
let serviceFactory = null;
|
|
21
|
+
let featureEnabled = false;
|
|
21
22
|
|
|
22
23
|
const BROWSER_DETAIL_OPS = new Set([
|
|
23
|
-
'get', 'create', 'update', 'start', 'cancel', 'resume', 'post_work_item_message', 'action_input', 'retry_action', 'guide', 'retry',
|
|
24
|
+
'get', 'create', 'update', 'start', 'cancel', 'resume', 'extend_budget', 'post_work_item_message', 'action_input', 'retry_action', 'guide', 'retry',
|
|
24
25
|
]);
|
|
25
26
|
const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
|
|
26
27
|
// `files` is an internal server-to-Agent field. The browser relay rejects any
|
|
@@ -39,7 +40,8 @@ const BROWSER_FILE_FIELDS = Object.freeze({
|
|
|
39
40
|
],
|
|
40
41
|
action_input: ['id', 'text', 'actionId', 'revision', 'generation', 'quote', 'files'],
|
|
41
42
|
retry_action: ['id', 'actionId', 'revision', 'generation'],
|
|
42
|
-
resume: ['id', 'revision'],
|
|
43
|
+
resume: ['id', 'revision', 'executionControlRevision'],
|
|
44
|
+
extend_budget: ['id', 'executionControlRevision', 'additions'],
|
|
43
45
|
delete: ['id', 'revision'],
|
|
44
46
|
guide: ['id', 'guidance', 'actionId', 'revision', 'generation', 'files'],
|
|
45
47
|
get_action_messages: ['id', 'actionId', 'generation', 'cursor', 'limit'],
|
|
@@ -160,8 +162,9 @@ async function createDefaultService() {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
async function ensureWorkCenter() {
|
|
163
|
-
if (
|
|
165
|
+
if (!featureEnabled) throw new Error('Work Center is disabled');
|
|
164
166
|
if (shuttingDown) throw new Error('Work Center is shutting down');
|
|
167
|
+
if (service) return service;
|
|
165
168
|
if (initPromise) return initPromise;
|
|
166
169
|
initPromise = (async () => {
|
|
167
170
|
const created = serviceFactory ? await serviceFactory() : await createDefaultService();
|
|
@@ -181,9 +184,14 @@ async function ensureWorkCenter() {
|
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
export async function bootWorkCenter() {
|
|
187
|
+
featureEnabled = true;
|
|
184
188
|
return ensureWorkCenter();
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
export function setWorkCenterFeatureEnabled(enabled) {
|
|
192
|
+
featureEnabled = enabled === true;
|
|
193
|
+
}
|
|
194
|
+
|
|
187
195
|
export async function snapshotCurrentSessionContext(sessionId) {
|
|
188
196
|
const runtime = await getRuntime();
|
|
189
197
|
return snapshotSessionContext(runtime?.conversationStore, sessionId);
|
|
@@ -268,6 +276,7 @@ export async function shutdownWorkCenter() {
|
|
|
268
276
|
} finally {
|
|
269
277
|
initPromise = null;
|
|
270
278
|
shutdownPromise = null;
|
|
279
|
+
shuttingDown = false;
|
|
271
280
|
}
|
|
272
281
|
}
|
|
273
282
|
|
|
@@ -276,6 +285,7 @@ export function __testSetWorkCenterService(next) {
|
|
|
276
285
|
initPromise = null;
|
|
277
286
|
shuttingDown = false;
|
|
278
287
|
shutdownPromise = null;
|
|
288
|
+
featureEnabled = !!next;
|
|
279
289
|
}
|
|
280
290
|
|
|
281
291
|
export function __testSetWorkCenterFactory(factory) {
|
|
@@ -284,4 +294,5 @@ export function __testSetWorkCenterFactory(factory) {
|
|
|
284
294
|
initPromise = null;
|
|
285
295
|
shuttingDown = false;
|
|
286
296
|
shutdownPromise = null;
|
|
297
|
+
featureEnabled = false;
|
|
287
298
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Center reuses builtin tool implementations, but owns their lifecycle.
|
|
3
|
+
* Keep the executable subset and the Coordinator's capability description in
|
|
4
|
+
* one place. This is a host policy, not a claim that shell/MCP is sandboxed.
|
|
5
|
+
*/
|
|
6
|
+
export const WORK_ITEM_TOOL_NAMES = Object.freeze([
|
|
7
|
+
'FileRead', 'FileWrite', 'FileEdit', 'ApplyPatch', 'Glob', 'Grep',
|
|
8
|
+
'ListDir', 'Bash', 'WebSearch', 'WebFetch', 'ViewImage', 'Skill',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function workItemBuiltinToolNames(hasAttachments = false) {
|
|
12
|
+
return WORK_ITEM_TOOL_NAMES.filter(name => !hasAttachments || name !== 'Bash');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Only identities/roles needed for assignment, never VP souls or credentials. */
|
|
16
|
+
export function workItemCapabilityContext(vps = [], { hasAttachments = false } = {}) {
|
|
17
|
+
const bounded = (value, limit) => typeof value === 'string' ? value.slice(0, limit) : '';
|
|
18
|
+
const selected = [];
|
|
19
|
+
for (const vp of vps.slice(0, 48)) {
|
|
20
|
+
// An id must round-trip to the registry; never offer a truncated identity.
|
|
21
|
+
if (typeof vp.id !== 'string' || !vp.id || vp.id.length > 128) continue;
|
|
22
|
+
const candidate = {
|
|
23
|
+
id: vp.id,
|
|
24
|
+
name: bounded(vp.name, 120),
|
|
25
|
+
role: bounded(vp.role, 200),
|
|
26
|
+
traits: (Array.isArray(vp.traits) ? vp.traits : []).slice(0, 8).map(value => bounded(value, 80)),
|
|
27
|
+
};
|
|
28
|
+
if (Buffer.byteLength(JSON.stringify([...selected, candidate]), 'utf8') > 8 * 1024) break;
|
|
29
|
+
selected.push(candidate);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
executor: 'yeaft-engine',
|
|
33
|
+
tools: workItemBuiltinToolNames(hasAttachments),
|
|
34
|
+
vps: selected,
|
|
35
|
+
omittedVpCount: Math.max(0, vps.length - selected.length),
|
|
36
|
+
mcp: 'Workspace-configured MCP tools are resolved by the executor. Availability and authorization must be verified, not inferred from a VP name.',
|
|
37
|
+
limitations: [
|
|
38
|
+
'No unmanaged background jobs, recursive sub-agents, or Session transcript access.',
|
|
39
|
+
'VP roles provide expertise, not missing tools, credentials, permissions, or external environments.',
|
|
40
|
+
'Use one Action for local investigation, implementation and tests when no independent boundary is needed.',
|
|
41
|
+
'Missing permission or an unavailable required capability is a blocker, not a reason to create more roles.',
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -14,20 +14,28 @@ export function normalizeContractPatch(value) {
|
|
|
14
14
|
patch.acceptanceCriteria = criteria;
|
|
15
15
|
}
|
|
16
16
|
if (Object.hasOwn(value, 'deliveryTarget')) {
|
|
17
|
-
if (!['workspace_files', 'pull_request', 'merge'].includes(value.deliveryTarget)) {
|
|
18
|
-
throw new Error('contractPatch.deliveryTarget must be workspace_files, pull_request, or merge');
|
|
17
|
+
if (!['response', 'workspace_files', 'pull_request', 'merge'].includes(value.deliveryTarget)) {
|
|
18
|
+
throw new Error('contractPatch.deliveryTarget must be response, workspace_files, pull_request, or merge');
|
|
19
19
|
}
|
|
20
20
|
patch.deliveryTarget = value.deliveryTarget;
|
|
21
21
|
}
|
|
22
22
|
return Object.keys(patch).length > 0 ? patch : null;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Reject even falsy/raw patch fields before normalization can hide an attempted change.
|
|
26
|
+
export function assertCoordinatorContractAuthority(value, userOriginated) {
|
|
27
|
+
if (userOriginated === true || !value || typeof value !== 'object') return;
|
|
28
|
+
if (['title', 'goal', 'acceptanceCriteria', 'deliveryTarget'].some(key => Object.hasOwn(value, key))) {
|
|
29
|
+
throw new Error('Automatic Work Center Coordinator contract and delivery target changes are forbidden; refinement requires a user-originated turn');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
function normalizeAcceptanceChecks(value, criteria) {
|
|
26
34
|
if (!Array.isArray(value) || value.length !== criteria.length) return null;
|
|
27
35
|
const checks = value.map((raw, index) => {
|
|
28
36
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
29
37
|
const criterion = typeof raw.criterion === 'string' ? raw.criterion.trim() : '';
|
|
30
|
-
const status = ['passed', 'deferred', 'not_applicable'].includes(raw.status) ? raw.status : '';
|
|
38
|
+
const status = ['passed', 'failed', 'deferred', 'not_applicable'].includes(raw.status) ? raw.status : '';
|
|
31
39
|
const evidence = typeof raw.evidence === 'string' ? raw.evidence.trim().slice(0, 1_000) : '';
|
|
32
40
|
if (criterion !== criteria[index] || !status || !evidence) return null;
|
|
33
41
|
return { criterion, status, evidence };
|
|
@@ -176,6 +176,13 @@ export class WorkflowController {
|
|
|
176
176
|
return this.store.getWorkItemDetail(id);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
extendBudget(id, input = {}) {
|
|
180
|
+
if (!Number.isSafeInteger(input.executionControlRevision)) {
|
|
181
|
+
throw new Error('executionControlRevision is required to extend execution budget');
|
|
182
|
+
}
|
|
183
|
+
return this.store.extendExecutionBudget(id, input.executionControlRevision, input.additions || {});
|
|
184
|
+
}
|
|
185
|
+
|
|
179
186
|
resume(id, input = {}) {
|
|
180
187
|
const revision = Number(input.revision);
|
|
181
188
|
if (!Number.isInteger(revision) || revision < 1) {
|
|
@@ -195,7 +202,7 @@ export class WorkflowController {
|
|
|
195
202
|
renderSessionContextSnapshot(workItem.sessionContext),
|
|
196
203
|
),
|
|
197
204
|
};
|
|
198
|
-
});
|
|
205
|
+
}, input.executionControlRevision);
|
|
199
206
|
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
200
207
|
return detail;
|
|
201
208
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { resolveMaxOutputTokens } from '../models.js';
|
|
3
|
+
import { callCoordinatorWithResourceControl } from './resource-control.js';
|
|
3
4
|
import { normalizeSessionMessageQuote, sessionMessageQuotePrompt } from '../session-message-quote.js';
|
|
4
5
|
import {
|
|
5
6
|
LLMAuthError,
|
|
@@ -8,8 +9,9 @@ import {
|
|
|
8
9
|
LLMServerError,
|
|
9
10
|
} from '../llm/adapter.js';
|
|
10
11
|
import { resolveWorkItemModel, selectWorkItemVp } from './assignment.js';
|
|
11
|
-
import { normalizeContractPatch } from './completion-contract.js';
|
|
12
|
+
import { assertCoordinatorContractAuthority, normalizeContractPatch } from './completion-contract.js';
|
|
12
13
|
import { normalizeOutputs } from './evidence.js';
|
|
14
|
+
import { deriveGoalProgress } from './goal-state.js';
|
|
13
15
|
import {
|
|
14
16
|
normalizeDynamicActionClosures,
|
|
15
17
|
prepareDynamicActionMutation,
|
|
@@ -19,6 +21,7 @@ import { applyCoordinatorReplan } from './plan-mutation.js';
|
|
|
19
21
|
import { buildWorkItemAttachmentContext } from './attachments.js';
|
|
20
22
|
import { sanitizeDiagnosticText } from './debug-projection.js';
|
|
21
23
|
import { generatedActionGraphRules } from './workflow.js';
|
|
24
|
+
import { workItemCapabilityContext } from './capabilities.js';
|
|
22
25
|
|
|
23
26
|
const COORDINATOR_MAX_REPLY_CHARS = 8_000;
|
|
24
27
|
const COORDINATOR_MAX_INSTRUCTION_CHARS = 8_000;
|
|
@@ -184,7 +187,7 @@ const COORDINATOR_SYSTEM_PROMPT = `You are the Work Center Coordinator. The user
|
|
|
184
187
|
|
|
185
188
|
Your responsibilities:
|
|
186
189
|
- Explain the current WorkItem state and blockers in plain language.
|
|
187
|
-
-
|
|
190
|
+
- Change title, goal, acceptance criteria, or delivery target only in an explicit user-originated refinement turn. Automatic advance/recovery must preserve the user contract and address its gaps, never relax it.
|
|
188
191
|
- Give targeted instructions to unfinished Actions when the contract and topology do not need to change.
|
|
189
192
|
- Replan unfinished work when the goal, acceptance criteria, Action purpose, dependencies, or validation strategy must change.
|
|
190
193
|
- Preserve completed Action history. Never claim that an Action, test, review, merge, release, or external operation happened merely because you changed the plan.
|
|
@@ -237,14 +240,17 @@ Return exactly one JSON object and no surrounding prose:
|
|
|
237
240
|
|
|
238
241
|
Rules:
|
|
239
242
|
- answer: explain state only. Never use it for an automatic advance trigger.
|
|
243
|
+
- 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.
|
|
240
244
|
- 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.
|
|
241
|
-
-
|
|
245
|
+
- 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.
|
|
242
246
|
- 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.
|
|
243
247
|
- guide_actions: target 1..8 unfinished non-running Actions by durable actionId.
|
|
244
|
-
- request_human: use when external information or a user decision is genuinely required. Before creating mutating or delivery Actions, ask whether the delivery boundary is
|
|
248
|
+
- request_human: use when external information or a user decision is genuinely required. Before creating mutating or delivery Actions, ask whether the delivery boundary is a response/report, workspace files, PR, or merge when the contract does not already say. After the user answers, persist it with contractPatch.deliveryTarget = response | workspace_files | pull_request | merge before creating more Actions.
|
|
245
249
|
- complete: only when every acceptance criterion has canonical completed Run evidence and there are no unfinished Actions after applying optional closeActions. Include summary, ordered acceptanceResults with evidenceRunIds, evidenceRunIds, and residualRisks. Reuse structured outputs already present on canonical Runs; do not create repetitive evidence-packaging Actions.
|
|
246
250
|
- Preserve completed and closed Action history. Never claim tests, review, merge, release, or external effects without canonical Run evidence.
|
|
247
|
-
- Action templates are reusable capabilities, not a prescribed workflow.
|
|
251
|
+
- Action templates are reusable capabilities, not a prescribed workflow. A simple Action includes its local tools and necessary tests; do not impose research/design/implement/test/review/deliver stages.
|
|
252
|
+
- Read goalProgress.remainingCriteria, delivery, and blockers first. Resource limits are shared by coordination and all execution, including retries; reserve enough for verification and delivery. Never create a new Action or role merely to evade an exhausted attempt limit. Each new Action must close a concrete current gap. Prefer optional goalRefs: {"criteria":["exact unmet criterion"],"blockerActionIds":["current blocker Action id"],"delivery":false}, plus rationale explaining why this work changes the observed state. Repeating an objective requires goalRefs and a concrete new rationale; do not package already sufficient evidence.
|
|
253
|
+
- response delivery is a substantive answer/report in a canonical completed Run summary with evidence and valid checks; do not invent a file, PR, or extra delivery Action.
|
|
248
254
|
- Never return destructive cancellation. The user owns the explicit cancel control.`;
|
|
249
255
|
|
|
250
256
|
function coordinatorSystemPrompt(language, detail) {
|
|
@@ -438,6 +444,8 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
438
444
|
const source = parsed?.decision && typeof parsed.decision === 'object' && !Array.isArray(parsed.decision)
|
|
439
445
|
? parsed.decision
|
|
440
446
|
: {};
|
|
447
|
+
const automatic = options.automatic === true || (options.recovery === true && options.userOriginated !== true);
|
|
448
|
+
assertCoordinatorContractAuthority(source.contractPatch, !automatic && options.userOriginated !== false);
|
|
441
449
|
const dynamic = isDynamicWorkItem(detail);
|
|
442
450
|
const allowedKinds = dynamic
|
|
443
451
|
? (options.automatic === true
|
|
@@ -474,9 +482,6 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
474
482
|
};
|
|
475
483
|
}
|
|
476
484
|
if (kind === 'request_human') {
|
|
477
|
-
if (options.automatic === true && source.contractPatch?.deliveryTarget) {
|
|
478
|
-
throw new Error('Automatic Work Center Coordinator delivery target changes are forbidden');
|
|
479
|
-
}
|
|
480
485
|
const contractPatch = dynamic ? normalizeContractPatch(source.contractPatch) : null;
|
|
481
486
|
return {
|
|
482
487
|
reply,
|
|
@@ -505,9 +510,6 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
505
510
|
};
|
|
506
511
|
}
|
|
507
512
|
if (dynamic && kind === 'create_actions') {
|
|
508
|
-
if (options.automatic === true && source.contractPatch?.deliveryTarget) {
|
|
509
|
-
throw new Error('Automatic Work Center Coordinator delivery target changes are forbidden');
|
|
510
|
-
}
|
|
511
513
|
const contractPatch = normalizeContractPatch(source.contractPatch);
|
|
512
514
|
if (requiresDeliveryBoundaryDecision(detail, source.actions)) {
|
|
513
515
|
throw new Error('Work Center delivery target is unconfirmed; the delivery boundary requires request_human before creating mutating or delivery Actions');
|
|
@@ -530,6 +532,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
530
532
|
actions: detail.actions || [],
|
|
531
533
|
decision,
|
|
532
534
|
availableVpIds: options.availableVpIds,
|
|
535
|
+
automatic,
|
|
533
536
|
}),
|
|
534
537
|
};
|
|
535
538
|
}
|
|
@@ -583,7 +586,7 @@ function finalizedCriteria(detail, contractPatch) {
|
|
|
583
586
|
return criteria;
|
|
584
587
|
}
|
|
585
588
|
|
|
586
|
-
function coordinatorSnapshot(detail) {
|
|
589
|
+
export function coordinatorSnapshot(detail) {
|
|
587
590
|
const runs = Array.isArray(detail.runs) ? detail.runs : [];
|
|
588
591
|
const canonicalRunByAction = new Map();
|
|
589
592
|
for (const action of detail.actions || []) {
|
|
@@ -655,8 +658,25 @@ function coordinatorSnapshot(detail) {
|
|
|
655
658
|
throw new Error('Active Actions cannot be represented within the Coordinator snapshot budget');
|
|
656
659
|
}
|
|
657
660
|
|
|
661
|
+
const progress = deriveGoalProgress(detail);
|
|
662
|
+
const goalProgress = {
|
|
663
|
+
contractRevision: progress.contractRevision,
|
|
664
|
+
completedCriteriaCount: progress.completedCriteriaCount,
|
|
665
|
+
totalCriteriaCount: progress.totalCriteriaCount,
|
|
666
|
+
remainingCriteria: boundedJsonArray(progress.remainingCriteria.map(value => truncateUtf8(value, 768)), 2 * 1024),
|
|
667
|
+
delivery: { ...progress.delivery, evidenceRunIds: progress.delivery.evidenceRunIds.slice(0, 24) },
|
|
668
|
+
criteria: boundedJsonArray(progress.criteria.map(item => ({ ...item,
|
|
669
|
+
criterion: truncateUtf8(item.criterion, 768), evidenceRunIds: item.evidenceRunIds.slice(0, 24),
|
|
670
|
+
...(item.conflictingRunIds ? { conflictingRunIds: item.conflictingRunIds.slice(0, 24) } : {}),
|
|
671
|
+
})), 2 * 1024),
|
|
672
|
+
blockers: boundedJsonArray(progress.blockers.map(item => ({ ...item, reason: truncateUtf8(item.reason, 384) })), 1 * 1024),
|
|
673
|
+
};
|
|
674
|
+
goalProgress.omittedCriteriaCount = progress.criteria.length - goalProgress.criteria.length;
|
|
675
|
+
goalProgress.omittedRemainingCriteriaCount = progress.remainingCriteria.length - goalProgress.remainingCriteria.length;
|
|
676
|
+
goalProgress.omittedBlockerCount = progress.blockers.length - goalProgress.blockers.length;
|
|
658
677
|
return {
|
|
659
678
|
workItem,
|
|
679
|
+
goalProgress,
|
|
660
680
|
actions,
|
|
661
681
|
omittedCompletedActionCount: Math.max(0, completed.length - actions.filter(action => ['completed', 'closed'].includes(action.status)).length),
|
|
662
682
|
conversation: coordinatorHistory(detail.messages),
|
|
@@ -772,7 +792,8 @@ export class WorkItemCoordinator {
|
|
|
772
792
|
: detail?.actions?.find(candidate => (
|
|
773
793
|
candidate.id === detail.currentActionId && candidate.status === 'failed'
|
|
774
794
|
));
|
|
775
|
-
if (!detail || ['done', 'cancelled'].includes(detail.status) || action?.status !== 'failed'
|
|
795
|
+
if (!detail || ['done', 'cancelled'].includes(detail.status) || action?.status !== 'failed'
|
|
796
|
+
|| !this.store.canAutomaticallyCoordinate(id)) return null;
|
|
776
797
|
const started = this.store.beginCoordinatorTurn(id, '', {
|
|
777
798
|
revision: detail.revision,
|
|
778
799
|
planRevision: detail.planRevision,
|
|
@@ -896,7 +917,10 @@ export class WorkItemCoordinator {
|
|
|
896
917
|
try {
|
|
897
918
|
let result;
|
|
898
919
|
try {
|
|
899
|
-
const
|
|
920
|
+
const capabilities = JSON.stringify(workItemCapabilityContext(vps, {
|
|
921
|
+
hasAttachments: started.detail.attachments?.length > 0,
|
|
922
|
+
}));
|
|
923
|
+
const latestMessage = `Available execution capabilities (role metadata is descriptive, not authorization):\n${capabilities}\n\nCurrent WorkItem snapshot:\n${snapshotText}\n\n${recovery ? 'Automatic failure recovery trigger' : 'Latest user message'}:\n${text}${attachmentContext.promptBlock}${correction}`;
|
|
900
924
|
const content = attachmentContext.promptParts.length > 0
|
|
901
925
|
? [{ type: 'text', text: latestMessage }, ...attachmentContext.promptParts]
|
|
902
926
|
: latestMessage;
|
|
@@ -925,15 +949,9 @@ export class WorkItemCoordinator {
|
|
|
925
949
|
result = providerTurn.response;
|
|
926
950
|
} else {
|
|
927
951
|
result = await Promise.race([
|
|
928
|
-
runtime.adapter.
|
|
952
|
+
callCoordinatorWithResourceControl(runtime.adapter, this.store, providerTurn, claim, {
|
|
929
953
|
...requestBody,
|
|
930
954
|
signal: abortController.signal,
|
|
931
|
-
onRequestStart: () => {
|
|
932
|
-
if (!this.store.dispatchCoordinatorProviderTurn(providerTurn.id, claim)) {
|
|
933
|
-
abortController.abort('work_center_coordinator_dispatch_fence_lost');
|
|
934
|
-
throw new Error('Coordinator provider turn lost its dispatch fence');
|
|
935
|
-
}
|
|
936
|
-
},
|
|
937
955
|
}).then(response => {
|
|
938
956
|
const persisted = this.store.respondCoordinatorProviderTurn(
|
|
939
957
|
providerTurn.id, providerTurn.requestHash, response, claim,
|
|
@@ -956,6 +974,7 @@ export class WorkItemCoordinator {
|
|
|
956
974
|
normalized = normalizeCoordinatorResponse(result?.text, started.detail, {
|
|
957
975
|
recovery,
|
|
958
976
|
automatic: started.fence.automatic === true,
|
|
977
|
+
userOriginated: started.fence.userOriginated === true,
|
|
959
978
|
recoveryActionId: started.fence.recovery?.actionId || null,
|
|
960
979
|
availableVpIds: vps.map(vp => vp.id),
|
|
961
980
|
});
|
|
@@ -1037,12 +1056,14 @@ export class WorkItemCoordinator {
|
|
|
1037
1056
|
: recovery ? 'coordinator.recovery_completed' : 'coordinator.turn_completed', detail);
|
|
1038
1057
|
return detail;
|
|
1039
1058
|
} catch (error) {
|
|
1059
|
+
if (providerTurn) this.store.settleCoordinatorRequest(providerTurn.id, null, false);
|
|
1040
1060
|
if (providerTurn?.status === 'responded') {
|
|
1041
1061
|
this.store.rejectCoordinatorProviderTurn(providerTurn.id, error, started.fence.claim);
|
|
1042
1062
|
}
|
|
1043
1063
|
const detail = this.store.failCoordinatorTurn(started.turnId, error, {
|
|
1044
1064
|
...started.fence,
|
|
1045
1065
|
speaker,
|
|
1066
|
+
interrupted: abortController.signal.aborted || this.shuttingDown,
|
|
1046
1067
|
});
|
|
1047
1068
|
if (detail) {
|
|
1048
1069
|
options.onUpdate?.('coordinator.turn_failed', detail);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { assertCoordinatorContractAuthority, normalizeContractPatch } from './completion-contract.js';
|
|
3
|
+
import { deriveGoalProgress } from './goal-state.js';
|
|
2
4
|
import {
|
|
3
5
|
BUILT_IN_ACTION_TYPES,
|
|
4
6
|
MAX_WORK_ITEM_ACTIONS,
|
|
@@ -110,7 +112,9 @@ export function prepareDynamicActionMutation({
|
|
|
110
112
|
actions,
|
|
111
113
|
decision,
|
|
112
114
|
availableVpIds = null,
|
|
115
|
+
automatic = false,
|
|
113
116
|
}) {
|
|
117
|
+
assertCoordinatorContractAuthority(decision?.contractPatch, !automatic);
|
|
114
118
|
if (!isDynamicWorkItem(workItem)) {
|
|
115
119
|
throw new Error('Work Center dynamic Action creation requires a Coordinator-driven WorkItem');
|
|
116
120
|
}
|
|
@@ -131,10 +135,20 @@ export function prepareDynamicActionMutation({
|
|
|
131
135
|
if (supersedeActionIds.some(actionId => closeActionIds.has(actionId))) {
|
|
132
136
|
throw new Error('Work Center cannot both close and supersede the same Action');
|
|
133
137
|
}
|
|
138
|
+
const contractPatch = normalizeContractPatch(decision.contractPatch);
|
|
134
139
|
const effectiveWorkItem = {
|
|
135
140
|
...workItem,
|
|
136
|
-
...(
|
|
141
|
+
...(contractPatch || {}),
|
|
137
142
|
};
|
|
143
|
+
if (contractPatch && ['title', 'goal', 'acceptanceCriteria', 'deliveryTarget']
|
|
144
|
+
.some(key => JSON.stringify(effectiveWorkItem[key]) !== JSON.stringify(workItem[key]))) {
|
|
145
|
+
effectiveWorkItem.revision = (Number(workItem.revision) || 1) + 1;
|
|
146
|
+
}
|
|
147
|
+
const progress = deriveGoalProgress({ ...effectiveWorkItem, actions });
|
|
148
|
+
if (progress.totalCriteriaCount > 0 && !progress.remainingCriteria.length
|
|
149
|
+
&& progress.delivery.status === 'passed' && !progress.blockers.length) {
|
|
150
|
+
throw new Error('Action creation requires an unmet goal condition or blocker; current evidence already supports completion');
|
|
151
|
+
}
|
|
138
152
|
const createdActions = requested.map((raw, index) => {
|
|
139
153
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
140
154
|
throw new Error('Work Center Coordinator Action specification must be an object');
|
|
@@ -187,6 +201,18 @@ export function prepareDynamicActionMutation({
|
|
|
187
201
|
throw new Error(`Work Center integrate Action source is not a completed isolated write: ${invalid}`);
|
|
188
202
|
}
|
|
189
203
|
}
|
|
204
|
+
// Optional refs are persisted inside the existing brief envelope, keeping
|
|
205
|
+
// historical Actions readable without a schema migration.
|
|
206
|
+
const goalRefs = normalizeDynamicGoalRefs(raw.goalRefs, effectiveWorkItem, actions);
|
|
207
|
+
const repeated = actions.some(previous => !['closed', 'superseded', 'cancelled'].includes(previous.status)
|
|
208
|
+
&& previous.type === type && previous.brief?.objective === brief.objective)
|
|
209
|
+
|| requested.slice(0, index).some(previous => previous.type === type && previous.objective?.trim() === brief.objective);
|
|
210
|
+
const rationale = raw.rationale || (goalRefs ? raw.goalRefs.rationale : '');
|
|
211
|
+
if (repeated && (!goalRefs || typeof rationale !== 'string' || rationale.trim().length < 20)) {
|
|
212
|
+
throw new Error('Repeated Action requires goalRefs and a concrete rationale explaining the remaining gap or changed evidence');
|
|
213
|
+
}
|
|
214
|
+
if (goalRefs) brief.goalRefs = goalRefs;
|
|
215
|
+
if (typeof rationale === 'string' && rationale.trim()) brief.rationale = rationale.trim().slice(0, 2_000);
|
|
190
216
|
const action = {
|
|
191
217
|
id,
|
|
192
218
|
type,
|
|
@@ -233,11 +259,34 @@ export function prepareDynamicActionMutation({
|
|
|
233
259
|
createdActions,
|
|
234
260
|
closeActions,
|
|
235
261
|
supersedeActionIds,
|
|
236
|
-
contractPatch
|
|
262
|
+
contractPatch,
|
|
237
263
|
workItemType,
|
|
238
264
|
};
|
|
239
265
|
}
|
|
240
266
|
|
|
267
|
+
/** Optional explicit connection to an unmet criterion, delivery boundary, or blocker. */
|
|
268
|
+
export function normalizeDynamicGoalRefs(value, workItem, actions = []) {
|
|
269
|
+
if (value == null) return null;
|
|
270
|
+
if (typeof value !== 'object' || Array.isArray(value)) throw new Error('Action goalRefs must be an object');
|
|
271
|
+
const criteria = uniqueStrings(value.criteria);
|
|
272
|
+
const blockerActionIds = uniqueStrings(value.blockerActionIds);
|
|
273
|
+
const delivery = value.delivery === true;
|
|
274
|
+
const progress = deriveGoalProgress({ ...workItem, actions });
|
|
275
|
+
if (!criteria.length && !blockerActionIds.length && !delivery) {
|
|
276
|
+
throw new Error('Action goalRefs must address an unmet criterion, delivery condition, or blocker');
|
|
277
|
+
}
|
|
278
|
+
if (criteria.some(criterion => !progress.remainingCriteria.includes(criterion))) {
|
|
279
|
+
throw new Error('Action goalRefs references an unknown or already satisfied criterion');
|
|
280
|
+
}
|
|
281
|
+
if (blockerActionIds.some(id => !progress.blockers.some(blocker => blocker.actionId === id))) {
|
|
282
|
+
throw new Error('Action goalRefs references an unknown or resolved blocker');
|
|
283
|
+
}
|
|
284
|
+
if (delivery && progress.delivery.status === 'passed') {
|
|
285
|
+
throw new Error('Action goalRefs references an already satisfied delivery condition');
|
|
286
|
+
}
|
|
287
|
+
return { criteria, blockerActionIds, delivery };
|
|
288
|
+
}
|
|
289
|
+
|
|
241
290
|
function normalizeEvidenceRunIds(value) {
|
|
242
291
|
return uniqueStrings(value);
|
|
243
292
|
}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Work Center
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @param {NodeJS.ProcessEnv|Record<string, string|undefined>} [env]
|
|
6
|
-
* @returns {boolean}
|
|
2
|
+
* Resolve the Work Center gate. An explicitly present legacy environment
|
|
3
|
+
* variable overrides the persisted Agent setting; otherwise config defaults off.
|
|
7
4
|
*/
|
|
8
|
-
export function
|
|
9
|
-
|
|
5
|
+
export function getWorkCenterFeatureState(config = {}, env = process.env) {
|
|
6
|
+
const overridden = Object.prototype.hasOwnProperty.call(env || {}, 'YEAFT_WORK_CENTER_ENABLED');
|
|
7
|
+
return {
|
|
8
|
+
enabled: overridden ? env.YEAFT_WORK_CENTER_ENABLED === 'true' : config?.workCenter?.enabled === true,
|
|
9
|
+
source: overridden ? 'environment' : 'config',
|
|
10
|
+
overridden,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isWorkCenterEnabled(env = process.env, config = {}) {
|
|
15
|
+
return getWorkCenterFeatureState(config, env).enabled;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Boot the persisted feature without changing its configured value. */
|
|
19
|
+
export async function startWorkCenterFeature(enabled, boot) {
|
|
20
|
+
if (enabled !== true) return { effective: false, runtimeError: null };
|
|
21
|
+
try {
|
|
22
|
+
await boot();
|
|
23
|
+
return { effective: true, runtimeError: null };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return { effective: false, runtimeError: error?.message || String(error) };
|
|
26
|
+
}
|
|
10
27
|
}
|