evolcore 0.0.26 → 0.0.27
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 +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +12 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +522 -233
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +35 -23
- package/dist/cli/diag.js +2 -2
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +9 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +16 -2
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +15 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import { CronExpressionParser } from 'cron-parser';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
5
|
import { matchTriggerEvent, TriggerEventSource } from './event-source.js';
|
|
6
|
-
import { definitionRevision, parseDurationMs, previewText, renderTemplate, resolveScriptPath, sha256, validateConcurrencyCombination, } from './validation.js';
|
|
6
|
+
import { definitionRevision, executionTimeoutMs, parseDurationMs, previewText, renderTemplate, resolveScriptPath, sha256, validateConcurrencyCombination, } from './validation.js';
|
|
7
7
|
import { TargetSessionLockRegistry } from './session-lock.js';
|
|
8
8
|
import { enterTrigger, formatTriggerPath } from '../core/causation/context.js';
|
|
9
9
|
import { createRootCausation } from '../core/causation/context.js';
|
|
@@ -13,6 +13,13 @@ const MAX_TIMER_MS = 2_147_483_647;
|
|
|
13
13
|
const RUN_ABORT_SETTLE_GRACE_MS = 1_000;
|
|
14
14
|
const RUN_COMPLETION_BARRIER_MS = 1_500;
|
|
15
15
|
const TRIGGER_RESTART_RESUME_PROMPT = 'EvolCore 服务已重启。请基于当前会话记录继续完成刚才被中断的 Trigger 任务;不要重新从头执行已经完成的步骤。';
|
|
16
|
+
function eventSessionKey(payload, fallbackAgentAid) {
|
|
17
|
+
const agentAid = typeof payload.agentAid === 'string' && payload.agentAid.trim()
|
|
18
|
+
? payload.agentAid.trim()
|
|
19
|
+
: fallbackAgentAid;
|
|
20
|
+
const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId : '';
|
|
21
|
+
return `${agentAid}:${sessionId}`;
|
|
22
|
+
}
|
|
16
23
|
export class TriggerRuntimeScheduler {
|
|
17
24
|
manager;
|
|
18
25
|
state;
|
|
@@ -818,6 +825,7 @@ export class TriggerRuntimeScheduler {
|
|
|
818
825
|
attemptId: `${runId}:attempt:${attempt}`,
|
|
819
826
|
firedAt,
|
|
820
827
|
prompt,
|
|
828
|
+
sourcePayload,
|
|
821
829
|
dryRun,
|
|
822
830
|
causation,
|
|
823
831
|
signal,
|
|
@@ -869,7 +877,17 @@ export class TriggerRuntimeScheduler {
|
|
|
869
877
|
+ ` model=${definition.execution.model ?? '<agent-default>'}`
|
|
870
878
|
+ ` effort=${definition.execution.effort ?? '<agent-default>'}`);
|
|
871
879
|
const attemptId = `${runId}:attempt:${attempt}`;
|
|
872
|
-
const
|
|
880
|
+
const executionStartedAt = Date.now();
|
|
881
|
+
const configuredExecutionTimeout = executionTimeoutMs(definition.execution);
|
|
882
|
+
const daemonExecutionDeadlineAt = this.daemonExecutionDeadlineAt(executionStartedAt);
|
|
883
|
+
const configuredExecutionDeadlineAt = configuredExecutionTimeout === undefined
|
|
884
|
+
? undefined
|
|
885
|
+
: executionStartedAt + configuredExecutionTimeout;
|
|
886
|
+
const executionDeadlineAt = daemonExecutionDeadlineAt === undefined
|
|
887
|
+
? configuredExecutionDeadlineAt
|
|
888
|
+
: configuredExecutionDeadlineAt === undefined
|
|
889
|
+
? daemonExecutionDeadlineAt
|
|
890
|
+
: Math.min(daemonExecutionDeadlineAt, configuredExecutionDeadlineAt);
|
|
873
891
|
const executionSession = await this.daemonChannel.getOrCreateConversationSession(definition, this.runtime.projectPath, runtimeBaseagent, runId, attemptId);
|
|
874
892
|
const activeRun = this.state.startExecutionAttempt?.(definition.id, runId, {
|
|
875
893
|
attempt,
|
|
@@ -963,7 +981,13 @@ export class TriggerRuntimeScheduler {
|
|
|
963
981
|
});
|
|
964
982
|
}
|
|
965
983
|
async checkConcurrency(definition, nextRunId, source) {
|
|
966
|
-
const
|
|
984
|
+
const allRuns = this.running.get(definition.id);
|
|
985
|
+
// An event-session trigger owns many independent sessions. Forbid/replace
|
|
986
|
+
// applies within the source session, so one failure cannot suppress another.
|
|
987
|
+
const runs = definition.feedback.strategy === 'event_session' && allRuns
|
|
988
|
+
? new Map([...allRuns].filter(([, runtime]) => eventSessionKey(runtime.source.payload, definition.agentAid)
|
|
989
|
+
=== eventSessionKey(source.payload, definition.agentAid)))
|
|
990
|
+
: allRuns;
|
|
967
991
|
if (!runs || runs.size === 0 || definition.reliability.concurrency === 'allow')
|
|
968
992
|
return undefined;
|
|
969
993
|
const first = runs.values().next().value;
|
|
@@ -1753,7 +1777,7 @@ export class TriggerRuntimeScheduler {
|
|
|
1753
1777
|
publishTriggerRunOutcome(definition, audit) {
|
|
1754
1778
|
if (!this.eventBus || audit.status === 'dry-run')
|
|
1755
1779
|
return;
|
|
1756
|
-
const target = primaryTarget(definition);
|
|
1780
|
+
const target = audit.feedback?.target ?? primaryTarget(definition);
|
|
1757
1781
|
const targetChannel = target?.channelKey ?? 'daemon';
|
|
1758
1782
|
const targetChannelId = target?.channelId ?? definition.agentAid;
|
|
1759
1783
|
const base = {
|
|
@@ -2027,6 +2051,8 @@ function summarizeReply(reply, opts = {}) {
|
|
|
2027
2051
|
function primaryTarget(definition) {
|
|
2028
2052
|
if (definition.feedback.strategy === 'target')
|
|
2029
2053
|
return definition.feedback.target;
|
|
2054
|
+
if (definition.feedback.strategy === 'event_session')
|
|
2055
|
+
return undefined;
|
|
2030
2056
|
if (!definition.origin)
|
|
2031
2057
|
return undefined;
|
|
2032
2058
|
return {
|
|
@@ -3,6 +3,8 @@ import crypto from 'crypto';
|
|
|
3
3
|
import { normalizePermissionMode as normalizePermissionModeContract } from '../core/permission/mode.js';
|
|
4
4
|
import { formatChannelKey } from '../core/channel-loader.js';
|
|
5
5
|
const MAX_SCRIPT_TIMEOUT_MS = 900_000;
|
|
6
|
+
const TRIGGER_SESSION_TIMEOUT_MS = 30 * 60_000;
|
|
7
|
+
const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
|
|
6
8
|
export const TRIGGER_SCHEMA_VERSION = 3.1;
|
|
7
9
|
const LEGACY_TRIGGER_SCHEMA_VERSION = 4;
|
|
8
10
|
const DEFAULT_NOOP_SENTINEL = '[[NOOP]]';
|
|
@@ -35,7 +37,7 @@ export function normalizeTriggerDefinition(input, opts = {}) {
|
|
|
35
37
|
* channelId shape inference at runtime.
|
|
36
38
|
*/
|
|
37
39
|
export function assertExplicitAunDeliveryRoutes(definition) {
|
|
38
|
-
if (definition.feedback.strategy === 'silent')
|
|
40
|
+
if (definition.feedback.strategy === 'silent' || definition.feedback.strategy === 'event_session')
|
|
39
41
|
return;
|
|
40
42
|
const target = definition.feedback.strategy === 'target'
|
|
41
43
|
? definition.feedback.target
|
|
@@ -58,6 +60,11 @@ export function assertExplicitAunDeliveryRoutes(definition) {
|
|
|
58
60
|
export function validateTriggerDefinition(definition) {
|
|
59
61
|
normalizeTriggerDefinition(definition);
|
|
60
62
|
}
|
|
63
|
+
/** Resolve a model-backed attempt's deadline; target sessions are unlimited by default. */
|
|
64
|
+
export function executionTimeoutMs(execution) {
|
|
65
|
+
return execution.timeoutMs ?? (execution.type === 'target_session'
|
|
66
|
+
? undefined : TRIGGER_SESSION_TIMEOUT_MS);
|
|
67
|
+
}
|
|
61
68
|
/**
|
|
62
69
|
* Keep legacy `per_run` definitions byte-stable while giving runtime code one
|
|
63
70
|
* canonical representation for session selection and validation.
|
|
@@ -428,8 +435,14 @@ function normalizeExecution(value, _opts) {
|
|
|
428
435
|
throw new Error('execution.type must be script, trigger_session, or target_session');
|
|
429
436
|
}
|
|
430
437
|
const thread = normalizeExecutionThread(raw.thread);
|
|
438
|
+
if (raw.timeoutMs !== undefined && (type === 'script'
|
|
439
|
+
|| typeof raw.timeoutMs !== 'number' || !Number.isSafeInteger(raw.timeoutMs)
|
|
440
|
+
|| raw.timeoutMs <= 0 || raw.timeoutMs > MAX_EXECUTION_TIMEOUT_MS)) {
|
|
441
|
+
throw new Error('execution.timeoutMs must be an integer between 1 and 2147483647 for a model-backed Trigger');
|
|
442
|
+
}
|
|
431
443
|
const execution = {
|
|
432
444
|
type,
|
|
445
|
+
...(raw.timeoutMs !== undefined ? { timeoutMs: raw.timeoutMs } : {}),
|
|
433
446
|
prompt: type === 'script' ? optionalString(raw.prompt) : requiredString(raw.prompt, 'execution.prompt'),
|
|
434
447
|
script: type === 'script' ? normalizeScript(raw.script) : undefined,
|
|
435
448
|
thread: type === 'trigger_session' ? (thread ?? 'by_trigger') : thread,
|
|
@@ -511,8 +524,8 @@ function normalizeFeedback(value, agentAid) {
|
|
|
511
524
|
throw new Error('feedback must be an object');
|
|
512
525
|
const raw = value;
|
|
513
526
|
const strategy = requiredString(raw.strategy, 'feedback.strategy');
|
|
514
|
-
if (strategy !== 'origin' && strategy !== 'target' && strategy !== 'silent') {
|
|
515
|
-
throw new Error('feedback.strategy must be origin, target, or silent');
|
|
527
|
+
if (strategy !== 'origin' && strategy !== 'target' && strategy !== 'event_session' && strategy !== 'silent') {
|
|
528
|
+
throw new Error('feedback.strategy must be origin, target, event_session, or silent');
|
|
516
529
|
}
|
|
517
530
|
return {
|
|
518
531
|
strategy,
|
|
@@ -630,6 +643,10 @@ function normalizeOnError(value) {
|
|
|
630
643
|
}
|
|
631
644
|
function validateTriggerSemantics(definition) {
|
|
632
645
|
const execution = definition.execution;
|
|
646
|
+
if (definition.feedback.strategy === 'event_session'
|
|
647
|
+
&& (definition.source.type !== 'event' || execution.type !== 'target_session')) {
|
|
648
|
+
throw new Error('feedback.strategy=event_session requires an event source and target_session execution');
|
|
649
|
+
}
|
|
633
650
|
if (execution.type === 'script') {
|
|
634
651
|
if (!execution.script)
|
|
635
652
|
throw new Error('execution.script is required when execution.type=script');
|
|
@@ -70,6 +70,7 @@ export async function fetchRemotePairingCode(authorizedBy) {
|
|
|
70
70
|
try {
|
|
71
71
|
const response = await ipcQuery(resolvePaths().socket, {
|
|
72
72
|
type: 'remote-auth.pair-code',
|
|
73
|
+
source: 'host-cli',
|
|
73
74
|
...(authorizedBy?.trim() ? { authorizedBy: authorizedBy.trim() } : {}),
|
|
74
75
|
}, 2_000);
|
|
75
76
|
if (!response?.ok || typeof response.code !== 'string' || typeof response.expiresAt !== 'number')
|
|
@@ -3,6 +3,27 @@ import path from 'path';
|
|
|
3
3
|
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
5
|
import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
|
|
6
|
+
/** A deadline imposed by an outer executor, handled by the ordinary task timeout path. */
|
|
7
|
+
export class ExecutionTimeoutError extends Error {
|
|
8
|
+
reason;
|
|
9
|
+
timeoutMs;
|
|
10
|
+
constructor(reason, timeoutMs) {
|
|
11
|
+
super('TOTAL_EXECUTION_TIMEOUT');
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
this.timeoutMs = timeoutMs;
|
|
14
|
+
this.name = 'ExecutionTimeoutError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class OrphanedTargetSessionError extends Error {
|
|
18
|
+
reason = 'orphaned_target_session';
|
|
19
|
+
constructor() {
|
|
20
|
+
super('ORPHANED_TARGET_SESSION');
|
|
21
|
+
this.name = 'OrphanedTargetSessionError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function isExecutionFailure(error) {
|
|
25
|
+
return error instanceof ExecutionTimeoutError || error instanceof OrphanedTargetSessionError;
|
|
26
|
+
}
|
|
6
27
|
export const CLAUDE_STARTUP_ARGUMENTS_TOO_LONG = 'claude_startup_arguments_too_long';
|
|
7
28
|
/** A completed provider response exhausted its output budget, rather than a transport failure. */
|
|
8
29
|
export function isOutputTokenLimitError(error) {
|
|
@@ -288,7 +309,15 @@ function hasRetryableHttpStatus(text) {
|
|
|
288
309
|
return RETRYABLE_HTTP_STATUS_PATTERN.test(text);
|
|
289
310
|
}
|
|
290
311
|
export function classifyError(error) {
|
|
312
|
+
if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
|
|
313
|
+
return ErrorType.SESSION_UNAVAILABLE;
|
|
291
314
|
const msg = (error?.message || '').toLowerCase();
|
|
315
|
+
// A damaged ecagent JSONL transcript is deterministic local state. Retrying
|
|
316
|
+
// the provider request cannot repair it and can append more entries to the
|
|
317
|
+
// same unusable file.
|
|
318
|
+
if (msg.includes('invalid jsonl session file') || msg.includes('invalid session file')) {
|
|
319
|
+
return ErrorType.SESSION_UNAVAILABLE;
|
|
320
|
+
}
|
|
292
321
|
// The broad dictionary rule for "stream" must not hide a provider budget limit.
|
|
293
322
|
if (isOutputTokenLimitError(error))
|
|
294
323
|
return ErrorType.MAX_OUTPUT_TOKENS;
|
|
@@ -346,11 +375,15 @@ export function classifyError(error) {
|
|
|
346
375
|
* 注意:401 认证错误不可重试(API Key 无效不会因重试恢复)
|
|
347
376
|
*/
|
|
348
377
|
export function isRetryableError(error) {
|
|
378
|
+
if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
|
|
379
|
+
return false;
|
|
349
380
|
// Replaying the same request does not increase its output budget.
|
|
350
381
|
if (isOutputTokenLimitError(error))
|
|
351
382
|
return false;
|
|
352
383
|
const msg = error?.message || String(error);
|
|
353
384
|
const lower = msg.toLowerCase();
|
|
385
|
+
if (lower.includes('invalid jsonl session file') || lower.includes('invalid session file'))
|
|
386
|
+
return false;
|
|
354
387
|
// 字典优先 — 命中则直接返回
|
|
355
388
|
const rule = matchErrorRule(lower);
|
|
356
389
|
if (rule)
|
|
@@ -366,6 +399,8 @@ export function isRetryableError(error) {
|
|
|
366
399
|
return false;
|
|
367
400
|
}
|
|
368
401
|
export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
402
|
+
if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
|
|
403
|
+
return `${includeEmoji ? '⚠️ ' : ''}${error.message}`;
|
|
369
404
|
if (terminalReason === 'max_output_tokens' || isOutputTokenLimitError(error)) {
|
|
370
405
|
const prefix = includeEmoji ? '⚠️ ' : '';
|
|
371
406
|
return `${prefix}模型响应达到输出上限(max_output_tokens),本轮未完成。`
|
|
@@ -395,7 +430,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
395
430
|
case 'permission_denied':
|
|
396
431
|
return `${prefix}权限被拒绝,操作已取消`;
|
|
397
432
|
case 'sandbox_initialization_failed':
|
|
398
|
-
return `${warnPrefix}
|
|
433
|
+
return `${warnPrefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请检查文件系统挂载规则及 user namespace/seccomp 配置`;
|
|
399
434
|
case 'claude_startup_arguments_too_long':
|
|
400
435
|
return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
|
|
401
436
|
case 'aborted_streaming':
|
|
@@ -407,6 +442,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
407
442
|
const msg = error?.message || String(error);
|
|
408
443
|
const warnPrefix = includeEmoji ? '⚠️ ' : '';
|
|
409
444
|
const errPrefix = includeEmoji ? '❌ ' : '';
|
|
445
|
+
if (/invalid (?:jsonl )?session file/i.test(msg)) {
|
|
446
|
+
return `${errPrefix}ecagent 会话文件损坏,已停止自动重试;请使用 /repair 或 /new 恢复会话`;
|
|
447
|
+
}
|
|
410
448
|
// Codex app-server reports this when another app-server still owns the
|
|
411
449
|
// thread writer. Surface the handoff condition instead of hiding it behind
|
|
412
450
|
// the generic "processing failed" message.
|
|
@@ -429,7 +467,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
429
467
|
: `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
|
|
430
468
|
}
|
|
431
469
|
if (isSandboxInitializationFailure(error)) {
|
|
432
|
-
return `${warnPrefix}
|
|
470
|
+
return `${warnPrefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请检查文件系统挂载规则及 user namespace/seccomp 配置`;
|
|
433
471
|
}
|
|
434
472
|
if (msg.includes('401') || msg.includes('authentication_error') || msg.toLowerCase().includes('unauthorized')) {
|
|
435
473
|
return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
|
package/dist/utils/logger.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolvePaths } from '../paths.js';
|
|
|
3
3
|
import { LogWriter } from './log-writer.js';
|
|
4
4
|
import { classifyToolErrorCode, normalizeToolErrorCode } from '../core/permission/tool-error-code.js';
|
|
5
5
|
import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
6
|
-
import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
|
|
6
|
+
import { buildToolLifecycleEventKey, normalizeToolCorrelation } from '../core/audit/event-key.js';
|
|
7
7
|
let currentLevel = process.env.LOG_LEVEL || 'INFO';
|
|
8
8
|
const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
9
9
|
const config = {
|
|
@@ -53,8 +53,18 @@ export function normalizeStructuredLog(data) {
|
|
|
53
53
|
const nested = data.event && typeof data.event === 'object' ? data.event : undefined;
|
|
54
54
|
const eventType = data.type ?? nested?.type;
|
|
55
55
|
const eventKey = data.eventKey ?? data.event_key ?? nested?.eventKey ?? nested?.event_key;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
|
|
57
|
+
const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
|
|
58
|
+
const lifecycleRecord = isToolUse || isToolResult;
|
|
59
|
+
const eventPhase = data.eventPhase ?? data.event_phase ?? nested?.eventPhase ?? nested?.event_phase
|
|
60
|
+
?? (isToolUse ? 'use' : isToolResult ? 'result' : undefined);
|
|
61
|
+
const toolCorrelation = normalizeToolCorrelation({
|
|
62
|
+
callId: data.callId ?? data.call_id ?? data.toolUseId ?? data.tool_use_id
|
|
63
|
+
?? nested?.callId ?? nested?.call_id ?? nested?.toolUseId ?? nested?.tool_use_id,
|
|
64
|
+
correlationId: data.correlationId ?? data.correlation_id ?? nested?.correlationId ?? nested?.correlation_id,
|
|
65
|
+
requestId: data.requestId ?? data.request_id ?? nested?.requestId ?? nested?.request_id,
|
|
66
|
+
});
|
|
67
|
+
const correlationId = lifecycleRecord ? toolCorrelation.correlationId : data.correlationId
|
|
58
68
|
?? data.correlation_id
|
|
59
69
|
?? data.callId
|
|
60
70
|
?? data.call_id
|
|
@@ -69,7 +79,9 @@ export function normalizeStructuredLog(data) {
|
|
|
69
79
|
?? nested?.callId
|
|
70
80
|
?? nested?.call_id
|
|
71
81
|
?? nested?.toolUseId
|
|
72
|
-
?? nested?.tool_use_id
|
|
82
|
+
?? nested?.tool_use_id
|
|
83
|
+
?? nested?.requestId
|
|
84
|
+
?? nested?.request_id;
|
|
73
85
|
const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
|
|
74
86
|
const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid ?? nested?.selfAid;
|
|
75
87
|
const rawAgentName = data.agentName ?? data.agent_name ?? nested?.agentName ?? nested?.agent_name;
|
|
@@ -91,18 +103,17 @@ export function normalizeStructuredLog(data) {
|
|
|
91
103
|
? normalizePermissionMode(rawPermissionMode).mode
|
|
92
104
|
: rawPermissionMode;
|
|
93
105
|
const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
|
|
94
|
-
const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
|
|
95
|
-
const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
|
|
96
106
|
const isError = data.isError ?? data.is_error ?? (data.ok === false ? true : undefined)
|
|
97
107
|
?? nested?.isError ?? nested?.is_error ?? (nested?.ok === false ? true : undefined);
|
|
98
108
|
const decisionSource = data.decisionSource ?? data.decision_source
|
|
99
|
-
?? nested?.decisionSource ?? nested?.decision_source
|
|
109
|
+
?? nested?.decisionSource ?? nested?.decision_source
|
|
110
|
+
?? (lifecycleRecord ? 'runner' : undefined);
|
|
100
111
|
const decision = data.decision
|
|
112
|
+
?? nested?.decision
|
|
101
113
|
?? data.status
|
|
102
114
|
?? (isToolResult && isError === true ? 'error' : undefined)
|
|
103
115
|
?? (isToolResult && isError === false ? 'allow' : undefined)
|
|
104
|
-
?? (isToolUse ? '
|
|
105
|
-
?? nested?.decision;
|
|
116
|
+
?? (isToolUse ? 'pending' : undefined);
|
|
106
117
|
const executed = data.executed ?? nested?.executed
|
|
107
118
|
?? (isToolResult ? true : isToolUse ? false : undefined);
|
|
108
119
|
const executionState = data.executionState ?? data.execution_state
|
|
@@ -118,13 +129,10 @@ export function normalizeStructuredLog(data) {
|
|
|
118
129
|
const classificationMissing = isToolResult && isError === true
|
|
119
130
|
&& !normalizeToolErrorCode(explicitErrorCode)
|
|
120
131
|
&& decisionSource !== 'policy' && decisionSource !== 'approval';
|
|
121
|
-
const lifecycleRecord = isToolUse || isToolResult;
|
|
122
132
|
const lifecycleEventKey = lifecycleRecord
|
|
123
133
|
? (eventKey ?? buildToolLifecycleEventKey({
|
|
124
134
|
sessionId,
|
|
125
|
-
|
|
126
|
-
correlationId: data.correlationId ?? data.correlation_id ?? nested?.correlationId ?? nested?.correlation_id,
|
|
127
|
-
requestId: data.requestId ?? data.request_id ?? nested?.requestId ?? nested?.request_id,
|
|
135
|
+
...toolCorrelation,
|
|
128
136
|
}))
|
|
129
137
|
: undefined;
|
|
130
138
|
const missingContextFields = lifecycleRecord
|
|
@@ -134,6 +142,8 @@ export function normalizeStructuredLog(data) {
|
|
|
134
142
|
...data,
|
|
135
143
|
...(lifecycleEventKey ? { eventKey: lifecycleEventKey } : {}),
|
|
136
144
|
...(eventPhase ? { eventPhase } : {}),
|
|
145
|
+
...(lifecycleRecord ? toolCorrelation : {}),
|
|
146
|
+
...(lifecycleRecord && !toolCorrelation.callId ? { correlationMissing: true } : {}),
|
|
137
147
|
...(correlationId ? { correlationId } : {}),
|
|
138
148
|
...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
|
|
139
149
|
...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
|
|
@@ -51,19 +51,75 @@ function displayShellCommand(command) {
|
|
|
51
51
|
const carrier = resolveCodexShellCarrierString(command);
|
|
52
52
|
return carrier?.dialect === 'powershell' ? carrier.command : undefined;
|
|
53
53
|
}
|
|
54
|
+
/** Hide an initial cd through its first unquoted && or ;, regardless of the directory. */
|
|
55
|
+
function displayBashCommand(command) {
|
|
56
|
+
const prefix = command.match(/^[ \t]*cd(?=[ \t;]|&&)/);
|
|
57
|
+
if (!prefix)
|
|
58
|
+
return command;
|
|
59
|
+
let quote;
|
|
60
|
+
for (let index = prefix[0].length; index < command.length; index++) {
|
|
61
|
+
const char = command[index];
|
|
62
|
+
if (quote === "'") {
|
|
63
|
+
if (char === quote)
|
|
64
|
+
quote = undefined;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (char === '\\') {
|
|
68
|
+
index++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Nested shell expressions need a full shell parser; keep them visible.
|
|
72
|
+
if (char === '`' || (char === '$' && command[index + 1] === '('))
|
|
73
|
+
return command;
|
|
74
|
+
if (quote === '"') {
|
|
75
|
+
if (char === quote)
|
|
76
|
+
quote = undefined;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (char === "'" || char === '"') {
|
|
80
|
+
quote = char;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (char === ';' || (char === '&' && command[index + 1] === '&')) {
|
|
84
|
+
if (char === ';' && /[;&]/.test(command[index + 1] ?? ''))
|
|
85
|
+
return command;
|
|
86
|
+
const body = command.slice(index + (char === ';' ? 1 : 2)).trimStart();
|
|
87
|
+
return body || command;
|
|
88
|
+
}
|
|
89
|
+
// Do not skip over other commands, redirections, or shell comments.
|
|
90
|
+
if (/[\r\n|&<>()]/.test(char) || (char === '#' && /[ \t]/.test(command[index - 1])))
|
|
91
|
+
return command;
|
|
92
|
+
}
|
|
93
|
+
return command;
|
|
94
|
+
}
|
|
54
95
|
/**
|
|
55
96
|
* Build the tool arguments projected into activity/thought payloads.
|
|
56
97
|
* Execution, permission checks, and audit logging keep using the original
|
|
57
|
-
* input; only the human-facing
|
|
98
|
+
* input; only the human-facing command is simplified here.
|
|
58
99
|
*/
|
|
59
100
|
export function toolInputForDisplay(toolName, input) {
|
|
60
|
-
if (!input ||
|
|
101
|
+
if (!input || typeof input.command !== 'string')
|
|
102
|
+
return input;
|
|
103
|
+
let command;
|
|
104
|
+
if (toolName === 'Shell') {
|
|
105
|
+
command = displayShellCommand(input.command) ?? input.command;
|
|
106
|
+
// Keep explicit shell invocations, but omit standard executable directories.
|
|
107
|
+
command = command.replace(/^(\s*)(['"]?)\/(?:usr\/)?bin\/(bash|sh)\2(?=\s|$)/, '$1$2$3$2');
|
|
108
|
+
}
|
|
109
|
+
else if (toolName === 'Bash') {
|
|
110
|
+
command = displayBashCommand(input.command);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
61
113
|
return input;
|
|
62
|
-
|
|
114
|
+
}
|
|
63
115
|
if (!command || command === input.command)
|
|
64
116
|
return input;
|
|
65
117
|
return { ...input, command };
|
|
66
118
|
}
|
|
119
|
+
/** Activity summaries use the same command projection as displayed arguments. */
|
|
120
|
+
export function summarizeToolInputForDisplay(toolName, input) {
|
|
121
|
+
return summarizeToolInput(toolName, toolInputForDisplay(toolName, input) ?? {});
|
|
122
|
+
}
|
|
67
123
|
/**
|
|
68
124
|
* 工具输入摘要(提取工具调用的可读描述,供权限审批和消息展示使用)
|
|
69
125
|
*/
|
package/dist/utils/welcome.js
CHANGED
|
@@ -8,10 +8,154 @@
|
|
|
8
8
|
* - 所有 IM 渠道(Feishu/WeChat/DingTalk/QQBot/WeCom):首次交互时发送给交互用户(被动响应)
|
|
9
9
|
*/
|
|
10
10
|
import fs from 'fs';
|
|
11
|
-
import
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createHash } from 'crypto';
|
|
13
|
+
import { parseDocument } from 'yaml';
|
|
14
|
+
import { agentWelcomePath, channelStatePath, resolveRoot } from '../paths.js';
|
|
15
|
+
import { isValidAid } from '../aun/aid/validation.js';
|
|
12
16
|
import { loadAgent, saveAgent } from '../config-store.js';
|
|
13
17
|
import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
|
|
18
|
+
import { renderTemplate } from '../eck/manifest-engine.js';
|
|
19
|
+
import { atomicWriteText } from './atomic-write.js';
|
|
14
20
|
import { logger } from './logger.js';
|
|
21
|
+
export const MAX_WELCOME_TEMPLATE_BYTES = 4 * 1024;
|
|
22
|
+
export const MAX_RENDERED_WELCOME_BYTES = 8 * 1024;
|
|
23
|
+
const WELCOME_TEMPLATE_VARS = new Set([
|
|
24
|
+
'agentName', 'agentDescription', 'agentAid', 'agentTags', 'agentTagsText',
|
|
25
|
+
'ownerName', 'ownerAid', 'channel',
|
|
26
|
+
]);
|
|
27
|
+
/** Validate the small, data-only template language used by Bootstrap welcome text. */
|
|
28
|
+
export function validateWelcomeTemplate(template) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
if (typeof template !== 'string' || !template.trim()) {
|
|
31
|
+
return { valid: false, errors: ['welcome template must be non-empty'] };
|
|
32
|
+
}
|
|
33
|
+
if (template.includes('\0') || Buffer.from(template, 'utf8').toString('utf8') !== template) {
|
|
34
|
+
errors.push('welcome template must contain valid UTF-8 text only');
|
|
35
|
+
}
|
|
36
|
+
if (Buffer.byteLength(template, 'utf8') > MAX_WELCOME_TEMPLATE_BYTES) {
|
|
37
|
+
return { valid: false, errors: [`welcome template must be at most ${MAX_WELCOME_TEMPLATE_BYTES} bytes`] };
|
|
38
|
+
}
|
|
39
|
+
const stack = [];
|
|
40
|
+
const tokenPattern = /\{\{|\}\}/g;
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = tokenPattern.exec(template)) !== null) {
|
|
43
|
+
const end = template.indexOf('}}', tokenPattern.lastIndex);
|
|
44
|
+
if (match[0] !== '{{' || end < 0) {
|
|
45
|
+
errors.push('unmatched template delimiter');
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const token = template.slice(tokenPattern.lastIndex, end);
|
|
49
|
+
tokenPattern.lastIndex = end + 2;
|
|
50
|
+
if (token === '#each agentTags') {
|
|
51
|
+
if (stack.includes('each'))
|
|
52
|
+
errors.push('nested tag loops are not supported');
|
|
53
|
+
stack.push('each');
|
|
54
|
+
}
|
|
55
|
+
else if (token === '/each' || token === '/') {
|
|
56
|
+
const expected = token === '/each' ? 'each' : 'condition';
|
|
57
|
+
if (stack.pop() !== expected)
|
|
58
|
+
errors.push(`unexpected {{${token}}}`);
|
|
59
|
+
}
|
|
60
|
+
else if (/^\?\w+$/.test(token) && WELCOME_TEMPLATE_VARS.has(token.slice(1))) {
|
|
61
|
+
stack.push('condition');
|
|
62
|
+
}
|
|
63
|
+
else if ((token === '.' || token === '@index') && stack.includes('each')) {
|
|
64
|
+
// Loop-local variables are valid only inside the tag loop.
|
|
65
|
+
}
|
|
66
|
+
else if (!WELCOME_TEMPLATE_VARS.has(token)) {
|
|
67
|
+
errors.push(`unsupported template variable or block: ${token}`);
|
|
68
|
+
}
|
|
69
|
+
if (stack.length > 8) {
|
|
70
|
+
errors.push('template blocks are nested too deeply');
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (stack.includes('each'))
|
|
75
|
+
errors.push('unclosed {{#each}} block');
|
|
76
|
+
if (stack.includes('condition'))
|
|
77
|
+
errors.push('unclosed condition block');
|
|
78
|
+
return { valid: errors.length === 0, errors };
|
|
79
|
+
}
|
|
80
|
+
function checkedWelcomePath(aid) {
|
|
81
|
+
if (!isValidAid(aid))
|
|
82
|
+
throw new Error('invalid Agent AID');
|
|
83
|
+
const target = agentWelcomePath(aid);
|
|
84
|
+
const root = path.resolve(resolveRoot());
|
|
85
|
+
for (let cursor = path.resolve(target); cursor !== root; cursor = path.dirname(cursor)) {
|
|
86
|
+
try {
|
|
87
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
88
|
+
throw new Error('welcome path must not contain symbolic links');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code !== 'ENOENT')
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return target;
|
|
96
|
+
}
|
|
97
|
+
export function saveAgentWelcomeTemplate(aid, template) {
|
|
98
|
+
const validation = validateWelcomeTemplate(template);
|
|
99
|
+
if (!validation.valid)
|
|
100
|
+
throw new Error(`invalid welcome template: ${validation.errors.join('; ')}`);
|
|
101
|
+
const target = checkedWelcomePath(aid);
|
|
102
|
+
const staging = target + '.tmp.' + process.pid;
|
|
103
|
+
try {
|
|
104
|
+
if (fs.lstatSync(staging).isSymbolicLink())
|
|
105
|
+
throw new Error('invalid welcome staging path');
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code !== 'ENOENT')
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
atomicWriteText(target, template);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
fs.rmSync(staging, { force: true });
|
|
116
|
+
}
|
|
117
|
+
return { path: target, bytes: Buffer.byteLength(template, 'utf8'), sha256: createHash('sha256').update(template).digest('hex') };
|
|
118
|
+
}
|
|
119
|
+
export function readAgentWelcomeTemplate(aid) {
|
|
120
|
+
const target = checkedWelcomePath(aid);
|
|
121
|
+
let stat;
|
|
122
|
+
try {
|
|
123
|
+
stat = fs.lstatSync(target);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code === 'ENOENT')
|
|
127
|
+
return null;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (!stat.isFile() || stat.size > MAX_WELCOME_TEMPLATE_BYTES)
|
|
131
|
+
throw new Error('invalid welcome template file');
|
|
132
|
+
const template = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(target));
|
|
133
|
+
const validation = validateWelcomeTemplate(template);
|
|
134
|
+
if (!validation.valid)
|
|
135
|
+
throw new Error(`invalid welcome template at ${target}: ${validation.errors.join('; ')}`);
|
|
136
|
+
return template;
|
|
137
|
+
}
|
|
138
|
+
export function parseAgentWelcomeMetadata(content) {
|
|
139
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
140
|
+
if (!frontmatter)
|
|
141
|
+
return { tags: [] };
|
|
142
|
+
try {
|
|
143
|
+
const document = parseDocument(frontmatter);
|
|
144
|
+
if (document.errors.length)
|
|
145
|
+
return { tags: [] };
|
|
146
|
+
const value = document.toJS();
|
|
147
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
148
|
+
return { tags: [] };
|
|
149
|
+
return {
|
|
150
|
+
name: typeof value.name === 'string' ? value.name.trim() : undefined,
|
|
151
|
+
description: typeof value.description === 'string' ? value.description.trim() : undefined,
|
|
152
|
+
tags: Array.isArray(value.tags) ? value.tags.filter((tag) => typeof tag === 'string').map(tag => tag.trim()) : [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return { tags: [] };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
15
159
|
// ============================================================================
|
|
16
160
|
// 欢迎消息生成
|
|
17
161
|
// ============================================================================
|
|
@@ -50,6 +194,35 @@ ${agentIntro}${channelInfo}
|
|
|
50
194
|
|
|
51
195
|
现在就可以开始工作了!`;
|
|
52
196
|
}
|
|
197
|
+
export function renderPostBootstrapWelcomeMessage(options) {
|
|
198
|
+
const validation = validateWelcomeTemplate(options.template);
|
|
199
|
+
if (!validation.valid)
|
|
200
|
+
throw new Error(`invalid welcome template: ${validation.errors.join('; ')}`);
|
|
201
|
+
// ECK renders loop bodies recursively. Mask data so names/descriptions containing
|
|
202
|
+
// template syntax remain literal even when inserted inside a loop or condition.
|
|
203
|
+
const values = [];
|
|
204
|
+
const literal = (value) => {
|
|
205
|
+
if (!value)
|
|
206
|
+
return '';
|
|
207
|
+
values.push(value);
|
|
208
|
+
return `\0WELCOME${values.length - 1}\0`;
|
|
209
|
+
};
|
|
210
|
+
const rendered = renderTemplate(options.template, {
|
|
211
|
+
agentName: literal(options.agentName),
|
|
212
|
+
agentDescription: literal(options.agentDescription),
|
|
213
|
+
agentAid: literal(options.agentAid),
|
|
214
|
+
agentTags: (options.agentTags ?? []).map(literal),
|
|
215
|
+
agentTagsText: literal((options.agentTags ?? []).join(', ')),
|
|
216
|
+
ownerName: literal(options.ownerName),
|
|
217
|
+
ownerAid: literal(options.ownerAid),
|
|
218
|
+
channel: literal('aun'),
|
|
219
|
+
}, false).replace(/\0WELCOME(\d+)\0/g, (_match, index) => values[Number(index)]);
|
|
220
|
+
if (!rendered.trim())
|
|
221
|
+
throw new Error('welcome template rendered to an empty message');
|
|
222
|
+
if (Buffer.byteLength(rendered, 'utf8') > MAX_RENDERED_WELCOME_BYTES)
|
|
223
|
+
throw new Error('rendered welcome exceeds 8192 bytes');
|
|
224
|
+
return rendered;
|
|
225
|
+
}
|
|
53
226
|
/**
|
|
54
227
|
* 生成简化版欢迎消息(用于快速场景)
|
|
55
228
|
*/
|
|
@@ -412,6 +412,9 @@ export class AgentHarness {
|
|
|
412
412
|
}
|
|
413
413
|
async handleAgentEvent(event, signal) {
|
|
414
414
|
if (event.type === "message_end") {
|
|
415
|
+
// Persist the same multimodal message that was sent to the provider.
|
|
416
|
+
// Provider rollouts (Claude/Codex) retain image bytes in their own
|
|
417
|
+
// session files, so ecagent must do the same for resumable context.
|
|
415
418
|
await this.session.appendMessage(event.message);
|
|
416
419
|
await this.emitAny(event, signal);
|
|
417
420
|
return;
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { SessionError, toError } from "../types.js";
|
|
2
2
|
import { getFileSystemResultOrThrow } from "./repo-utils.js";
|
|
3
3
|
import { uuidv7 } from "./uuid.js";
|
|
4
|
+
// Multiple harness instances can briefly reference the same session during a
|
|
5
|
+
// replacement turn. Node's appendFile does not guarantee that large writes
|
|
6
|
+
// from separate calls stay contiguous, so serialize appends per physical file.
|
|
7
|
+
const appendQueues = new Map();
|
|
8
|
+
async function appendSerialized(fs, filePath, content) {
|
|
9
|
+
const previous = appendQueues.get(filePath) ?? Promise.resolve();
|
|
10
|
+
let release;
|
|
11
|
+
const current = new Promise((resolve) => { release = resolve; });
|
|
12
|
+
appendQueues.set(filePath, current);
|
|
13
|
+
try {
|
|
14
|
+
await previous.catch(() => undefined);
|
|
15
|
+
getFileSystemResultOrThrow(await fs.appendFile(filePath, content), `Failed to append session entry ${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
release();
|
|
19
|
+
if (appendQueues.get(filePath) === current)
|
|
20
|
+
appendQueues.delete(filePath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
4
23
|
function updateLabelCache(labelsById, entry) {
|
|
5
24
|
if (entry.type !== "label")
|
|
6
25
|
return;
|
|
@@ -185,7 +204,7 @@ export class JsonlSessionStorage {
|
|
|
185
204
|
timestamp: new Date().toISOString(),
|
|
186
205
|
targetId: leafId,
|
|
187
206
|
};
|
|
188
|
-
|
|
207
|
+
await appendSerialized(this.fs, this.filePath, `${JSON.stringify(entry)}\n`);
|
|
189
208
|
this.entries.push(entry);
|
|
190
209
|
this.byId.set(entry.id, entry);
|
|
191
210
|
this.currentLeafId = leafId;
|
|
@@ -194,7 +213,7 @@ export class JsonlSessionStorage {
|
|
|
194
213
|
return generateEntryId(this.byId);
|
|
195
214
|
}
|
|
196
215
|
async appendEntry(entry) {
|
|
197
|
-
|
|
216
|
+
await appendSerialized(this.fs, this.filePath, `${JSON.stringify(entry)}\n`);
|
|
198
217
|
this.entries.push(entry);
|
|
199
218
|
this.byId.set(entry.id, entry);
|
|
200
219
|
updateLabelCache(this.labelsById, entry);
|