@lemoncat7/dsh-partner 1.12.0 → 1.14.0
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/README.md +22 -2
- package/docs/architecture.md +4 -0
- package/docs/releases/1.14.0.md +24 -0
- package/lib/agent-runtime.d.ts +23 -2
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +45 -282
- package/lib/agent-runtime.js.map +1 -1
- package/lib/api/features/workspace-api.d.ts.map +1 -1
- package/lib/api/features/workspace-api.js +7 -0
- package/lib/api/features/workspace-api.js.map +1 -1
- package/lib/api/http.d.ts +1 -1
- package/lib/api/http.d.ts.map +1 -1
- package/lib/api/http.js +2 -2
- package/lib/api/http.js.map +1 -1
- package/lib/api.js +34 -2
- package/lib/api.js.map +1 -1
- package/lib/autonomy.d.ts.map +1 -1
- package/lib/autonomy.js +13 -5
- package/lib/autonomy.js.map +1 -1
- package/lib/client-api.d.ts +8 -1
- package/lib/client-api.d.ts.map +1 -1
- package/lib/client-api.js.map +1 -1
- package/lib/client.js +1875 -1379
- package/lib/client.js.map +4 -4
- package/lib/collaboration/composition.js +7 -3
- package/lib/collaboration/composition.js.map +1 -1
- package/lib/companions/session-recovery.d.ts +7 -0
- package/lib/companions/session-recovery.d.ts.map +1 -0
- package/lib/companions/session-recovery.js +22 -0
- package/lib/companions/session-recovery.js.map +1 -0
- package/lib/concern-domain.d.ts +9 -0
- package/lib/concern-domain.d.ts.map +1 -1
- package/lib/concern-domain.js.map +1 -1
- package/lib/concern-recording.d.ts +20 -0
- package/lib/concern-recording.d.ts.map +1 -0
- package/lib/concern-recording.js +37 -0
- package/lib/concern-recording.js.map +1 -0
- package/lib/concern-store.d.ts +20 -1
- package/lib/concern-store.d.ts.map +1 -1
- package/lib/concern-store.js +170 -1
- package/lib/concern-store.js.map +1 -1
- package/lib/heartbeat.d.ts +16 -0
- package/lib/heartbeat.d.ts.map +1 -1
- package/lib/heartbeat.js +70 -5
- package/lib/heartbeat.js.map +1 -1
- package/lib/memory-errors.d.ts +6 -0
- package/lib/memory-errors.d.ts.map +1 -0
- package/lib/memory-errors.js +12 -0
- package/lib/memory-errors.js.map +1 -0
- package/lib/memory-journal.js +4 -4
- package/lib/memory-journal.js.map +1 -1
- package/lib/memory-reflection.d.ts.map +1 -1
- package/lib/memory-reflection.js +11 -3
- package/lib/memory-reflection.js.map +1 -1
- package/lib/memory-store.d.ts +4 -0
- package/lib/memory-store.d.ts.map +1 -1
- package/lib/memory-store.js +11 -3
- package/lib/memory-store.js.map +1 -1
- package/lib/observation-budget.d.ts +10 -0
- package/lib/observation-budget.d.ts.map +1 -0
- package/lib/observation-budget.js +11 -0
- package/lib/observation-budget.js.map +1 -0
- package/lib/observation-errors.d.ts +22 -0
- package/lib/observation-errors.d.ts.map +1 -0
- package/lib/observation-errors.js +37 -0
- package/lib/observation-errors.js.map +1 -0
- package/lib/observation-executor.d.ts +7 -0
- package/lib/observation-executor.d.ts.map +1 -0
- package/lib/observation-executor.js +23 -0
- package/lib/observation-executor.js.map +1 -0
- package/lib/observation-loop.d.ts +17 -0
- package/lib/observation-loop.d.ts.map +1 -0
- package/lib/observation-loop.js +195 -0
- package/lib/observation-loop.js.map +1 -0
- package/lib/recording-snapshot.d.ts +11 -0
- package/lib/recording-snapshot.d.ts.map +1 -0
- package/lib/recording-snapshot.js +19 -0
- package/lib/recording-snapshot.js.map +1 -0
- package/lib/skills/import-request.d.ts +3 -0
- package/lib/skills/import-request.d.ts.map +1 -0
- package/lib/skills/import-request.js +26 -0
- package/lib/skills/import-request.js.map +1 -0
- package/lib/skills/package-zip.d.ts +5 -0
- package/lib/skills/package-zip.d.ts.map +1 -0
- package/lib/skills/package-zip.js +75 -0
- package/lib/skills/package-zip.js.map +1 -0
- package/lib/skills/package.d.ts +15 -0
- package/lib/skills/package.d.ts.map +1 -0
- package/lib/skills/package.js +48 -0
- package/lib/skills/package.js.map +1 -0
- package/lib/skills/repository.d.ts +2 -0
- package/lib/skills/repository.d.ts.map +1 -1
- package/lib/skills/repository.js +7 -0
- package/lib/skills/repository.js.map +1 -1
- package/lib/skills/service.d.ts +2 -0
- package/lib/skills/service.d.ts.map +1 -1
- package/lib/skills/service.js +10 -2
- package/lib/skills/service.js.map +1 -1
- package/lib/skills/task-planning.d.ts +2 -2
- package/lib/skills/task-planning.d.ts.map +1 -1
- package/lib/skills/task-planning.js +8 -1
- package/lib/skills/task-planning.js.map +1 -1
- package/lib/ui/memory/concern-board.d.ts +4 -2
- package/lib/ui/memory/concern-board.d.ts.map +1 -1
- package/lib/ui/memory/concern-board.js +17 -6
- package/lib/ui/memory/concern-board.js.map +1 -1
- package/lib/ui/memory/concern-deletion.d.ts +12 -0
- package/lib/ui/memory/concern-deletion.d.ts.map +1 -0
- package/lib/ui/memory/concern-deletion.js +43 -0
- package/lib/ui/memory/concern-deletion.js.map +1 -0
- package/lib/ui/memory/concern-editor.d.ts +8 -0
- package/lib/ui/memory/concern-editor.d.ts.map +1 -0
- package/lib/ui/memory/concern-editor.js +37 -0
- package/lib/ui/memory/concern-editor.js.map +1 -0
- package/lib/ui/memory/concern-panel.d.ts.map +1 -1
- package/lib/ui/memory/concern-panel.js +51 -12
- package/lib/ui/memory/concern-panel.js.map +1 -1
- package/lib/ui/memory/concern-record-fields.d.ts +10 -0
- package/lib/ui/memory/concern-record-fields.d.ts.map +1 -0
- package/lib/ui/memory/concern-record-fields.js +7 -0
- package/lib/ui/memory/concern-record-fields.js.map +1 -0
- package/lib/ui/memory/job-feedback.d.ts +5 -0
- package/lib/ui/memory/job-feedback.d.ts.map +1 -0
- package/lib/ui/memory/job-feedback.js +7 -0
- package/lib/ui/memory/job-feedback.js.map +1 -0
- package/lib/ui/memory/memory-panel.d.ts.map +1 -1
- package/lib/ui/memory/memory-panel.js +4 -2
- package/lib/ui/memory/memory-panel.js.map +1 -1
- package/lib/ui/memory/record-target-picker.d.ts +7 -0
- package/lib/ui/memory/record-target-picker.d.ts.map +1 -0
- package/lib/ui/memory/record-target-picker.js +33 -0
- package/lib/ui/memory/record-target-picker.js.map +1 -0
- package/lib/ui/memory/types.d.ts +4 -0
- package/lib/ui/memory/types.d.ts.map +1 -1
- package/lib/ui/skill-import-dialog.d.ts +5 -0
- package/lib/ui/skill-import-dialog.d.ts.map +1 -0
- package/lib/ui/skill-import-dialog.js +72 -0
- package/lib/ui/skill-import-dialog.js.map +1 -0
- package/lib/ui/skills-panel.d.ts.map +1 -1
- package/lib/ui/skills-panel.js +3 -1
- package/lib/ui/skills-panel.js.map +1 -1
- package/package.json +2 -1
package/lib/agent-runtime.js
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, realpath, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
5
|
+
import { scopeOf, scopeParentOf } from '@deepseek-ai/dsh-scope';
|
|
6
6
|
import { SessionConfigurationIndex } from './companions/session-configuration.js';
|
|
7
|
+
import { isMissingSession, SessionRecoveryJobs } from './companions/session-recovery.js';
|
|
7
8
|
import { removeOwnedWorkspace } from './companions/workspace-cleanup.js';
|
|
8
9
|
import { assertOwnedDirectory, removeOwnedDirectory } from './companions/directory-cleanup.js';
|
|
9
|
-
import { concernObservationPrompt } from './autonomy.js';
|
|
10
10
|
import { boundedConcernCheckMinutes } from './concern-domain.js';
|
|
11
11
|
import { taskWorkContext } from './tasks/context.js';
|
|
12
12
|
import { PARTNER_MEDIA_MAX_BYTES, safeMediaName } from './channel-message.js';
|
|
13
13
|
import { listConcernFileSources } from './concern-sources.js';
|
|
14
14
|
import { CONCERN_CREATED_NOTICE, renderConcernCreatedNotice } from './concern-notification.js';
|
|
15
|
-
import {
|
|
15
|
+
import { executeObservationLoop } from './observation-loop.js';
|
|
16
16
|
import { assistantTextAfter, renderPartnerPersona, renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
|
|
17
17
|
import { channelReplyPartsAfter } from './channels/delivery-policy.js';
|
|
18
18
|
import { prepareChannelReply } from './channels/outbound-media.js';
|
|
19
19
|
export { extractOutboundAttachments } from './channels/outbound-media.js';
|
|
20
20
|
export { renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
|
|
21
|
-
const HEARTBEAT_TIMEOUT_MS = 115_000;
|
|
22
|
-
const HEARTBEAT_DISCOVERY_BUDGET_MS = 72_000;
|
|
23
|
-
const HEARTBEAT_TOOL_TIMEOUT_MS = 18_000;
|
|
24
|
-
const HEARTBEAT_WEB_TOOL_TIMEOUT_MS = 30_000;
|
|
25
|
-
const HEARTBEAT_TOOL_RESULT_LIMIT = 5_000;
|
|
26
|
-
const HEARTBEAT_KNOWLEDGE_RESULT_LIMIT = 8_000;
|
|
27
|
-
const HEARTBEAT_WEB_FETCH_RESULT_LIMIT = 40_000;
|
|
28
|
-
const HEARTBEAT_WEB_SOURCE_RESULT_LIMIT = 125_000;
|
|
29
|
-
const HEARTBEAT_AUDIT_INPUT_LIMIT = 320;
|
|
30
|
-
const HEARTBEAT_AUDIT_OUTPUT_LIMIT = 520;
|
|
31
21
|
const PARTNER_TASK_TIMEOUT_MS = 30 * 60_000;
|
|
32
22
|
const HEARTBEAT_READ_ONLY_TOOLS = new Set([
|
|
33
23
|
'knowledge_base_search',
|
|
@@ -41,8 +31,6 @@ const HEARTBEAT_READ_ONLY_TOOLS = new Set([
|
|
|
41
31
|
'read',
|
|
42
32
|
]);
|
|
43
33
|
const HEARTBEAT_FILE_UPDATE_TOOLS = new Set(['write', 'edit', 'str_replace_editor']);
|
|
44
|
-
const HEARTBEAT_TOOLS = new Set([...HEARTBEAT_READ_ONLY_TOOLS, ...HEARTBEAT_FILE_UPDATE_TOOLS]);
|
|
45
|
-
HEARTBEAT_TOOLS.add(HEARTBEAT_LOCAL_COMMAND);
|
|
46
34
|
export class PartnerAgentRuntime {
|
|
47
35
|
ctx;
|
|
48
36
|
store;
|
|
@@ -66,6 +54,7 @@ export class PartnerAgentRuntime {
|
|
|
66
54
|
preparedTurns = new WeakMap();
|
|
67
55
|
observationJobs = new Map();
|
|
68
56
|
agentSetupJobs = new Map();
|
|
57
|
+
sessionRecoveryJobs = new SessionRecoveryJobs();
|
|
69
58
|
closed = false;
|
|
70
59
|
questionAnswerer;
|
|
71
60
|
constructor(ctx, store, defaultCwd, memory, reflection, concerns, composer) {
|
|
@@ -328,44 +317,6 @@ export class PartnerAgentRuntime {
|
|
|
328
317
|
this.heartbeatQueues.delete(key);
|
|
329
318
|
}
|
|
330
319
|
}
|
|
331
|
-
async persistKnowledgeObservations(companion, route, concerns, observations) {
|
|
332
|
-
const service = this.ctx.get('dshKnowledgeTracking');
|
|
333
|
-
if (service === undefined || observations.length === 0)
|
|
334
|
-
return;
|
|
335
|
-
const agent = await this.ensureAgent(companion, route);
|
|
336
|
-
const byId = new Map(concerns.map(item => [item.id, item]));
|
|
337
|
-
const controller = new AbortController();
|
|
338
|
-
const timeout = setTimeout(() => controller.abort(new Error('知识库观察记录超时')), 30_000);
|
|
339
|
-
timeout.unref?.();
|
|
340
|
-
try {
|
|
341
|
-
for (const observation of observations) {
|
|
342
|
-
if (observation.decision === 'drop')
|
|
343
|
-
continue;
|
|
344
|
-
const concern = byId.get(observation.concernId);
|
|
345
|
-
if (concern === undefined)
|
|
346
|
-
continue;
|
|
347
|
-
const references = concern.resources.filter(item => item.kind === 'knowledge').map(item => item.locator).slice(0, 4);
|
|
348
|
-
if (references.length === 0 && concern.watchKind !== 'knowledge')
|
|
349
|
-
continue;
|
|
350
|
-
for (const reference of references.length > 0 ? references : [undefined]) {
|
|
351
|
-
await service.record(agent, {
|
|
352
|
-
id: observation.id,
|
|
353
|
-
subject: concern.subject,
|
|
354
|
-
event: observation.event,
|
|
355
|
-
evidence: observation.evidence,
|
|
356
|
-
source: observation.source,
|
|
357
|
-
...(reference === undefined ? {} : { reference }),
|
|
358
|
-
at: observation.createdAt,
|
|
359
|
-
}, controller.signal).catch(error => {
|
|
360
|
-
this.ctx.logger.warn(`dsh-partner: knowledge observation fell back to local storage: ${errorMessage(error)}`);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
finally {
|
|
366
|
-
clearTimeout(timeout);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
320
|
async concernSources(companion, query = '') {
|
|
370
321
|
const routes = this.store.snapshot().sessions
|
|
371
322
|
.filter(item => item.companionId === companion.id)
|
|
@@ -390,6 +341,24 @@ export class PartnerAgentRuntime {
|
|
|
390
341
|
clearTimeout(timeout);
|
|
391
342
|
}
|
|
392
343
|
}
|
|
344
|
+
async recordingSources(companion, query = '') {
|
|
345
|
+
const route = this.store.snapshot().sessions.filter(item => item.companionId === companion.id).sort((a, b) => b.lastMessageAt - a.lastMessageAt)[0];
|
|
346
|
+
const files = await listConcernFileSources(route?.cwd ?? partnerCwd(this.defaultCwd, companion.id), query, 24);
|
|
347
|
+
const bridge = this.ctx.get('dshKnowledgeNoteRecording');
|
|
348
|
+
const notes = bridge?.version === 1 ? await bridge.list(query, AbortSignal.timeout(8000)) : [];
|
|
349
|
+
return [...files.map(file => ({ kind: 'file', locator: file.label, label: file.label })), ...notes.map(note => ({ kind: 'note', locator: note.id, label: note.name }))];
|
|
350
|
+
}
|
|
351
|
+
async validateRecordingTarget(companion, raw) {
|
|
352
|
+
if (raw === undefined || raw === null)
|
|
353
|
+
return undefined;
|
|
354
|
+
if (!isRecord(raw) || typeof raw.locator !== 'string' || typeof raw.label !== 'string')
|
|
355
|
+
throw new Error('记录位置无效,请重新选择');
|
|
356
|
+
const choices = await this.recordingSources(companion, raw.kind === 'file' ? raw.locator : raw.label);
|
|
357
|
+
const selected = choices.find(item => item.kind === raw.kind && item.locator === raw.locator);
|
|
358
|
+
if (!selected)
|
|
359
|
+
throw new Error('记录位置已不存在或不可写,请重新选择');
|
|
360
|
+
return selected;
|
|
361
|
+
}
|
|
393
362
|
async runHeartbeatWhenIdle(key, companion, route, concerns) {
|
|
394
363
|
for (;;) {
|
|
395
364
|
await this.queues.get(key)?.catch(() => { });
|
|
@@ -486,38 +455,9 @@ export class PartnerAgentRuntime {
|
|
|
486
455
|
return true;
|
|
487
456
|
}
|
|
488
457
|
async driveScopedHeartbeat(conversation, companion, concerns) {
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
finally {
|
|
494
|
-
await scope.dispose();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
async driveEphemeralHeartbeat(agent, companion, concerns) {
|
|
498
|
-
const controller = new AbortController();
|
|
499
|
-
const timeout = setTimeout(() => controller.abort(new Error('伙伴心跳超过安全时限')), HEARTBEAT_TIMEOUT_MS);
|
|
500
|
-
timeout.unref?.();
|
|
501
|
-
try {
|
|
502
|
-
const execution = await runHeartbeatInference(this.ctx, agent, companion, concerns, controller.signal);
|
|
503
|
-
return controller.signal.aborted
|
|
504
|
-
? { ...execution, error: '伙伴心跳超过安全时限,已收束并终止' }
|
|
505
|
-
: execution;
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
const now = Date.now();
|
|
509
|
-
return {
|
|
510
|
-
concerns,
|
|
511
|
-
candidates: [],
|
|
512
|
-
startedAt: now,
|
|
513
|
-
completedAt: now,
|
|
514
|
-
tools: [],
|
|
515
|
-
error: controller.signal.aborted ? '伙伴心跳超过安全时限,已收束并终止' : errorMessage(error),
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
finally {
|
|
519
|
-
clearTimeout(timeout);
|
|
520
|
-
}
|
|
458
|
+
const access = await resolveHeartbeatFileAccess(conversation.session.header.cwd, concerns);
|
|
459
|
+
return executeObservationLoop({ ctx: this.ctx, conversation, companion, concerns, guard: (name, args) => heartbeatToolDenial(name, args, access), parse: parseConcernObservations, checkpoint: async (item, snapshot) => { if (!this.concerns)
|
|
460
|
+
throw new Error('关注存储不可用'); await this.concerns.checkpointRecording(item, snapshot); } });
|
|
521
461
|
}
|
|
522
462
|
async recordHeartbeatActivity(companion, route, execution, outcome, deliveryError) {
|
|
523
463
|
const conversation = await this.ensureAgent(companion, route);
|
|
@@ -734,7 +674,7 @@ export class PartnerAgentRuntime {
|
|
|
734
674
|
const id = companion.id;
|
|
735
675
|
this.agentSetupJobs.set(id, (this.agentSetupJobs.get(id) ?? 0) + 1);
|
|
736
676
|
try {
|
|
737
|
-
return await this.ensureAgentReady(companion, route);
|
|
677
|
+
return await this.sessionRecoveryJobs.run(route.sessionId, () => this.ensureAgentReady(companion, route));
|
|
738
678
|
}
|
|
739
679
|
finally {
|
|
740
680
|
const left = (this.agentSetupJobs.get(id) ?? 1) - 1;
|
|
@@ -779,7 +719,10 @@ export class PartnerAgentRuntime {
|
|
|
779
719
|
setup,
|
|
780
720
|
});
|
|
781
721
|
}
|
|
782
|
-
catch {
|
|
722
|
+
catch (error) {
|
|
723
|
+
// Never turn a preset/loading/permission failure into a duplicate create.
|
|
724
|
+
if (!isMissingSession(error, route.sessionId))
|
|
725
|
+
throw error;
|
|
783
726
|
handle = await this.ctx.agents.create({
|
|
784
727
|
sessionId: route.sessionId,
|
|
785
728
|
meta: {
|
|
@@ -960,123 +903,13 @@ export function renewedSession(previous, now = Date.now()) {
|
|
|
960
903
|
export function partnerCwd(root, companionId) {
|
|
961
904
|
return join(root, 'partners', companionId);
|
|
962
905
|
}
|
|
963
|
-
async function createHeartbeatScope(ctx, conversation, concerns) {
|
|
964
|
-
const parent = scopeOf(conversation.ctx);
|
|
965
|
-
if (parent === undefined)
|
|
966
|
-
throw new Error('伙伴心跳无法解析当前 Agent 作用域');
|
|
967
|
-
const availableTools = [...HEARTBEAT_TOOLS].filter(name => ctx.tools.get(name, conversation) !== undefined);
|
|
968
|
-
const policy = heartbeatToolPolicy(concerns, availableTools);
|
|
969
|
-
const allowedTools = [...policy.allowed];
|
|
970
|
-
const cwd = conversation.session.header.cwd;
|
|
971
|
-
if (!cwd)
|
|
972
|
-
throw new Error('伙伴心跳缺少当前伙伴工作目录');
|
|
973
|
-
const fileAccess = await resolveHeartbeatFileAccess(cwd, concerns);
|
|
974
|
-
const agent = {};
|
|
975
|
-
const scope = createScope(ctx, agent, { parent });
|
|
976
|
-
const agentCtx = scope.ctx.extend({ agent });
|
|
977
|
-
Object.assign(agent, {
|
|
978
|
-
id: conversation.id,
|
|
979
|
-
options: conversation.options,
|
|
980
|
-
session: conversation.session,
|
|
981
|
-
inbox: conversation.inbox,
|
|
982
|
-
status: 'idle',
|
|
983
|
-
ctx: agentCtx,
|
|
984
|
-
cancel: () => { },
|
|
985
|
-
whenIdle: async () => { },
|
|
986
|
-
runMaintenance: async (job) => job(new AbortController().signal),
|
|
987
|
-
send: () => { throw new Error('心跳作用域不接受会话消息'); },
|
|
988
|
-
followup: () => { throw new Error('心跳作用域不接受会话消息'); },
|
|
989
|
-
steer: () => { throw new Error('心跳作用域不接受会话消息'); },
|
|
990
|
-
inject: () => { throw new Error('心跳作用域不接受会话消息'); },
|
|
991
|
-
});
|
|
992
|
-
agentCtx.tools.presentAs('native');
|
|
993
|
-
agentCtx.tools.register(heartbeatLocalCommandTool(cwd));
|
|
994
|
-
if (allowedTools.length > 0)
|
|
995
|
-
agentCtx.tools.restrict({ allow: allowedTools });
|
|
996
|
-
agentCtx.tools.guard(exec => {
|
|
997
|
-
if (exec.name !== HEARTBEAT_LOCAL_COMMAND && !policy.allowed.has(exec.name))
|
|
998
|
-
return `当前挂念的来源策略不允许调用 ${exec.name}`;
|
|
999
|
-
return heartbeatToolDenial(exec.name, exec.arguments, fileAccess);
|
|
1000
|
-
});
|
|
1001
|
-
return { agent, dispose: () => scope.dispose() };
|
|
1002
|
-
}
|
|
1003
906
|
export function heartbeatToolPolicy(concerns, available) {
|
|
1004
907
|
const installed = new Set(available);
|
|
1005
|
-
const hasLinkedFile = concerns.some(item => item.
|
|
908
|
+
const hasLinkedFile = concerns.some(item => item.recordTarget?.kind === 'file');
|
|
1006
909
|
return {
|
|
1007
910
|
allowed: new Set([...installed].filter(name => HEARTBEAT_READ_ONLY_TOOLS.has(name) || (hasLinkedFile && HEARTBEAT_FILE_UPDATE_TOOLS.has(name)))),
|
|
1008
911
|
};
|
|
1009
912
|
}
|
|
1010
|
-
async function runHeartbeatInference(ctx, agent, companion, concerns, signal) {
|
|
1011
|
-
const startedAt = Date.now();
|
|
1012
|
-
const traces = [];
|
|
1013
|
-
const discoveryDeadline = startedAt + HEARTBEAT_DISCOVERY_BUDGET_MS;
|
|
1014
|
-
try {
|
|
1015
|
-
const selection = resolveAgentOptions(ctx.agentDefaultModel, companion);
|
|
1016
|
-
if (!selection.provider || !selection.model)
|
|
1017
|
-
throw new Error('伙伴心跳没有可用的模型路由');
|
|
1018
|
-
const tools = ctx.tools.schemas(agent).filter(tool => HEARTBEAT_TOOLS.has(tool.name));
|
|
1019
|
-
if (tools.length === 0)
|
|
1020
|
-
throw new Error('伙伴心跳没有可用的只读发现工具');
|
|
1021
|
-
const messages = [createUserMessage({
|
|
1022
|
-
content: [{ type: 'text', text: concernObservationPrompt(concerns, agent.session.header.cwd) }],
|
|
1023
|
-
source: { kind: 'plugin', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: '伙伴观察挂念变化' },
|
|
1024
|
-
})];
|
|
1025
|
-
for (;;) {
|
|
1026
|
-
const discoveryOpen = Date.now() < discoveryDeadline;
|
|
1027
|
-
const prepared = await ctx.llm.prepareCall(selection, signal);
|
|
1028
|
-
const assembler = new BlockAssembler();
|
|
1029
|
-
for await (const chunk of prepared.stream({
|
|
1030
|
-
...prepared.config,
|
|
1031
|
-
messages,
|
|
1032
|
-
system: renderPartnerPersona(companion, 'heartbeat'),
|
|
1033
|
-
tools: discoveryOpen ? tools : [],
|
|
1034
|
-
signal,
|
|
1035
|
-
}))
|
|
1036
|
-
assembler.push(chunk);
|
|
1037
|
-
const finish = assembler.finish;
|
|
1038
|
-
if (finish.kind === 'error' || finish.kind === 'aborted') {
|
|
1039
|
-
throw new Error(`伙伴心跳模型调用失败:${finish.failure.message}`);
|
|
1040
|
-
}
|
|
1041
|
-
const response = assembler.message({ kind: 'model', provider: prepared.config.provider, model: prepared.config.model });
|
|
1042
|
-
messages.push(response);
|
|
1043
|
-
const pending = response.content.filter((block) => block.type === 'tool-call');
|
|
1044
|
-
if (pending.length === 0) {
|
|
1045
|
-
const output = textContent(response.content);
|
|
1046
|
-
return {
|
|
1047
|
-
concerns,
|
|
1048
|
-
candidates: parseConcernObservations(output, new Set(concerns.map(item => item.id))),
|
|
1049
|
-
startedAt,
|
|
1050
|
-
completedAt: Date.now(),
|
|
1051
|
-
output,
|
|
1052
|
-
tools: traces,
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
for (const call of pending) {
|
|
1056
|
-
const argumentsValue = boundHeartbeatToolArguments(call.name, parseHeartbeatToolArguments(call));
|
|
1057
|
-
const toolStartedAt = Date.now();
|
|
1058
|
-
const remainingDiscoveryMs = discoveryDeadline - toolStartedAt;
|
|
1059
|
-
const result = remainingDiscoveryMs <= 0
|
|
1060
|
-
? { isError: true, content: [{ type: 'text', text: '本轮发现阶段的时间预算已用完,请根据已有结果立即输出最终 Observation JSON。' }] }
|
|
1061
|
-
: await executeHeartbeatTool(ctx, agent, call, argumentsValue, signal, remainingDiscoveryMs);
|
|
1062
|
-
const toolCompletedAt = Date.now();
|
|
1063
|
-
const visibleContent = heartbeatToolContent(call.name, result.content);
|
|
1064
|
-
traces.push({
|
|
1065
|
-
name: call.name,
|
|
1066
|
-
input: auditValue(argumentsValue, HEARTBEAT_AUDIT_INPUT_LIMIT),
|
|
1067
|
-
output: auditText(textContent(visibleContent) || '工具返回了非文本结果', HEARTBEAT_AUDIT_OUTPUT_LIMIT),
|
|
1068
|
-
startedAt: toolStartedAt,
|
|
1069
|
-
completedAt: toolCompletedAt,
|
|
1070
|
-
status: result.isError ? 'failed' : 'completed',
|
|
1071
|
-
});
|
|
1072
|
-
messages.push(createToolResultMessage({ callId: call.id, content: visibleContent, isError: result.isError }));
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
return { concerns, candidates: [], startedAt, completedAt: Date.now(), tools: traces, error: errorMessage(error) };
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
913
|
export function parseConcernObservations(raw, allowedIds) {
|
|
1081
914
|
const match = raw.trim().match(/\{[\s\S]*\}/);
|
|
1082
915
|
if (!match)
|
|
@@ -1115,18 +948,10 @@ export function parseConcernObservations(raw, allowedIds) {
|
|
|
1115
948
|
function boundedScore(value) {
|
|
1116
949
|
return typeof value === 'number' && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : .5;
|
|
1117
950
|
}
|
|
1118
|
-
function parseHeartbeatToolArguments(call) {
|
|
1119
|
-
try {
|
|
1120
|
-
return JSON.parse(call.arguments);
|
|
1121
|
-
}
|
|
1122
|
-
catch {
|
|
1123
|
-
return { __invalidArguments: call.arguments };
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
951
|
async function resolveHeartbeatFileAccess(cwd, concerns) {
|
|
1127
952
|
const root = await realpath(cwd);
|
|
1128
953
|
const writable = new Set();
|
|
1129
|
-
const locators = concerns.flatMap(item => item.
|
|
954
|
+
const locators = concerns.flatMap(item => item.recordTarget?.kind === 'file' ? [item.recordTarget.locator] : []);
|
|
1130
955
|
for (const locator of new Set(locators)) {
|
|
1131
956
|
const lexical = resolve(root, locator);
|
|
1132
957
|
if (!pathInside(root, lexical) || isPartnerMemoryPath(relative(root, lexical)))
|
|
@@ -1144,6 +969,8 @@ async function resolveHeartbeatFileAccess(cwd, concerns) {
|
|
|
1144
969
|
return { root, writable };
|
|
1145
970
|
}
|
|
1146
971
|
export function heartbeatToolDenial(name, argumentsValue, access) {
|
|
972
|
+
if (['knowledge_note_references', 'knowledge_note_read', 'knowledge_note_update'].includes(name))
|
|
973
|
+
return '关注不处理依据中的笔记引用;记录仅使用手动配置的 recordTarget 和专用记录工具。';
|
|
1147
974
|
if (!isRecord(argumentsValue))
|
|
1148
975
|
return undefined;
|
|
1149
976
|
const isRead = ['glob', 'grep', 'read'].includes(name);
|
|
@@ -1165,70 +992,10 @@ export function heartbeatToolDenial(name, argumentsValue, access) {
|
|
|
1165
992
|
}
|
|
1166
993
|
return undefined;
|
|
1167
994
|
}
|
|
1168
|
-
async function executeHeartbeatTool(ctx, agent, call, argumentsValue, signal, remainingDiscoveryMs) {
|
|
1169
|
-
const configuredTimeoutMs = call.name === 'web_fetch' || call.name === 'web_source' ? HEARTBEAT_WEB_TOOL_TIMEOUT_MS : HEARTBEAT_TOOL_TIMEOUT_MS;
|
|
1170
|
-
const timeoutMs = Math.max(1, Math.min(configuredTimeoutMs, remainingDiscoveryMs));
|
|
1171
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1172
|
-
const toolSignal = AbortSignal.any([signal, timeoutSignal]);
|
|
1173
|
-
try {
|
|
1174
|
-
return await ctx.tools.execute({ callId: call.id, name: call.name, arguments: argumentsValue, agent, signal: toolSignal });
|
|
1175
|
-
}
|
|
1176
|
-
catch (error) {
|
|
1177
|
-
const message = timeoutSignal.aborted && !signal.aborted
|
|
1178
|
-
? `工具 ${call.name} 超过 ${timeoutMs / 1_000} 秒,已跳过;请按既有规则判断来源不可用,或根据已有结果收束。`
|
|
1179
|
-
: `工具 ${call.name} 调用失败:${errorMessage(error)}`;
|
|
1180
|
-
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
995
|
function pathInside(root, target) {
|
|
1184
996
|
const value = relative(root, target);
|
|
1185
997
|
return value === '' || (value !== '..' && !value.startsWith(`..${sep}`) && !isAbsolute(value));
|
|
1186
998
|
}
|
|
1187
|
-
function boundHeartbeatToolArguments(name, value) {
|
|
1188
|
-
if (!isRecord(value))
|
|
1189
|
-
return value;
|
|
1190
|
-
const bounded = { ...value };
|
|
1191
|
-
if (name === 'knowledge_base_search' || name === 'knowledge_search')
|
|
1192
|
-
bounded.limit = boundedInteger(value.limit, 4);
|
|
1193
|
-
if (name === 'knowledge_read')
|
|
1194
|
-
bounded.maxChars = boundedInteger(value.maxChars, HEARTBEAT_KNOWLEDGE_RESULT_LIMIT);
|
|
1195
|
-
if (name === 'web_source')
|
|
1196
|
-
bounded.max_chars = boundedInteger(value.max_chars, 120_000);
|
|
1197
|
-
if (name === 'read')
|
|
1198
|
-
bounded.limit = boundedInteger(value.limit, 80);
|
|
1199
|
-
return bounded;
|
|
1200
|
-
}
|
|
1201
|
-
function heartbeatToolContent(name, content) {
|
|
1202
|
-
let remaining = heartbeatToolResultLimit(name);
|
|
1203
|
-
const bounded = [];
|
|
1204
|
-
for (const block of content) {
|
|
1205
|
-
if (remaining <= 0)
|
|
1206
|
-
break;
|
|
1207
|
-
if (block.type !== 'text') {
|
|
1208
|
-
bounded.push(block);
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
const sanitized = name === 'glob'
|
|
1212
|
-
? block.text.split('\n').filter(line => !isPartnerMemoryPath(line.trim())).join('\n')
|
|
1213
|
-
: block.text;
|
|
1214
|
-
const text = sanitized.length <= remaining ? sanitized : `${sanitized.slice(0, Math.max(0, remaining - 1))}…`;
|
|
1215
|
-
if (text)
|
|
1216
|
-
bounded.push({ ...block, text });
|
|
1217
|
-
remaining -= text.length;
|
|
1218
|
-
}
|
|
1219
|
-
if (bounded.length === 0 && name === 'glob')
|
|
1220
|
-
return [{ type: 'text', text: '未发现可供心跳检查的普通工作文件;伙伴记忆与会话归档已从结果中排除。' }];
|
|
1221
|
-
return bounded;
|
|
1222
|
-
}
|
|
1223
|
-
function heartbeatToolResultLimit(name) {
|
|
1224
|
-
if (name === 'knowledge_read')
|
|
1225
|
-
return HEARTBEAT_KNOWLEDGE_RESULT_LIMIT;
|
|
1226
|
-
if (name === 'web_fetch')
|
|
1227
|
-
return HEARTBEAT_WEB_FETCH_RESULT_LIMIT;
|
|
1228
|
-
if (name === 'web_source')
|
|
1229
|
-
return HEARTBEAT_WEB_SOURCE_RESULT_LIMIT;
|
|
1230
|
-
return HEARTBEAT_TOOL_RESULT_LIMIT;
|
|
1231
|
-
}
|
|
1232
999
|
function isPartnerMemoryPath(value) {
|
|
1233
1000
|
const normalized = value.replaceAll('\\', '/').replace(/^\.\//, '/');
|
|
1234
1001
|
return /(?:^|\/)(?:memory(?:\/|-backup(?:-|\/)|$)|concerns(?:\/|$))/i.test(normalized);
|
|
@@ -1240,45 +1007,41 @@ function isRecord(value) {
|
|
|
1240
1007
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1241
1008
|
}
|
|
1242
1009
|
export function renderHeartbeatActivity(execution, outcome, deliveryError) {
|
|
1243
|
-
const status = outcome === 'notified' ? '已通过微信提醒' : outcome === 'quiet' ? '
|
|
1010
|
+
const status = execution.blocked?.length ? '检查受阻' : outcome === 'notified' ? '已通过微信提醒' : outcome === 'quiet' ? '检查完成,无需主动提醒' : '执行失败';
|
|
1244
1011
|
const lines = [
|
|
1245
1012
|
`状态:${status}`,
|
|
1246
1013
|
`本轮挂念:${execution.concerns.map(item => item.subject).join(';')}`,
|
|
1247
1014
|
`用时:${formatDuration(execution.completedAt - execution.startedAt)}`,
|
|
1248
1015
|
`工具:${execution.tools.length > 0 ? `${execution.tools.length} 次调用` : '未调用'}`,
|
|
1016
|
+
...(execution.recording ?? []).map(item => `记录同步:${execution.concerns.find(concern => concern.id === item.concernId)?.subject ?? item.concernId} · ${item.state === 'synced' ? '已写入手动指定位置' : '待同步(本轮未完成写入,下轮继续处理)'}`),
|
|
1017
|
+
...(execution.rounds?.length ? [`模型轮次:${execution.rounds.map(item => `${item.round}${item.phase === 'final' ? '(收尾)' : ''} / ${(item.modelMs / 1000).toFixed(1)}秒 / ${item.outputChars}字`).join(';')}`] : []),
|
|
1249
1018
|
];
|
|
1250
1019
|
execution.tools.forEach((trace, index) => {
|
|
1251
1020
|
lines.push('', `${index + 1}. ${trace.name} · ${trace.status === 'completed' ? '完成' : '失败'} · ${formatDuration(trace.completedAt - trace.startedAt)}`, `输入:${trace.input}`, `结果:${trace.output}`);
|
|
1252
1021
|
});
|
|
1022
|
+
for (const id of execution.blocked ?? [])
|
|
1023
|
+
lines.push(`受阻事项:${execution.concerns.find(item => item.id === id)?.subject ?? id};${execution.blockedReasons?.[id] ?? '目标数据尚未核实'}`);
|
|
1253
1024
|
const changed = execution.observations ?? [];
|
|
1254
1025
|
const conclusion = execution.error || deliveryError
|
|
1255
1026
|
? `失败原因:${deliveryError ?? execution.error}`
|
|
1256
1027
|
: changed.length > 0
|
|
1257
1028
|
? `新变化:\n${changed.map(item => `- ${item.event}(${decisionLabel(item.decision)} · ${Math.round(item.interruptScore * 100)})`).join('\n')}`
|
|
1258
|
-
: '最终结论:本轮没有发现经过校验的新变化。';
|
|
1029
|
+
: execution.concerns.some(item => item.recordingSnapshot) ? '最终结论:本轮仅执行记录同步,未抓取来源或发送变化提醒。' : '最终结论:本轮没有发现经过校验的新变化。';
|
|
1259
1030
|
lines.push('', conclusion);
|
|
1260
1031
|
return lines.join('\n');
|
|
1261
1032
|
}
|
|
1262
1033
|
function decisionLabel(value) {
|
|
1263
1034
|
return value === 'notify' ? '已提醒' : value === 'feed' ? '伙伴动态' : value === 'defer' ? '顺手一提' : value === 'remember' ? '静默记下' : '忽略';
|
|
1264
1035
|
}
|
|
1265
|
-
function auditValue(value, limit) {
|
|
1266
|
-
try {
|
|
1267
|
-
return auditText(JSON.stringify(value), limit);
|
|
1268
|
-
}
|
|
1269
|
-
catch {
|
|
1270
|
-
return auditText(String(value), limit);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
function auditText(value, limit) {
|
|
1274
|
-
const text = value.trim().replace(/\n{3,}/g, '\n\n');
|
|
1275
|
-
return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
|
|
1276
|
-
}
|
|
1277
1036
|
function formatDuration(milliseconds) {
|
|
1278
1037
|
if (milliseconds < 1_000)
|
|
1279
1038
|
return `${Math.max(0, milliseconds)} ms`;
|
|
1280
1039
|
return `${(milliseconds / 1_000).toFixed(milliseconds < 10_000 ? 1 : 0)} 秒`;
|
|
1281
1040
|
}
|
|
1041
|
+
function auditText(value, limit) {
|
|
1042
|
+
const text = value.trim().replace(/\n{3,}/g, '\n\n');
|
|
1043
|
+
return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
|
|
1044
|
+
}
|
|
1282
1045
|
function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
|
|
1283
1046
|
function textContent(content) {
|
|
1284
1047
|
return content.filter((block) => block.type === 'text' && typeof block.text === 'string')
|