evolcore 0.0.3 → 0.0.5

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.
Files changed (148) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. 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' || instance.state === 'return_pending');
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
- const delivery = message.handoffDelivery;
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
- if (!delivery)
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 items = [];
226
+ const instances = [];
223
227
  for (const handoffId of ids) {
224
- const item = this.buildPromptItem({
225
- ...message,
226
- handoffDelivery: { direction: delivery.direction, handoffId },
227
- });
228
- if (item)
229
- items.push(item);
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
- return items;
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' || item.state === 'return_pending') && !item.attention_required)
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 = 'return_pending';
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)
@@ -3,6 +3,7 @@ export const HANDOFF_STATES = [
3
3
  'queued',
4
4
  'failed',
5
5
  'target_sent',
6
+ 'target_context_pending',
6
7
  'return_pending',
7
8
  'origin_queued',
8
9
  'origin_delivered',
@@ -1,10 +1,9 @@
1
- import { createHash } from 'crypto';
2
- import { resolveAnthropicConfig, resolveOpenaiConfig, } from '../../agents/baseagent.js';
3
- const MODEL_LIST_TTL_MS = 5 * 60 * 1000;
4
- const MODEL_LIST_MAX_PAGES = 20;
5
- const anthropicModelListCache = new Map();
6
- function apiEndpoint(baseUrl, defaultBaseUrl, resource) {
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, 'https://api.anthropic.com', 'messages'), {
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, 'https://api.openai.com', 'responses'), {
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
- if (baseagent === 'claude') {
159
- const override = config.baseagents?.claude;
160
- if (!override)
161
- return undefined;
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 (baseagent === 'codex') {
166
- const override = config.baseagents?.codex;
167
- if (!override)
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 { CONTEXT_TOO_LONG_PATTERN, getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
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
- let itemsForBatch = this.suppressActivityItems() ? [] : items.filter(it => it.kind !== 'text');
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;