evolcore 0.0.4 → 0.0.6
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 +65 -3
- package/README.md +4 -12
- package/dist/agents/baseagent.js +39 -0
- package/dist/agents/claude-runner.js +10 -2
- package/dist/agents/codex-runner.js +37 -8
- package/dist/agents/ecagent-runner.js +1159 -0
- package/dist/agents/gemini-runner.js +4 -1
- package/dist/agents/runner-types.js +20 -4
- package/dist/aun/msg/history.js +32 -9
- package/dist/aun/outbox.js +4 -9
- package/dist/channels/aun.js +32 -6
- package/dist/channels/daemon.js +7 -4
- package/dist/channels/dingtalk.js +3 -5
- package/dist/channels/feishu.js +16 -108
- package/dist/channels/wechat.js +4 -4
- package/dist/channels/wecom.js +4 -6
- package/dist/cli/agent-command.js +1 -1
- package/dist/cli/agent.js +42 -11
- package/dist/cli/aun-commands.js +50 -3
- package/dist/cli/daemon-commands.js +127 -21
- package/dist/cli/data-command.js +132 -0
- package/dist/cli/index.js +11 -3
- package/dist/cli/init.js +116 -37
- package/dist/cli/net-check.js +9 -20
- package/dist/cli/restart-monitor.js +11 -2
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-msg.js +12 -25
- package/dist/config/access-policy-domain.js +0 -20
- package/dist/config/access-policy.js +5 -29
- package/dist/config/builtin-roles.js +3 -1
- package/dist/config/config-field-policy.js +1 -1
- package/dist/config/gateway-config.js +21 -4
- package/dist/config/resolved-config-op.js +10 -0
- package/dist/core/auth/operation-catalog.js +1 -1
- package/dist/core/command/command-handler.js +2 -2
- package/dist/core/command/connect-menu.js +20 -42
- package/dist/core/command/menu-handler.js +11 -9
- package/dist/core/command/role-menu.js +27 -0
- package/dist/core/command/slash-handler.js +18 -14
- package/dist/core/data-migration.js +1434 -0
- package/dist/core/handoff/store.js +14 -4
- package/dist/core/inference/text-inference.js +42 -1
- package/dist/core/message/inbound-admission.js +23 -13
- package/dist/core/message/pending-hints.js +1 -1
- package/dist/core/message/response-engine.js +406 -167
- package/dist/core/message/retry-scheduler.js +71 -0
- package/dist/core/model/model-catalog.js +7 -2
- package/dist/core/model/model-fallback.js +81 -0
- package/dist/core/protected-paths.js +41 -8
- package/dist/core/session/adapters/ecagent-session-file-adapter.js +112 -0
- package/dist/core/session/session-fs-store.js +300 -61
- package/dist/core/session/session-manager.js +225 -21
- package/dist/core/session/session-title.js +1 -0
- package/dist/core/session/session-turn-coordinator.js +8 -1
- package/dist/core/system-channels.js +1 -0
- package/dist/index.js +10 -9
- package/dist/paths.js +35 -27
- package/dist/trigger/feedback.js +16 -0
- package/dist/trigger/legacy-session-history.js +19 -0
- package/dist/trigger/manager.js +12 -1
- package/dist/trigger/parser.js +4 -2
- package/dist/trigger/scheduler.js +185 -15
- package/dist/trigger/session-lock.js +62 -0
- package/dist/trigger/state.js +64 -0
- package/dist/trigger/validation.js +32 -2
- package/dist/utils/aid-bind.js +1 -1
- package/dist/utils/cross-platform.js +8 -2
- package/dist/utils/error-utils.js +9 -0
- package/dist/utils/process-introspect.js +19 -3
- package/dist/utils/welcome.js +2 -3
- package/ecagent/LICENSE +21 -0
- package/ecagent/NOTICE +11 -0
- package/ecagent/README.md +61 -0
- package/ecagent/dist/agent-loop.d.ts +24 -0
- package/ecagent/dist/agent-loop.d.ts.map +1 -0
- package/ecagent/dist/agent-loop.js +547 -0
- package/ecagent/dist/agent-loop.js.map +1 -0
- package/ecagent/dist/agent.d.ts +127 -0
- package/ecagent/dist/agent.d.ts.map +1 -0
- package/ecagent/dist/agent.js +386 -0
- package/ecagent/dist/agent.js.map +1 -0
- package/ecagent/dist/harness/agent-harness.d.ts +95 -0
- package/ecagent/dist/harness/agent-harness.d.ts.map +1 -0
- package/ecagent/dist/harness/agent-harness.js +926 -0
- package/ecagent/dist/harness/agent-harness.js.map +1 -0
- package/ecagent/dist/harness/compaction/branch-summarization.d.ts +51 -0
- package/ecagent/dist/harness/compaction/branch-summarization.d.ts.map +1 -0
- package/ecagent/dist/harness/compaction/branch-summarization.js +174 -0
- package/ecagent/dist/harness/compaction/branch-summarization.js.map +1 -0
- package/ecagent/dist/harness/compaction/compaction.d.ts +95 -0
- package/ecagent/dist/harness/compaction/compaction.d.ts.map +1 -0
- package/ecagent/dist/harness/compaction/compaction.js +528 -0
- package/ecagent/dist/harness/compaction/compaction.js.map +1 -0
- package/ecagent/dist/harness/compaction/utils.d.ts +25 -0
- package/ecagent/dist/harness/compaction/utils.d.ts.map +1 -0
- package/ecagent/dist/harness/compaction/utils.js +131 -0
- package/ecagent/dist/harness/compaction/utils.js.map +1 -0
- package/ecagent/dist/harness/env/nodejs.d.ts +51 -0
- package/ecagent/dist/harness/env/nodejs.d.ts.map +1 -0
- package/ecagent/dist/harness/env/nodejs.js +513 -0
- package/ecagent/dist/harness/env/nodejs.js.map +1 -0
- package/ecagent/dist/harness/messages.d.ts +51 -0
- package/ecagent/dist/harness/messages.d.ts.map +1 -0
- package/ecagent/dist/harness/messages.js +102 -0
- package/ecagent/dist/harness/messages.js.map +1 -0
- package/ecagent/dist/harness/prompt-templates.d.ts +48 -0
- package/ecagent/dist/harness/prompt-templates.d.ts.map +1 -0
- package/ecagent/dist/harness/prompt-templates.js +230 -0
- package/ecagent/dist/harness/prompt-templates.js.map +1 -0
- package/ecagent/dist/harness/session/jsonl-repo.d.ts +26 -0
- package/ecagent/dist/harness/session/jsonl-repo.d.ts.map +1 -0
- package/ecagent/dist/harness/session/jsonl-repo.js +100 -0
- package/ecagent/dist/harness/session/jsonl-repo.js.map +1 -0
- package/ecagent/dist/harness/session/jsonl-storage.d.ts +34 -0
- package/ecagent/dist/harness/session/jsonl-storage.d.ts.map +1 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +234 -0
- package/ecagent/dist/harness/session/jsonl-storage.js.map +1 -0
- package/ecagent/dist/harness/session/memory-repo.d.ts +18 -0
- package/ecagent/dist/harness/session/memory-repo.d.ts.map +1 -0
- package/ecagent/dist/harness/session/memory-repo.js +44 -0
- package/ecagent/dist/harness/session/memory-repo.js.map +1 -0
- package/ecagent/dist/harness/session/memory-storage.d.ts +25 -0
- package/ecagent/dist/harness/session/memory-storage.d.ts.map +1 -0
- package/ecagent/dist/harness/session/memory-storage.js +111 -0
- package/ecagent/dist/harness/session/memory-storage.js.map +1 -0
- package/ecagent/dist/harness/session/repo-utils.d.ts +11 -0
- package/ecagent/dist/harness/session/repo-utils.d.ts.map +1 -0
- package/ecagent/dist/harness/session/repo-utils.js +39 -0
- package/ecagent/dist/harness/session/repo-utils.js.map +1 -0
- package/ecagent/dist/harness/session/session.d.ts +47 -0
- package/ecagent/dist/harness/session/session.d.ts.map +1 -0
- package/ecagent/dist/harness/session/session.js +245 -0
- package/ecagent/dist/harness/session/session.js.map +1 -0
- package/ecagent/dist/harness/session/uuid.d.ts +2 -0
- package/ecagent/dist/harness/session/uuid.d.ts.map +1 -0
- package/ecagent/dist/harness/session/uuid.js +50 -0
- package/ecagent/dist/harness/session/uuid.js.map +1 -0
- package/ecagent/dist/harness/skills.d.ts +44 -0
- package/ecagent/dist/harness/skills.d.ts.map +1 -0
- package/ecagent/dist/harness/skills.js +311 -0
- package/ecagent/dist/harness/skills.js.map +1 -0
- package/ecagent/dist/harness/system-prompt.d.ts +3 -0
- package/ecagent/dist/harness/system-prompt.d.ts.map +1 -0
- package/ecagent/dist/harness/system-prompt.js +30 -0
- package/ecagent/dist/harness/system-prompt.js.map +1 -0
- package/ecagent/dist/harness/types.d.ts +619 -0
- package/ecagent/dist/harness/types.d.ts.map +1 -0
- package/ecagent/dist/harness/types.js +81 -0
- package/ecagent/dist/harness/types.js.map +1 -0
- package/ecagent/dist/harness/utils/shell-output.d.ts +14 -0
- package/ecagent/dist/harness/utils/shell-output.d.ts.map +1 -0
- package/ecagent/dist/harness/utils/shell-output.js +126 -0
- package/ecagent/dist/harness/utils/shell-output.js.map +1 -0
- package/ecagent/dist/harness/utils/truncate.d.ts +70 -0
- package/ecagent/dist/harness/utils/truncate.d.ts.map +1 -0
- package/ecagent/dist/harness/utils/truncate.js +290 -0
- package/ecagent/dist/harness/utils/truncate.js.map +1 -0
- package/ecagent/dist/index.d.ts +22 -0
- package/ecagent/dist/index.d.ts.map +1 -0
- package/ecagent/dist/index.js +27 -0
- package/ecagent/dist/index.js.map +1 -0
- package/ecagent/dist/node.d.ts +3 -0
- package/ecagent/dist/node.d.ts.map +1 -0
- package/ecagent/dist/node.js +3 -0
- package/ecagent/dist/node.js.map +1 -0
- package/ecagent/dist/proxy.d.ts +69 -0
- package/ecagent/dist/proxy.d.ts.map +1 -0
- package/ecagent/dist/proxy.js +278 -0
- package/ecagent/dist/proxy.js.map +1 -0
- package/ecagent/dist/runtime/api/lazy.d.ts +15 -0
- package/ecagent/dist/runtime/api/lazy.d.ts.map +1 -0
- package/ecagent/dist/runtime/api/lazy.js +59 -0
- package/ecagent/dist/runtime/api/lazy.js.map +1 -0
- package/ecagent/dist/runtime/api/pi-messages.d.ts +97 -0
- package/ecagent/dist/runtime/api/pi-messages.d.ts.map +1 -0
- package/ecagent/dist/runtime/api/pi-messages.js +307 -0
- package/ecagent/dist/runtime/api/pi-messages.js.map +1 -0
- package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts +3 -0
- package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts.map +1 -0
- package/ecagent/dist/runtime/api/pi-messages.lazy.js +3 -0
- package/ecagent/dist/runtime/api/pi-messages.lazy.js.map +1 -0
- package/ecagent/dist/runtime/auth/context.d.ts +7 -0
- package/ecagent/dist/runtime/auth/context.d.ts.map +1 -0
- package/ecagent/dist/runtime/auth/context.js +42 -0
- package/ecagent/dist/runtime/auth/context.js.map +1 -0
- package/ecagent/dist/runtime/auth/credential-store.d.ts +16 -0
- package/ecagent/dist/runtime/auth/credential-store.d.ts.map +1 -0
- package/ecagent/dist/runtime/auth/credential-store.js +39 -0
- package/ecagent/dist/runtime/auth/credential-store.js.map +1 -0
- package/ecagent/dist/runtime/auth/helpers.d.ts +20 -0
- package/ecagent/dist/runtime/auth/helpers.d.ts.map +1 -0
- package/ecagent/dist/runtime/auth/helpers.js +46 -0
- package/ecagent/dist/runtime/auth/helpers.js.map +1 -0
- package/ecagent/dist/runtime/auth/resolve.d.ts +26 -0
- package/ecagent/dist/runtime/auth/resolve.d.ts.map +1 -0
- package/ecagent/dist/runtime/auth/resolve.js +100 -0
- package/ecagent/dist/runtime/auth/resolve.js.map +1 -0
- package/ecagent/dist/runtime/auth/types.d.ts +180 -0
- package/ecagent/dist/runtime/auth/types.d.ts.map +1 -0
- package/ecagent/dist/runtime/auth/types.js +2 -0
- package/ecagent/dist/runtime/auth/types.js.map +1 -0
- package/ecagent/dist/runtime/compat.d.ts +28 -0
- package/ecagent/dist/runtime/compat.d.ts.map +1 -0
- package/ecagent/dist/runtime/compat.js +86 -0
- package/ecagent/dist/runtime/compat.js.map +1 -0
- package/ecagent/dist/runtime/index.d.ts +20 -0
- package/ecagent/dist/runtime/index.d.ts.map +1 -0
- package/ecagent/dist/runtime/index.js +17 -0
- package/ecagent/dist/runtime/index.js.map +1 -0
- package/ecagent/dist/runtime/models.d.ts +142 -0
- package/ecagent/dist/runtime/models.d.ts.map +1 -0
- package/ecagent/dist/runtime/models.js +245 -0
- package/ecagent/dist/runtime/models.js.map +1 -0
- package/ecagent/dist/runtime/oauth.d.ts +2 -0
- package/ecagent/dist/runtime/oauth.d.ts.map +1 -0
- package/ecagent/dist/runtime/oauth.js +2 -0
- package/ecagent/dist/runtime/oauth.js.map +1 -0
- package/ecagent/dist/runtime/providers/faux.d.ts +97 -0
- package/ecagent/dist/runtime/providers/faux.d.ts.map +1 -0
- package/ecagent/dist/runtime/providers/faux.js +395 -0
- package/ecagent/dist/runtime/providers/faux.js.map +1 -0
- package/ecagent/dist/runtime/types.d.ts +607 -0
- package/ecagent/dist/runtime/types.d.ts.map +1 -0
- package/ecagent/dist/runtime/types.js +2 -0
- package/ecagent/dist/runtime/types.js.map +1 -0
- package/ecagent/dist/runtime/utils/diagnostics.d.ts +19 -0
- package/ecagent/dist/runtime/utils/diagnostics.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/diagnostics.js +25 -0
- package/ecagent/dist/runtime/utils/diagnostics.js.map +1 -0
- package/ecagent/dist/runtime/utils/event-stream.d.ts +21 -0
- package/ecagent/dist/runtime/utils/event-stream.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/event-stream.js +77 -0
- package/ecagent/dist/runtime/utils/event-stream.js.map +1 -0
- package/ecagent/dist/runtime/utils/headers.d.ts +4 -0
- package/ecagent/dist/runtime/utils/headers.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/headers.js +18 -0
- package/ecagent/dist/runtime/utils/headers.js.map +1 -0
- package/ecagent/dist/runtime/utils/json-parse.d.ts +16 -0
- package/ecagent/dist/runtime/utils/json-parse.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/json-parse.js +113 -0
- package/ecagent/dist/runtime/utils/json-parse.js.map +1 -0
- package/ecagent/dist/runtime/utils/oauth/types.d.ts +64 -0
- package/ecagent/dist/runtime/utils/oauth/types.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/oauth/types.js +2 -0
- package/ecagent/dist/runtime/utils/oauth/types.js.map +1 -0
- package/ecagent/dist/runtime/utils/overflow.d.ts +58 -0
- package/ecagent/dist/runtime/utils/overflow.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/overflow.js +158 -0
- package/ecagent/dist/runtime/utils/overflow.js.map +1 -0
- package/ecagent/dist/runtime/utils/provider-env.d.ts +7 -0
- package/ecagent/dist/runtime/utils/provider-env.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/provider-env.js +44 -0
- package/ecagent/dist/runtime/utils/provider-env.js.map +1 -0
- package/ecagent/dist/runtime/utils/retry.d.ts +12 -0
- package/ecagent/dist/runtime/utils/retry.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/retry.js +90 -0
- package/ecagent/dist/runtime/utils/retry.js.map +1 -0
- package/ecagent/dist/runtime/utils/typebox-helpers.d.ts +17 -0
- package/ecagent/dist/runtime/utils/typebox-helpers.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/typebox-helpers.js +21 -0
- package/ecagent/dist/runtime/utils/typebox-helpers.js.map +1 -0
- package/ecagent/dist/runtime/utils/validation.d.ts +18 -0
- package/ecagent/dist/runtime/utils/validation.d.ts.map +1 -0
- package/ecagent/dist/runtime/utils/validation.js +269 -0
- package/ecagent/dist/runtime/utils/validation.js.map +1 -0
- package/ecagent/dist/types.d.ts +401 -0
- package/ecagent/dist/types.d.ts.map +1 -0
- package/ecagent/dist/types.js +2 -0
- package/ecagent/dist/types.js.map +1 -0
- package/ecagent/package.json +93 -0
- package/ecagent/src/agent-loop.ts +792 -0
- package/ecagent/src/agent.ts +575 -0
- package/ecagent/src/harness/agent-harness.ts +1029 -0
- package/ecagent/src/harness/compaction/branch-summarization.ts +261 -0
- package/ecagent/src/harness/compaction/compaction.ts +753 -0
- package/ecagent/src/harness/compaction/utils.ts +144 -0
- package/ecagent/src/harness/env/nodejs.ts +569 -0
- package/ecagent/src/harness/messages.ts +164 -0
- package/ecagent/src/harness/prompt-templates.ts +267 -0
- package/ecagent/src/harness/session/jsonl-repo.ts +179 -0
- package/ecagent/src/harness/session/jsonl-storage.ts +314 -0
- package/ecagent/src/harness/session/memory-repo.ts +50 -0
- package/ecagent/src/harness/session/memory-storage.ts +133 -0
- package/ecagent/src/harness/session/repo-utils.ts +51 -0
- package/ecagent/src/harness/session/session.ts +338 -0
- package/ecagent/src/harness/session/uuid.ts +54 -0
- package/ecagent/src/harness/skills.ts +375 -0
- package/ecagent/src/harness/system-prompt.ts +34 -0
- package/ecagent/src/harness/types.ts +838 -0
- package/ecagent/src/harness/utils/shell-output.ts +135 -0
- package/ecagent/src/harness/utils/truncate.ts +344 -0
- package/ecagent/src/index.ts +46 -0
- package/ecagent/src/node.ts +2 -0
- package/ecagent/src/proxy.ts +367 -0
- package/ecagent/src/runtime/api/lazy.ts +70 -0
- package/ecagent/src/runtime/api/pi-messages.lazy.ts +4 -0
- package/ecagent/src/runtime/api/pi-messages.ts +436 -0
- package/ecagent/src/runtime/auth/context.ts +45 -0
- package/ecagent/src/runtime/auth/credential-store.ts +47 -0
- package/ecagent/src/runtime/auth/helpers.ts +46 -0
- package/ecagent/src/runtime/auth/resolve.ts +141 -0
- package/ecagent/src/runtime/auth/types.ts +182 -0
- package/ecagent/src/runtime/compat.ts +158 -0
- package/ecagent/src/runtime/index.ts +31 -0
- package/ecagent/src/runtime/models.ts +452 -0
- package/ecagent/src/runtime/oauth.ts +1 -0
- package/ecagent/src/runtime/providers/faux.ts +538 -0
- package/ecagent/src/runtime/types.ts +718 -0
- package/ecagent/src/runtime/utils/diagnostics.ts +45 -0
- package/ecagent/src/runtime/utils/event-stream.ts +88 -0
- package/ecagent/src/runtime/utils/headers.ts +18 -0
- package/ecagent/src/runtime/utils/json-parse.ts +124 -0
- package/ecagent/src/runtime/utils/oauth/types.ts +79 -0
- package/ecagent/src/runtime/utils/overflow.ts +165 -0
- package/ecagent/src/runtime/utils/provider-env.ts +52 -0
- package/ecagent/src/runtime/utils/retry.ts +101 -0
- package/ecagent/src/runtime/utils/typebox-helpers.ts +24 -0
- package/ecagent/src/runtime/utils/validation.ts +310 -0
- package/ecagent/src/types.ts +430 -0
- package/ecagent/tsconfig.build.json +32 -0
- package/kits/docs/evolcore/config.md +4 -4
- package/kits/docs/evolcore/contact.md +2 -1
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.10.json +370 -0
- package/kits/schemas/agent-config.schema.9.json +2 -12
- package/kits/schemas/defaults.schema.4.json +93 -0
- package/kits/schemas/relation-config.schema.8.json +69 -0
- package/kits/templates/roles/admin.json +1 -1
- package/package.json +22 -6
- package/skills/eclink/SKILL.md +70 -0
- package/skills/eclink/agents/openai.yaml +4 -0
- package/assets/.env.template +0 -4
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { AgentHarness, JsonlSessionRepo, NodeExecutionEnv, } from '../../ecagent/dist/node.js';
|
|
4
|
+
import { Type, createModels, createProvider, } from '../../ecagent/dist/runtime/index.js';
|
|
5
|
+
import { AssistantMessageEventStream } from '../../ecagent/dist/runtime/utils/event-stream.js';
|
|
6
|
+
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
7
|
+
import { agentSessionsDir, resolveRoot } from '../paths.js';
|
|
8
|
+
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
9
|
+
import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
10
|
+
import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import { resolveEcagentConfig } from './baseagent.js';
|
|
13
|
+
const PROVIDER_ID = 'evolcore-gateway';
|
|
14
|
+
const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
|
|
15
|
+
Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
|
|
16
|
+
Use the provided tools for filesystem and shell work. Never claim a tool action succeeded unless its result confirms success.`;
|
|
17
|
+
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
18
|
+
const MODEL_LIST_TIMEOUT_MS = 8_000;
|
|
19
|
+
const MAX_READ_BYTES = 1024 * 1024;
|
|
20
|
+
const MAX_TOOL_OUTPUT = 200_000;
|
|
21
|
+
const MAX_SCAN_FILES = 5_000;
|
|
22
|
+
const MAX_SEARCH_RESULTS = 500;
|
|
23
|
+
const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
|
24
|
+
class AsyncEventQueue {
|
|
25
|
+
values = [];
|
|
26
|
+
waiters = [];
|
|
27
|
+
ended = false;
|
|
28
|
+
push(value) {
|
|
29
|
+
if (this.ended)
|
|
30
|
+
return;
|
|
31
|
+
const waiter = this.waiters.shift();
|
|
32
|
+
if (waiter)
|
|
33
|
+
waiter({ value, done: false });
|
|
34
|
+
else
|
|
35
|
+
this.values.push(value);
|
|
36
|
+
}
|
|
37
|
+
end() {
|
|
38
|
+
if (this.ended)
|
|
39
|
+
return;
|
|
40
|
+
this.ended = true;
|
|
41
|
+
for (const waiter of this.waiters.splice(0))
|
|
42
|
+
waiter({ value: undefined, done: true });
|
|
43
|
+
}
|
|
44
|
+
async *[Symbol.asyncIterator]() {
|
|
45
|
+
while (true) {
|
|
46
|
+
if (this.values.length > 0) {
|
|
47
|
+
yield this.values.shift();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (this.ended)
|
|
51
|
+
return;
|
|
52
|
+
const next = await new Promise(resolve => this.waiters.push(resolve));
|
|
53
|
+
if (next.done)
|
|
54
|
+
return;
|
|
55
|
+
yield next.value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function emptyUsage() {
|
|
60
|
+
return {
|
|
61
|
+
input: 0,
|
|
62
|
+
output: 0,
|
|
63
|
+
cacheRead: 0,
|
|
64
|
+
cacheWrite: 0,
|
|
65
|
+
totalTokens: 0,
|
|
66
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function createAssistantMessage(model) {
|
|
70
|
+
return {
|
|
71
|
+
role: 'assistant',
|
|
72
|
+
content: [],
|
|
73
|
+
api: model.api,
|
|
74
|
+
provider: model.provider,
|
|
75
|
+
model: model.id,
|
|
76
|
+
usage: emptyUsage(),
|
|
77
|
+
stopReason: 'stop',
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function clonePartial(message) {
|
|
82
|
+
return { ...message, content: message.content.map(block => ({ ...block })) };
|
|
83
|
+
}
|
|
84
|
+
function errorMessage(model, error, aborted) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
return {
|
|
87
|
+
...createAssistantMessage(model),
|
|
88
|
+
stopReason: aborted ? 'aborted' : 'error',
|
|
89
|
+
errorMessage: message,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function truncate(value, max = MAX_TOOL_OUTPUT) {
|
|
93
|
+
if (value.length <= max)
|
|
94
|
+
return value;
|
|
95
|
+
return `${value.slice(0, max)}\n... [truncated ${value.length - max} characters]`;
|
|
96
|
+
}
|
|
97
|
+
function responseHeaders(headers) {
|
|
98
|
+
return Object.fromEntries(headers.entries());
|
|
99
|
+
}
|
|
100
|
+
function toOpenAiContent(content) {
|
|
101
|
+
if (typeof content === 'string')
|
|
102
|
+
return content;
|
|
103
|
+
if (!Array.isArray(content))
|
|
104
|
+
return String(content ?? '');
|
|
105
|
+
const output = [];
|
|
106
|
+
for (const block of content) {
|
|
107
|
+
if (block?.type === 'text')
|
|
108
|
+
output.push({ type: 'text', text: block.text ?? '' });
|
|
109
|
+
if (block?.type === 'image') {
|
|
110
|
+
output.push({
|
|
111
|
+
type: 'image_url',
|
|
112
|
+
image_url: { url: `data:${block.mimeType || 'image/png'};base64,${block.data}` },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return output;
|
|
117
|
+
}
|
|
118
|
+
function openAiMessages(context) {
|
|
119
|
+
const messages = [];
|
|
120
|
+
if (context.systemPrompt)
|
|
121
|
+
messages.push({ role: 'system', content: context.systemPrompt });
|
|
122
|
+
for (const message of context.messages) {
|
|
123
|
+
if (message.role === 'user') {
|
|
124
|
+
messages.push({ role: 'user', content: toOpenAiContent(message.content) });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (message.role === 'assistant') {
|
|
128
|
+
const text = message.content
|
|
129
|
+
.filter((block) => block.type === 'text')
|
|
130
|
+
.map(block => block.text)
|
|
131
|
+
.join('');
|
|
132
|
+
const thinking = message.content
|
|
133
|
+
.filter((block) => block.type === 'thinking')
|
|
134
|
+
.map((block) => block.thinking)
|
|
135
|
+
.join('');
|
|
136
|
+
const toolCalls = message.content
|
|
137
|
+
.filter((block) => block.type === 'toolCall')
|
|
138
|
+
.map((block) => ({
|
|
139
|
+
id: block.id,
|
|
140
|
+
type: 'function',
|
|
141
|
+
function: { name: block.name, arguments: JSON.stringify(block.arguments ?? {}) },
|
|
142
|
+
}));
|
|
143
|
+
messages.push({
|
|
144
|
+
role: 'assistant',
|
|
145
|
+
content: text || null,
|
|
146
|
+
...(thinking ? { reasoning_content: thinking } : {}),
|
|
147
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const content = message.content
|
|
152
|
+
.filter((block) => block.type === 'text')
|
|
153
|
+
.map(block => block.text)
|
|
154
|
+
.join('\n');
|
|
155
|
+
messages.push({ role: 'tool', tool_call_id: message.toolCallId, name: message.toolName, content });
|
|
156
|
+
}
|
|
157
|
+
return messages;
|
|
158
|
+
}
|
|
159
|
+
async function* readSseEvents(body) {
|
|
160
|
+
const reader = body.getReader();
|
|
161
|
+
const decoder = new TextDecoder();
|
|
162
|
+
let buffer = '';
|
|
163
|
+
try {
|
|
164
|
+
while (true) {
|
|
165
|
+
const { done, value } = await reader.read();
|
|
166
|
+
buffer += decoder.decode(value, { stream: !done }).replace(/\r\n/g, '\n');
|
|
167
|
+
let boundary;
|
|
168
|
+
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
|
169
|
+
const block = buffer.slice(0, boundary);
|
|
170
|
+
buffer = buffer.slice(boundary + 2);
|
|
171
|
+
const data = block.split('\n')
|
|
172
|
+
.filter(line => line.startsWith('data:'))
|
|
173
|
+
.map(line => line.slice(5).trimStart())
|
|
174
|
+
.join('\n');
|
|
175
|
+
if (data)
|
|
176
|
+
yield data;
|
|
177
|
+
}
|
|
178
|
+
if (done)
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
const data = buffer.split('\n')
|
|
182
|
+
.filter(line => line.startsWith('data:'))
|
|
183
|
+
.map(line => line.slice(5).trimStart())
|
|
184
|
+
.join('\n');
|
|
185
|
+
if (data)
|
|
186
|
+
yield data;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
reader.releaseLock();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function retryDelay(response, attempt, cap) {
|
|
193
|
+
const retryAfter = response?.headers.get('retry-after');
|
|
194
|
+
const requested = retryAfter && Number.isFinite(Number(retryAfter)) ? Number(retryAfter) * 1000 : undefined;
|
|
195
|
+
const exponential = Math.min(250 * (2 ** attempt), cap || 4_000);
|
|
196
|
+
return Math.max(0, Math.min(requested ?? exponential, cap || exponential));
|
|
197
|
+
}
|
|
198
|
+
function delay(ms, signal) {
|
|
199
|
+
if (ms <= 0)
|
|
200
|
+
return Promise.resolve();
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
let timer;
|
|
203
|
+
const cleanup = () => signal?.removeEventListener('abort', abort);
|
|
204
|
+
const complete = () => { cleanup(); resolve(); };
|
|
205
|
+
timer = setTimeout(complete, ms);
|
|
206
|
+
const abort = () => {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
cleanup();
|
|
209
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('Request aborted'));
|
|
210
|
+
};
|
|
211
|
+
if (signal?.aborted)
|
|
212
|
+
abort();
|
|
213
|
+
else
|
|
214
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function createOpenAiTransport(config) {
|
|
218
|
+
const stream = (model, context, options) => {
|
|
219
|
+
const output = new AssistantMessageEventStream();
|
|
220
|
+
void (async () => {
|
|
221
|
+
const hasToolResults = context.messages.some(message => message.role === 'toolResult');
|
|
222
|
+
const attempts = hasToolResults ? 1 : Math.max(1, config.maxRetries + 1);
|
|
223
|
+
let lastError;
|
|
224
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
225
|
+
let response;
|
|
226
|
+
let retryScheduled = false;
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const onAbort = () => controller.abort(options?.signal?.reason);
|
|
229
|
+
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
230
|
+
const timeout = setTimeout(() => controller.abort(new Error(`Gateway request timed out after ${config.timeoutMs}ms`)), config.timeoutMs);
|
|
231
|
+
try {
|
|
232
|
+
let payload = {
|
|
233
|
+
model: model.id,
|
|
234
|
+
messages: openAiMessages(context),
|
|
235
|
+
tools: context.tools?.map(tool => ({
|
|
236
|
+
type: 'function',
|
|
237
|
+
function: { name: tool.name, description: tool.description, parameters: tool.parameters },
|
|
238
|
+
})),
|
|
239
|
+
tool_choice: context.tools?.length ? 'auto' : undefined,
|
|
240
|
+
stream: true,
|
|
241
|
+
stream_options: { include_usage: true },
|
|
242
|
+
max_tokens: options?.maxTokens ?? model.maxTokens,
|
|
243
|
+
...(options?.reasoning ? { reasoning_effort: options.reasoning } : {}),
|
|
244
|
+
};
|
|
245
|
+
const replacement = await options?.onPayload?.(payload, model);
|
|
246
|
+
if (replacement !== undefined)
|
|
247
|
+
payload = replacement;
|
|
248
|
+
response = await fetch(`${model.baseUrl.replace(/\/+$/, '')}/chat/completions`, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: {
|
|
251
|
+
accept: 'text/event-stream',
|
|
252
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
253
|
+
'content-type': 'application/json',
|
|
254
|
+
...Object.fromEntries(Object.entries(options?.headers ?? {}).filter((entry) => entry[1] !== null)),
|
|
255
|
+
},
|
|
256
|
+
body: JSON.stringify(payload),
|
|
257
|
+
signal: controller.signal,
|
|
258
|
+
});
|
|
259
|
+
await options?.onResponse?.({ status: response.status, headers: responseHeaders(response.headers) }, model);
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const body = truncate(await response.text(), 2_000);
|
|
262
|
+
const kind = response.status === 401 || response.status === 403 ? 'auth' : 'network';
|
|
263
|
+
const error = new Error(`[${kind}] Gateway HTTP ${response.status}: ${body || response.statusText}`);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (!response.body)
|
|
267
|
+
throw new Error('[network] Gateway response has no body');
|
|
268
|
+
await consumeOpenAiStream(output, response.body, model);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
controller.abort();
|
|
273
|
+
lastError = error;
|
|
274
|
+
const aborted = options?.signal?.aborted === true;
|
|
275
|
+
const status = response?.status;
|
|
276
|
+
const retryableHttp = response === undefined || status === 429 || (status !== undefined && status >= 500);
|
|
277
|
+
const retryable = !aborted
|
|
278
|
+
&& retryableHttp
|
|
279
|
+
&& !(error instanceof Error && /\[auth\]/.test(error.message));
|
|
280
|
+
if (retryable && attempt + 1 < attempts) {
|
|
281
|
+
retryScheduled = true;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const message = errorMessage(model, error, aborted);
|
|
285
|
+
output.push({ type: 'error', reason: aborted ? 'aborted' : 'error', error: message });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
clearTimeout(timeout);
|
|
291
|
+
options?.signal?.removeEventListener('abort', onAbort);
|
|
292
|
+
}
|
|
293
|
+
if (retryScheduled) {
|
|
294
|
+
try {
|
|
295
|
+
await delay(retryDelay(response, attempt, config.maxRetryDelayMs), options?.signal);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const aborted = options?.signal?.aborted === true;
|
|
299
|
+
const message = errorMessage(model, error, aborted);
|
|
300
|
+
output.push({ type: 'error', reason: aborted ? 'aborted' : 'error', error: message });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const message = errorMessage(model, lastError ?? new Error('Gateway request failed'), false);
|
|
306
|
+
output.push({ type: 'error', reason: 'error', error: message });
|
|
307
|
+
})();
|
|
308
|
+
return output;
|
|
309
|
+
};
|
|
310
|
+
return { stream, streamSimple: stream };
|
|
311
|
+
}
|
|
312
|
+
async function consumeOpenAiStream(output, body, model) {
|
|
313
|
+
const message = createAssistantMessage(model);
|
|
314
|
+
const toolCalls = new Map();
|
|
315
|
+
let textIndex;
|
|
316
|
+
let thinkingIndex;
|
|
317
|
+
let finishReason;
|
|
318
|
+
let responseId;
|
|
319
|
+
let responseModel;
|
|
320
|
+
output.push({ type: 'start', partial: clonePartial(message) });
|
|
321
|
+
const finish = () => {
|
|
322
|
+
if (thinkingIndex !== undefined) {
|
|
323
|
+
const content = message.content[thinkingIndex].thinking;
|
|
324
|
+
output.push({ type: 'thinking_end', contentIndex: thinkingIndex, content, partial: clonePartial(message) });
|
|
325
|
+
}
|
|
326
|
+
if (textIndex !== undefined) {
|
|
327
|
+
const content = message.content[textIndex].text;
|
|
328
|
+
output.push({ type: 'text_end', contentIndex: textIndex, content, partial: clonePartial(message) });
|
|
329
|
+
}
|
|
330
|
+
for (const call of [...toolCalls.values()].sort((a, b) => a.contentIndex - b.contentIndex)) {
|
|
331
|
+
let args;
|
|
332
|
+
try {
|
|
333
|
+
args = call.arguments.trim() ? JSON.parse(call.arguments) : {};
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
throw new Error(`[network] Invalid tool arguments for ${call.name}`);
|
|
337
|
+
}
|
|
338
|
+
const toolCall = { type: 'toolCall', id: call.id, name: call.name, arguments: args };
|
|
339
|
+
message.content[call.contentIndex] = toolCall;
|
|
340
|
+
output.push({ type: 'toolcall_end', contentIndex: call.contentIndex, toolCall, partial: clonePartial(message) });
|
|
341
|
+
}
|
|
342
|
+
message.stopReason = toolCalls.size > 0 || finishReason === 'tool_calls'
|
|
343
|
+
? 'toolUse'
|
|
344
|
+
: finishReason === 'length' ? 'length' : 'stop';
|
|
345
|
+
message.responseId = responseId;
|
|
346
|
+
message.responseModel = responseModel;
|
|
347
|
+
message.timestamp = Date.now();
|
|
348
|
+
output.push({ type: 'done', reason: message.stopReason, message });
|
|
349
|
+
};
|
|
350
|
+
let sawDone = false;
|
|
351
|
+
for await (const data of readSseEvents(body)) {
|
|
352
|
+
if (data === '[DONE]') {
|
|
353
|
+
sawDone = true;
|
|
354
|
+
finish();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
let chunk;
|
|
358
|
+
try {
|
|
359
|
+
chunk = JSON.parse(data);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
throw new Error('[network] Gateway returned malformed SSE JSON');
|
|
363
|
+
}
|
|
364
|
+
responseId = typeof chunk.id === 'string' ? chunk.id : responseId;
|
|
365
|
+
responseModel = typeof chunk.model === 'string' ? chunk.model : responseModel;
|
|
366
|
+
if (chunk.usage) {
|
|
367
|
+
const promptTokens = Number(chunk.usage.prompt_tokens) || 0;
|
|
368
|
+
const cachedTokens = Number(chunk.usage.prompt_tokens_details?.cached_tokens) || 0;
|
|
369
|
+
// OpenAI reports prompt_tokens inclusive of cached_tokens; ecagent's
|
|
370
|
+
// usage contract stores cacheRead separately so context totals do not
|
|
371
|
+
// double-count the cached portion.
|
|
372
|
+
message.usage.input = Math.max(0, promptTokens - cachedTokens);
|
|
373
|
+
message.usage.output = Number(chunk.usage.completion_tokens) || 0;
|
|
374
|
+
message.usage.cacheRead = cachedTokens;
|
|
375
|
+
message.usage.cacheWrite = 0;
|
|
376
|
+
message.usage.totalTokens = Number(chunk.usage.total_tokens) || message.usage.input + message.usage.output;
|
|
377
|
+
}
|
|
378
|
+
const choice = chunk.choices?.[0];
|
|
379
|
+
if (!choice)
|
|
380
|
+
continue;
|
|
381
|
+
if (choice.finish_reason)
|
|
382
|
+
finishReason = choice.finish_reason;
|
|
383
|
+
const delta = choice.delta ?? {};
|
|
384
|
+
const reasoning = delta.reasoning_content ?? delta.reasoning ?? delta.thinking;
|
|
385
|
+
if (typeof reasoning === 'string' && reasoning) {
|
|
386
|
+
if (thinkingIndex === undefined) {
|
|
387
|
+
thinkingIndex = message.content.length;
|
|
388
|
+
message.content.push({ type: 'thinking', thinking: '' });
|
|
389
|
+
output.push({ type: 'thinking_start', contentIndex: thinkingIndex, partial: clonePartial(message) });
|
|
390
|
+
}
|
|
391
|
+
message.content[thinkingIndex].thinking += reasoning;
|
|
392
|
+
output.push({ type: 'thinking_delta', contentIndex: thinkingIndex, delta: reasoning, partial: clonePartial(message) });
|
|
393
|
+
}
|
|
394
|
+
if (typeof delta.content === 'string' && delta.content) {
|
|
395
|
+
if (textIndex === undefined) {
|
|
396
|
+
textIndex = message.content.length;
|
|
397
|
+
message.content.push({ type: 'text', text: '' });
|
|
398
|
+
output.push({ type: 'text_start', contentIndex: textIndex, partial: clonePartial(message) });
|
|
399
|
+
}
|
|
400
|
+
message.content[textIndex].text += delta.content;
|
|
401
|
+
output.push({ type: 'text_delta', contentIndex: textIndex, delta: delta.content, partial: clonePartial(message) });
|
|
402
|
+
}
|
|
403
|
+
for (const update of delta.tool_calls ?? []) {
|
|
404
|
+
const index = Number(update.index) || 0;
|
|
405
|
+
let call = toolCalls.get(index);
|
|
406
|
+
if (!call) {
|
|
407
|
+
const contentIndex = message.content.length;
|
|
408
|
+
call = { contentIndex, id: update.id ?? `call_${index}`, name: '', arguments: '' };
|
|
409
|
+
toolCalls.set(index, call);
|
|
410
|
+
message.content.push({ type: 'toolCall', id: call.id, name: '', arguments: {} });
|
|
411
|
+
output.push({ type: 'toolcall_start', contentIndex, partial: clonePartial(message) });
|
|
412
|
+
}
|
|
413
|
+
if (typeof update.id === 'string')
|
|
414
|
+
call.id = update.id;
|
|
415
|
+
if (typeof update.function?.name === 'string')
|
|
416
|
+
call.name += update.function.name;
|
|
417
|
+
if (typeof update.function?.arguments === 'string') {
|
|
418
|
+
call.arguments += update.function.arguments;
|
|
419
|
+
output.push({ type: 'toolcall_delta', contentIndex: call.contentIndex, delta: update.function.arguments, partial: clonePartial(message) });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (!sawDone && finishReason) {
|
|
424
|
+
finish();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
throw new Error('[network] Gateway stream ended without a terminal event');
|
|
428
|
+
}
|
|
429
|
+
function createModel(id, baseUrl) {
|
|
430
|
+
const isClaude = id.toLowerCase().startsWith('claude-');
|
|
431
|
+
return {
|
|
432
|
+
id,
|
|
433
|
+
name: id,
|
|
434
|
+
api: 'openai-completions',
|
|
435
|
+
provider: PROVIDER_ID,
|
|
436
|
+
baseUrl,
|
|
437
|
+
reasoning: true,
|
|
438
|
+
input: ['text', 'image'],
|
|
439
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
440
|
+
contextWindow: isClaude ? 200_000 : 128_000,
|
|
441
|
+
maxTokens: isClaude ? 8_192 : 16_384,
|
|
442
|
+
compat: {
|
|
443
|
+
maxTokensField: 'max_tokens',
|
|
444
|
+
supportsUsageInStreaming: true,
|
|
445
|
+
supportsReasoningEffort: true,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function normalizeThinkingLevel(value) {
|
|
450
|
+
return ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'].includes(value ?? '')
|
|
451
|
+
? value
|
|
452
|
+
: 'off';
|
|
453
|
+
}
|
|
454
|
+
function isWithin(root, candidate) {
|
|
455
|
+
const relative = path.relative(root, candidate);
|
|
456
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
457
|
+
}
|
|
458
|
+
function resolveToolPath(projectPath, candidate, forWrite = false) {
|
|
459
|
+
const root = fs.realpathSync(projectPath);
|
|
460
|
+
const resolved = path.resolve(root, candidate || '.');
|
|
461
|
+
if (!isWithin(root, resolved))
|
|
462
|
+
throw new Error(`Path is outside the project: ${candidate}`);
|
|
463
|
+
const existing = forWrite ? path.dirname(resolved) : resolved;
|
|
464
|
+
let cursor = existing;
|
|
465
|
+
while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
|
|
466
|
+
cursor = path.dirname(cursor);
|
|
467
|
+
const canonical = fs.realpathSync(cursor);
|
|
468
|
+
if (!isWithin(root, canonical))
|
|
469
|
+
throw new Error(`Path resolves outside the project: ${candidate}`);
|
|
470
|
+
return resolved;
|
|
471
|
+
}
|
|
472
|
+
function globRegex(pattern) {
|
|
473
|
+
let source = '^';
|
|
474
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
475
|
+
const char = pattern[index];
|
|
476
|
+
if (char === '*') {
|
|
477
|
+
if (pattern[index + 1] === '*' && pattern[index + 2] === '/') {
|
|
478
|
+
source += '(?:.*/)?';
|
|
479
|
+
index += 2;
|
|
480
|
+
}
|
|
481
|
+
else if (pattern[index + 1] === '*') {
|
|
482
|
+
source += '.*';
|
|
483
|
+
index++;
|
|
484
|
+
}
|
|
485
|
+
else
|
|
486
|
+
source += '[^/]*';
|
|
487
|
+
}
|
|
488
|
+
else if (char === '?')
|
|
489
|
+
source += '[^/]';
|
|
490
|
+
else
|
|
491
|
+
source += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
492
|
+
}
|
|
493
|
+
return new RegExp(`${source}$`);
|
|
494
|
+
}
|
|
495
|
+
async function walkFiles(root, signal) {
|
|
496
|
+
const output = [];
|
|
497
|
+
const stack = [root];
|
|
498
|
+
while (stack.length > 0 && output.length < MAX_SCAN_FILES) {
|
|
499
|
+
if (signal?.aborted)
|
|
500
|
+
throw new Error('Tool execution aborted');
|
|
501
|
+
const dir = stack.pop();
|
|
502
|
+
for (const entry of await fs.promises.readdir(dir, { withFileTypes: true })) {
|
|
503
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
504
|
+
continue;
|
|
505
|
+
const full = path.join(dir, entry.name);
|
|
506
|
+
if (entry.isSymbolicLink())
|
|
507
|
+
continue;
|
|
508
|
+
if (entry.isDirectory())
|
|
509
|
+
stack.push(full);
|
|
510
|
+
else if (entry.isFile())
|
|
511
|
+
output.push(full);
|
|
512
|
+
if (output.length >= MAX_SCAN_FILES)
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return output;
|
|
517
|
+
}
|
|
518
|
+
function textResult(text, details = {}) {
|
|
519
|
+
return { content: [{ type: 'text', text: truncate(text) }], details };
|
|
520
|
+
}
|
|
521
|
+
function createTools(env, projectPath, runtimeEnv) {
|
|
522
|
+
const read = {
|
|
523
|
+
name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
|
|
524
|
+
parameters: Type.Object({
|
|
525
|
+
file_path: Type.String(),
|
|
526
|
+
offset: Type.Optional(Type.Number({ minimum: 1 })),
|
|
527
|
+
limit: Type.Optional(Type.Number({ minimum: 1 })),
|
|
528
|
+
}),
|
|
529
|
+
async execute(_id, params, signal) {
|
|
530
|
+
const file = resolveToolPath(projectPath, params.file_path);
|
|
531
|
+
const stat = await fs.promises.stat(file);
|
|
532
|
+
if (stat.size > MAX_READ_BYTES)
|
|
533
|
+
throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
|
|
534
|
+
const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
|
|
535
|
+
if (value.includes('\0'))
|
|
536
|
+
throw new Error('Binary files are not supported');
|
|
537
|
+
const lines = value.split('\n');
|
|
538
|
+
const offset = Math.max(1, params.offset ?? 1);
|
|
539
|
+
const selected = lines.slice(offset - 1, params.limit ? offset - 1 + params.limit : undefined);
|
|
540
|
+
return textResult(selected.map((line, index) => `${offset + index}\t${line}`).join('\n'), { file });
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
const glob = {
|
|
544
|
+
name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
|
|
545
|
+
parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
|
|
546
|
+
async execute(_id, params, signal) {
|
|
547
|
+
const root = resolveToolPath(projectPath, params.path ?? '.');
|
|
548
|
+
const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
|
|
549
|
+
const files = (await walkFiles(root, signal))
|
|
550
|
+
.map(file => path.relative(root, file).replaceAll(path.sep, '/'))
|
|
551
|
+
.filter(file => matcher.test(file))
|
|
552
|
+
.slice(0, MAX_SEARCH_RESULTS);
|
|
553
|
+
return textResult(files.join('\n') || 'No files matched.', { count: files.length });
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
const grep = {
|
|
557
|
+
name: 'Grep', label: 'Grep', description: 'Search text files in the project.',
|
|
558
|
+
parameters: Type.Object({
|
|
559
|
+
pattern: Type.String(), path: Type.Optional(Type.String()), glob: Type.Optional(Type.String()),
|
|
560
|
+
case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
|
|
561
|
+
}),
|
|
562
|
+
async execute(_id, params, signal) {
|
|
563
|
+
const root = resolveToolPath(projectPath, params.path ?? '.');
|
|
564
|
+
const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
|
|
565
|
+
const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
|
|
566
|
+
const flags = params.case_insensitive ? 'i' : '';
|
|
567
|
+
const pattern = new RegExp(params.literal ? params.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : params.pattern, flags);
|
|
568
|
+
const matches = [];
|
|
569
|
+
for (const file of files) {
|
|
570
|
+
if (signal?.aborted)
|
|
571
|
+
throw new Error('Tool execution aborted');
|
|
572
|
+
const relative = path.relative(projectPath, file).replaceAll(path.sep, '/');
|
|
573
|
+
if (fileMatcher && !fileMatcher.test(relative))
|
|
574
|
+
continue;
|
|
575
|
+
const stat = await fs.promises.stat(file);
|
|
576
|
+
if (stat.size > MAX_READ_BYTES)
|
|
577
|
+
continue;
|
|
578
|
+
const value = await fs.promises.readFile(file, 'utf8').catch(() => '');
|
|
579
|
+
if (value.includes('\0'))
|
|
580
|
+
continue;
|
|
581
|
+
value.split('\n').forEach((line, index) => {
|
|
582
|
+
if (matches.length < MAX_SEARCH_RESULTS && pattern.test(line))
|
|
583
|
+
matches.push(`${relative}:${index + 1}:${truncate(line, 1_000)}`);
|
|
584
|
+
pattern.lastIndex = 0;
|
|
585
|
+
});
|
|
586
|
+
if (matches.length >= MAX_SEARCH_RESULTS)
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
return textResult(matches.join('\n') || 'No matches found.', { count: matches.length });
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
const write = {
|
|
593
|
+
name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
|
|
594
|
+
parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
|
|
595
|
+
async execute(_id, params, signal) {
|
|
596
|
+
const file = resolveToolPath(projectPath, params.file_path, true);
|
|
597
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
598
|
+
await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
|
|
599
|
+
return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
const edit = {
|
|
603
|
+
name: 'Edit', label: 'Edit', description: 'Replace an exact string in a project file.',
|
|
604
|
+
parameters: Type.Object({
|
|
605
|
+
file_path: Type.String(), old_string: Type.String(), new_string: Type.String(),
|
|
606
|
+
replace_all: Type.Optional(Type.Boolean()),
|
|
607
|
+
}), executionMode: 'sequential',
|
|
608
|
+
async execute(_id, params, signal) {
|
|
609
|
+
const file = resolveToolPath(projectPath, params.file_path);
|
|
610
|
+
const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
|
|
611
|
+
if (!params.old_string)
|
|
612
|
+
throw new Error('old_string must not be empty');
|
|
613
|
+
const count = value.split(params.old_string).length - 1;
|
|
614
|
+
if (count === 0)
|
|
615
|
+
throw new Error('old_string was not found');
|
|
616
|
+
if (count > 1 && !params.replace_all)
|
|
617
|
+
throw new Error(`old_string matched ${count} locations; set replace_all or provide more context`);
|
|
618
|
+
const next = params.replace_all
|
|
619
|
+
? value.split(params.old_string).join(params.new_string)
|
|
620
|
+
: value.replace(params.old_string, params.new_string);
|
|
621
|
+
await fs.promises.writeFile(file, next, { encoding: 'utf8', signal });
|
|
622
|
+
return textResult(`Updated ${path.relative(projectPath, file)} (${params.replace_all ? count : 1} replacement${count === 1 ? '' : 's'})`, { file, replacements: params.replace_all ? count : 1 });
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
const bash = {
|
|
626
|
+
name: 'Bash', label: 'Bash', description: 'Run a shell command in the project.',
|
|
627
|
+
parameters: Type.Object({
|
|
628
|
+
command: Type.String(), timeout: Type.Optional(Type.Number({ minimum: 1, maximum: 600_000 })),
|
|
629
|
+
description: Type.Optional(Type.String()),
|
|
630
|
+
}), executionMode: 'sequential',
|
|
631
|
+
async execute(_id, params, signal) {
|
|
632
|
+
const result = await env.exec(params.command, {
|
|
633
|
+
cwd: projectPath,
|
|
634
|
+
env: runtimeEnv,
|
|
635
|
+
timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_MS,
|
|
636
|
+
abortSignal: signal,
|
|
637
|
+
});
|
|
638
|
+
if (!result.ok)
|
|
639
|
+
throw result.error;
|
|
640
|
+
const combined = [result.value.stdout, result.value.stderr].filter(Boolean).join('\n');
|
|
641
|
+
if (result.value.exitCode !== 0)
|
|
642
|
+
throw new Error(`Command exited with ${result.value.exitCode}\n${truncate(combined)}`);
|
|
643
|
+
return textResult(combined || `Command exited with ${result.value.exitCode}`, { exitCode: result.value.exitCode });
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
return [read, glob, grep, write, edit, bash];
|
|
647
|
+
}
|
|
648
|
+
function extractToolResult(result) {
|
|
649
|
+
const content = Array.isArray(result?.content) ? result.content : [];
|
|
650
|
+
return content.filter((block) => block?.type === 'text').map((block) => block.text).join('\n');
|
|
651
|
+
}
|
|
652
|
+
function errorType(message) {
|
|
653
|
+
const normalized = message.toLowerCase();
|
|
654
|
+
if (normalized.includes('[auth]') || normalized.includes('401') || normalized.includes('403'))
|
|
655
|
+
return 'auth';
|
|
656
|
+
if (normalized.includes('context') && (normalized.includes('long') || normalized.includes('length')))
|
|
657
|
+
return 'context_too_long';
|
|
658
|
+
if (normalized.includes('[network]') || normalized.includes('fetch') || normalized.includes('timeout') || normalized.includes('gateway'))
|
|
659
|
+
return 'network';
|
|
660
|
+
return 'unknown';
|
|
661
|
+
}
|
|
662
|
+
export class EcagentRunner {
|
|
663
|
+
name = 'ecagent';
|
|
664
|
+
capabilities = {
|
|
665
|
+
clear: true, compact: true, fork: true, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported',
|
|
666
|
+
};
|
|
667
|
+
config;
|
|
668
|
+
onSessionIdUpdate;
|
|
669
|
+
model;
|
|
670
|
+
effort;
|
|
671
|
+
permissionMode = DEFAULT_PERMISSION_MODE;
|
|
672
|
+
runtimes = new Map();
|
|
673
|
+
activeStreams = new Map();
|
|
674
|
+
pendingInterrupts = new Set();
|
|
675
|
+
permissionContexts = new Map();
|
|
676
|
+
permissionGateway;
|
|
677
|
+
sendPromptFn;
|
|
678
|
+
onCompactStart;
|
|
679
|
+
modelCache;
|
|
680
|
+
constructor(config, callbacks) {
|
|
681
|
+
this.config = config;
|
|
682
|
+
this.model = config.model;
|
|
683
|
+
this.effort = config.effort;
|
|
684
|
+
this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
|
|
685
|
+
}
|
|
686
|
+
sessionsRoot() {
|
|
687
|
+
if (!this.config.evolcoreAgentAid)
|
|
688
|
+
throw new Error('ecagent requires an EvolCore Agent AID');
|
|
689
|
+
return agentSessionsDir(this.config.evolcoreAgentAid);
|
|
690
|
+
}
|
|
691
|
+
setModel(model) { this.model = model; }
|
|
692
|
+
getModel() { return this.model; }
|
|
693
|
+
resolveModelId(model) { return model; }
|
|
694
|
+
setEffort(effort) { this.effort = effort; }
|
|
695
|
+
getEffort() { return this.effort; }
|
|
696
|
+
setMode(mode) { this.permissionMode = normalizePermissionMode(mode).mode; }
|
|
697
|
+
getMode() { return normalizePermissionMode(this.permissionMode).mode; }
|
|
698
|
+
setPermissionGateway(gateway) { this.permissionGateway = gateway; }
|
|
699
|
+
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
700
|
+
setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
|
|
701
|
+
setCompactStartCallback(callback) { this.onCompactStart = callback; }
|
|
702
|
+
listModes() {
|
|
703
|
+
return [
|
|
704
|
+
{ key: 'readonly', nameZh: '只读', description: '只读操作自动执行,写入直接拒绝', available: true },
|
|
705
|
+
{ key: 'auto', nameZh: '自动', description: '常规操作自动执行,危险操作自动拒绝', available: true },
|
|
706
|
+
{ key: 'request', nameZh: '审批', description: '默认能力自动执行,危险操作进入人工审批', available: true },
|
|
707
|
+
{ key: 'bypass', nameZh: '放行', description: '普通审批清单自动执行,强危险操作仍需审批', available: true },
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
async listModels() {
|
|
711
|
+
if (this.modelCache && Date.now() - this.modelCache.fetchedAt < MODEL_CACHE_TTL_MS)
|
|
712
|
+
return [...this.modelCache.ids];
|
|
713
|
+
const stale = this.modelCache?.ids;
|
|
714
|
+
try {
|
|
715
|
+
const signal = AbortSignal.timeout(MODEL_LIST_TIMEOUT_MS);
|
|
716
|
+
const response = await fetch(`${this.config.baseUrl.replace(/\/+$/, '')}/models`, {
|
|
717
|
+
headers: { authorization: `Bearer ${this.config.apiKey}` }, signal,
|
|
718
|
+
});
|
|
719
|
+
if (!response.ok)
|
|
720
|
+
throw new Error(`Failed to list ecagent models: HTTP ${response.status}`);
|
|
721
|
+
const payload = await response.json();
|
|
722
|
+
const ids = [...new Set((payload.data ?? [])
|
|
723
|
+
.map(item => typeof item.id === 'string' ? item.id : '')
|
|
724
|
+
.filter(id => id.startsWith('claude-') || (id.startsWith('gpt-') && !id.startsWith('gpt-image-'))))];
|
|
725
|
+
if (ids.length === 0)
|
|
726
|
+
throw new Error('Gateway returned no supported Claude or GPT models');
|
|
727
|
+
this.modelCache = { ids, fetchedAt: Date.now() };
|
|
728
|
+
return [...ids];
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
if (stale && stale.length > 0)
|
|
732
|
+
return [...stale];
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
registerStream(key, stream) { this.activeStreams.set(key, stream); }
|
|
737
|
+
cleanupStream(key) { this.activeStreams.delete(key); }
|
|
738
|
+
hasActiveStream(key) { return this.activeStreams.has(key) || !!this.runtimes.get(key)?.harness; }
|
|
739
|
+
updateSessionId(sessionId, agentSessionId) {
|
|
740
|
+
if (!agentSessionId) {
|
|
741
|
+
this.runtimes.delete(sessionId);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
const runtime = this.runtimes.get(sessionId);
|
|
745
|
+
if (runtime)
|
|
746
|
+
runtime.agentSessionId = agentSessionId;
|
|
747
|
+
}
|
|
748
|
+
void this.onSessionIdUpdate?.(sessionId, agentSessionId);
|
|
749
|
+
}
|
|
750
|
+
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, _sessionManager, modelOverride, runtimeEnv = {}) {
|
|
751
|
+
const queue = new AsyncEventQueue();
|
|
752
|
+
this.activeStreams.set(sessionId, queue);
|
|
753
|
+
void this.executeRun(queue, {
|
|
754
|
+
sessionId, prompt, projectPath: path.resolve(projectPath), initialAgentSessionId, images,
|
|
755
|
+
systemPromptAppend, modelOverride, runtimeEnv,
|
|
756
|
+
});
|
|
757
|
+
return queue;
|
|
758
|
+
}
|
|
759
|
+
async executeRun(queue, request) {
|
|
760
|
+
const stats = { startedAt: Date.now(), text: '', numTurns: 0, usage: {}, modelCalls: [] };
|
|
761
|
+
let completeSent = false;
|
|
762
|
+
try {
|
|
763
|
+
await fs.promises.mkdir(request.projectPath, { recursive: true });
|
|
764
|
+
const previousRuntime = this.runtimes.get(request.sessionId);
|
|
765
|
+
const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId);
|
|
766
|
+
if (!previousRuntime || request.initialAgentSessionId !== runtime.agentSessionId) {
|
|
767
|
+
queue.push({ type: 'session_id', sessionId: runtime.agentSessionId });
|
|
768
|
+
}
|
|
769
|
+
const modelId = request.modelOverride?.model || this.model;
|
|
770
|
+
const effort = request.modelOverride?.effortMode === 'model_default'
|
|
771
|
+
? undefined
|
|
772
|
+
: request.modelOverride?.effort ?? this.effort;
|
|
773
|
+
const permissionMode = normalizePermissionMode(request.modelOverride?.permissionMode || this.permissionMode).mode;
|
|
774
|
+
const model = createModel(modelId, this.config.baseUrl);
|
|
775
|
+
const nodeEnv = new NodeExecutionEnv({ cwd: request.projectPath, shellEnv: { ...process.env, ...request.runtimeEnv, EVOLCORE_SESSION_ID: request.sessionId, EVOLCORE_HOME: resolveRoot() } });
|
|
776
|
+
const models = createModels();
|
|
777
|
+
const transport = createOpenAiTransport(this.config);
|
|
778
|
+
models.setProvider(createProvider({
|
|
779
|
+
id: PROVIDER_ID,
|
|
780
|
+
name: 'EvolCore Gateway',
|
|
781
|
+
baseUrl: this.config.baseUrl,
|
|
782
|
+
models: [model],
|
|
783
|
+
auth: { apiKey: {
|
|
784
|
+
name: 'EvolCore gateway',
|
|
785
|
+
login: async () => ({ type: 'api_key', key: this.config.apiKey }),
|
|
786
|
+
resolve: async () => ({ auth: { apiKey: this.config.apiKey }, source: 'baseagents.ecagent.apiKey' }),
|
|
787
|
+
} },
|
|
788
|
+
api: transport,
|
|
789
|
+
}));
|
|
790
|
+
const harness = new AgentHarness({
|
|
791
|
+
env: nodeEnv,
|
|
792
|
+
session: runtime.session,
|
|
793
|
+
models,
|
|
794
|
+
model,
|
|
795
|
+
thinkingLevel: normalizeThinkingLevel(effort),
|
|
796
|
+
tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv),
|
|
797
|
+
systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
|
|
798
|
+
streamOptions: {
|
|
799
|
+
timeoutMs: this.config.timeoutMs,
|
|
800
|
+
maxRetries: this.config.maxRetries,
|
|
801
|
+
maxRetryDelayMs: this.config.maxRetryDelayMs,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
runtime.harness = harness;
|
|
805
|
+
if (this.pendingInterrupts.delete(request.sessionId)) {
|
|
806
|
+
await harness.abort();
|
|
807
|
+
runtime.harness = undefined;
|
|
808
|
+
throw new Error('ecagent run interrupted');
|
|
809
|
+
}
|
|
810
|
+
harness.on('tool_call', event => this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input));
|
|
811
|
+
const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
|
|
812
|
+
queue.push({ type: 'state_changed', state: 'running' });
|
|
813
|
+
queue.push({ type: 'status', subtype: 'started', message: `ecagent is using ${modelId}` });
|
|
814
|
+
const ecImages = request.images?.map(image => ({
|
|
815
|
+
type: 'image', data: image.data, mimeType: image.mimeType || 'image/png',
|
|
816
|
+
}));
|
|
817
|
+
let result;
|
|
818
|
+
try {
|
|
819
|
+
result = await harness.prompt(request.prompt, { images: ecImages });
|
|
820
|
+
}
|
|
821
|
+
finally {
|
|
822
|
+
unsubscribe();
|
|
823
|
+
runtime.harness = undefined;
|
|
824
|
+
}
|
|
825
|
+
if (result.stopReason === 'error' || result.stopReason === 'aborted') {
|
|
826
|
+
const message = result.errorMessage || (result.stopReason === 'aborted' ? 'ecagent run interrupted' : 'ecagent run failed');
|
|
827
|
+
queue.push({ type: 'error', error: message, errorType: errorType(message) });
|
|
828
|
+
}
|
|
829
|
+
const title = await runtime.session.getSessionName();
|
|
830
|
+
queue.push({ type: 'state_changed', state: 'idle' });
|
|
831
|
+
queue.push({
|
|
832
|
+
type: 'complete',
|
|
833
|
+
result: stats.text,
|
|
834
|
+
isError: result.stopReason === 'error' || result.stopReason === 'aborted',
|
|
835
|
+
errors: result.errorMessage ? [result.errorMessage] : undefined,
|
|
836
|
+
terminalReason: result.stopReason === 'aborted' ? 'interrupted' : result.stopReason,
|
|
837
|
+
durationMs: Date.now() - stats.startedAt,
|
|
838
|
+
ttftMs: stats.firstTextAt ? stats.firstTextAt - stats.startedAt : undefined,
|
|
839
|
+
sessionTitle: title,
|
|
840
|
+
numTurns: stats.numTurns,
|
|
841
|
+
tokenUsage: stats.usage,
|
|
842
|
+
contextUsage: this.contextUsage(model, stats.usage, effort),
|
|
843
|
+
lastModelCall: stats.modelCalls.at(-1),
|
|
844
|
+
modelCalls: stats.modelCalls,
|
|
845
|
+
});
|
|
846
|
+
completeSent = true;
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
850
|
+
const interrupted = message === 'ecagent run interrupted';
|
|
851
|
+
logger.error(`[EcagentRunner] run failed for session ${request.sessionId}:`, error);
|
|
852
|
+
queue.push({ type: 'error', error: message, errorType: errorType(message) });
|
|
853
|
+
queue.push({ type: 'state_changed', state: 'idle' });
|
|
854
|
+
queue.push({
|
|
855
|
+
type: 'complete', isError: true, errors: [message],
|
|
856
|
+
terminalReason: interrupted ? 'interrupted' : 'error', durationMs: Date.now() - stats.startedAt,
|
|
857
|
+
});
|
|
858
|
+
completeSent = true;
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
if (!completeSent)
|
|
862
|
+
queue.push({ type: 'complete', isError: true, terminalReason: 'error', durationMs: Date.now() - stats.startedAt });
|
|
863
|
+
this.pendingInterrupts.delete(request.sessionId);
|
|
864
|
+
this.activeStreams.delete(request.sessionId);
|
|
865
|
+
queue.end();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async getOrCreateRuntime(sessionId, projectPath, requestedId) {
|
|
869
|
+
const current = this.runtimes.get(sessionId);
|
|
870
|
+
const targetId = requestedId || current?.agentSessionId;
|
|
871
|
+
if (current && current.projectPath === projectPath && (!targetId || current.agentSessionId === targetId))
|
|
872
|
+
return current;
|
|
873
|
+
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
874
|
+
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
875
|
+
let session;
|
|
876
|
+
if (targetId) {
|
|
877
|
+
const metadata = (await repo.list({ cwd: projectPath })).find(item => item.id === targetId);
|
|
878
|
+
if (!metadata) {
|
|
879
|
+
logger.warn(`[EcagentRunner] backing session ${targetId} was not found; creating a new session`);
|
|
880
|
+
session = await repo.create({ cwd: projectPath });
|
|
881
|
+
}
|
|
882
|
+
else
|
|
883
|
+
session = await repo.open(metadata);
|
|
884
|
+
}
|
|
885
|
+
else
|
|
886
|
+
session = await repo.create({ cwd: projectPath, id: sessionId });
|
|
887
|
+
const metadata = await session.getMetadata();
|
|
888
|
+
const runtime = { agentSessionId: metadata.id, projectPath, repo, session };
|
|
889
|
+
this.runtimes.set(sessionId, runtime);
|
|
890
|
+
if (metadata.id !== targetId) {
|
|
891
|
+
await this.onSessionIdUpdate?.(sessionId, metadata.id);
|
|
892
|
+
// The public event is emitted by the active query immediately after creation.
|
|
893
|
+
}
|
|
894
|
+
return runtime;
|
|
895
|
+
}
|
|
896
|
+
mapHarnessEvent(queue, event, stats, model, effort) {
|
|
897
|
+
if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
|
|
898
|
+
if (!stats.firstTextAt)
|
|
899
|
+
stats.firstTextAt = Date.now();
|
|
900
|
+
stats.text += event.assistantMessageEvent.delta;
|
|
901
|
+
queue.push({ type: 'text', text: event.assistantMessageEvent.delta, turn: stats.numTurns + 1 });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (event.type === 'tool_execution_start') {
|
|
905
|
+
queue.push({ type: 'tool_use', name: event.toolName, input: event.args, callId: event.toolCallId, turn: stats.numTurns + 1 });
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (event.type === 'tool_execution_end') {
|
|
909
|
+
queue.push({
|
|
910
|
+
type: 'tool_result', name: event.toolName, result: extractToolResult(event.result),
|
|
911
|
+
isError: event.isError, error: event.isError ? extractToolResult(event.result) : undefined, callId: event.toolCallId,
|
|
912
|
+
});
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (event.type === 'turn_end' && event.message.role === 'assistant') {
|
|
916
|
+
stats.numTurns++;
|
|
917
|
+
const usage = event.message.usage;
|
|
918
|
+
stats.usage.input_tokens = Number(stats.usage.input_tokens ?? 0) + usage.input;
|
|
919
|
+
stats.usage.output_tokens = Number(stats.usage.output_tokens ?? 0) + usage.output;
|
|
920
|
+
stats.usage.cache_read_input_tokens = Number(stats.usage.cache_read_input_tokens ?? 0) + usage.cacheRead;
|
|
921
|
+
stats.usage.cache_creation_input_tokens = Number(stats.usage.cache_creation_input_tokens ?? 0) + usage.cacheWrite;
|
|
922
|
+
const tokenUsage = {
|
|
923
|
+
input_tokens: usage.input, output_tokens: usage.output,
|
|
924
|
+
cache_read_input_tokens: usage.cacheRead, cache_creation_input_tokens: usage.cacheWrite,
|
|
925
|
+
};
|
|
926
|
+
stats.modelCalls.push({
|
|
927
|
+
call_index: stats.modelCalls.length,
|
|
928
|
+
model: event.message.model || model.id,
|
|
929
|
+
request_id: event.message.responseId,
|
|
930
|
+
tokenUsage,
|
|
931
|
+
contextUsage: this.contextUsage(model, tokenUsage, effort),
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
contextUsage(model, usage, effort) {
|
|
936
|
+
const totalTokens = Number(usage.input_tokens ?? 0) + Number(usage.output_tokens ?? 0)
|
|
937
|
+
+ Number(usage.cache_read_input_tokens ?? 0) + Number(usage.cache_creation_input_tokens ?? 0);
|
|
938
|
+
return {
|
|
939
|
+
totalTokens, maxTokens: model.contextWindow,
|
|
940
|
+
percentage: model.contextWindow > 0 ? totalTokens / model.contextWindow * 100 : 0,
|
|
941
|
+
model: model.id, effort,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
async authorizeTool(sessionId, projectPath, mode, toolName, input) {
|
|
945
|
+
const permissionContext = this.permissionContexts.get(sessionId);
|
|
946
|
+
const policy = permissionContext?.policyHook?.(toolName, input);
|
|
947
|
+
if (policy?.block)
|
|
948
|
+
return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
|
|
949
|
+
const preflight = evaluateToolPreflight(toolName, input, {
|
|
950
|
+
sessionId,
|
|
951
|
+
channel: permissionContext?.channel,
|
|
952
|
+
userId: permissionContext?.userId,
|
|
953
|
+
role: permissionContext?.role,
|
|
954
|
+
permissionMode: mode,
|
|
955
|
+
projectPath,
|
|
956
|
+
});
|
|
957
|
+
if (preflight.behavior === 'deny')
|
|
958
|
+
return { block: true, reason: preflight.message };
|
|
959
|
+
if (preflight.behavior === 'allow')
|
|
960
|
+
return {};
|
|
961
|
+
const checkedInput = preflight.input;
|
|
962
|
+
if (mode === 'readonly') {
|
|
963
|
+
const decision = checkReadonly(toolName, checkedInput, projectPath, {
|
|
964
|
+
sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
|
|
965
|
+
});
|
|
966
|
+
return decision.behavior === 'deny' ? { block: true, reason: decision.message } : {};
|
|
967
|
+
}
|
|
968
|
+
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
969
|
+
if (mode === 'auto') {
|
|
970
|
+
return dangerous.isDangerous ? { block: true, reason: `危险操作已由 auto 模式拒绝:${dangerous.reason}` } : {};
|
|
971
|
+
}
|
|
972
|
+
const prompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
|
|
973
|
+
const decision = await requestDangerousCommandPermission(this.permissionGateway, sessionId, toolName, checkedInput, prompt, permissionContext, `ecagent:${mode}`, mode);
|
|
974
|
+
return decision.matched && decision.decision === 'deny' ? { block: true, reason: '危险操作未获人工批准' } : {};
|
|
975
|
+
}
|
|
976
|
+
async interrupt(sessionKey) {
|
|
977
|
+
this.permissionGateway?.cancelAll(sessionKey, 'interrupted');
|
|
978
|
+
const harness = this.runtimes.get(sessionKey)?.harness;
|
|
979
|
+
if (harness) {
|
|
980
|
+
await harness.abort();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (this.activeStreams.has(sessionKey))
|
|
984
|
+
this.pendingInterrupts.add(sessionKey);
|
|
985
|
+
}
|
|
986
|
+
injectUserMessage(sessionId, text) {
|
|
987
|
+
const harness = this.runtimes.get(sessionId)?.harness;
|
|
988
|
+
if (harness)
|
|
989
|
+
void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
|
|
990
|
+
}
|
|
991
|
+
async closeSession(sessionId) {
|
|
992
|
+
await this.interrupt(sessionId).catch(() => { });
|
|
993
|
+
this.runtimes.delete(sessionId);
|
|
994
|
+
this.activeStreams.delete(sessionId);
|
|
995
|
+
this.pendingInterrupts.delete(sessionId);
|
|
996
|
+
this.permissionContexts.delete(sessionId);
|
|
997
|
+
}
|
|
998
|
+
async clearSession(sessionId, agentSessionId, projectPath) {
|
|
999
|
+
try {
|
|
1000
|
+
await this.interrupt(sessionId);
|
|
1001
|
+
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
1002
|
+
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1003
|
+
const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
|
|
1004
|
+
if (metadata)
|
|
1005
|
+
await repo.delete(metadata);
|
|
1006
|
+
this.runtimes.delete(sessionId);
|
|
1007
|
+
await this.onSessionIdUpdate?.(sessionId, '');
|
|
1008
|
+
return !!metadata;
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
logger.error('[EcagentRunner] clear failed:', error);
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
1016
|
+
try {
|
|
1017
|
+
if (this.activeStreams.has(sessionId) || this.runtimes.get(sessionId)?.harness) {
|
|
1018
|
+
logger.warn(`[EcagentRunner] compact skipped while session ${sessionId} is active`);
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const cwd = path.resolve(projectPath);
|
|
1022
|
+
const cached = this.runtimes.get(sessionId);
|
|
1023
|
+
const runtime = cached?.agentSessionId === agentSessionId && cached.projectPath === cwd
|
|
1024
|
+
? cached
|
|
1025
|
+
: await this.openExistingRuntime(agentSessionId, cwd);
|
|
1026
|
+
this.onCompactStart?.(sessionId);
|
|
1027
|
+
const model = createModel(this.model, this.config.baseUrl);
|
|
1028
|
+
const env = new NodeExecutionEnv({ cwd: runtime.projectPath });
|
|
1029
|
+
const models = createModels();
|
|
1030
|
+
const transport = createOpenAiTransport(this.config);
|
|
1031
|
+
models.setProvider(createProvider({
|
|
1032
|
+
id: PROVIDER_ID, auth: { apiKey: { name: 'EvolCore gateway', login: async () => ({ type: 'api_key', key: this.config.apiKey }), resolve: async () => ({ auth: { apiKey: this.config.apiKey } }) } },
|
|
1033
|
+
models: [model], api: transport,
|
|
1034
|
+
}));
|
|
1035
|
+
const harness = new AgentHarness({ env, session: runtime.session, models, model, thinkingLevel: normalizeThinkingLevel(this.effort), tools: [] });
|
|
1036
|
+
runtime.harness = harness;
|
|
1037
|
+
try {
|
|
1038
|
+
await harness.compact();
|
|
1039
|
+
}
|
|
1040
|
+
finally {
|
|
1041
|
+
runtime.harness = undefined;
|
|
1042
|
+
}
|
|
1043
|
+
this.runtimes.set(sessionId, runtime);
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
logger.error('[EcagentRunner] compact failed:', error);
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
compact(sessionId, agentSessionId, projectPath) {
|
|
1052
|
+
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
1053
|
+
}
|
|
1054
|
+
resolveSessionFile(agentSessionId, projectPath) {
|
|
1055
|
+
const dir = path.join(this.sessionsRoot(), `--${path.resolve(projectPath).replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`);
|
|
1056
|
+
try {
|
|
1057
|
+
const file = fs.readdirSync(dir).find(name => name.endsWith(`_${agentSessionId}.jsonl`));
|
|
1058
|
+
return file ? path.join(dir, file) : null;
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async forkSession(agentSessionId, projectPath, title) {
|
|
1065
|
+
const cwd = path.resolve(projectPath);
|
|
1066
|
+
const env = new NodeExecutionEnv({ cwd });
|
|
1067
|
+
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1068
|
+
const source = (await repo.list({ cwd })).find(item => item.id === agentSessionId);
|
|
1069
|
+
if (!source)
|
|
1070
|
+
throw new Error(`ecagent session not found: ${agentSessionId}`);
|
|
1071
|
+
const fork = await repo.fork(source, { cwd });
|
|
1072
|
+
if (title)
|
|
1073
|
+
await fork.appendSessionName(title);
|
|
1074
|
+
return (await fork.getMetadata()).id;
|
|
1075
|
+
}
|
|
1076
|
+
async getSessionMessages(agentSessionId, projectPath) {
|
|
1077
|
+
const session = await this.openSession(agentSessionId, projectPath);
|
|
1078
|
+
const entries = await session.getEntries();
|
|
1079
|
+
return entries.flatMap(entry => {
|
|
1080
|
+
if (entry.type !== 'message' || (entry.message.role !== 'user' && entry.message.role !== 'assistant'))
|
|
1081
|
+
return [];
|
|
1082
|
+
return [{
|
|
1083
|
+
type: entry.message.role,
|
|
1084
|
+
uuid: entry.id,
|
|
1085
|
+
session_id: agentSessionId,
|
|
1086
|
+
message: entry.message,
|
|
1087
|
+
parent_tool_use_id: null,
|
|
1088
|
+
}];
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
async setSessionName(agentSessionId, name) {
|
|
1092
|
+
try {
|
|
1093
|
+
await (await this.openSession(agentSessionId)).appendSessionName(name);
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async updateSessionMetadata(agentSessionId, metadata) {
|
|
1101
|
+
const title = typeof metadata.title === 'string' ? metadata.title : typeof metadata.name === 'string' ? metadata.name : undefined;
|
|
1102
|
+
return title ? this.setSessionName(agentSessionId, title) : true;
|
|
1103
|
+
}
|
|
1104
|
+
async rewindFiles() {
|
|
1105
|
+
return { canRewind: false, error: 'ecagent does not support file rewind' };
|
|
1106
|
+
}
|
|
1107
|
+
async openSession(agentSessionId, projectPath) {
|
|
1108
|
+
const cwd = path.resolve(projectPath || process.cwd());
|
|
1109
|
+
const env = new NodeExecutionEnv({ cwd });
|
|
1110
|
+
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1111
|
+
const metadata = (await repo.list(projectPath ? { cwd } : {})).find(item => item.id === agentSessionId);
|
|
1112
|
+
if (!metadata)
|
|
1113
|
+
throw new Error(`ecagent session not found: ${agentSessionId}`);
|
|
1114
|
+
return repo.open(metadata);
|
|
1115
|
+
}
|
|
1116
|
+
async openExistingRuntime(agentSessionId, projectPath) {
|
|
1117
|
+
await fs.promises.mkdir(this.sessionsRoot(), { recursive: true });
|
|
1118
|
+
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
1119
|
+
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1120
|
+
const metadata = (await repo.list({ cwd: projectPath })).find(item => item.id === agentSessionId);
|
|
1121
|
+
if (!metadata)
|
|
1122
|
+
throw new Error(`ecagent session not found: ${agentSessionId}`);
|
|
1123
|
+
return {
|
|
1124
|
+
agentSessionId,
|
|
1125
|
+
projectPath,
|
|
1126
|
+
repo,
|
|
1127
|
+
session: await repo.open(metadata),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
async dispose() {
|
|
1131
|
+
await Promise.allSettled([...this.runtimes.keys()].map(sessionId => this.closeSession(sessionId)));
|
|
1132
|
+
this.runtimes.clear();
|
|
1133
|
+
this.activeStreams.clear();
|
|
1134
|
+
this.pendingInterrupts.clear();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
export class EcagentAgentPlugin {
|
|
1138
|
+
name = 'ecagent';
|
|
1139
|
+
isEnabled(agent) {
|
|
1140
|
+
const override = agent.config.baseagents?.ecagent;
|
|
1141
|
+
if (!override)
|
|
1142
|
+
return false;
|
|
1143
|
+
try {
|
|
1144
|
+
resolveEcagentConfig({ agents: { ecagent: override } }, override);
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
createAgent(agent, callbacks) {
|
|
1152
|
+
const override = agent.config.baseagents?.ecagent;
|
|
1153
|
+
if (!override)
|
|
1154
|
+
return null;
|
|
1155
|
+
const configured = { ...override, evolcoreAgentAid: agent.aid, evolcoreAgentConfig: agent.config };
|
|
1156
|
+
const resolved = resolveEcagentConfig({ agents: { ecagent: configured } }, configured);
|
|
1157
|
+
return { evolagentName: agent.name, baseagent: 'ecagent', agent: new EcagentRunner(resolved, callbacks) };
|
|
1158
|
+
}
|
|
1159
|
+
}
|