evolcore 0.0.3 → 0.0.4
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/CHANGELOG.md +64 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -5,6 +5,10 @@ import { KeyedFairMutex } from './mutex.js';
|
|
|
5
5
|
import { HandoffStore } from './store.js';
|
|
6
6
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
7
7
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
8
|
+
import { logger } from '../../utils/logger.js';
|
|
9
|
+
const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
|
|
10
|
+
const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
|
|
11
|
+
const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
|
|
8
12
|
export class HandoffTargetBlockedError extends Error {
|
|
9
13
|
blockingHandoffId;
|
|
10
14
|
attentionReason;
|
|
@@ -25,6 +29,20 @@ export class HandoffDeliveryFailedError extends Error {
|
|
|
25
29
|
this.name = 'HandoffDeliveryFailedError';
|
|
26
30
|
}
|
|
27
31
|
}
|
|
32
|
+
export class HandoffReturnPolicyRequiredError extends Error {
|
|
33
|
+
code = 'HANDOFF_RETURN_POLICY_REQUIRED';
|
|
34
|
+
constructor() {
|
|
35
|
+
super('跨会话 ec msg/group send 必须显式指定 --return none|required');
|
|
36
|
+
this.name = 'HandoffReturnPolicyRequiredError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class TriggerHandoffRequiredForbiddenError extends Error {
|
|
40
|
+
code = 'TRIGGER_HANDOFF_REQUIRED_FORBIDDEN';
|
|
41
|
+
constructor() {
|
|
42
|
+
super('Trigger 的跨会话 ec msg/group send 只允许 --return none,不能使用 required');
|
|
43
|
+
this.name = 'TriggerHandoffRequiredForbiddenError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
28
46
|
export class HandoffRuntime {
|
|
29
47
|
sessionManager;
|
|
30
48
|
messageQueue;
|
|
@@ -32,6 +50,8 @@ export class HandoffRuntime {
|
|
|
32
50
|
mutexes = new KeyedFairMutex();
|
|
33
51
|
dispatcher;
|
|
34
52
|
now;
|
|
53
|
+
staleWarned = new Set();
|
|
54
|
+
largeAggregateWarned = new Set();
|
|
35
55
|
constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
|
|
36
56
|
this.sessionManager = sessionManager;
|
|
37
57
|
this.messageQueue = messageQueue;
|
|
@@ -56,6 +76,11 @@ export class HandoffRuntime {
|
|
|
56
76
|
if (target.id === origin.id) {
|
|
57
77
|
return { crossSession: false, targetSession: target };
|
|
58
78
|
}
|
|
79
|
+
if (!input.explicitReturnPolicy)
|
|
80
|
+
throw new HandoffReturnPolicyRequiredError();
|
|
81
|
+
if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
|
|
82
|
+
throw new TriggerHandoffRequiredForbiddenError();
|
|
83
|
+
}
|
|
59
84
|
await this.dispatcher.expireTargetQueued(input.selfAid, target.id);
|
|
60
85
|
throwIfAborted(input.signal);
|
|
61
86
|
const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
|
|
@@ -91,7 +116,8 @@ export class HandoffRuntime {
|
|
|
91
116
|
}
|
|
92
117
|
hasOpenTarget(selfAid, targetSessionId) {
|
|
93
118
|
return this.store.listByTarget(selfAid, targetSessionId)
|
|
94
|
-
.some(instance => instance.state === 'target_sent'
|
|
119
|
+
.some(instance => instance.state === 'target_sent'
|
|
120
|
+
|| (instance.state === 'return_pending' && instance.return_policy === 'required'));
|
|
95
121
|
}
|
|
96
122
|
async bindReply(input) {
|
|
97
123
|
const key = `${input.selfAid}\u0000${input.targetSessionId}`;
|
|
@@ -105,6 +131,8 @@ export class HandoffRuntime {
|
|
|
105
131
|
responseMessageId: input.responseMessageId,
|
|
106
132
|
refMessageId: input.refMessageId,
|
|
107
133
|
});
|
|
134
|
+
for (const instance of attached)
|
|
135
|
+
this.warnIfStale(instance);
|
|
108
136
|
const handoffIds = attached.map(instance => instance.handoff_id);
|
|
109
137
|
return {
|
|
110
138
|
candidate: handoffIds.length > 0,
|
|
@@ -157,6 +185,7 @@ export class HandoffRuntime {
|
|
|
157
185
|
messageId,
|
|
158
186
|
timestamp: Date.now(),
|
|
159
187
|
source: 'handoff',
|
|
188
|
+
boundSessionId: origin.id,
|
|
160
189
|
causation: instance.return_causation
|
|
161
190
|
? deriveCausation(instance.return_causation)
|
|
162
191
|
: instance.causation
|
|
@@ -186,55 +215,120 @@ export class HandoffRuntime {
|
|
|
186
215
|
}
|
|
187
216
|
}
|
|
188
217
|
buildPromptItem(message) {
|
|
189
|
-
|
|
190
|
-
const selfAid = message.selfAID;
|
|
191
|
-
if (!delivery || !selfAid)
|
|
192
|
-
return null;
|
|
193
|
-
const instance = this.store.get(selfAid, delivery.handoffId);
|
|
194
|
-
if (!instance)
|
|
195
|
-
return null;
|
|
196
|
-
if (delivery.direction === 'target' && instance.state !== 'return_pending')
|
|
197
|
-
return null;
|
|
198
|
-
if (delivery.direction === 'origin' && instance.state !== 'origin_delivered')
|
|
199
|
-
return null;
|
|
200
|
-
return {
|
|
201
|
-
kind: 'handoff',
|
|
202
|
-
peerId: message.peerId,
|
|
203
|
-
peerName: message.peerName,
|
|
204
|
-
peerType: message.peerType,
|
|
205
|
-
content: message.content,
|
|
206
|
-
timestamp: message.timestamp,
|
|
207
|
-
causation: message.causation,
|
|
208
|
-
handoff: {
|
|
209
|
-
kind: delivery.direction === 'target' ? 'request_to_target' : 'response_to_origin',
|
|
210
|
-
handoffId: instance.handoff_id,
|
|
211
|
-
previousContent: this.requestSummary(instance),
|
|
212
|
-
previousMessageId: instance.target_message_id,
|
|
213
|
-
returnPolicy: instance.return_policy,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
218
|
+
return this.buildPromptItems(message)[0] ?? null;
|
|
216
219
|
}
|
|
217
220
|
buildPromptItems(message) {
|
|
218
221
|
const delivery = message.handoffDelivery;
|
|
219
|
-
|
|
222
|
+
const selfAid = message.selfAID;
|
|
223
|
+
if (!delivery || !selfAid)
|
|
220
224
|
return [];
|
|
221
225
|
const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
|
|
222
|
-
const
|
|
226
|
+
const instances = [];
|
|
223
227
|
for (const handoffId of ids) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const instance = this.store.get(selfAid, handoffId);
|
|
229
|
+
if (!instance || instance.attention_required)
|
|
230
|
+
continue;
|
|
231
|
+
if (delivery.direction === 'target') {
|
|
232
|
+
const expectedState = instance.return_policy === 'none'
|
|
233
|
+
? 'target_context_pending'
|
|
234
|
+
: 'return_pending';
|
|
235
|
+
if (instance.state !== expectedState)
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
else if (instance.state !== 'origin_delivered') {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
instances.push(instance);
|
|
242
|
+
}
|
|
243
|
+
if (instances.length === 0)
|
|
244
|
+
return [];
|
|
245
|
+
if (delivery.direction === 'origin') {
|
|
246
|
+
this.warnIfLargeAggregate(message, instances, 'origin');
|
|
247
|
+
const first = instances[0];
|
|
248
|
+
return [{
|
|
249
|
+
kind: 'handoff',
|
|
250
|
+
peerId: message.peerId,
|
|
251
|
+
peerName: message.peerName,
|
|
252
|
+
peerType: message.peerType,
|
|
253
|
+
content: message.content,
|
|
254
|
+
timestamp: message.timestamp,
|
|
255
|
+
causation: message.causation,
|
|
256
|
+
handoff: {
|
|
257
|
+
kind: 'response_to_origin',
|
|
258
|
+
handoffId: first.handoff_id,
|
|
259
|
+
handoffIds: instances.map(instance => instance.handoff_id),
|
|
260
|
+
originEntries: instances.map(instance => ({
|
|
261
|
+
previousContent: this.requestSummary(instance),
|
|
262
|
+
responseContent: instance.return_content ?? '',
|
|
263
|
+
previousMessageId: instance.target_message_id,
|
|
264
|
+
})),
|
|
265
|
+
previousContent: this.requestSummary(first),
|
|
266
|
+
previousMessageId: first.target_message_id,
|
|
267
|
+
returnPolicy: first.return_policy,
|
|
268
|
+
},
|
|
269
|
+
}];
|
|
230
270
|
}
|
|
231
|
-
|
|
271
|
+
const requiredEntries = instances
|
|
272
|
+
.filter(instance => instance.return_policy === 'required')
|
|
273
|
+
.map(instance => ({
|
|
274
|
+
handoffId: instance.handoff_id,
|
|
275
|
+
previousContent: this.requestSummary(instance),
|
|
276
|
+
previousMessageId: instance.target_message_id,
|
|
277
|
+
}));
|
|
278
|
+
const contextEntries = instances
|
|
279
|
+
.filter(instance => instance.return_policy === 'none')
|
|
280
|
+
.map(instance => ({
|
|
281
|
+
previousContent: this.requestSummary(instance),
|
|
282
|
+
previousMessageId: instance.target_message_id,
|
|
283
|
+
}));
|
|
284
|
+
this.warnIfLargeAggregate(message, instances, 'target');
|
|
285
|
+
const first = instances[0];
|
|
286
|
+
return [{
|
|
287
|
+
kind: 'handoff',
|
|
288
|
+
peerId: message.peerId,
|
|
289
|
+
peerName: message.peerName,
|
|
290
|
+
peerType: message.peerType,
|
|
291
|
+
content: message.content,
|
|
292
|
+
timestamp: message.timestamp,
|
|
293
|
+
causation: message.causation,
|
|
294
|
+
handoff: {
|
|
295
|
+
kind: 'request_to_target',
|
|
296
|
+
handoffId: first.handoff_id,
|
|
297
|
+
handoffIds: instances.map(instance => instance.handoff_id),
|
|
298
|
+
requiredEntries,
|
|
299
|
+
contextEntries,
|
|
300
|
+
previousContent: this.requestSummary(first),
|
|
301
|
+
previousMessageId: first.target_message_id,
|
|
302
|
+
returnPolicy: requiredEntries.length > 0 ? 'required' : 'none',
|
|
303
|
+
},
|
|
304
|
+
}];
|
|
232
305
|
}
|
|
233
306
|
completeOriginContext(selfAid, handoffId) {
|
|
234
307
|
const instance = this.store.get(selfAid, handoffId);
|
|
235
308
|
if (instance?.state === 'origin_delivered')
|
|
236
309
|
this.store.completeOriginContext(selfAid, handoffId);
|
|
237
310
|
}
|
|
311
|
+
completeTargetContext(selfAid, handoffId) {
|
|
312
|
+
const instance = this.store.get(selfAid, handoffId);
|
|
313
|
+
if (instance?.state === 'target_context_pending' && instance.return_policy === 'none') {
|
|
314
|
+
this.store.completeTargetContext(selfAid, handoffId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
validateTargetSessionBinding(selfAid, handoffIds, executionSessionId) {
|
|
318
|
+
const invalidHandoffIds = [];
|
|
319
|
+
for (const handoffId of handoffIds) {
|
|
320
|
+
const instance = this.store.get(selfAid, handoffId);
|
|
321
|
+
if (!instance
|
|
322
|
+
|| instance.target_session_id !== executionSessionId
|
|
323
|
+
|| instance.consumed_target_session_id !== executionSessionId) {
|
|
324
|
+
invalidHandoffIds.push(handoffId);
|
|
325
|
+
if (instance && !instance.attention_required) {
|
|
326
|
+
this.store.markAttention(selfAid, handoffId, 'HANDOFF_SESSION_BINDING_MISMATCH');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { ok: invalidHandoffIds.length === 0, invalidHandoffIds };
|
|
331
|
+
}
|
|
238
332
|
status(selfAid, handoffId) {
|
|
239
333
|
return this.store.status(selfAid, handoffId);
|
|
240
334
|
}
|
|
@@ -276,6 +370,10 @@ export class HandoffRuntime {
|
|
|
276
370
|
async recover(selfAids) {
|
|
277
371
|
const originEnqueues = [];
|
|
278
372
|
for (const selfAid of selfAids) {
|
|
373
|
+
// v1 initially represented return=none as return_pending and required an
|
|
374
|
+
// empty manual return. It has already been shown to a target turn, so
|
|
375
|
+
// finish it before version reconciliation instead of prompting again.
|
|
376
|
+
this.store.migrateLegacyNonePending(selfAid, this.now?.());
|
|
279
377
|
this.expireQueued([selfAid]);
|
|
280
378
|
const events = this.store.events(selfAid);
|
|
281
379
|
const lastSendEvent = new Map();
|
|
@@ -285,6 +383,7 @@ export class HandoffRuntime {
|
|
|
285
383
|
}
|
|
286
384
|
}
|
|
287
385
|
for (const instance of this.store.list(selfAid)) {
|
|
386
|
+
this.warnIfStale(instance);
|
|
288
387
|
const versionedEvents = events.filter(event => event.handoff_id === instance.handoff_id && event.to_version !== undefined);
|
|
289
388
|
const latestEventVersion = versionedEvents.reduce((latest, event) => Math.max(latest, event.to_version ?? 0), 0);
|
|
290
389
|
if (latestEventVersion !== instance.version) {
|
|
@@ -313,6 +412,32 @@ export class HandoffRuntime {
|
|
|
313
412
|
}
|
|
314
413
|
await Promise.all(originEnqueues);
|
|
315
414
|
}
|
|
415
|
+
warnIfStale(instance) {
|
|
416
|
+
if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
|
|
417
|
+
return;
|
|
418
|
+
const ageMs = (this.now?.() ?? Date.now()) - instance.created_at;
|
|
419
|
+
const key = instance.handoff_id;
|
|
420
|
+
if (ageMs < HANDOFF_STALE_WARN_MS || this.staleWarned.has(key))
|
|
421
|
+
return;
|
|
422
|
+
this.staleWarned.add(key);
|
|
423
|
+
logger.warn(`[Handoff] stale open handoff id=${instance.handoff_id}`
|
|
424
|
+
+ ` state=${instance.state} returnPolicy=${instance.return_policy}`
|
|
425
|
+
+ ` ageHours=${Math.floor(ageMs / 3_600_000)}`
|
|
426
|
+
+ ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
|
|
427
|
+
}
|
|
428
|
+
warnIfLargeAggregate(message, instances, direction) {
|
|
429
|
+
const chars = message.content.length
|
|
430
|
+
+ instances.reduce((total, instance) => total + this.requestSummary(instance).length, 0);
|
|
431
|
+
if (instances.length < HANDOFF_LARGE_AGGREGATE_COUNT && chars < HANDOFF_LARGE_AGGREGATE_CHARS)
|
|
432
|
+
return;
|
|
433
|
+
const key = `${message.messageId ?? '<none>'}:${instances.map(instance => instance.handoff_id).join(',')}`;
|
|
434
|
+
if (this.largeAggregateWarned.has(key))
|
|
435
|
+
return;
|
|
436
|
+
this.largeAggregateWarned.add(key);
|
|
437
|
+
logger.warn(`[Handoff] large prompt aggregate direction=${direction}`
|
|
438
|
+
+ ` session=${direction === 'target' ? instances[0].target_session_id : instances[0].origin_session_id}`
|
|
439
|
+
+ ` count=${instances.length} chars=${chars} responseMessage=${message.messageId ?? '<none>'}`);
|
|
440
|
+
}
|
|
316
441
|
requestSummary(instance) {
|
|
317
442
|
return classifyAunPayloadForLog(instance.request.payload).content || '[payload]';
|
|
318
443
|
}
|
|
@@ -272,7 +272,8 @@ export class HandoffStore {
|
|
|
272
272
|
}
|
|
273
273
|
attachSessionContext(input) {
|
|
274
274
|
const open = this.listByTarget(input.selfAid, input.targetSessionId)
|
|
275
|
-
.filter(item => (item.state === 'target_sent'
|
|
275
|
+
.filter(item => (item.state === 'target_sent'
|
|
276
|
+
|| (item.state === 'return_pending' && item.return_policy === 'required')) && !item.attention_required)
|
|
276
277
|
.sort((left, right) => {
|
|
277
278
|
const leftRef = input.refMessageId && left.target_message_id === input.refMessageId ? 0 : 1;
|
|
278
279
|
const rightRef = input.refMessageId && right.target_message_id === input.refMessageId ? 0 : 1;
|
|
@@ -283,7 +284,9 @@ export class HandoffStore {
|
|
|
283
284
|
const previousVersion = instance.version;
|
|
284
285
|
const firstAttachment = instance.state === 'target_sent';
|
|
285
286
|
if (firstAttachment) {
|
|
286
|
-
instance.state = '
|
|
287
|
+
instance.state = instance.return_policy === 'none'
|
|
288
|
+
? 'target_context_pending'
|
|
289
|
+
: 'return_pending';
|
|
287
290
|
instance.response_message_id = input.responseMessageId;
|
|
288
291
|
instance.consumed_at = now;
|
|
289
292
|
instance.consumed_target_session_id = input.targetSessionId;
|
|
@@ -308,6 +311,47 @@ export class HandoffStore {
|
|
|
308
311
|
bindExactReply(input) {
|
|
309
312
|
return this.attachSessionContext(input)[0] ?? null;
|
|
310
313
|
}
|
|
314
|
+
completeTargetContext(selfAid, handoffId, now = Date.now()) {
|
|
315
|
+
const instance = this.requireState(selfAid, handoffId, 'target_context_pending');
|
|
316
|
+
if (instance.return_policy !== 'none') {
|
|
317
|
+
throw new Error('target context auto-completion requires return=none');
|
|
318
|
+
}
|
|
319
|
+
const previous = instance.version;
|
|
320
|
+
instance.state = 'completed';
|
|
321
|
+
instance.version++;
|
|
322
|
+
instance.updated_at = now;
|
|
323
|
+
instance.completed_at = now;
|
|
324
|
+
this.appendEvent(selfAid, 'target_context_consumed', handoffId, {
|
|
325
|
+
operationKey: instance.response_message_id ?? undefined,
|
|
326
|
+
fromVersion: previous,
|
|
327
|
+
toVersion: instance.version,
|
|
328
|
+
now,
|
|
329
|
+
});
|
|
330
|
+
this.write(selfAid, instance);
|
|
331
|
+
return instance;
|
|
332
|
+
}
|
|
333
|
+
migrateLegacyNonePending(selfAid, now = Date.now()) {
|
|
334
|
+
const migrated = [];
|
|
335
|
+
for (const instance of this.list(selfAid)) {
|
|
336
|
+
if (instance.return_policy !== 'none' || instance.state !== 'return_pending')
|
|
337
|
+
continue;
|
|
338
|
+
const previous = instance.version;
|
|
339
|
+
instance.state = 'completed';
|
|
340
|
+
instance.version++;
|
|
341
|
+
instance.updated_at = now;
|
|
342
|
+
instance.completed_at = now;
|
|
343
|
+
this.appendEvent(selfAid, 'target_context_consumed', instance.handoff_id, {
|
|
344
|
+
operationKey: instance.response_message_id ?? undefined,
|
|
345
|
+
fromVersion: previous,
|
|
346
|
+
toVersion: instance.version,
|
|
347
|
+
details: { migration: true, previous_state: 'return_pending' },
|
|
348
|
+
now,
|
|
349
|
+
});
|
|
350
|
+
this.write(selfAid, instance);
|
|
351
|
+
migrated.push(instance);
|
|
352
|
+
}
|
|
353
|
+
return migrated;
|
|
354
|
+
}
|
|
311
355
|
returnHandoff(input) {
|
|
312
356
|
const selected = this.selectReturnCandidate(input);
|
|
313
357
|
if ('ok' in selected)
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const base = (baseUrl || defaultBaseUrl).replace(/\/+$/, '');
|
|
1
|
+
import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
|
|
2
|
+
function apiEndpoint(baseUrl, resource) {
|
|
3
|
+
const configured = baseUrl?.trim();
|
|
4
|
+
if (!configured)
|
|
5
|
+
throw new Error('Direct text inference baseUrl is not configured');
|
|
6
|
+
const base = configured.replace(/\/+$/, '');
|
|
8
7
|
return base.endsWith('/v1') ? `${base}/${resource}` : `${base}/v1/${resource}`;
|
|
9
8
|
}
|
|
10
9
|
async function parseError(response) {
|
|
@@ -28,53 +27,8 @@ export class AnthropicTextInferenceProvider {
|
|
|
28
27
|
constructor(config) {
|
|
29
28
|
this.config = config;
|
|
30
29
|
}
|
|
31
|
-
async listModels(signal) {
|
|
32
|
-
const cacheKey = this.modelListCacheKey();
|
|
33
|
-
const cached = anthropicModelListCache.get(cacheKey);
|
|
34
|
-
if (cached && Date.now() - cached.fetchedAt < MODEL_LIST_TTL_MS)
|
|
35
|
-
return [...cached.models];
|
|
36
|
-
const models = [];
|
|
37
|
-
const seen = new Set();
|
|
38
|
-
let afterId;
|
|
39
|
-
for (let page = 0; page < MODEL_LIST_MAX_PAGES; page++) {
|
|
40
|
-
const endpoint = new URL(apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'models'));
|
|
41
|
-
endpoint.searchParams.set('limit', '100');
|
|
42
|
-
if (afterId)
|
|
43
|
-
endpoint.searchParams.set('after_id', afterId);
|
|
44
|
-
const response = await fetch(endpoint, {
|
|
45
|
-
method: 'GET',
|
|
46
|
-
signal,
|
|
47
|
-
headers: this.headers(),
|
|
48
|
-
});
|
|
49
|
-
if (!response.ok)
|
|
50
|
-
throw await parseError(response);
|
|
51
|
-
const body = await response.json();
|
|
52
|
-
const entries = Array.isArray(body.data)
|
|
53
|
-
? body.data
|
|
54
|
-
: (Array.isArray(body.models) ? body.models : []);
|
|
55
|
-
for (const entry of entries) {
|
|
56
|
-
const id = typeof entry === 'string'
|
|
57
|
-
? entry
|
|
58
|
-
: entry?.id || entry?.name || entry?.model;
|
|
59
|
-
if (typeof id !== 'string' || !id.trim() || seen.has(id))
|
|
60
|
-
continue;
|
|
61
|
-
seen.add(id);
|
|
62
|
-
models.push(id);
|
|
63
|
-
}
|
|
64
|
-
if (body.has_more !== true)
|
|
65
|
-
break;
|
|
66
|
-
const next = typeof body.last_id === 'string' && body.last_id ? body.last_id : undefined;
|
|
67
|
-
if (!next || next === afterId)
|
|
68
|
-
throw new Error('Claude model list pagination returned no next cursor');
|
|
69
|
-
afterId = next;
|
|
70
|
-
}
|
|
71
|
-
if (models.length === 0)
|
|
72
|
-
throw new Error('Claude model list API returned no models');
|
|
73
|
-
anthropicModelListCache.set(cacheKey, { models, fetchedAt: Date.now() });
|
|
74
|
-
return [...models];
|
|
75
|
-
}
|
|
76
30
|
async completeText(request) {
|
|
77
|
-
const response = await fetch(apiEndpoint(this.config.baseUrl, '
|
|
31
|
+
const response = await fetch(apiEndpoint(this.config.baseUrl, 'messages'), {
|
|
78
32
|
method: 'POST',
|
|
79
33
|
signal: request.signal,
|
|
80
34
|
headers: this.headers(),
|
|
@@ -83,6 +37,7 @@ export class AnthropicTextInferenceProvider {
|
|
|
83
37
|
max_tokens: 256,
|
|
84
38
|
system: request.system,
|
|
85
39
|
messages: [{ role: 'user', content: request.input }],
|
|
40
|
+
...(request.effort ? { output_config: { effort: request.effort } } : {}),
|
|
86
41
|
}),
|
|
87
42
|
});
|
|
88
43
|
if (!response.ok)
|
|
@@ -106,14 +61,6 @@ export class AnthropicTextInferenceProvider {
|
|
|
106
61
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
107
62
|
};
|
|
108
63
|
}
|
|
109
|
-
modelListCacheKey() {
|
|
110
|
-
const endpoint = apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'models');
|
|
111
|
-
const credential = createHash('sha256').update(this.config.apiKey).digest('hex');
|
|
112
|
-
return `${endpoint}\0${credential}`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
export function _resetTextInferenceModelCacheForTests() {
|
|
116
|
-
anthropicModelListCache.clear();
|
|
117
64
|
}
|
|
118
65
|
export class OpenAITextInferenceProvider {
|
|
119
66
|
config;
|
|
@@ -122,7 +69,7 @@ export class OpenAITextInferenceProvider {
|
|
|
122
69
|
this.config = config;
|
|
123
70
|
}
|
|
124
71
|
async completeText(request) {
|
|
125
|
-
const response = await fetch(apiEndpoint(this.config.baseUrl, '
|
|
72
|
+
const response = await fetch(apiEndpoint(this.config.baseUrl, 'responses'), {
|
|
126
73
|
method: 'POST',
|
|
127
74
|
signal: request.signal,
|
|
128
75
|
headers: {
|
|
@@ -155,19 +102,14 @@ export class OpenAITextInferenceProvider {
|
|
|
155
102
|
}
|
|
156
103
|
}
|
|
157
104
|
export function createTextInferenceProvider(baseagent, config) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const synthetic = { agents: { claude: override } };
|
|
163
|
-
return new AnthropicTextInferenceProvider(resolveAnthropicConfig(synthetic, override));
|
|
105
|
+
const canonical = normalizeBaseagent(baseagent).canonical;
|
|
106
|
+
if (canonical === 'claude') {
|
|
107
|
+
const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
|
|
108
|
+
return resolved ? new AnthropicTextInferenceProvider(resolved) : undefined;
|
|
164
109
|
}
|
|
165
|
-
if (
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
return undefined;
|
|
169
|
-
const synthetic = { agents: { codex: override } };
|
|
170
|
-
return new OpenAITextInferenceProvider(resolveOpenaiConfig(synthetic, override));
|
|
110
|
+
if (canonical === 'codex') {
|
|
111
|
+
const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
|
|
112
|
+
return resolved ? new OpenAITextInferenceProvider(resolved) : undefined;
|
|
171
113
|
}
|
|
172
114
|
return undefined;
|
|
173
115
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file marker may contain surrounding whitespace inside its brackets so
|
|
3
|
+
* formatted responses such as `[ SEND_FILE:./report.md ]` remain actionable.
|
|
4
|
+
*/
|
|
5
|
+
export function createSendFileMarkerPattern() {
|
|
6
|
+
return /\[\s*SEND_FILE:(?:(\w+):)?([^\]]*?\S[^\]]*?)\s*\]/g;
|
|
7
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from '../../utils/logger.js';
|
|
2
2
|
import { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { resolvePaths } from '../../paths.js';
|
|
@@ -289,6 +289,22 @@ export class IMRenderer {
|
|
|
289
289
|
this.messageTimestamps.push(Date.now());
|
|
290
290
|
this.scheduleFlush();
|
|
291
291
|
}
|
|
292
|
+
/** interactive 下,以普通非最终文本发送用户需要看到的运行状态。 */
|
|
293
|
+
async sendOperationalNoticeAsText(text) {
|
|
294
|
+
if (!this.opts.operationalNoticesAsText || this.opts.envelope.chatmode === 'proactive' || !text)
|
|
295
|
+
return false;
|
|
296
|
+
// 保持与流式正文的到达顺序:先发出已缓冲的正文,再投影运行状态。
|
|
297
|
+
await this.flushText();
|
|
298
|
+
const payload = { kind: 'result.text', text, isFinal: false };
|
|
299
|
+
this.sentContent = true;
|
|
300
|
+
this.sendChain = this.sendChain
|
|
301
|
+
.then(() => this.opts.send(payload))
|
|
302
|
+
.catch(e => logger.warn('[IMRenderer] notice text send failed:', e));
|
|
303
|
+
await this.sendChain;
|
|
304
|
+
this.lastFlush = Date.now();
|
|
305
|
+
this.flushCount++;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
292
308
|
// ── 内部:interactive 模式聚合窗口 ──
|
|
293
309
|
synthCallId() {
|
|
294
310
|
return `synth-${this.opts.envelope.taskId}-${++this.syntheticCallSeq}`;
|
|
@@ -387,18 +403,6 @@ export class IMRenderer {
|
|
|
387
403
|
item.text = item.text.replace(this.opts.fileMarkerPattern, '');
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
|
-
if (isFinal) {
|
|
391
|
-
// 上下文错误短语过滤:剔除错误关键词本身,保留前后内容。
|
|
392
|
-
// 只在最终 flush 清理,避免中间定时 flush trim 掉 Markdown 块级换行。
|
|
393
|
-
const ctxErrPattern = new RegExp(CONTEXT_TOO_LONG_PATTERN.source, 'gi');
|
|
394
|
-
const stripCtxErr = (s) => s.replace(ctxErrPattern, '').trim();
|
|
395
|
-
this.textBuffer = stripCtxErr(this.textBuffer);
|
|
396
|
-
this.allText = stripCtxErr(this.allText);
|
|
397
|
-
for (const item of this.itemsQueue) {
|
|
398
|
-
if (item.kind === 'text')
|
|
399
|
-
item.text = stripCtxErr(item.text);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
406
|
// 清掉空 text items
|
|
403
407
|
const items = this.itemsQueue.filter(it => {
|
|
404
408
|
if (it.kind === 'text')
|
|
@@ -419,7 +423,7 @@ export class IMRenderer {
|
|
|
419
423
|
});
|
|
420
424
|
}
|
|
421
425
|
// 1. interactive 模式下:不发 text items(由 result.text 统一发送最终文本)
|
|
422
|
-
|
|
426
|
+
const itemsForBatch = this.suppressActivityItems() ? [] : items.filter(it => it.kind !== 'text');
|
|
423
427
|
if (itemsForBatch.length > 0) {
|
|
424
428
|
const payload = { kind: 'activity.batch', items: itemsForBatch };
|
|
425
429
|
this.sentContent = true;
|