evolcore 0.0.26 → 0.0.28
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 +36 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +70 -26
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +363 -421
- 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} +13 -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 +544 -239
- 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 +111 -162
- package/dist/cli/diag.js +3 -3
- 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 +13 -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 +81 -19
- 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 +105 -40
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +292 -27
- 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 +364 -96
- package/dist/core/command/menu-protocol.js +43 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +61 -37
- 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 +94 -16
- package/dist/core/message/message-bridge.js +140 -13
- 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 +729 -118
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +76 -36
- 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 +8 -8
- 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 +38 -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 +322 -207
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +3 -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/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- 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/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/dist/utils/windows-shell-trust.js +37 -0
- 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 +23 -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
1
|
+
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, TRANSPORT_EFFORTS, resolveCatalogModel, } from '../core/model/model-catalog.js';
|
|
2
2
|
// Ecagent/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
|
|
3
3
|
import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
|
|
4
4
|
import fs from 'fs';
|
|
@@ -12,13 +12,14 @@ import { requestDangerousCommandPermission } from '../core/permission/approval-g
|
|
|
12
12
|
import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
|
|
13
13
|
import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
14
14
|
import { logger } from '../utils/logger.js';
|
|
15
|
+
import { ErrorType } from '../utils/error-utils.js';
|
|
15
16
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
16
17
|
import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
17
18
|
import { resolveEcagentConfig } from './baseagent.js';
|
|
18
19
|
import { buildModelRequestHeaders } from './request-identity.js';
|
|
19
20
|
import { appendRequestPath } from './request-identity.js';
|
|
20
21
|
import { getManagedTaskTempDir } from '../cli/task-context.js';
|
|
21
|
-
import { hasTrustedFullAccessContext } from './runner-types.js';
|
|
22
|
+
import { unsettledInterruptError, withInterruptDeadline, hasTrustedFullAccessContext } from './runner-types.js';
|
|
22
23
|
const PROVIDER_ID = 'evolcore-gateway';
|
|
23
24
|
const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
|
|
24
25
|
Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
|
|
@@ -679,6 +680,8 @@ function extractToolResult(result) {
|
|
|
679
680
|
}
|
|
680
681
|
function errorType(message) {
|
|
681
682
|
const normalized = message.toLowerCase();
|
|
683
|
+
if (normalized.includes('invalid jsonl session file') || normalized.includes('session file'))
|
|
684
|
+
return ErrorType.SESSION_UNAVAILABLE;
|
|
682
685
|
if (normalized.includes('[auth]') || normalized.includes('401') || normalized.includes('403'))
|
|
683
686
|
return 'auth';
|
|
684
687
|
if (normalized.includes('context') && (normalized.includes('long') || normalized.includes('length')))
|
|
@@ -690,6 +693,7 @@ function errorType(message) {
|
|
|
690
693
|
export class EcagentRunner {
|
|
691
694
|
name = 'ecagent';
|
|
692
695
|
capabilities = {
|
|
696
|
+
taskRecovery: true,
|
|
693
697
|
fullaccess: true,
|
|
694
698
|
clear: true, compact: true, fork: true, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported',
|
|
695
699
|
};
|
|
@@ -700,6 +704,9 @@ export class EcagentRunner {
|
|
|
700
704
|
effort;
|
|
701
705
|
permissionMode = DEFAULT_PERMISSION_MODE;
|
|
702
706
|
runtimes = new Map();
|
|
707
|
+
/** Serialize harness lifecycles that share one durable ecagent session file. */
|
|
708
|
+
sessionRunTails = new Map();
|
|
709
|
+
sessionRuns = new Map();
|
|
703
710
|
activeStreams = new Map();
|
|
704
711
|
/** Queue identity prevents an old run's finally from consuming a new run's interrupt. */
|
|
705
712
|
pendingInterrupts = new Map();
|
|
@@ -755,7 +762,15 @@ export class EcagentRunner {
|
|
|
755
762
|
return catalog.models.map(m => m.id);
|
|
756
763
|
}
|
|
757
764
|
async getAvailableEfforts(model) {
|
|
758
|
-
|
|
765
|
+
const catalog = await this.getModelCatalog();
|
|
766
|
+
const efforts = catalogEfforts(catalog, model, 'ecagent');
|
|
767
|
+
if (efforts.length > 0)
|
|
768
|
+
return efforts;
|
|
769
|
+
// The embedded transport advertises reasoning support for every model it
|
|
770
|
+
// constructs. Older gateways may omit reasoningLevels from /models; keep
|
|
771
|
+
// menu.options aligned with the actual runner capability in that case.
|
|
772
|
+
const entry = catalog.models.find(item => item.id === resolveCatalogModel(catalog, model));
|
|
773
|
+
return entry && entry.reasoningLevels === undefined ? [...TRANSPORT_EFFORTS.ecagent] : efforts;
|
|
759
774
|
}
|
|
760
775
|
registerStream(key, stream) { this.activeStreams.set(key, stream); }
|
|
761
776
|
cleanupStream(key) {
|
|
@@ -799,6 +814,7 @@ export class EcagentRunner {
|
|
|
799
814
|
// Keep managed tasks from silently inheriting the daemon-wide TMPDIR when
|
|
800
815
|
// their session-owned runtime directory is unavailable.
|
|
801
816
|
getManagedTaskTempDir(runtimeEnv);
|
|
817
|
+
this.assertInterruptSettled(sessionId);
|
|
802
818
|
const topicBinding = modelOverride?.backend?.kind === 'topic';
|
|
803
819
|
if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
|
|
804
820
|
const error = new Error('topic backend run requires an active TurnLease and binding callback');
|
|
@@ -806,20 +822,43 @@ export class EcagentRunner {
|
|
|
806
822
|
throw error;
|
|
807
823
|
}
|
|
808
824
|
const queue = new AsyncEventQueue();
|
|
825
|
+
const lifecycle = { queue, cancelled: false };
|
|
826
|
+
const runs = this.sessionRuns.get(sessionId) ?? new Set();
|
|
827
|
+
runs.add(lifecycle);
|
|
828
|
+
this.sessionRuns.set(sessionId, runs);
|
|
809
829
|
this.activeStreams.set(sessionId, queue);
|
|
810
|
-
|
|
830
|
+
const request = {
|
|
811
831
|
sessionId, prompt, projectPath: path.resolve(projectPath), initialAgentSessionId, images,
|
|
812
832
|
systemPromptAppend, modelOverride, runtimeEnv,
|
|
833
|
+
};
|
|
834
|
+
const previous = this.sessionRunTails.get(sessionId) ?? Promise.resolve();
|
|
835
|
+
const run = previous.catch(() => undefined).then(() => {
|
|
836
|
+
if (!lifecycle.cancelled)
|
|
837
|
+
return this.executeRun(queue, request, lifecycle);
|
|
813
838
|
});
|
|
839
|
+
lifecycle.done = run;
|
|
840
|
+
this.sessionRunTails.set(sessionId, run);
|
|
841
|
+
const release = () => {
|
|
842
|
+
runs.delete(lifecycle);
|
|
843
|
+
if (this.sessionRuns.get(sessionId) === runs && runs.size === 0)
|
|
844
|
+
this.sessionRuns.delete(sessionId);
|
|
845
|
+
if (this.sessionRunTails.get(sessionId) === run)
|
|
846
|
+
this.sessionRunTails.delete(sessionId);
|
|
847
|
+
};
|
|
848
|
+
void run.then(release, release);
|
|
814
849
|
return queue;
|
|
815
850
|
}
|
|
816
|
-
async executeRun(queue, request) {
|
|
817
|
-
const stats = {
|
|
851
|
+
async executeRun(queue, request, lifecycle) {
|
|
852
|
+
const stats = {
|
|
853
|
+
startedAt: Date.now(), text: '', textBlocks: new Map(), numTurns: 0, usage: {}, modelCalls: [],
|
|
854
|
+
};
|
|
818
855
|
let completeSent = false;
|
|
819
856
|
try {
|
|
820
857
|
await fs.promises.mkdir(request.projectPath, { recursive: true });
|
|
821
858
|
const previousRuntime = this.runtimes.get(request.sessionId);
|
|
822
859
|
const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId, request.modelOverride);
|
|
860
|
+
if (lifecycle.cancelled)
|
|
861
|
+
throw new Error('ecagent run interrupted');
|
|
823
862
|
if (previousRuntime !== runtime) {
|
|
824
863
|
queue.push({ type: 'session_id', sessionId: runtime.agentSessionId });
|
|
825
864
|
}
|
|
@@ -870,13 +909,16 @@ export class EcagentRunner {
|
|
|
870
909
|
throw new Error('ecagent run interrupted');
|
|
871
910
|
}
|
|
872
911
|
harness.on('tool_call', async (event, signal) => {
|
|
912
|
+
if (lifecycle.cancelled || signal?.aborted)
|
|
913
|
+
return { block: true, reason: '工具调用已取消' };
|
|
873
914
|
const pauseController = this.permissionContexts.get(request.sessionId)?.pauseController;
|
|
874
915
|
if (pauseController) {
|
|
875
916
|
const pauseResult = await pauseController.waitAtToolBoundary(signal);
|
|
876
917
|
if (pauseResult === 'cancelled')
|
|
877
918
|
return { block: true, reason: '工具调用已取消' };
|
|
878
919
|
}
|
|
879
|
-
|
|
920
|
+
const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
|
|
921
|
+
return lifecycle.cancelled || signal?.aborted ? { block: true, reason: '工具调用已取消' } : decision;
|
|
880
922
|
});
|
|
881
923
|
const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
|
|
882
924
|
queue.push({ type: 'state_changed', state: 'running' });
|
|
@@ -890,20 +932,36 @@ export class EcagentRunner {
|
|
|
890
932
|
}
|
|
891
933
|
finally {
|
|
892
934
|
unsubscribe();
|
|
893
|
-
|
|
935
|
+
this.finishTextBlocks(queue, stats);
|
|
936
|
+
if (runtime.harness === harness)
|
|
937
|
+
runtime.harness = undefined;
|
|
894
938
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
939
|
+
const outputLimit = result.stopReason === 'length';
|
|
940
|
+
if (result.stopReason === 'error' || result.stopReason === 'aborted' || outputLimit) {
|
|
941
|
+
const message = result.errorMessage || (result.stopReason === 'aborted'
|
|
942
|
+
? 'ecagent run interrupted'
|
|
943
|
+
: outputLimit
|
|
944
|
+
? 'ecagent response reached the output limit (max_output_tokens)'
|
|
945
|
+
: 'ecagent run failed');
|
|
946
|
+
queue.push({ type: 'error', error: message, errorType: outputLimit ? ErrorType.MAX_OUTPUT_TOKENS : errorType(message) });
|
|
898
947
|
}
|
|
899
948
|
const title = await runtime.session.getSessionName();
|
|
900
949
|
queue.push({ type: 'state_changed', state: 'idle' });
|
|
901
950
|
queue.push({
|
|
902
951
|
type: 'complete',
|
|
903
952
|
result: stats.text,
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
953
|
+
...(outputLimit ? { subtype: 'max_output_tokens' } : {}),
|
|
954
|
+
isError: result.stopReason === 'error' || result.stopReason === 'aborted' || outputLimit,
|
|
955
|
+
errors: result.errorMessage
|
|
956
|
+
? [result.errorMessage]
|
|
957
|
+
: outputLimit
|
|
958
|
+
? ['max_output_tokens']
|
|
959
|
+
: undefined,
|
|
960
|
+
terminalReason: result.stopReason === 'aborted'
|
|
961
|
+
? 'interrupted'
|
|
962
|
+
: outputLimit
|
|
963
|
+
? 'max_output_tokens'
|
|
964
|
+
: result.stopReason,
|
|
907
965
|
durationMs: Date.now() - stats.startedAt,
|
|
908
966
|
ttftMs: stats.firstTextAt ? stats.firstTextAt - stats.startedAt : undefined,
|
|
909
967
|
sessionTitle: title,
|
|
@@ -916,6 +974,7 @@ export class EcagentRunner {
|
|
|
916
974
|
completeSent = true;
|
|
917
975
|
}
|
|
918
976
|
catch (error) {
|
|
977
|
+
this.finishTextBlocks(queue, stats);
|
|
919
978
|
const message = error instanceof Error ? error.message : String(error);
|
|
920
979
|
const interrupted = message === 'ecagent run interrupted';
|
|
921
980
|
logger.error(`[EcagentRunner] run failed for session ${request.sessionId}:`, error);
|
|
@@ -948,7 +1007,11 @@ export class EcagentRunner {
|
|
|
948
1007
|
const targetId = topicBinding
|
|
949
1008
|
? (requestedTopicAgentSessionId || undefined)
|
|
950
1009
|
: requestedId || current?.agentSessionId;
|
|
951
|
-
|
|
1010
|
+
const recoverySessionId = modelOverride?.recovery?.agentSessionId;
|
|
1011
|
+
if (modelOverride?.recovery && (!recoverySessionId || targetId !== recoverySessionId)) {
|
|
1012
|
+
throw new Error('Task recovery requires the original ecagent session binding');
|
|
1013
|
+
}
|
|
1014
|
+
if (current && !recoverySessionId
|
|
952
1015
|
&& current.projectPath === projectPath
|
|
953
1016
|
&& (!targetId || current.agentSessionId === targetId)
|
|
954
1017
|
&& (!topicBinding || (!!targetId && current.agentSessionId === targetId)))
|
|
@@ -959,6 +1022,9 @@ export class EcagentRunner {
|
|
|
959
1022
|
if (targetId) {
|
|
960
1023
|
const metadata = (await repo.list({ cwd: projectPath })).find(item => item.id === targetId);
|
|
961
1024
|
if (!metadata) {
|
|
1025
|
+
if (recoverySessionId) {
|
|
1026
|
+
throw new Error(`Task recovery backend is unavailable: ${recoverySessionId}`);
|
|
1027
|
+
}
|
|
962
1028
|
if (topicBinding) {
|
|
963
1029
|
const error = new Error(`bound topic backend is unavailable: ${targetId}`);
|
|
964
1030
|
error.code = 'TOPIC_BACKEND_UNAVAILABLE';
|
|
@@ -979,6 +1045,9 @@ export class EcagentRunner {
|
|
|
979
1045
|
: await repo.create({ cwd: projectPath, id: sessionId });
|
|
980
1046
|
}
|
|
981
1047
|
const metadata = await session.getMetadata();
|
|
1048
|
+
if (recoverySessionId && metadata.id !== recoverySessionId) {
|
|
1049
|
+
throw new Error('Task recovery cannot replace the original ecagent session');
|
|
1050
|
+
}
|
|
982
1051
|
const runtime = { agentSessionId: metadata.id, projectPath, repo, session };
|
|
983
1052
|
if ((topicBinding && metadata.id !== requestedTopicAgentSessionId) || metadata.id !== targetId) {
|
|
984
1053
|
const turn = modelOverride?.turn;
|
|
@@ -997,6 +1066,29 @@ export class EcagentRunner {
|
|
|
997
1066
|
this.runtimes.set(sessionId, runtime);
|
|
998
1067
|
return runtime;
|
|
999
1068
|
}
|
|
1069
|
+
endTextBlock(queue, stats, contentIndex, content) {
|
|
1070
|
+
if (stats.textBlocks.get(contentIndex)?.ended)
|
|
1071
|
+
return;
|
|
1072
|
+
// Commit synchronously before enqueueing. message_end/finally may close the
|
|
1073
|
+
// same block again, but cannot append or project its text a second time.
|
|
1074
|
+
stats.textBlocks.set(contentIndex, { text: content, ended: true });
|
|
1075
|
+
stats.text += content;
|
|
1076
|
+
// Match Codex: only the completed body enters the common runner protocol.
|
|
1077
|
+
queue.push({ type: 'text', text: content, turn: stats.numTurns + 1 });
|
|
1078
|
+
}
|
|
1079
|
+
/** Recover a missing text_end from the final message, or the received deltas. */
|
|
1080
|
+
finishTextBlocks(queue, stats, message) {
|
|
1081
|
+
if (message) {
|
|
1082
|
+
message.content.forEach((block, index) => {
|
|
1083
|
+
if (block.type === 'text')
|
|
1084
|
+
this.endTextBlock(queue, stats, index, block.text);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
for (const [index, block] of stats.textBlocks) {
|
|
1088
|
+
if (!block.ended)
|
|
1089
|
+
this.endTextBlock(queue, stats, index, block.text);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1000
1092
|
mapHarnessEvent(queue, event, stats, model, effort) {
|
|
1001
1093
|
if (event.type === 'message_update'
|
|
1002
1094
|
&& (event.assistantMessageEvent.type === 'thinking_start'
|
|
@@ -1005,14 +1097,38 @@ export class EcagentRunner {
|
|
|
1005
1097
|
queue.push({ type: 'heartbeat', source: 'reasoning', turn: stats.numTurns + 1 });
|
|
1006
1098
|
return;
|
|
1007
1099
|
}
|
|
1008
|
-
if (event.type === 'message_update'
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1100
|
+
if (event.type === 'message_update') {
|
|
1101
|
+
const update = event.assistantMessageEvent;
|
|
1102
|
+
if (update.type === 'text_start') {
|
|
1103
|
+
if (!stats.textBlocks.has(update.contentIndex)) {
|
|
1104
|
+
stats.textBlocks.set(update.contentIndex, { text: '', ended: false });
|
|
1105
|
+
queue.push({ type: 'text_start', contentIndex: update.contentIndex, turn: stats.numTurns + 1 });
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (update.type === 'text_delta') {
|
|
1110
|
+
const block = stats.textBlocks.get(update.contentIndex) ?? { text: '', ended: false };
|
|
1111
|
+
if (block.ended)
|
|
1112
|
+
return;
|
|
1113
|
+
if (!stats.firstTextAt)
|
|
1114
|
+
stats.firstTextAt = Date.now();
|
|
1115
|
+
block.text += update.delta;
|
|
1116
|
+
stats.textBlocks.set(update.contentIndex, block);
|
|
1117
|
+
// Keep the stream alive without exposing partial text to the renderer.
|
|
1118
|
+
queue.push({ type: 'heartbeat', source: 'text', turn: stats.numTurns + 1 });
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (update.type === 'text_end') {
|
|
1122
|
+
this.endTextBlock(queue, stats, update.contentIndex, update.content);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (event.type === 'message_end' && event.message.role === 'assistant') {
|
|
1127
|
+
this.finishTextBlocks(queue, stats, event.message);
|
|
1013
1128
|
return;
|
|
1014
1129
|
}
|
|
1015
1130
|
if (event.type === 'tool_execution_start') {
|
|
1131
|
+
this.finishTextBlocks(queue, stats);
|
|
1016
1132
|
queue.push({ type: 'tool_use', name: event.toolName, input: event.args, callId: event.toolCallId, turn: stats.numTurns + 1 });
|
|
1017
1133
|
return;
|
|
1018
1134
|
}
|
|
@@ -1025,6 +1141,8 @@ export class EcagentRunner {
|
|
|
1025
1141
|
return;
|
|
1026
1142
|
}
|
|
1027
1143
|
if (event.type === 'turn_end' && event.message.role === 'assistant') {
|
|
1144
|
+
this.finishTextBlocks(queue, stats, event.message);
|
|
1145
|
+
stats.textBlocks.clear();
|
|
1028
1146
|
stats.numTurns++;
|
|
1029
1147
|
const usage = event.message.usage;
|
|
1030
1148
|
stats.usage.input_tokens = Number(stats.usage.input_tokens ?? 0) + usage.input;
|
|
@@ -1150,13 +1268,36 @@ export class EcagentRunner {
|
|
|
1150
1268
|
this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
|
|
1151
1269
|
this.permissionGateway?.cancelAll(sessionKey, 'interrupted');
|
|
1152
1270
|
const harness = this.runtimes.get(sessionKey)?.harness;
|
|
1153
|
-
|
|
1154
|
-
|
|
1271
|
+
const runs = [...(this.sessionRuns.get(sessionKey) ?? [])];
|
|
1272
|
+
for (const run of runs) {
|
|
1273
|
+
if (run.cancelled)
|
|
1274
|
+
continue;
|
|
1275
|
+
run.cancelled = true;
|
|
1276
|
+
run.queue.push({ type: 'complete', isError: true, terminalReason: 'interrupted' });
|
|
1277
|
+
run.queue.end();
|
|
1278
|
+
}
|
|
1279
|
+
if (runs.length === 0 && !harness) {
|
|
1280
|
+
const stream = this.activeStreams.get(sessionKey);
|
|
1281
|
+
if (stream)
|
|
1282
|
+
this.pendingInterrupts.set(sessionKey, stream);
|
|
1155
1283
|
return;
|
|
1156
1284
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1285
|
+
// Ending the event stream releases the message queue, not the durable
|
|
1286
|
+
// session writer. Keep the run tail until all old code has actually exited.
|
|
1287
|
+
try {
|
|
1288
|
+
await withInterruptDeadline(Promise.all([
|
|
1289
|
+
harness?.abort(),
|
|
1290
|
+
...runs.map(run => run.done),
|
|
1291
|
+
]), 'ecagent');
|
|
1292
|
+
}
|
|
1293
|
+
catch (error) {
|
|
1294
|
+
logger.warn(`[EcagentRunner] Interrupted execution still requires quiescence: session=${sessionKey} error=${String(error)}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
assertInterruptSettled(sessionId) {
|
|
1298
|
+
if ([...(this.sessionRuns.get(sessionId) ?? [])].some(run => run.cancelled)) {
|
|
1299
|
+
throw unsettledInterruptError('ecagent');
|
|
1300
|
+
}
|
|
1160
1301
|
}
|
|
1161
1302
|
injectUserMessage(sessionId, text) {
|
|
1162
1303
|
const harness = this.runtimes.get(sessionId)?.harness;
|
|
@@ -1169,6 +1310,7 @@ export class EcagentRunner {
|
|
|
1169
1310
|
const runtime = this.runtimes.get(sessionId);
|
|
1170
1311
|
const stream = this.activeStreams.get(sessionId);
|
|
1171
1312
|
await this.interrupt(sessionId).catch(() => { });
|
|
1313
|
+
this.assertInterruptSettled(sessionId);
|
|
1172
1314
|
if (runtime && this.runtimes.get(sessionId) === runtime)
|
|
1173
1315
|
this.runtimes.delete(sessionId);
|
|
1174
1316
|
if (stream && this.activeStreams.get(sessionId) === stream) {
|
|
@@ -1183,6 +1325,7 @@ export class EcagentRunner {
|
|
|
1183
1325
|
try {
|
|
1184
1326
|
const currentRuntime = this.runtimes.get(sessionId);
|
|
1185
1327
|
await this.interrupt(sessionId);
|
|
1328
|
+
this.assertInterruptSettled(sessionId);
|
|
1186
1329
|
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
1187
1330
|
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1188
1331
|
const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
|
|
@@ -1328,6 +1471,8 @@ export class EcagentRunner {
|
|
|
1328
1471
|
async dispose() {
|
|
1329
1472
|
await Promise.allSettled([...this.runtimes.keys()].map(sessionId => this.closeSession(sessionId)));
|
|
1330
1473
|
this.runtimes.clear();
|
|
1474
|
+
this.sessionRuns.clear();
|
|
1475
|
+
this.sessionRunTails.clear();
|
|
1331
1476
|
this.activeStreams.clear();
|
|
1332
1477
|
this.pendingInterrupts.clear();
|
|
1333
1478
|
}
|
|
@@ -12,6 +12,7 @@ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js
|
|
|
12
12
|
* GeminiRunner → spawn `gemini -p ...` → stdout JSONL stream
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
|
+
import { MAIN_CLI_BIN } from '../product.js';
|
|
15
16
|
import { createInterface } from 'readline';
|
|
16
17
|
import fs from 'fs';
|
|
17
18
|
import path from 'path';
|
|
@@ -143,6 +144,10 @@ const GEMINI_EC_COMMAND_PATTERNS = [
|
|
|
143
144
|
String.raw `\x00"command":"[ ]*ec[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))*"\x00`,
|
|
144
145
|
];
|
|
145
146
|
const GEMINI_BOOTSTRAP_PERSONA_COMMAND_PATTERN = String.raw `^\{\x00"command":"[ ]*ec[ ]+agent[ ]+persona(?:[ ]+show|[ ]+--(?:text|file)[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))+)[ ]*"\x00\}$`;
|
|
147
|
+
// A single-quoted shell argument can safely carry multiline Markdown, backticks,
|
|
148
|
+
// dollar signs and semicolons. Outside that argument only --format json is legal.
|
|
149
|
+
const GEMINI_WELCOME_ESCAPED_QUOTE = escapeGeminiArgsRegex(JSON.stringify("'\\''").slice(1, -1));
|
|
150
|
+
const GEMINI_BOOTSTRAP_WELCOME_COMMAND_PATTERN = String.raw `^\{\x00"command":"[ ]*${escapeGeminiArgsRegex(MAIN_CLI_BIN)}[ ]+agent[ ]+welcome[ ]+(?:show|--text[ ]+'(?:[^'"\x00\\]|\\(?:\\|"|n|r|t)|${GEMINI_WELCOME_ESCAPED_QUOTE})*')(?:[ ]+--format[ ]+json)?[ ]*"\x00\}$`;
|
|
146
151
|
export function resolveGeminiPermissionProfile(value, bootstrap = false) {
|
|
147
152
|
if (value === 'fullaccess')
|
|
148
153
|
return { mode: 'fullaccess', approvalMode: 'yolo' };
|
|
@@ -212,6 +217,7 @@ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath, personalMd
|
|
|
212
217
|
appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec aid agentmd put ${bootstrap.aid}`));
|
|
213
218
|
appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec agent ready ${bootstrap.aid}`));
|
|
214
219
|
appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, GEMINI_BOOTSTRAP_PERSONA_COMMAND_PATTERN);
|
|
220
|
+
appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, GEMINI_BOOTSTRAP_WELCOME_COMMAND_PATTERN);
|
|
215
221
|
if (personalMdPath) {
|
|
216
222
|
const personalPathPattern = geminiManagedIdentityPathPattern(personalMdPath);
|
|
217
223
|
appendGeminiPolicyRule(lines, 'read_file', 'allow', 989, personalPathPattern);
|
|
@@ -13,6 +13,30 @@ export class BaseagentRunnerUnavailableError extends Error {
|
|
|
13
13
|
this.name = 'BaseagentRunnerUnavailableError';
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/** Codex's native asynchronous reply envelope used by its rich clients. */
|
|
17
|
+
export function formatAsyncQuestionAnswer(answer) {
|
|
18
|
+
return '<send_user_message_question_reply>' + JSON.stringify({
|
|
19
|
+
questionItemId: answer.questionItemId,
|
|
20
|
+
question: answer.question,
|
|
21
|
+
answer: answer.answer,
|
|
22
|
+
}) + '</send_user_message_question_reply>';
|
|
23
|
+
}
|
|
24
|
+
export function parseAsyncUserQuestions(value) {
|
|
25
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
|
|
26
|
+
throw new Error('Invalid asynchronous question list');
|
|
27
|
+
}
|
|
28
|
+
return value.map(question => {
|
|
29
|
+
if (!question || typeof question !== 'object' || typeof question.title !== 'string'
|
|
30
|
+
|| !question.title.trim() || question.title.length > 16_384) {
|
|
31
|
+
throw new Error('Invalid asynchronous question title');
|
|
32
|
+
}
|
|
33
|
+
if (question.options != null && (!Array.isArray(question.options) || !question.options.length
|
|
34
|
+
|| question.options.length > 20 || question.options.some((option) => typeof option !== 'string' || !option.trim() || option.length > 4_096))) {
|
|
35
|
+
throw new Error('Invalid asynchronous question options');
|
|
36
|
+
}
|
|
37
|
+
return { title: question.title, ...(question.options ? { options: [...question.options] } : {}) };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
16
40
|
export function hasTrustedFullAccessContext(context) {
|
|
17
41
|
const authorization = context?.executionPermission;
|
|
18
42
|
if (!authorization
|
|
@@ -20,8 +44,10 @@ export function hasTrustedFullAccessContext(context) {
|
|
|
20
44
|
|| authorization.processRole !== 'fullaccess-run'
|
|
21
45
|
|| authorization.dataScope !== 'daemon')
|
|
22
46
|
return false;
|
|
23
|
-
if (authorization.source === 'fullaccess-command') {
|
|
24
|
-
return typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
|
|
47
|
+
if (authorization.source === 'fullaccess-command' || authorization.source === 'agent-policy') {
|
|
48
|
+
return typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
|
|
49
|
+
&& (authorization.source !== 'agent-policy'
|
|
50
|
+
|| (typeof authorization.agentAid === 'string' && authorization.agentAid.length > 0));
|
|
25
51
|
}
|
|
26
52
|
return authorization.source === 'trigger'
|
|
27
53
|
&& typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
|
|
@@ -32,6 +58,28 @@ export function hasTrustedFullAccessContext(context) {
|
|
|
32
58
|
// being mistaken for a runnable privileged invocation.
|
|
33
59
|
&& typeof authorization.attemptId === 'string' && authorization.attemptId.length > 0;
|
|
34
60
|
}
|
|
61
|
+
/** Bounds waiting only; the caller must retain ownership until execution stops. */
|
|
62
|
+
export async function withInterruptDeadline(operation, label, timeoutMs = 5_000) {
|
|
63
|
+
let timer;
|
|
64
|
+
try {
|
|
65
|
+
return await Promise.race([
|
|
66
|
+
operation,
|
|
67
|
+
new Promise((_, reject) => {
|
|
68
|
+
timer = setTimeout(() => reject(new Error(`${label}: interrupt deadline exceeded`)), timeoutMs);
|
|
69
|
+
timer.unref?.();
|
|
70
|
+
}),
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (timer)
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function unsettledInterruptError(runner) {
|
|
79
|
+
return Object.assign(new Error(`${runner} 的上一次执行尚未确认停止,本次任务未启动;旧执行结束后可在原会话继续。`), {
|
|
80
|
+
code: 'RUNNER_INTERRUPT_UNCONFIRMED',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
35
83
|
// ── 类型守卫 ──
|
|
36
84
|
export function hasModelSwitcher(agent) {
|
|
37
85
|
return typeof agent.setModel === 'function' && typeof agent.listModels === 'function';
|