@yeaft/webchat-agent 1.0.295 → 1.0.298
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/index.js +1 -2
- package/local-runtime/server/db/connection.js +13 -0
- package/local-runtime/server/db/session-db.js +3 -0
- package/local-runtime/server/db/yeaft-session-db.js +9 -0
- package/local-runtime/server/handlers/agent-output.js +27 -0
- package/local-runtime/server/handlers/client-conversation.js +2 -2
- package/local-runtime/server/handlers/client-work-center.js +5 -4
- package/local-runtime/server/session-catalog.js +4 -7
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +156 -169
- 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/engine.js +30 -3
- package/yeaft/sessions/session-crud.js +9 -6
- package/yeaft/sessions/session-store.js +1 -0
- package/yeaft/tools/file-edit.js +8 -6
- package/yeaft/tools/file-write.js +2 -2
- package/yeaft/tools/registry.js +28 -10
- package/yeaft/tools/types.js +4 -0
- package/yeaft/web-bridge.js +10 -2
- package/yeaft/work-center/bridge.js +5 -1
- package/yeaft/work-center/controller.js +6 -2
- package/yeaft/work-center/coordinator.js +84 -17
- package/yeaft/work-center/durable-model.js +674 -0
- package/yeaft/work-center/projection.js +10 -1
- package/yeaft/work-center/runner.js +111 -16
- package/yeaft/work-center/service.js +102 -4
- package/yeaft/work-center/store.js +963 -23
|
Binary file
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -1838,7 +1838,7 @@ export class Engine {
|
|
|
1838
1838
|
* string-prompt shape (no regression for existing callers).
|
|
1839
1839
|
* @yields {EngineEvent}
|
|
1840
1840
|
*/
|
|
1841
|
-
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
|
|
1841
|
+
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
|
|
1842
1842
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1843
1843
|
yield {
|
|
1844
1844
|
type: 'error',
|
|
@@ -1915,7 +1915,7 @@ export class Engine {
|
|
|
1915
1915
|
};
|
|
1916
1916
|
try {
|
|
1917
1917
|
this.#currentThreadId = threadId || MAIN_THREAD_ID;
|
|
1918
|
-
yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
|
|
1918
|
+
yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
|
|
1919
1919
|
} finally {
|
|
1920
1920
|
// Closing the async generator at a visible retry boundary means the
|
|
1921
1921
|
// continuation never reached a provider. Keep it out of history and
|
|
@@ -1963,7 +1963,7 @@ export class Engine {
|
|
|
1963
1963
|
* in a try/finally without indenting the whole loop.
|
|
1964
1964
|
* @private
|
|
1965
1965
|
*/
|
|
1966
|
-
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
|
|
1966
|
+
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
|
|
1967
1967
|
|
|
1968
1968
|
const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
|
|
1969
1969
|
? collabToolPolicy
|
|
@@ -2367,6 +2367,7 @@ export class Engine {
|
|
|
2367
2367
|
let currentModel = this.#config.model;
|
|
2368
2368
|
let cumulativeInputTokens = 0;
|
|
2369
2369
|
let cumulativeOutputTokens = 0;
|
|
2370
|
+
let activeProviderRequest = null;
|
|
2370
2371
|
// task-707: tool-callable end-turn signal. Tools (currently only
|
|
2371
2372
|
// `route_forward`) can set this via toolCtx.requestEndTurn(reason)
|
|
2372
2373
|
// to break out of the tool-loop after the current batch finishes
|
|
@@ -2645,6 +2646,16 @@ export class Engine {
|
|
|
2645
2646
|
retryLifecycle.pendingContinuation = null;
|
|
2646
2647
|
}
|
|
2647
2648
|
|
|
2649
|
+
activeProviderRequest = typeof prepareProviderRequest === 'function'
|
|
2650
|
+
? prepareProviderRequest({
|
|
2651
|
+
turnNumber,
|
|
2652
|
+
entries: appendedBeforeStream,
|
|
2653
|
+
system: systemPrompt,
|
|
2654
|
+
messages: wireMessages.map(mapDebugMessage),
|
|
2655
|
+
model: currentModel,
|
|
2656
|
+
}) || null
|
|
2657
|
+
: null;
|
|
2658
|
+
|
|
2648
2659
|
// Snapshot task results carried by this exact request. Request start
|
|
2649
2660
|
// is not delivery: fetch may remain pending and then be aborted before
|
|
2650
2661
|
// the provider processes anything. Ack only after a normal stream end
|
|
@@ -2661,6 +2672,7 @@ export class Engine {
|
|
|
2661
2672
|
effortSource: userEffort ? 'user' : 'auto',
|
|
2662
2673
|
signal,
|
|
2663
2674
|
onRawExchange: captureRawExchange,
|
|
2675
|
+
onRequestStart: () => startProviderRequest?.(activeProviderRequest),
|
|
2664
2676
|
})) {
|
|
2665
2677
|
// task-325a (abort-stop fix): per-event abort short-circuit.
|
|
2666
2678
|
// The adapter is expected to throw AbortError when fetch's
|
|
@@ -2768,6 +2780,14 @@ export class Engine {
|
|
|
2768
2780
|
// escrow so retry or final rescue can deliver the payload.
|
|
2769
2781
|
if (sawProviderStop) {
|
|
2770
2782
|
this.#confirmAsyncTaskResults(requestAsyncTaskIds);
|
|
2783
|
+
const completedProviderRequest = activeProviderRequest;
|
|
2784
|
+
activeProviderRequest = null;
|
|
2785
|
+
finishProviderRequest?.(completedProviderRequest, {
|
|
2786
|
+
responseText,
|
|
2787
|
+
stopReason,
|
|
2788
|
+
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
2789
|
+
thinkingBlocks,
|
|
2790
|
+
});
|
|
2771
2791
|
}
|
|
2772
2792
|
traceRequest('llm.request_complete', {
|
|
2773
2793
|
durationMs: perfNowMs() - requestPerfStart,
|
|
@@ -2785,6 +2805,10 @@ export class Engine {
|
|
|
2785
2805
|
consecutiveRetryableErrors = 0;
|
|
2786
2806
|
} catch (err) {
|
|
2787
2807
|
const latencyMs = Date.now() - startTime;
|
|
2808
|
+
if (activeProviderRequest) {
|
|
2809
|
+
failProviderRequest?.(activeProviderRequest, err);
|
|
2810
|
+
activeProviderRequest = null;
|
|
2811
|
+
}
|
|
2788
2812
|
|
|
2789
2813
|
const endAttemptTrace = (attemptStopReason) => {
|
|
2790
2814
|
this.#trace.endTurn(turnId, {
|
|
@@ -3571,6 +3595,7 @@ export class Engine {
|
|
|
3571
3595
|
let displayImages = [];
|
|
3572
3596
|
let isError = false;
|
|
3573
3597
|
let toolErrorOutput = null;
|
|
3598
|
+
let fatalToolError = null;
|
|
3574
3599
|
currentToolCallForAsyncTask = {
|
|
3575
3600
|
id: tc.id,
|
|
3576
3601
|
name: tc.name,
|
|
@@ -3644,6 +3669,7 @@ export class Engine {
|
|
|
3644
3669
|
output = `Error: ${err.message}`;
|
|
3645
3670
|
isError = true;
|
|
3646
3671
|
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
|
|
3672
|
+
if (err?.fatalToolTimeout === true) fatalToolError = err;
|
|
3647
3673
|
}
|
|
3648
3674
|
}
|
|
3649
3675
|
|
|
@@ -3732,6 +3758,7 @@ export class Engine {
|
|
|
3732
3758
|
isError,
|
|
3733
3759
|
}));
|
|
3734
3760
|
queryToolCount += 1;
|
|
3761
|
+
if (fatalToolError) throw fatalToolError;
|
|
3735
3762
|
}
|
|
3736
3763
|
|
|
3737
3764
|
// PR-L: flush any duplicate-call reminders queued during the batch.
|
|
@@ -541,7 +541,7 @@ export function renameSession(yeaftDir, sessionId, newName) {
|
|
|
541
541
|
if (!name) throw new SessionCrudError('invalid_name', sessionId);
|
|
542
542
|
const handle = requireSession(yeaftDir, sessionId);
|
|
543
543
|
const meta = handle.getMeta();
|
|
544
|
-
handle.saveMeta({ ...meta, name });
|
|
544
|
+
handle.saveMeta({ ...meta, name, metadataUpdatedAt: new Date().toISOString() });
|
|
545
545
|
const next = handle.getMeta();
|
|
546
546
|
handle.close();
|
|
547
547
|
return next;
|
|
@@ -561,7 +561,7 @@ export function updateSessionAnnouncement(yeaftDir, sessionId, text) {
|
|
|
561
561
|
const announcement = text.trim();
|
|
562
562
|
const handle = requireSession(yeaftDir, sessionId);
|
|
563
563
|
const meta = handle.getMeta();
|
|
564
|
-
handle.saveMeta({ ...meta, announcement });
|
|
564
|
+
handle.saveMeta({ ...meta, announcement, metadataUpdatedAt: new Date().toISOString() });
|
|
565
565
|
const next = handle.getMeta();
|
|
566
566
|
handle.close();
|
|
567
567
|
return next;
|
|
@@ -577,7 +577,10 @@ export function updateSessionAnnouncement(yeaftDir, sessionId, text) {
|
|
|
577
577
|
export function updateSessionConfig(yeaftDir, sessionId, partial) {
|
|
578
578
|
const handle = requireSession(yeaftDir, sessionId);
|
|
579
579
|
try {
|
|
580
|
-
|
|
580
|
+
const saved = saveSessionConfig(yeaftDir, sessionId, partial || {});
|
|
581
|
+
const meta = handle.getMeta();
|
|
582
|
+
handle.saveMeta({ ...meta, metadataUpdatedAt: new Date().toISOString() });
|
|
583
|
+
return saved;
|
|
581
584
|
} finally {
|
|
582
585
|
handle.close();
|
|
583
586
|
}
|
|
@@ -708,7 +711,7 @@ export function addMember(yeaftDir, sessionId, vpId) {
|
|
|
708
711
|
try {
|
|
709
712
|
const meta = handle.getMeta();
|
|
710
713
|
const next = rosterAdd(meta, vpId);
|
|
711
|
-
handle.saveMeta(next);
|
|
714
|
+
handle.saveMeta({ ...next, metadataUpdatedAt: new Date().toISOString() });
|
|
712
715
|
return handle.getMeta();
|
|
713
716
|
} finally {
|
|
714
717
|
handle.close();
|
|
@@ -728,7 +731,7 @@ export function removeMember(yeaftDir, sessionId, vpId) {
|
|
|
728
731
|
return meta;
|
|
729
732
|
}
|
|
730
733
|
const next = rosterRemove(meta, vpId);
|
|
731
|
-
handle.saveMeta(next);
|
|
734
|
+
handle.saveMeta({ ...next, metadataUpdatedAt: new Date().toISOString() });
|
|
732
735
|
return handle.getMeta();
|
|
733
736
|
} finally {
|
|
734
737
|
handle.close();
|
|
@@ -741,7 +744,7 @@ export function setSessionDefaultVp(yeaftDir, sessionId, vpId) {
|
|
|
741
744
|
try {
|
|
742
745
|
const meta = handle.getMeta();
|
|
743
746
|
const next = setDefaultVp(meta, vpId);
|
|
744
|
-
handle.saveMeta(next);
|
|
747
|
+
handle.saveMeta({ ...next, metadataUpdatedAt: new Date().toISOString() });
|
|
745
748
|
return handle.getMeta();
|
|
746
749
|
} finally {
|
|
747
750
|
handle.close();
|
|
@@ -145,6 +145,7 @@ export function createSession(sessionsRoot, spec) {
|
|
|
145
145
|
workDir: typeof spec.workDir === 'string' ? spec.workDir.trim() : '',
|
|
146
146
|
workspaceKey: typeof spec.workspaceKey === 'string' ? spec.workspaceKey.trim() : '',
|
|
147
147
|
createdAt: spec.createdAt || new Date().toISOString(),
|
|
148
|
+
metadataUpdatedAt: spec.metadataUpdatedAt || spec.createdAt || new Date().toISOString(),
|
|
148
149
|
};
|
|
149
150
|
h.saveMeta(meta);
|
|
150
151
|
return h;
|
package/yeaft/tools/file-edit.js
CHANGED
|
@@ -75,16 +75,16 @@ Guidelines:
|
|
|
75
75
|
isDestructive: () => false,
|
|
76
76
|
async execute(input, ctx) {
|
|
77
77
|
const { file_path, old_string, new_string, replace_all = false } = input;
|
|
78
|
-
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
79
|
-
if (old_string === undefined) return JSON.stringify({ error: 'old_string is required' });
|
|
80
|
-
if (new_string === undefined) return JSON.stringify({ error: 'new_string is required' });
|
|
81
|
-
if (old_string === new_string) return JSON.stringify({ error: 'old_string and new_string are identical' });
|
|
78
|
+
if (!file_path) return JSON.stringify({ errorEffect: 'none', error: 'file_path is required' });
|
|
79
|
+
if (old_string === undefined) return JSON.stringify({ errorEffect: 'none', error: 'old_string is required' });
|
|
80
|
+
if (new_string === undefined) return JSON.stringify({ errorEffect: 'none', error: 'new_string is required' });
|
|
81
|
+
if (old_string === new_string) return JSON.stringify({ errorEffect: 'none', error: 'old_string and new_string are identical' });
|
|
82
82
|
|
|
83
83
|
const cwd = ctx?.cwd || process.cwd();
|
|
84
84
|
const absPath = resolve(cwd, file_path);
|
|
85
85
|
|
|
86
86
|
if (!existsSync(absPath)) {
|
|
87
|
-
return JSON.stringify({ error: `File not found: ${absPath}` });
|
|
87
|
+
return JSON.stringify({ errorEffect: 'none', error: `File not found: ${absPath}` });
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
try {
|
|
@@ -106,6 +106,7 @@ Guidelines:
|
|
|
106
106
|
? old_string.slice(0, 100) + '...'
|
|
107
107
|
: old_string;
|
|
108
108
|
return JSON.stringify({
|
|
109
|
+
errorEffect: 'none',
|
|
109
110
|
error: `old_string not found in file`,
|
|
110
111
|
hint: `The exact text "${preview}" was not found in ${absPath}. Check whitespace and indentation.`,
|
|
111
112
|
});
|
|
@@ -113,6 +114,7 @@ Guidelines:
|
|
|
113
114
|
|
|
114
115
|
if (count > 1 && !replace_all) {
|
|
115
116
|
return JSON.stringify({
|
|
117
|
+
errorEffect: 'none',
|
|
116
118
|
error: `old_string found ${count} times — not unique. Use replace_all: true to replace all occurrences, or provide more context to make it unique.`,
|
|
117
119
|
occurrences: count,
|
|
118
120
|
});
|
|
@@ -137,7 +139,7 @@ Guidelines:
|
|
|
137
139
|
message: `Replaced ${replace_all ? count : 1} occurrence(s) in ${absPath}`,
|
|
138
140
|
});
|
|
139
141
|
} catch (err) {
|
|
140
|
-
return JSON.stringify({ error: `Failed to edit file: ${err.message}` });
|
|
142
|
+
return JSON.stringify({ errorEffect: 'unknown', error: `Failed to edit file: ${err.message}` });
|
|
141
143
|
}
|
|
142
144
|
},
|
|
143
145
|
});
|
|
@@ -56,9 +56,9 @@ Guidelines:
|
|
|
56
56
|
isDestructive: () => false,
|
|
57
57
|
async execute(input, ctx) {
|
|
58
58
|
const { file_path, content } = input;
|
|
59
|
-
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
59
|
+
if (!file_path) return JSON.stringify({ errorEffect: 'none', error: 'file_path is required' });
|
|
60
60
|
if (content === undefined || content === null) {
|
|
61
|
-
return JSON.stringify({ error: 'content is required' });
|
|
61
|
+
return JSON.stringify({ errorEffect: 'none', error: 'content is required' });
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const cwd = ctx?.cwd || process.cwd();
|
package/yeaft/tools/registry.js
CHANGED
|
@@ -194,23 +194,31 @@ export function normalizeToolOutput(output) {
|
|
|
194
194
|
return text;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
function parseToolErrorOutput(output) {
|
|
198
198
|
const text = normalizeToolOutput(output).trim();
|
|
199
|
-
if (!text.startsWith('{')) return
|
|
199
|
+
if (!text.startsWith('{')) return null;
|
|
200
200
|
try {
|
|
201
201
|
const parsed = JSON.parse(text);
|
|
202
|
-
return
|
|
203
|
-
parsed
|
|
202
|
+
return parsed
|
|
204
203
|
&& typeof parsed === 'object'
|
|
205
204
|
&& !Array.isArray(parsed)
|
|
206
205
|
&& typeof parsed.error === 'string'
|
|
207
|
-
&& parsed.error.trim()
|
|
208
|
-
|
|
206
|
+
&& parsed.error.trim()
|
|
207
|
+
? parsed
|
|
208
|
+
: null;
|
|
209
209
|
} catch {
|
|
210
|
-
return
|
|
210
|
+
return null;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
export function isToolErrorOutput(output) {
|
|
215
|
+
return parseToolErrorOutput(output) !== null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function toolErrorEffect(output) {
|
|
219
|
+
return parseToolErrorOutput(output)?.errorEffect === 'none' ? 'none' : 'unknown';
|
|
220
|
+
}
|
|
221
|
+
|
|
214
222
|
function truncateUtf8(text, maxBytes) {
|
|
215
223
|
if (maxBytes <= 0) return '';
|
|
216
224
|
const buffer = Buffer.from(String(text), 'utf8');
|
|
@@ -435,9 +443,19 @@ export class ToolRegistry {
|
|
|
435
443
|
const rawTimeout = Number.isFinite(tool.timeoutMs) ? tool.timeoutMs : DEFAULT_TOOL_TIMEOUT_MS;
|
|
436
444
|
const useTimeout = rawTimeout > 0;
|
|
437
445
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
446
|
+
let output;
|
|
447
|
+
try {
|
|
448
|
+
output = useTimeout
|
|
449
|
+
? await runWithTimeout(tool.execute(input, ctx), rawTimeout, name)
|
|
450
|
+
: await tool.execute(input, ctx);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof ToolExecutionTimeoutError
|
|
453
|
+
&& tool.sideEffectScope !== 'run'
|
|
454
|
+
&& tool.isReadOnly?.(input) !== true) {
|
|
455
|
+
error.fatalToolTimeout = true;
|
|
456
|
+
}
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
441
459
|
|
|
442
460
|
return normalizeToolOutput(output);
|
|
443
461
|
}
|
package/yeaft/tools/types.js
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
* @property {(input?: object) => boolean} [isReadOnly] — read-only operation?
|
|
64
64
|
* @property {(input?: object) => boolean} [isDestructive] — destructive operation?
|
|
65
65
|
* @property {'json-error-envelope' | null} [errorOutput] — explicit returned-output error contract; null means only thrown errors fail
|
|
66
|
+
* @property {'external' | 'run'} [sideEffectScope] — whether mutations escape the current Run collector
|
|
66
67
|
*/
|
|
67
68
|
|
|
68
69
|
/**
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
* isReadOnly?: (input?: object) => boolean,
|
|
78
79
|
* isDestructive?: (input?: object) => boolean,
|
|
79
80
|
* errorOutput?: 'json-error-envelope' | null,
|
|
81
|
+
* sideEffectScope?: 'external' | 'run',
|
|
80
82
|
* timeoutMs?: number,
|
|
81
83
|
* }} def
|
|
82
84
|
* @returns {ToolDef}
|
|
@@ -91,6 +93,7 @@ export function defineTool({
|
|
|
91
93
|
isReadOnly = () => false,
|
|
92
94
|
isDestructive = () => false,
|
|
93
95
|
errorOutput = 'json-error-envelope',
|
|
96
|
+
sideEffectScope = 'external',
|
|
94
97
|
timeoutMs,
|
|
95
98
|
}) {
|
|
96
99
|
if (!name) throw new Error('Tool must have a name');
|
|
@@ -105,6 +108,7 @@ export function defineTool({
|
|
|
105
108
|
isReadOnly,
|
|
106
109
|
isDestructive,
|
|
107
110
|
errorOutput,
|
|
111
|
+
sideEffectScope,
|
|
108
112
|
};
|
|
109
113
|
// Legacy tool-name aliases. Registered as extra lookup keys so old
|
|
110
114
|
// jsonl tool_calls (e.g. `SendMessage` → `PromptAgent`) keep resolving,
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1098,6 +1098,10 @@ const ESCALATE_AFTER_ABORT_MS = 15_000;
|
|
|
1098
1098
|
/** Virtual conversationId for the Yeaft session */
|
|
1099
1099
|
let yeaftConversationId = null;
|
|
1100
1100
|
|
|
1101
|
+
function createYeaftConversationId() {
|
|
1102
|
+
return `yeaft-${randomUUID()}`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1101
1105
|
/** Last agent-level Yeaft slash command payload. Replayed after the web side
|
|
1102
1106
|
* creates/replaces the virtual Yeaft conversation id so `/` autocomplete never
|
|
1103
1107
|
* falls back to built-ins while full Session metadata is still loading. */
|
|
@@ -1316,7 +1320,7 @@ function loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq = null)
|
|
|
1316
1320
|
|
|
1317
1321
|
function ensureYeaftConversationId() {
|
|
1318
1322
|
if (!yeaftConversationId) {
|
|
1319
|
-
yeaftConversationId =
|
|
1323
|
+
yeaftConversationId = createYeaftConversationId();
|
|
1320
1324
|
replayCachedSkillSlashCommandsToYeaftConversation();
|
|
1321
1325
|
}
|
|
1322
1326
|
return yeaftConversationId;
|
|
@@ -3110,6 +3114,7 @@ function sendSessionRosterChanged(session) {
|
|
|
3110
3114
|
roster: session.roster,
|
|
3111
3115
|
defaultVpId: session.defaultVpId,
|
|
3112
3116
|
workDir: session.workDir || '',
|
|
3117
|
+
metadataUpdatedAt: session.metadataUpdatedAt || session.createdAt || null,
|
|
3113
3118
|
};
|
|
3114
3119
|
sendSessionEvent({ type: 'session_roster_changed', ...payload });
|
|
3115
3120
|
}
|
|
@@ -7029,7 +7034,7 @@ export async function resetYeaftSession() {
|
|
|
7029
7034
|
claimRuntimeOwnership(session);
|
|
7030
7035
|
installYeaftRuntimeBridge(session);
|
|
7031
7036
|
|
|
7032
|
-
yeaftConversationId =
|
|
7037
|
+
yeaftConversationId = createYeaftConversationId();
|
|
7033
7038
|
scheduleBaseRuntimeLoad();
|
|
7034
7039
|
hydrateYeaftStatusFromSession(session, { reason: 'reset', emitEvent: true });
|
|
7035
7040
|
broadcastSkillSlashCommands(session);
|
|
@@ -7346,6 +7351,9 @@ export const __testHooks = {
|
|
|
7346
7351
|
ensureYeaftConversationIdForTest() {
|
|
7347
7352
|
return ensureYeaftConversationId();
|
|
7348
7353
|
},
|
|
7354
|
+
setYeaftConversationIdForTest(value) {
|
|
7355
|
+
yeaftConversationId = value || null;
|
|
7356
|
+
},
|
|
7349
7357
|
preloadYeaftSkillSlashCommandsForTest() {
|
|
7350
7358
|
return broadcastSkillSlashCommands(session);
|
|
7351
7359
|
},
|
|
@@ -20,7 +20,7 @@ let shutdownPromise = null;
|
|
|
20
20
|
let serviceFactory = null;
|
|
21
21
|
|
|
22
22
|
const BROWSER_DETAIL_OPS = new Set([
|
|
23
|
-
'get', 'create', 'update', 'start', 'cancel', 'resume', 'action_input', 'retry_action', 'guide', 'retry',
|
|
23
|
+
'get', 'create', 'update', 'start', 'cancel', 'resume', 'post_work_item_message', 'action_input', 'retry_action', 'guide', 'retry',
|
|
24
24
|
]);
|
|
25
25
|
const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
|
|
26
26
|
// `files` is an internal server-to-Agent field. The browser relay rejects any
|
|
@@ -29,6 +29,10 @@ const BROWSER_FILE_FIELDS = Object.freeze({
|
|
|
29
29
|
create: [
|
|
30
30
|
'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'files', 'start',
|
|
31
31
|
],
|
|
32
|
+
post_work_item_message: [
|
|
33
|
+
'id', 'clientMessageId', 'text', 'target', 'revision', 'planRevision', 'ledgerRevision',
|
|
34
|
+
'coordinatorRevision', 'files',
|
|
35
|
+
],
|
|
32
36
|
work_item_message: [
|
|
33
37
|
'id', 'text', 'revision', 'planRevision', 'ledgerRevision', 'coordinatorRevision', 'files',
|
|
34
38
|
],
|
|
@@ -227,6 +227,8 @@ export class WorkflowController {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
input(id, input = {}) {
|
|
230
|
+
const existingClientMessage = this.store.hasActionInputClientMessage(id, input.actionId, input.clientMessageId);
|
|
231
|
+
if (existingClientMessage) return this.store.getWorkItemDetail(id);
|
|
230
232
|
const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
|
|
231
233
|
const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
|
|
232
234
|
if (!text && addedAttachmentCount === 0) throw new Error('Action input or attachments are required');
|
|
@@ -253,7 +255,7 @@ export class WorkflowController {
|
|
|
253
255
|
actionId: input.actionId,
|
|
254
256
|
generation: expectedGeneration,
|
|
255
257
|
revision: input.revision,
|
|
256
|
-
}, input.attachments, input.addedAttachments);
|
|
258
|
+
}, input.attachments, input.addedAttachments, input.clientMessageId);
|
|
257
259
|
}
|
|
258
260
|
if (!['waiting', 'failed'].includes(targetAction.status)) {
|
|
259
261
|
throw new Error(`Action in ${targetAction.status} cannot accept input`);
|
|
@@ -264,7 +266,9 @@ export class WorkflowController {
|
|
|
264
266
|
expected: { actionId: input.actionId, generation: input.generation, revision: input.revision },
|
|
265
267
|
attachments: input.attachments,
|
|
266
268
|
inputEvent: {
|
|
267
|
-
inputId: randomUUID(),
|
|
269
|
+
inputId: input.clientMessageId || randomUUID(),
|
|
270
|
+
clientMessageId: input.clientMessageId || null,
|
|
271
|
+
targetActionId: input.actionId,
|
|
268
272
|
text: text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`,
|
|
269
273
|
attachments: input.addedAttachments,
|
|
270
274
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { resolveMaxOutputTokens } from '../models.js';
|
|
3
3
|
import {
|
|
4
4
|
LLMAuthError,
|
|
@@ -531,6 +531,8 @@ export class WorkItemCoordinator {
|
|
|
531
531
|
this.runtimeProvider = options.runtimeProvider;
|
|
532
532
|
this.policyProvider = typeof options.policyProvider === 'function' ? options.policyProvider : async () => ({});
|
|
533
533
|
this.registry = options.registry;
|
|
534
|
+
this.ownerBootId = options.ownerBootId || randomUUID();
|
|
535
|
+
this.claimLeaseMs = Math.max(5_000, Number(options.claimLeaseMs) || 60_000);
|
|
534
536
|
this.attachmentRoot = options.attachmentRoot || null;
|
|
535
537
|
this.languageProvider = typeof options.languageProvider === 'function'
|
|
536
538
|
? options.languageProvider
|
|
@@ -550,7 +552,7 @@ export class WorkItemCoordinator {
|
|
|
550
552
|
throw new Error('Work Center Coordinator message or attachments are required');
|
|
551
553
|
}
|
|
552
554
|
const promptText = text || `The user added ${addedAttachments.length} attachment(s) for this WorkItem.`;
|
|
553
|
-
|
|
555
|
+
let started = this.store.beginCoordinatorTurn(id, text, {
|
|
554
556
|
revision: Number(input.revision),
|
|
555
557
|
planRevision: Number(input.planRevision),
|
|
556
558
|
ledgerRevision: Number(input.ledgerRevision),
|
|
@@ -558,8 +560,15 @@ export class WorkItemCoordinator {
|
|
|
558
560
|
}, {
|
|
559
561
|
attachments: input.attachments,
|
|
560
562
|
addedAttachments,
|
|
563
|
+
clientMessageId: input.clientMessageId,
|
|
561
564
|
});
|
|
562
565
|
if (!started) throw new Error(`WorkItem not found: ${id}`);
|
|
566
|
+
if (started.duplicate) {
|
|
567
|
+
return { detail: started.detail, task: Promise.resolve(started.detail), duplicate: true };
|
|
568
|
+
}
|
|
569
|
+
const claimed = this.store.claimStartedCoordinatorTurn(started, this.ownerBootId, this.claimLeaseMs);
|
|
570
|
+
if (!claimed) return { detail: started.detail, task: Promise.resolve(started.detail), duplicate: true };
|
|
571
|
+
started = claimed;
|
|
563
572
|
options.onUpdate?.('coordinator.turn_started', started.detail);
|
|
564
573
|
return this.#scheduleTurn(started, {
|
|
565
574
|
text: promptText,
|
|
@@ -569,6 +578,18 @@ export class WorkItemCoordinator {
|
|
|
569
578
|
});
|
|
570
579
|
}
|
|
571
580
|
|
|
581
|
+
resume(started, options = {}) {
|
|
582
|
+
if (!started?.turnId || !started?.detail || !started?.fence) {
|
|
583
|
+
throw new Error('Coordinator provider recovery target is invalid');
|
|
584
|
+
}
|
|
585
|
+
return this.#scheduleTurn(started, {
|
|
586
|
+
text: typeof options.text === 'string' ? options.text : '',
|
|
587
|
+
recovery: options.recovery === true,
|
|
588
|
+
addedAttachments: Array.isArray(options.addedAttachments) ? options.addedAttachments : [],
|
|
589
|
+
options,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
572
593
|
recover(id, options = {}) {
|
|
573
594
|
if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
|
|
574
595
|
const detail = this.store.getWorkItemDetail(id);
|
|
@@ -595,11 +616,13 @@ export class WorkItemCoordinator {
|
|
|
595
616
|
},
|
|
596
617
|
});
|
|
597
618
|
if (!started) return null;
|
|
598
|
-
|
|
619
|
+
const claimed = this.store.claimStartedCoordinatorTurn(started, this.ownerBootId, this.claimLeaseMs);
|
|
620
|
+
if (!claimed) return null;
|
|
621
|
+
options.onUpdate?.('coordinator.recovery_started', claimed.detail);
|
|
599
622
|
const text = `Action stage "${action.stageId}" failed. Decide the next safe control transition. `
|
|
600
623
|
+ 'Failure is not a terminal WorkItem state: guide or replan executable work whenever possible. '
|
|
601
624
|
+ 'Request human input only when the snapshot lacks information required for a safe decision.';
|
|
602
|
-
return this.#scheduleTurn(
|
|
625
|
+
return this.#scheduleTurn(claimed, { text, recovery: true, options });
|
|
603
626
|
}
|
|
604
627
|
|
|
605
628
|
#scheduleTurn(started, {
|
|
@@ -607,11 +630,19 @@ export class WorkItemCoordinator {
|
|
|
607
630
|
}) {
|
|
608
631
|
const abortController = new AbortController();
|
|
609
632
|
this.activeTurns.set(started.turnId, abortController);
|
|
633
|
+
const claim = started.fence?.claim;
|
|
634
|
+
const renewalTimer = claim ? setInterval(() => {
|
|
635
|
+
if (!this.store.renewCoordinatorMailbox(
|
|
636
|
+
claim.mailboxId, claim.ownerBootId, claim.claimEpoch, this.claimLeaseMs,
|
|
637
|
+
)) abortController.abort('work_center_coordinator_claim_lost');
|
|
638
|
+
}, Math.max(1_000, Math.floor(this.claimLeaseMs / 3))) : null;
|
|
639
|
+
renewalTimer?.unref?.();
|
|
610
640
|
const task = new Promise(resolve => setTimeout(resolve, 0))
|
|
611
641
|
.then(() => this.#executeTurn(started, {
|
|
612
642
|
text, recovery, addedAttachments, options, abortController,
|
|
613
643
|
}))
|
|
614
644
|
.finally(() => {
|
|
645
|
+
if (renewalTimer) clearInterval(renewalTimer);
|
|
615
646
|
this.activeTurns.delete(started.turnId);
|
|
616
647
|
this.activeTasks.delete(started.turnId);
|
|
617
648
|
});
|
|
@@ -622,6 +653,7 @@ export class WorkItemCoordinator {
|
|
|
622
653
|
async #executeTurn(started, {
|
|
623
654
|
text, recovery, addedAttachments, options, abortController,
|
|
624
655
|
}) {
|
|
656
|
+
let providerTurn = null;
|
|
625
657
|
try {
|
|
626
658
|
let normalized = null;
|
|
627
659
|
let mutation = null;
|
|
@@ -682,6 +714,7 @@ export class WorkItemCoordinator {
|
|
|
682
714
|
const correction = lastError
|
|
683
715
|
? `\n\nYour previous decision was rejected by the deterministic validator:\n${String(lastError.message || lastError).slice(0, 2_000)}\nReturn a corrected complete JSON decision.`
|
|
684
716
|
: '';
|
|
717
|
+
providerTurn = null;
|
|
685
718
|
try {
|
|
686
719
|
let result;
|
|
687
720
|
try {
|
|
@@ -689,19 +722,46 @@ export class WorkItemCoordinator {
|
|
|
689
722
|
const content = attachmentContext.promptParts.length > 0
|
|
690
723
|
? [{ type: 'text', text: latestMessage }, ...attachmentContext.promptParts]
|
|
691
724
|
: latestMessage;
|
|
692
|
-
|
|
725
|
+
const requestBody = {
|
|
726
|
+
model: resolved.model,
|
|
727
|
+
system: coordinatorSystemPrompt(language),
|
|
728
|
+
messages: [{ role: 'user', content }],
|
|
729
|
+
maxTokens: Math.min(
|
|
730
|
+
resolveMaxOutputTokens(resolved.model, runtime.config),
|
|
731
|
+
COORDINATOR_MAX_OUTPUT_TOKENS,
|
|
732
|
+
),
|
|
733
|
+
effort: resolved.effort,
|
|
734
|
+
effortSource: resolved.source,
|
|
735
|
+
effortContext: { scenario: 'work-center-coordinator' },
|
|
736
|
+
};
|
|
737
|
+
const claim = started.fence.claim;
|
|
738
|
+
providerTurn = this.store.prepareCoordinatorProviderTurn(
|
|
739
|
+
started.detail.id, started.turnId, attemptCount, requestBody, claim,
|
|
740
|
+
);
|
|
741
|
+
if (!providerTurn) return started.detail;
|
|
742
|
+
if (providerTurn.status === 'unknown') {
|
|
743
|
+
throw new Error('Coordinator provider dispatch outcome is unknown and requires review');
|
|
744
|
+
}
|
|
745
|
+
if (providerTurn.status === 'responded') {
|
|
746
|
+
result = providerTurn.response;
|
|
747
|
+
} else {
|
|
748
|
+
result = await Promise.race([
|
|
693
749
|
runtime.adapter.call({
|
|
694
|
-
|
|
695
|
-
system: coordinatorSystemPrompt(language),
|
|
696
|
-
messages: [{ role: 'user', content }],
|
|
697
|
-
maxTokens: Math.min(
|
|
698
|
-
resolveMaxOutputTokens(resolved.model, runtime.config),
|
|
699
|
-
COORDINATOR_MAX_OUTPUT_TOKENS,
|
|
700
|
-
),
|
|
701
|
-
effort: resolved.effort,
|
|
702
|
-
effortSource: resolved.source,
|
|
703
|
-
effortContext: { scenario: 'work-center-coordinator' },
|
|
750
|
+
...requestBody,
|
|
704
751
|
signal: abortController.signal,
|
|
752
|
+
onRequestStart: () => {
|
|
753
|
+
if (!this.store.dispatchCoordinatorProviderTurn(providerTurn.id, claim)) {
|
|
754
|
+
abortController.abort('work_center_coordinator_dispatch_fence_lost');
|
|
755
|
+
throw new Error('Coordinator provider turn lost its dispatch fence');
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
}).then(response => {
|
|
759
|
+
const persisted = this.store.respondCoordinatorProviderTurn(
|
|
760
|
+
providerTurn.id, providerTurn.requestHash, response, claim,
|
|
761
|
+
);
|
|
762
|
+
if (!persisted) throw new Error('Coordinator provider response lost its CAS fence');
|
|
763
|
+
providerTurn = persisted;
|
|
764
|
+
return response;
|
|
705
765
|
}),
|
|
706
766
|
new Promise((_, reject) => {
|
|
707
767
|
abortController.signal.addEventListener('abort', () => {
|
|
@@ -709,6 +769,7 @@ export class WorkItemCoordinator {
|
|
|
709
769
|
}, { once: true });
|
|
710
770
|
}),
|
|
711
771
|
]);
|
|
772
|
+
}
|
|
712
773
|
} catch (error) {
|
|
713
774
|
if (abortController.signal.aborted || this.shuttingDown) throw error;
|
|
714
775
|
throw coordinatorExecutionError(error, 'provider', language);
|
|
@@ -738,6 +799,9 @@ export class WorkItemCoordinator {
|
|
|
738
799
|
break;
|
|
739
800
|
} catch (error) {
|
|
740
801
|
normalized = null;
|
|
802
|
+
if (providerTurn?.status === 'responded') {
|
|
803
|
+
this.store.rejectCoordinatorProviderTurn(providerTurn.id, error, started.fence.claim);
|
|
804
|
+
}
|
|
741
805
|
if (abortController.signal.aborted || this.shuttingDown || error?.coordinatorClassified) {
|
|
742
806
|
throw error;
|
|
743
807
|
}
|
|
@@ -784,16 +848,19 @@ export class WorkItemCoordinator {
|
|
|
784
848
|
mutation,
|
|
785
849
|
attemptCount,
|
|
786
850
|
}, started.fence);
|
|
787
|
-
if (!detail)
|
|
851
|
+
if (!detail) return this.store.getWorkItemDetail(started.detail.id);
|
|
788
852
|
options.onUpdate?.(recovery ? 'coordinator.recovery_completed' : 'coordinator.turn_completed', detail);
|
|
789
853
|
return detail;
|
|
790
854
|
} catch (error) {
|
|
855
|
+
if (providerTurn?.status === 'responded') {
|
|
856
|
+
this.store.rejectCoordinatorProviderTurn(providerTurn.id, error, started.fence.claim);
|
|
857
|
+
}
|
|
791
858
|
const detail = this.store.failCoordinatorTurn(started.turnId, error, started.fence);
|
|
792
859
|
if (detail) {
|
|
793
860
|
options.onUpdate?.('coordinator.turn_failed', detail);
|
|
794
861
|
return detail;
|
|
795
862
|
}
|
|
796
|
-
|
|
863
|
+
return this.store.getWorkItemDetail(started.detail.id);
|
|
797
864
|
}
|
|
798
865
|
}
|
|
799
866
|
|