@yeaft/webchat-agent 1.0.634 → 1.0.636
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +149 -122
- 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/conversation/search.js +25 -9
- package/yeaft/engine.js +116 -14
- package/yeaft/query-progress.js +201 -0
- package/yeaft/sub-agent/prompt-queue.js +3 -0
- package/yeaft/sub-agent/result-reference.js +13 -0
- package/yeaft/sub-agent/runner.js +11 -6
- package/yeaft/sub-agent/task-identity.js +46 -0
- package/yeaft/tools/agent.js +73 -2
- package/yeaft/tools/history-search.js +28 -8
- package/yeaft/tools/types.js +2 -0
- package/yeaft/tools/wait-agent.js +21 -1
- package/yeaft/tools/web-fetch.js +9 -0
- package/yeaft/web-bridge.js +25 -2
- package/yeaft/work-center/runner.js +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/** Reference to the agent's current reply, not its initial delegation; liveness/status alone do not change it. */
|
|
4
|
+
export function getAgentResultRef(agent) {
|
|
5
|
+
const evidence = JSON.stringify({
|
|
6
|
+
result: agent.result ?? null,
|
|
7
|
+
preview: agent.lastResult || '',
|
|
8
|
+
finalReport: agent.finalReport || null,
|
|
9
|
+
error: agent.error || null,
|
|
10
|
+
});
|
|
11
|
+
const hash = createHash('sha256').update(evidence).digest('hex').slice(0, 16);
|
|
12
|
+
return `${agent.id}:${agent.resultRevision || 0}:${hash}`;
|
|
13
|
+
}
|
|
@@ -171,9 +171,10 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
171
171
|
agent.subEngine = subEngine;
|
|
172
172
|
agent.engineMessages = agent.engineMessages || [];
|
|
173
173
|
agent.liveness = agent.liveness || makeLiveness();
|
|
174
|
-
|
|
175
|
-
agent.
|
|
176
|
-
agent.
|
|
174
|
+
// Preserve tool-boundary ownership when host deps omit caller identifiers.
|
|
175
|
+
agent.parentVpId = agent.parentVpId || deps.parentVpId || null;
|
|
176
|
+
agent.parentSessionId = agent.parentSessionId || deps.parentSessionId || null;
|
|
177
|
+
agent.parentThreadId = agent.parentThreadId || deps.parentThreadId || 'main';
|
|
177
178
|
outputLog = createOutputLog(agent.id, deps.subAgentLogDir);
|
|
178
179
|
agent.outputLog = outputLog;
|
|
179
180
|
agent.outputFile = outputLog.path;
|
|
@@ -187,7 +188,7 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
187
188
|
parentName: deps.parentName || 'parent',
|
|
188
189
|
parentVpId: deps.parentVpId || null,
|
|
189
190
|
agentName: agent.name,
|
|
190
|
-
mission: agent.mission || agent.task || '',
|
|
191
|
+
mission: (agent.mission || agent.task || '') + (agent.baseline ? `\n\nWorkspace baseline: ${agent.baseline}` : ''),
|
|
191
192
|
expectedOutput: agent.expected_output,
|
|
192
193
|
presetPrompt: (agent.personaData || getPersona(agent.persona))?.systemPrompt,
|
|
193
194
|
budget: agent.budget,
|
|
@@ -457,6 +458,9 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
457
458
|
continue;
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
// Every actual query is a new evidence revision, even if its reply text
|
|
462
|
+
// happens to equal the previous reply. PromptAgent must stay collectable.
|
|
463
|
+
agent.resultRevision = (agent.resultRevision || 0) + 1;
|
|
460
464
|
// Partial evidence belongs to this query, never a previous follow-up.
|
|
461
465
|
agent.partial_output = '';
|
|
462
466
|
agent.lastResult = '';
|
|
@@ -495,8 +499,9 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
495
499
|
// Liveness — update first so even listener throws don't lose
|
|
496
500
|
// the bump.
|
|
497
501
|
bumpLivenessFromEvent(agent.liveness, evt);
|
|
498
|
-
//
|
|
499
|
-
|
|
502
|
+
// Provider activity and UI-only query counters are liveness only here:
|
|
503
|
+
// never persist them in the child log or churn task notifications.
|
|
504
|
+
if (evt?.type === 'provider_activity' || evt?.type === 'query_progress') continue;
|
|
500
505
|
|
|
501
506
|
// Mirror every raw event to the durable log. We still keep
|
|
502
507
|
// `sub_agent_event` text_delta suppressed so the inline transcript
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { realpathSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
function canonicalJSON(value) {
|
|
6
|
+
if (Array.isArray(value)) return value.map(canonicalJSON);
|
|
7
|
+
if (value && typeof value === 'object') {
|
|
8
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonicalJSON(value[key])]));
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve relative child paths against the caller workspace, not the process cwd. */
|
|
14
|
+
export function resolveAgentCwd(cwd, parentCwd) {
|
|
15
|
+
return path.resolve(parentCwd || process.cwd(), cwd || '.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canonicalWorkspace(cwd) {
|
|
19
|
+
let resolved = resolveAgentCwd(cwd);
|
|
20
|
+
try { resolved = realpathSync(resolved); } catch { /* non-existent paths keep lexical identity */ }
|
|
21
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Exact, query-local delegation identity. Names are labels, not task identity.
|
|
26
|
+
* No queryTurnId means a legacy caller: do not infer a scope or deduplicate it.
|
|
27
|
+
* The owner is checked separately with agentBelongsToScope before reuse.
|
|
28
|
+
* Keep this initial identity reserved across continuations; their latest mutable
|
|
29
|
+
* evidence must not be returned for an original-spec retry.
|
|
30
|
+
*/
|
|
31
|
+
export function buildDelegationIdentity(input, spec, grants, ctx = {}) {
|
|
32
|
+
const queryTurnId = typeof ctx?.queryTurnId === 'string' ? ctx.queryTurnId.trim() : '';
|
|
33
|
+
if (!queryTurnId) return null;
|
|
34
|
+
const workspace = canonicalWorkspace(ctx.cwd);
|
|
35
|
+
const taskSpec = {
|
|
36
|
+
mission: spec.mission,
|
|
37
|
+
persona: spec.persona,
|
|
38
|
+
cwd: canonicalWorkspace(resolveAgentCwd(input.cwd, ctx.cwd)),
|
|
39
|
+
baseline: input.baseline ?? null,
|
|
40
|
+
expected_output: spec.expected_output,
|
|
41
|
+
allow_tools: [...grants].sort(),
|
|
42
|
+
budget: spec.budget,
|
|
43
|
+
};
|
|
44
|
+
const specHash = createHash('sha256').update(JSON.stringify(canonicalJSON(taskSpec))).digest('hex');
|
|
45
|
+
return { queryTurnId, workspace, taskKey: input.taskKey?.trim() || null, specHash };
|
|
46
|
+
}
|
package/yeaft/tools/agent.js
CHANGED
|
@@ -33,6 +33,8 @@ import { STATUS, isTerminalAgentStatus } from '../sub-agent/status.js';
|
|
|
33
33
|
import { diagnoseAgentLiveness, makeLiveness } from '../sub-agent/liveness.js';
|
|
34
34
|
import { TASK_RESULT_DELIVERY } from '../tasks/store.js';
|
|
35
35
|
import { captureParentEffortDecision } from '../effort.js';
|
|
36
|
+
import { buildDelegationIdentity, resolveAgentCwd } from '../sub-agent/task-identity.js';
|
|
37
|
+
import { getAgentResultRef } from '../sub-agent/result-reference.js';
|
|
36
38
|
|
|
37
39
|
/** In-memory sub-agent registry. */
|
|
38
40
|
const agents = new Map();
|
|
@@ -95,6 +97,11 @@ export function validateSpec(input) {
|
|
|
95
97
|
return { ok: false, error: 'spec must be an object' };
|
|
96
98
|
}
|
|
97
99
|
const { name, task, mission, expected_output, persona, budget } = input;
|
|
100
|
+
for (const key of ['taskKey', 'baseline', 'cwd']) {
|
|
101
|
+
if (input[key] !== undefined && !cleanString(input[key])) {
|
|
102
|
+
return { ok: false, error: `${key} must be a non-empty string` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
98
105
|
if (!cleanString(name)) {
|
|
99
106
|
return { ok: false, error: 'name is required' };
|
|
100
107
|
}
|
|
@@ -237,6 +244,7 @@ Pick a preset persona to pre-wire a tool subset and model tier:
|
|
|
237
244
|
|
|
238
245
|
Guidelines:
|
|
239
246
|
- Give a clear, focused mission — what "done" looks like
|
|
247
|
+
- Exact spec retries in the same parent query/workspace reuse the existing agent (even renamed or terminal), unless follow-up work superseded its original evidence; that retry is rejected, not respawned. Use a new taskKey for an intentional independent run; taskKey conflicts require a new key, not a changed spec.
|
|
240
248
|
- Use expected_output when the return shape matters
|
|
241
249
|
- Delegate one clear result with the workspace/base and completion evidence. Let the child choose its steps; do simple work directly. Split unrelated goals, not individual reads.
|
|
242
250
|
- Usually omit budget so the child can finish. Add a budget only when the task actually needs a hard lifetime ceiling; tool exhaustion reserves one tool-free handoff and unfinished work stays budget_exceeded.
|
|
@@ -261,6 +269,7 @@ stops or finalizes work no longer needed.`,
|
|
|
261
269
|
|
|
262
270
|
使用指南:
|
|
263
271
|
- 给出清晰聚焦的 mission——"完成"是什么样子
|
|
272
|
+
- 同一父级 query/workspace 内完全相同的规格重试会复用已有 Agent(含改名或已结束任务);若后续工作已替代原任务证据,则拒绝重试,不自动重建。需要独立执行时使用新的 taskKey;同 key 规格冲突不能通过修改规格重试解决。
|
|
264
273
|
- 当返回结构重要时使用 expected_output
|
|
265
274
|
- 一次只委派一个明确结果,提供工作目录/基线和完成证据,让子 Agent 自主选择步骤;简单工作直接做。拆分不相关目标,不要拆成逐个读取任务。
|
|
266
275
|
- 通常省略 budget,让子 Agent 完成工作;仅当任务确实需要硬性累计上限时才设置。不要给多文件 review 人为设置极小额度。工具额度耗尽后保留一次无工具交付机会,未完成仍返回 budget_exceeded。
|
|
@@ -279,6 +288,20 @@ stops or finalizes work no longer needed.`,
|
|
|
279
288
|
zh: '子 Agent 的描述性名称(如 "test-writer"、"refactor-auth")',
|
|
280
289
|
},
|
|
281
290
|
},
|
|
291
|
+
taskKey: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: {
|
|
294
|
+
en: 'Optional exact task identity within this parent query/workspace. Retries reuse the agent even after completion, but are rejected after follow-up work supersedes its original evidence. Changing the spec under this key is an error. Use a new key for an intentional independent run.',
|
|
295
|
+
zh: '可选的父级 query/workspace 内精确任务标识;重试复用原 Agent(含已结束任务),后续工作替代原证据后则拒绝复用;同 key 修改任务规格会报冲突。明确需要独立执行时使用新 key。',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
baseline: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: {
|
|
301
|
+
en: 'Optional immutable workspace baseline (e.g. commit SHA); included in task identity and passed to the child.',
|
|
302
|
+
zh: '可选不可变 workspace 基线(如 commit SHA);纳入任务身份并传给子 Agent。',
|
|
303
|
+
},
|
|
304
|
+
},
|
|
282
305
|
task: {
|
|
283
306
|
type: 'string',
|
|
284
307
|
description: {
|
|
@@ -360,6 +383,9 @@ stops or finalizes work no longer needed.`,
|
|
|
360
383
|
// tools. A parent query therefore cannot safely reuse filesystem snapshots
|
|
361
384
|
// after a SpawnAgent call returns.
|
|
362
385
|
mayMutateWorkspaceAfterReturn: () => true,
|
|
386
|
+
// Execute retries through the identity check, never cache an old status.
|
|
387
|
+
cacheWithinQuery: false,
|
|
388
|
+
duplicateCallPolicy: () => 'allow',
|
|
363
389
|
async execute(input, ctx) {
|
|
364
390
|
// NB: every envelope below puts `next_steps` (or `error_next_steps`) at
|
|
365
391
|
// the FIRST position because `agent/yeaft/tools/registry.js` caps tool
|
|
@@ -377,8 +403,49 @@ stops or finalizes work no longer needed.`,
|
|
|
377
403
|
const spec = validation.spec;
|
|
378
404
|
const grants = validateToolGrants(input.allow_tools === undefined ? [] : input.allow_tools, ctx?.parentEngineDeps?.parentToolRegistry);
|
|
379
405
|
if (!grants.ok) return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: grants.error });
|
|
380
|
-
const { name
|
|
406
|
+
const { name } = input;
|
|
381
407
|
const callerScope = getCallerAgentScope(ctx);
|
|
408
|
+
const identity = buildDelegationIdentity(input, spec, grants.tools, ctx);
|
|
409
|
+
if (identity) {
|
|
410
|
+
for (const agent of agents.values()) {
|
|
411
|
+
const prior = agent.delegationIdentity;
|
|
412
|
+
if (!prior || !agentBelongsToScope(agent, callerScope)
|
|
413
|
+
|| prior.queryTurnId !== identity.queryTurnId || prior.workspace !== identity.workspace) continue;
|
|
414
|
+
const sameIdentity = identity.taskKey !== null
|
|
415
|
+
? prior.taskKey === identity.taskKey
|
|
416
|
+
: prior.taskKey === null && prior.specHash === identity.specHash;
|
|
417
|
+
if (!sameIdentity) continue;
|
|
418
|
+
if (prior.specHash !== identity.specHash) {
|
|
419
|
+
return JSON.stringify({
|
|
420
|
+
next_steps: ERROR_NEXT_STEPS,
|
|
421
|
+
error: `Task identity conflict: taskKey "${identity.taskKey}" already has a different spec. Use a new taskKey for independent work.`,
|
|
422
|
+
agentId: agent.id,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
// Identity stays reserved, but the mutable record no longer owns the
|
|
426
|
+
// original evidence after a continuation. Check accepted follow-up and
|
|
427
|
+
// finalization controls before the runner starts; revisions are a fallback.
|
|
428
|
+
if (agent.delegationSuperseded || agent.finalizationRequested || agent.resultRevision > 1) {
|
|
429
|
+
return JSON.stringify({
|
|
430
|
+
next_steps: 'Do not use this agent\'s latest reply as the original delegation evidence. Reuse the original reply if already collected; otherwise inspect the original turn in the agent log. Use a new taskKey only for intentional independent work.',
|
|
431
|
+
error: 'Delegation evidence superseded: this agent accepted follow-up work and its latest result no longer belongs to the original delegation.',
|
|
432
|
+
agentId: agent.id,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return JSON.stringify({
|
|
436
|
+
next_steps: 'This exact delegation already exists. Reuse its evidence; do not respawn it. Use ListAgents for status or WaitAgent to collect an uncollected reply; read outputFile only when the evidence is needed.',
|
|
437
|
+
success: true,
|
|
438
|
+
reused: true,
|
|
439
|
+
agentId: agent.id,
|
|
440
|
+
name: agent.name,
|
|
441
|
+
status: agent.status,
|
|
442
|
+
taskId: agent.taskId || null,
|
|
443
|
+
outputFile: agent.outputFile || null,
|
|
444
|
+
resultRef: getAgentResultRef(agent),
|
|
445
|
+
liveness: diagnoseAgentLiveness(agent),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
382
449
|
|
|
383
450
|
// Check for name collision — any non-terminal agent with the same
|
|
384
451
|
// name blocks the new spawn. Terminal agents (closed/failed/abandoned/
|
|
@@ -403,6 +470,9 @@ stops or finalizes work no longer needed.`,
|
|
|
403
470
|
name,
|
|
404
471
|
task: spec.task,
|
|
405
472
|
mission: spec.mission,
|
|
473
|
+
baseline: input.baseline || null,
|
|
474
|
+
delegationIdentity: identity,
|
|
475
|
+
delegationSuperseded: false,
|
|
406
476
|
expected_output: spec.expected_output,
|
|
407
477
|
persona: spec.persona,
|
|
408
478
|
personaData: persona || null,
|
|
@@ -410,7 +480,7 @@ stops or finalizes work no longer needed.`,
|
|
|
410
480
|
parentEffortDecision: captureParentEffortDecision(ctx),
|
|
411
481
|
budget: spec.budget,
|
|
412
482
|
allowTools: grants.tools,
|
|
413
|
-
cwd: cwd
|
|
483
|
+
cwd: resolveAgentCwd(input.cwd, ctx?.cwd),
|
|
414
484
|
status: STATUS.CREATED,
|
|
415
485
|
messages: [],
|
|
416
486
|
result: null,
|
|
@@ -501,6 +571,7 @@ stops or finalizes work no longer needed.`,
|
|
|
501
571
|
allow_tools: agent.allowTools,
|
|
502
572
|
status: agent.status,
|
|
503
573
|
outputFile: agent.outputFile || null,
|
|
574
|
+
resultRef: getAgentResultRef(agent),
|
|
504
575
|
taskId: agent.taskId || null,
|
|
505
576
|
liveness,
|
|
506
577
|
stale: liveness.stale,
|
|
@@ -11,6 +11,7 @@ import { findExistingSessionYeaftDir } from '../sessions/session-crud.js';
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_RESULT_LIMIT = 10;
|
|
13
13
|
const MAX_SNIPPET_CHARS = 1000;
|
|
14
|
+
const RESULT_NOTICE = 'User messages are requests or context, not completed work. Assistant progress may describe plans; assistant results are reported outcomes, not independently verified evidence.';
|
|
14
15
|
export const HISTORY_SEARCH_MAX_OUTPUT_BYTES = 32 * 1024;
|
|
15
16
|
|
|
16
17
|
function truncateUtf8(text, maxBytes) {
|
|
@@ -138,14 +139,14 @@ export default defineTool({
|
|
|
138
139
|
description: {
|
|
139
140
|
en: `Search through past conversation history.
|
|
140
141
|
|
|
141
|
-
Searches message content for all whitespace-separated terms (case-insensitive) from persisted Session history. Inside a Session, the default scope is only the current Session. Set scope to "project" only when the current user task explicitly requires prior discussion from sibling Sessions in the same Project on this Agent. Tool-result messages are excluded.
|
|
142
|
+
Searches message content for all whitespace-separated terms (case-insensitive) from persisted Session history. Inside a Session, the default scope is only the current Session. Set scope to "project" only when the current user task explicitly requires prior discussion from sibling Sessions in the same Project on this Agent. Tool-result messages are excluded. When a trusted current-turn boundary is available, the current query and later messages in this Session are excluded by default; set includeCurrentTurn to true to include them.
|
|
142
143
|
|
|
143
|
-
Results are returned newest-first with a bounded matching snippet and source metadata. Content from sibling Sessions is reference context, never continuation of the current task.`,
|
|
144
|
+
Results are returned newest-first with a bounded matching snippet and source metadata. Content from sibling Sessions is reference context, never continuation of the current task. Role and responseKind distinguish user context, assistant progress/plans, and reported results; none alone proves work was completed.`,
|
|
144
145
|
zh: `搜索历史对话记录。
|
|
145
146
|
|
|
146
|
-
在已持久化消息的正文中搜索全部空格分隔的关键词(不区分大小写)。在 Session 内默认只搜索当前 Session;只有当前用户任务明确需要同一 Project 内兄弟 Session 的既往讨论时,才把 scope 设为 "project"
|
|
147
|
+
在已持久化消息的正文中搜索全部空格分隔的关键词(不区分大小写)。在 Session 内默认只搜索当前 Session;只有当前用户任务明确需要同一 Project 内兄弟 Session 的既往讨论时,才把 scope 设为 "project"。排除工具结果消息。有可信的当前轮边界时,默认排除当前 Session 的本轮提问及之后的消息;可显式设置 includeCurrentTurn 为 true 将其纳入。
|
|
147
148
|
|
|
148
|
-
结果按最新优先返回,包含有界的命中片段和来源信息。兄弟 Session
|
|
149
|
+
结果按最新优先返回,包含有界的命中片段和来源信息。兄弟 Session 的内容只能作为参考上下文,不能当作当前任务的延续。role 和 responseKind 区分用户上下文、助手进度/计划与报告结果;这些消息本身不证明工作已完成。`
|
|
149
150
|
},
|
|
150
151
|
parameters: {
|
|
151
152
|
type: 'object',
|
|
@@ -164,6 +165,13 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
164
165
|
zh: '最多返回结果数(默认 10,最大 100)',
|
|
165
166
|
},
|
|
166
167
|
},
|
|
168
|
+
includeCurrentTurn: {
|
|
169
|
+
type: 'boolean',
|
|
170
|
+
description: {
|
|
171
|
+
en: 'Include the current query and later messages in this Session (default: false when a trusted turn boundary is available).',
|
|
172
|
+
zh: '纳入当前 Session 的本轮提问及之后的消息(有可信轮次边界时默认 false)。',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
167
175
|
scope: {
|
|
168
176
|
type: 'string',
|
|
169
177
|
enum: ['session', 'project'],
|
|
@@ -178,13 +186,17 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
178
186
|
isConcurrencySafe: () => true,
|
|
179
187
|
isReadOnly: () => true,
|
|
180
188
|
async execute(input, ctx) {
|
|
181
|
-
const { keyword, limit = DEFAULT_RESULT_LIMIT, scope = 'session' } = input;
|
|
189
|
+
const { keyword, limit = DEFAULT_RESULT_LIMIT, scope = 'session', includeCurrentTurn = false } = input;
|
|
182
190
|
const normalizedKeyword = typeof keyword === 'string' ? keyword.trim() : '';
|
|
183
191
|
if (!normalizedKeyword) return JSON.stringify({ error: 'keyword is required' });
|
|
184
192
|
if (scope !== 'session' && scope !== 'project') {
|
|
185
193
|
return JSON.stringify({ error: 'scope must be "session" or "project"' });
|
|
186
194
|
}
|
|
187
195
|
|
|
196
|
+
if (typeof includeCurrentTurn !== 'boolean') {
|
|
197
|
+
return JSON.stringify({ error: 'includeCurrentTurn must be a boolean' });
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
const yeaftDir = ctx?.yeaftDir;
|
|
189
201
|
if (!yeaftDir) {
|
|
190
202
|
return JSON.stringify({ error: 'Yeaft directory not configured — no conversation history available' });
|
|
@@ -214,6 +226,11 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
214
226
|
const messages = searchMessages(storeDir, normalizedKeyword, limit, {
|
|
215
227
|
telemetry: scanTelemetry,
|
|
216
228
|
sessionIds: [sessionId],
|
|
229
|
+
...(!includeCurrentTurn && sessionId === ctx.sessionId ? {
|
|
230
|
+
boundarySessionId: ctx.sessionId,
|
|
231
|
+
beforeSeq: ctx.historyBeforeSeq,
|
|
232
|
+
beforeMessageId: ctx.historyBeforeMessageId,
|
|
233
|
+
} : {}),
|
|
217
234
|
});
|
|
218
235
|
telemetry.scannedSessions = (telemetry.scannedSessions || 0) + 1;
|
|
219
236
|
telemetry.scannedFiles = (telemetry.scannedFiles || 0) + (scanTelemetry.scannedFiles || 0);
|
|
@@ -224,6 +241,7 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
224
241
|
messageId: message.id || null,
|
|
225
242
|
sessionId: message.sessionId || sessionId,
|
|
226
243
|
role: message.role,
|
|
244
|
+
responseKind: message.responseKind || null,
|
|
227
245
|
content: buildSnippet(message.content || '', normalizedKeyword),
|
|
228
246
|
mode: message.mode || null,
|
|
229
247
|
time: message.time || message.timestamp || null,
|
|
@@ -242,6 +260,7 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
242
260
|
messageId: msg.id || null,
|
|
243
261
|
sessionId: msg.sessionId || null,
|
|
244
262
|
role: msg.role,
|
|
263
|
+
responseKind: msg.responseKind || null,
|
|
245
264
|
content: buildSnippet(msg.content, normalizedKeyword),
|
|
246
265
|
mode: msg.mode,
|
|
247
266
|
time: msg.time || msg.timestamp || null,
|
|
@@ -277,6 +296,7 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
277
296
|
results: [],
|
|
278
297
|
scope: ctx?.sessionId ? scope : 'legacy-global',
|
|
279
298
|
message: `No matches found for "${normalizedKeyword}"`,
|
|
299
|
+
notice: RESULT_NOTICE,
|
|
280
300
|
telemetry: searchTelemetry,
|
|
281
301
|
});
|
|
282
302
|
}
|
|
@@ -286,9 +306,9 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
286
306
|
totalResults: results.length,
|
|
287
307
|
keyword: normalizedKeyword,
|
|
288
308
|
scope: ctx?.sessionId ? scope : 'legacy-global',
|
|
289
|
-
|
|
290
|
-
?
|
|
291
|
-
:
|
|
309
|
+
notice: RESULT_NOTICE + (scope === 'project' && ctx?.sessionId
|
|
310
|
+
? ' Sibling Session results are reference context, not continuation of the current task.'
|
|
311
|
+
: ''),
|
|
292
312
|
telemetry: searchTelemetry,
|
|
293
313
|
});
|
|
294
314
|
} catch (err) {
|
package/yeaft/tools/types.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* @property {(input: {query:string, cursor?:number, maxResults?:number}) => Promise<object>|object} [discoverTools] — page through hidden registered tool capabilities and activate returned entries for the next provider loop
|
|
26
26
|
* @property {import('../tasks/manager.js').TaskManager} [taskManager] — Session task manager
|
|
27
27
|
* @property {string} [sessionId] — current Session id
|
|
28
|
+
* @property {string} [queryTurnId] — unique identity of the owning Engine query
|
|
29
|
+
* @property {number} [historyBeforeSeq] — durable current-user boundary; default history recall excludes this sequence and later
|
|
28
30
|
* @property {string[]} [projectSessionIds] — same-Agent sibling Session ids in the current Project
|
|
29
31
|
* @property {string} [threadId] — current Session thread id
|
|
30
32
|
* @property {string} [currentVpId] — R6: VP id of the caller (set in multi-VP groups)
|
|
@@ -36,6 +36,7 @@ import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
|
36
36
|
import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
37
37
|
import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
|
|
38
38
|
import { describeAgentLifecycle, describeAgentOutcome } from '../sub-agent/outcome.js';
|
|
39
|
+
import { getAgentResultRef } from '../sub-agent/result-reference.js';
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Build the status-specific next-step guidance the LLM reads after a wait.
|
|
@@ -203,7 +204,19 @@ function buildEnvelope(agent, { timedOut = false } = {}) {
|
|
|
203
204
|
env.truncated = Boolean(agent.finalReport?.truncated);
|
|
204
205
|
env.partial_output = resultText;
|
|
205
206
|
}
|
|
206
|
-
env.
|
|
207
|
+
env.resultRef = getAgentResultRef(agent);
|
|
208
|
+
const settled = !timedOut && (status === STATUS.IDLE || isTerminalAgentStatus(status));
|
|
209
|
+
if (settled && agent.collectedResultRef === env.resultRef) {
|
|
210
|
+
env.alreadyCollected = true;
|
|
211
|
+
env.next_steps = 'This unchanged reply was already collected. Reuse resultRef; read outputFile only if you need the original evidence. Do not wait again for this reply. PromptAgent queues new work; CloseAgent finalizes idle work.';
|
|
212
|
+
// Preserve lifecycle/outcome and live diagnostics, not duplicate evidence.
|
|
213
|
+
if (env.error) env.error = env.error.slice(0, 400);
|
|
214
|
+
delete env.partial_output;
|
|
215
|
+
delete env.final_report;
|
|
216
|
+
} else {
|
|
217
|
+
env.result = resultText;
|
|
218
|
+
if (settled) agent.collectedResultRef = env.resultRef;
|
|
219
|
+
}
|
|
207
220
|
return env;
|
|
208
221
|
}
|
|
209
222
|
|
|
@@ -235,6 +248,10 @@ wait loop but does not cancel or prove failure: inspect outputFile before decidi
|
|
|
235
248
|
For ordinary SpawnAgent background work, use ListAgents or later completion
|
|
236
249
|
notifications instead of repeatedly re-waiting.
|
|
237
250
|
|
|
251
|
+
An unchanged reply already collected by WaitAgent returns alreadyCollected=true and
|
|
252
|
+
resultRef instead of repeating result text. PromptAgent starts a new collectable revision,
|
|
253
|
+
even when its reply text is identical. Lifecycle and liveness remain current.
|
|
254
|
+
|
|
238
255
|
The default wait is a bounded 5000ms poll; callers may request up to 300000ms
|
|
239
256
|
(5 minutes). This caps one WaitAgent call only, not the sub-agent lifetime.`,
|
|
240
257
|
zh: `等待子 Agent 的下一次状态变更(turn 结束、终止或等待超时)并获取状态信封。
|
|
@@ -256,6 +273,9 @@ PromptAgent 后若非 stale/stalled 的有界等待超时,必须在同一父
|
|
|
256
273
|
若信封显示 stale/stalled,应停止盲目等待并检查 outputFile;它只是诊断,不会取消任务,也不能证明
|
|
257
274
|
provider 或工具已经卡死。仅在确实要取消时使用 CloseAgent。
|
|
258
275
|
|
|
276
|
+
WaitAgent 已收集且未变化的回复仅返回 alreadyCollected=true 和 resultRef,不重印结果。
|
|
277
|
+
PromptAgent 会产生新的可收集 revision,即使回复文本相同;生命周期和 liveness 仍实时返回。
|
|
278
|
+
|
|
259
279
|
普通 SpawnAgent 异步流程仍用 ListAgents 做非阻塞状态检查,并依赖后续 completion
|
|
260
280
|
notification;默认 5000ms、最大 300000ms 仅限制单次 WaitAgent 调用,不限制子 Agent 生命周期。`
|
|
261
281
|
},
|
package/yeaft/tools/web-fetch.js
CHANGED
|
@@ -79,6 +79,13 @@ Guidelines:
|
|
|
79
79
|
zh: '最大内容长度,单位字符(默认 50000)',
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
|
+
polling: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: {
|
|
85
|
+
en: 'Set true only for user-requested live status polling while waiting for external work. Not for repeatedly reading documents, schemas or static API responses. Does not change response formatting.',
|
|
86
|
+
zh: '仅在用户要求等待外部任务并轮询实时状态时设为 true,不用于重复读取文档、schema 或静态 API 响应;不改变响应格式。',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
82
89
|
raw: {
|
|
83
90
|
type: 'boolean',
|
|
84
91
|
description: {
|
|
@@ -91,6 +98,8 @@ Guidelines:
|
|
|
91
98
|
},
|
|
92
99
|
isConcurrencySafe: () => true,
|
|
93
100
|
isReadOnly: () => true,
|
|
101
|
+
// Live waiting is explicit; raw formatting alone never exempts static reads.
|
|
102
|
+
duplicateCallPolicy: (input) => input?.polling === true ? 'allow' : 'warn',
|
|
94
103
|
async execute(input, ctx) {
|
|
95
104
|
const { url, max_length = 50000, raw = false } = input;
|
|
96
105
|
if (!url) return JSON.stringify({ error: 'url is required' });
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -4153,6 +4153,27 @@ export function __testHandleEngineEvent(event, hctx) {
|
|
|
4153
4153
|
}
|
|
4154
4154
|
|
|
4155
4155
|
function handleEngineEvent(event, hctx) {
|
|
4156
|
+
// Observability is not provider activity: never touch watchdogs, text or history.
|
|
4157
|
+
if (event.type === 'query_progress') {
|
|
4158
|
+
const counters = ['revision', 'modelRequests', 'toolExecutions', 'reusedCalls', 'repeatedCalls', 'failedCalls', 'stagnantBatches'];
|
|
4159
|
+
if (hctx.queryProgressTerminal || hctx.signal?.aborted
|
|
4160
|
+
|| event.turnId !== hctx.turnId || event.sessionId !== hctx.sessionId || event.vpId !== hctx.vpId
|
|
4161
|
+
|| event.threadId !== (hctx.threadId || 'main')
|
|
4162
|
+
|| ![event.turnId, event.sessionId, event.vpId].every(value => typeof value === 'string' && value.length > 0)
|
|
4163
|
+
|| !counters.every(key => Number.isSafeInteger(event[key]) && event[key] >= 0)
|
|
4164
|
+
|| event.revision <= (hctx.queryProgressRevision || 0)
|
|
4165
|
+
|| !['running', 'warning', 'recovering', 'stopped'].includes(event.stage)
|
|
4166
|
+
|| ![null, 'repeated_results', 'repeated_failure'].includes(event.reason)
|
|
4167
|
+
|| !(event.lastEvidenceAt === null || (Number.isFinite(event.lastEvidenceAt) && event.lastEvidenceAt >= 0))) return;
|
|
4168
|
+
hctx.queryProgressRevision = event.revision;
|
|
4169
|
+
const { turnId, threadId, sessionId, vpId, revision, modelRequests, toolExecutions, reusedCalls,
|
|
4170
|
+
repeatedCalls, failedCalls, stage, reason, lastEvidenceAt, stagnantBatches } = event;
|
|
4171
|
+
sendToServer({ type: 'yeaft_output', conversationId: yeaftConversationId, sessionId, vpId, turnId, threadId,
|
|
4172
|
+
event: { type: 'query_progress', turnId, threadId, sessionId, vpId, revision, modelRequests,
|
|
4173
|
+
toolExecutions, reusedCalls, repeatedCalls, failedCalls, stage, reason, lastEvidenceAt, stagnantBatches } });
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
if (event.type === 'aborted' || (event.type === 'turn_end' && event.terminal === true)) hctx.queryProgressTerminal = true;
|
|
4156
4177
|
// Hidden progress has no wire/history/status projection and must not flush
|
|
4157
4178
|
// pending visible text or resume a paused/terminal provider watchdog.
|
|
4158
4179
|
if (event.type === 'provider_activity') {
|
|
@@ -4437,7 +4458,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
4437
4458
|
}
|
|
4438
4459
|
break;
|
|
4439
4460
|
}
|
|
4440
|
-
if (event.stopReason === 'error') {
|
|
4461
|
+
if (event.stopReason === 'error' || event.stopReason === 'no_progress') {
|
|
4441
4462
|
if (typeof hctx.markEngineTerminal === 'function') {
|
|
4442
4463
|
hctx.markEngineTerminal('error', hctx.lastEngineErrorDetail || event.detail || null);
|
|
4443
4464
|
}
|
|
@@ -4694,7 +4715,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
4694
4715
|
// Move the broker out of its running-only `retrying` state here so
|
|
4695
4716
|
// reconnect snapshots cannot resurrect a finished Session as active.
|
|
4696
4717
|
maybeTransitionVpStatus(hctx, 'error');
|
|
4697
|
-
const errMsg = event.error?.message || 'Unknown error';
|
|
4718
|
+
const errMsg = (typeof event.error === 'string' ? event.error : event.error?.message) || 'Unknown error';
|
|
4698
4719
|
const retryAttempts = Number.isFinite(event.retryAttempts) ? event.retryAttempts : 0;
|
|
4699
4720
|
const exhaustedIdle = event.reason === 'stream_idle_timeout' && event.retryExhausted;
|
|
4700
4721
|
const contentPolicyDenied = event.reason === 'content_policy_denied'
|
|
@@ -4706,6 +4727,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
4706
4727
|
: errMsg;
|
|
4707
4728
|
hctx.lastEngineErrorDetail = {
|
|
4708
4729
|
message: visibleErrMsg,
|
|
4730
|
+
...(event.code ? { code: event.code } : {}),
|
|
4709
4731
|
...(event.reason ? { reason: event.reason } : {}),
|
|
4710
4732
|
...(event.retryExhausted !== undefined ? { retryExhausted: !!event.retryExhausted } : {}),
|
|
4711
4733
|
...(Number.isFinite(event.retryAttempts) ? { retryAttempts: event.retryAttempts } : {}),
|
|
@@ -8028,6 +8050,7 @@ export const __testHooks = {
|
|
|
8028
8050
|
projectVisibleHistoryChunkMessages,
|
|
8029
8051
|
persistInboundMessageOnceByMsgId,
|
|
8030
8052
|
buildPendingRescueEnvelope,
|
|
8053
|
+
runVpTurnForTest: runVpTurn,
|
|
8031
8054
|
runYeaftSessionSendForTest(msg) {
|
|
8032
8055
|
return runYeaftSessionSend(msg);
|
|
8033
8056
|
},
|
|
@@ -1396,7 +1396,7 @@ export class WorkItemRunner {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
// Provider activity is liveness-only; keep control handling above, but
|
|
1398
1398
|
// never turn it into durable Run progress or a UI broadcast.
|
|
1399
|
-
if (event?.type === 'provider_activity') continue;
|
|
1399
|
+
if (event?.type === 'provider_activity' || event?.type === 'query_progress') continue;
|
|
1400
1400
|
if (event?.type === 'loop') {
|
|
1401
1401
|
loopCount += 1;
|
|
1402
1402
|
this.store.appendRunLoop?.(run.id, ownerBootId, run.leaseEpoch, {
|