@ai-setting/roy-agent-core 1.0.0
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/dist/index.js +99145 -0
- package/package.json +114 -0
- package/src/config/config-component.test.ts +627 -0
- package/src/config/config-component.ts +906 -0
- package/src/config/config-parser.test.ts +319 -0
- package/src/config/config-parser.ts +203 -0
- package/src/config/decentralized-config.test.ts +740 -0
- package/src/config/env-key.ts +210 -0
- package/src/config/env-source.test.ts +252 -0
- package/src/config/env-source.ts +301 -0
- package/src/config/file-source.test.ts +357 -0
- package/src/config/file-source.ts +421 -0
- package/src/config/index.ts +24 -0
- package/src/config/protocol-resolver.test.ts +217 -0
- package/src/config/protocol-resolver.ts +228 -0
- package/src/env/agent/agent-component.abort.test.ts +511 -0
- package/src/env/agent/agent-component.record-session.test.ts +349 -0
- package/src/env/agent/agent-component.test.ts +1389 -0
- package/src/env/agent/agent-component.tool-error.test.ts +327 -0
- package/src/env/agent/agent-component.ts +1711 -0
- package/src/env/agent/agent-config-registration.test.ts +226 -0
- package/src/env/agent/agent-config-registration.ts +46 -0
- package/src/env/agent/agent-reminder-plugin.integration.test.ts +243 -0
- package/src/env/agent/index.ts +10 -0
- package/src/env/agent/summary-agent.parse-hint.test.ts +360 -0
- package/src/env/agent/summary-agent.ts +508 -0
- package/src/env/agent/types.ts +536 -0
- package/src/env/commands/commands-component.test.ts +364 -0
- package/src/env/commands/commands-component.ts +604 -0
- package/src/env/commands/commands-config-registration.test.ts +198 -0
- package/src/env/commands/commands-config-registration.ts +38 -0
- package/src/env/commands/index.ts +21 -0
- package/src/env/commands/parser.test.ts +203 -0
- package/src/env/commands/parser.ts +115 -0
- package/src/env/commands/types.ts +184 -0
- package/src/env/commands-prompt-integration.test.ts +243 -0
- package/src/env/component-env.test.ts +119 -0
- package/src/env/component.ts +335 -0
- package/src/env/constants.test.ts +72 -0
- package/src/env/constants.ts +123 -0
- package/src/env/debug/debug-component.test.ts +114 -0
- package/src/env/debug/debug-component.ts +547 -0
- package/src/env/debug/formatters/index.ts +9 -0
- package/src/env/debug/formatters/repl-formatter.test.ts +139 -0
- package/src/env/debug/formatters/repl-formatter.ts +358 -0
- package/src/env/debug/formatters/trace-formatter.test.ts +119 -0
- package/src/env/debug/formatters/trace-formatter.ts +191 -0
- package/src/env/debug/formatters/tree-formatter.test.ts +107 -0
- package/src/env/debug/formatters/tree-formatter.ts +325 -0
- package/src/env/debug/index.ts +38 -0
- package/src/env/debug/parser/regex-parser.test.ts +201 -0
- package/src/env/debug/parser/regex-parser.ts +196 -0
- package/src/env/debug/parser/span-builder.test.ts +241 -0
- package/src/env/debug/parser/span-builder.ts +386 -0
- package/src/env/debug/reader/log-reader.test.ts +170 -0
- package/src/env/debug/reader/log-reader.ts +186 -0
- package/src/env/debug/reader/span-db-reader.test.ts +118 -0
- package/src/env/debug/reader/span-db-reader.ts +201 -0
- package/src/env/debug/types.test.ts +187 -0
- package/src/env/debug/types.ts +171 -0
- package/src/env/environment-init.test.ts +183 -0
- package/src/env/environment-lifecycle.test.ts +516 -0
- package/src/env/environment-service.test.ts +332 -0
- package/src/env/environment.handle-query.test.ts +96 -0
- package/src/env/environment.test.ts +232 -0
- package/src/env/environment.ts +708 -0
- package/src/env/errors.test.ts +165 -0
- package/src/env/errors.ts +157 -0
- package/src/env/event-source/event-source-agent-handler.test.ts +193 -0
- package/src/env/event-source/event-source-agent-handler.ts +111 -0
- package/src/env/event-source/event-source-component.process-cleanup.test.ts +236 -0
- package/src/env/event-source/event-source-component.stop.test.ts +346 -0
- package/src/env/event-source/event-source-component.test.ts +1207 -0
- package/src/env/event-source/event-source-component.ts +1379 -0
- package/src/env/event-source/event-source-config-registration.test.ts +242 -0
- package/src/env/event-source/event-source-config-registration.ts +37 -0
- package/src/env/event-source/event-source-integration.test.ts +320 -0
- package/src/env/event-source/event-source-platform.test.ts +630 -0
- package/src/env/event-source/types.ts +298 -0
- package/src/env/hook/global-hook-manager.ts +162 -0
- package/src/env/hook/hook-manager.test.ts +374 -0
- package/src/env/hook/hook-manager.ts +309 -0
- package/src/env/hook/index.ts +38 -0
- package/src/env/hook/types.ts +138 -0
- package/src/env/index.ts +144 -0
- package/src/env/interface.ts +203 -0
- package/src/env/llm/hooks.test.ts +293 -0
- package/src/env/llm/hooks.ts +316 -0
- package/src/env/llm/index.ts +61 -0
- package/src/env/llm/invoke-threshold-check.test.ts +88 -0
- package/src/env/llm/invoke-timeout.test.ts +54 -0
- package/src/env/llm/invoke.test.ts +71 -0
- package/src/env/llm/invoke.ts +1039 -0
- package/src/env/llm/llm-config.test.ts +523 -0
- package/src/env/llm/llm.test.ts +233 -0
- package/src/env/llm/llm.ts +568 -0
- package/src/env/llm/provider.test.ts +182 -0
- package/src/env/llm/provider.ts +108 -0
- package/src/env/llm/transform.test.ts +251 -0
- package/src/env/llm/transform.ts +286 -0
- package/src/env/llm/types.test.ts +580 -0
- package/src/env/llm/types.ts +424 -0
- package/src/env/log-trace/decorator-otel.test.ts +182 -0
- package/src/env/log-trace/decorator.ts +230 -0
- package/src/env/log-trace/index.ts +79 -0
- package/src/env/log-trace/log-trace-component.test.ts +242 -0
- package/src/env/log-trace/log-trace-component.ts +497 -0
- package/src/env/log-trace/log-trace-config-registration.test.ts +348 -0
- package/src/env/log-trace/log-trace-config-registration.ts +45 -0
- package/src/env/log-trace/logger.test.ts +149 -0
- package/src/env/log-trace/logger.ts +522 -0
- package/src/env/log-trace/opentelemetry/cli-propagation.test.ts +147 -0
- package/src/env/log-trace/opentelemetry/cli-propagation.ts +194 -0
- package/src/env/log-trace/opentelemetry/integration.test.ts +668 -0
- package/src/env/log-trace/opentelemetry/mod.ts +25 -0
- package/src/env/log-trace/opentelemetry/propagation-env.test.ts +181 -0
- package/src/env/log-trace/opentelemetry/propagation-env.ts +136 -0
- package/src/env/log-trace/opentelemetry/propagation.test.ts +259 -0
- package/src/env/log-trace/opentelemetry/propagation.ts +215 -0
- package/src/env/log-trace/opentelemetry/tracer-provider-context.test.ts +166 -0
- package/src/env/log-trace/opentelemetry/tracer-provider.test.ts +379 -0
- package/src/env/log-trace/opentelemetry/tracer-provider.ts +612 -0
- package/src/env/log-trace/span-storage.test.ts +145 -0
- package/src/env/log-trace/span-storage.ts +230 -0
- package/src/env/log-trace/trace-context.test.ts +187 -0
- package/src/env/log-trace/trace-context.ts +162 -0
- package/src/env/log-trace/types.test.ts +63 -0
- package/src/env/log-trace/types.ts +172 -0
- package/src/env/mcp/README.md +244 -0
- package/src/env/mcp/__integration__/mcp-component.integration.test.ts +373 -0
- package/src/env/mcp/config.test.ts +74 -0
- package/src/env/mcp/config.ts +116 -0
- package/src/env/mcp/index.ts +41 -0
- package/src/env/mcp/loader.test.ts +161 -0
- package/src/env/mcp/loader.ts +209 -0
- package/src/env/mcp/mcp-component.test.ts +111 -0
- package/src/env/mcp/mcp-component.ts +358 -0
- package/src/env/mcp/mcp-config-registration.test.ts +304 -0
- package/src/env/mcp/mcp-config-registration.ts +50 -0
- package/src/env/mcp/scanner.test.ts +170 -0
- package/src/env/mcp/scanner.ts +246 -0
- package/src/env/mcp/tool/adapter.test.ts +520 -0
- package/src/env/mcp/tool/adapter.ts +521 -0
- package/src/env/mcp/tool/index.ts +5 -0
- package/src/env/mcp/types.test.ts +171 -0
- package/src/env/mcp/types.ts +79 -0
- package/src/env/memory/README.md +177 -0
- package/src/env/memory/built-in/index.ts +59 -0
- package/src/env/memory/built-in/recall-memory.ts +103 -0
- package/src/env/memory/built-in/record-memory.ts +148 -0
- package/src/env/memory/index.ts +20 -0
- package/src/env/memory/memory-component.test.ts +239 -0
- package/src/env/memory/memory-component.ts +503 -0
- package/src/env/memory/memory-config-registration.test.ts +67 -0
- package/src/env/memory/memory-config-registration.ts +48 -0
- package/src/env/memory/memory-config.ts +45 -0
- package/src/env/memory/memory-file.test.ts +268 -0
- package/src/env/memory/plugin/index.ts +48 -0
- package/src/env/memory/plugin/memory-agent.test.ts +249 -0
- package/src/env/memory/plugin/memory-agent.ts +365 -0
- package/src/env/memory/plugin/memory-manager.ts +198 -0
- package/src/env/memory/plugin/memory-plugin-agent.test.ts +145 -0
- package/src/env/memory/plugin/memory-plugin.ts +210 -0
- package/src/env/memory/plugin/plugin-simplified.test.ts +51 -0
- package/src/env/memory/plugin/recall-memory.test.ts +106 -0
- package/src/env/memory/plugin/recall-memory.ts +53 -0
- package/src/env/memory/plugin/types.ts +101 -0
- package/src/env/memory/tools/memory-agent-tools.ts +228 -0
- package/src/env/memory/types.ts +85 -0
- package/src/env/paths.ts +118 -0
- package/src/env/prompt/index.ts +18 -0
- package/src/env/prompt/memory-prompts.test.ts +91 -0
- package/src/env/prompt/prompt-component.test.ts +491 -0
- package/src/env/prompt/prompt-component.ts +619 -0
- package/src/env/prompt/prompt-config-registration.test.ts +213 -0
- package/src/env/prompt/prompt-config-registration.ts +39 -0
- package/src/env/prompt/prompts-index.ts +504 -0
- package/src/env/prompt/renderer.ts +67 -0
- package/src/env/prompt/types.ts +136 -0
- package/src/env/session/hooks.ts +18 -0
- package/src/env/session/index.ts +37 -0
- package/src/env/session/search-query-parser.test.ts +425 -0
- package/src/env/session/search-query-parser.ts +171 -0
- package/src/env/session/session-checkpoint.test.ts +523 -0
- package/src/env/session/session-component.extract-recent-messages.test.ts +209 -0
- package/src/env/session/session-component.test.ts +132 -0
- package/src/env/session/session-component.ts +1249 -0
- package/src/env/session/session-config-registration.test.ts +138 -0
- package/src/env/session/session-config-registration.ts +52 -0
- package/src/env/session/session-message-converter.test.ts +763 -0
- package/src/env/session/session-message-converter.ts +415 -0
- package/src/env/session/session-message-e2e.test.ts +448 -0
- package/src/env/session/session-search.test.ts +391 -0
- package/src/env/session/session-store.test.ts +362 -0
- package/src/env/session/session-store.ts +141 -0
- package/src/env/session/storage/index.ts +6 -0
- package/src/env/session/storage/memory.ts +502 -0
- package/src/env/session/storage/sqlite.ts +794 -0
- package/src/env/session/types.ts +742 -0
- package/src/env/skill/config.ts +39 -0
- package/src/env/skill/index.ts +6 -0
- package/src/env/skill/parser.test.ts +116 -0
- package/src/env/skill/parser.ts +77 -0
- package/src/env/skill/scanner.test.ts +211 -0
- package/src/env/skill/scanner.ts +119 -0
- package/src/env/skill/skill-component.test.ts +234 -0
- package/src/env/skill/skill-component.ts +352 -0
- package/src/env/skill/skill-config-registration.test.ts +60 -0
- package/src/env/skill/skill-config-registration.ts +43 -0
- package/src/env/skill/tool/index.ts +1 -0
- package/src/env/skill/tool/skill-tool.test.ts +100 -0
- package/src/env/skill/tool/skill-tool.ts +72 -0
- package/src/env/skill/types.ts +64 -0
- package/src/env/task/delegate/delegate-tool.test.ts +498 -0
- package/src/env/task/delegate/delegate-tool.ts +1014 -0
- package/src/env/task/delegate/index.ts +18 -0
- package/src/env/task/delegate/stop-tool.test.ts +140 -0
- package/src/env/task/delegate/stop-tool.ts +119 -0
- package/src/env/task/delegate/task-events.test.ts +178 -0
- package/src/env/task/delegate/task-events.ts +143 -0
- package/src/env/task/hooks/contexts.test.ts +92 -0
- package/src/env/task/hooks/contexts.ts +192 -0
- package/src/env/task/hooks/index.ts +23 -0
- package/src/env/task/hooks/task-hook-points.test.ts +32 -0
- package/src/env/task/hooks/task-hook-points.ts +54 -0
- package/src/env/task/index.ts +7 -0
- package/src/env/task/plugins/index.ts +13 -0
- package/src/env/task/plugins/task-plugin.test.ts +74 -0
- package/src/env/task/plugins/task-plugin.ts +89 -0
- package/src/env/task/plugins/task-tag-plugin.test.ts +377 -0
- package/src/env/task/plugins/task-tag-plugin.ts +319 -0
- package/src/env/task/plugins/task-workflow-extractor.integration.test.ts +226 -0
- package/src/env/task/plugins/workflow-extractor-agent.test.ts +107 -0
- package/src/env/task/plugins/workflow-extractor-agent.ts +225 -0
- package/src/env/task/storage/index.ts +6 -0
- package/src/env/task/storage/sqlite-task-store.test.ts +283 -0
- package/src/env/task/storage/sqlite-task-store.ts +903 -0
- package/src/env/task/storage/task-search.test.ts +291 -0
- package/src/env/task/tag-service.test.ts +198 -0
- package/src/env/task/tag-service.ts +264 -0
- package/src/env/task/task-component.test.ts +193 -0
- package/src/env/task/task-component.ts +658 -0
- package/src/env/task/task-config-registration.test.ts +57 -0
- package/src/env/task/task-config-registration.ts +37 -0
- package/src/env/task/task-types.test.ts +137 -0
- package/src/env/task/tools/complete-tool.ts +44 -0
- package/src/env/task/tools/create-tool.ts +49 -0
- package/src/env/task/tools/delete-tool.ts +43 -0
- package/src/env/task/tools/get-tool.ts +59 -0
- package/src/env/task/tools/index.ts +10 -0
- package/src/env/task/tools/list-tool.ts +40 -0
- package/src/env/task/tools/operation/create-tool.ts +48 -0
- package/src/env/task/tools/operation/delete-tool.ts +43 -0
- package/src/env/task/tools/operation/get-tool.ts +43 -0
- package/src/env/task/tools/operation/index.ts +9 -0
- package/src/env/task/tools/operation/list-tool.ts +40 -0
- package/src/env/task/tools/operation/operation-tools.test.ts +274 -0
- package/src/env/task/tools/operation/operation-types.ts +75 -0
- package/src/env/task/tools/operation/update-tool.ts +47 -0
- package/src/env/task/tools/task-tools.test.ts +203 -0
- package/src/env/task/tools/task-types.test.ts +75 -0
- package/src/env/task/tools/task-types.ts +68 -0
- package/src/env/task/tools/update-tool.ts +70 -0
- package/src/env/task/types.ts +160 -0
- package/src/env/tool/built-in/bash.ts +201 -0
- package/src/env/tool/built-in/echo.ts +29 -0
- package/src/env/tool/built-in/edit-file.test.ts +136 -0
- package/src/env/tool/built-in/edit-file.ts +92 -0
- package/src/env/tool/built-in/glob.test.ts +94 -0
- package/src/env/tool/built-in/glob.ts +65 -0
- package/src/env/tool/built-in/grep.test.ts +122 -0
- package/src/env/tool/built-in/grep.ts +108 -0
- package/src/env/tool/built-in/index.ts +44 -0
- package/src/env/tool/built-in/read-file.test.ts +84 -0
- package/src/env/tool/built-in/read-file.ts +75 -0
- package/src/env/tool/built-in/write-file.test.ts +119 -0
- package/src/env/tool/built-in/write-file.ts +68 -0
- package/src/env/tool/index.ts +24 -0
- package/src/env/tool/registry.test.ts +257 -0
- package/src/env/tool/registry.ts +167 -0
- package/src/env/tool/tool-component.test.ts +559 -0
- package/src/env/tool/tool-component.ts +563 -0
- package/src/env/tool/tool-config-registration.test.ts +249 -0
- package/src/env/tool/tool-config-registration.ts +46 -0
- package/src/env/tool/types.ts +267 -0
- package/src/env/tool/validator.test.ts +143 -0
- package/src/env/tool/validator.ts +44 -0
- package/src/env/types.ts +180 -0
- package/src/env/workflow/ask-user-tool-registration.test.ts +216 -0
- package/src/env/workflow/complex-workflow.integration.test.ts +1900 -0
- package/src/env/workflow/decorators/decorator-node.ts +229 -0
- package/src/env/workflow/decorators/decorator.test.ts +196 -0
- package/src/env/workflow/decorators/edge.ts +82 -0
- package/src/env/workflow/decorators/index.ts +31 -0
- package/src/env/workflow/decorators/node-as.ts +98 -0
- package/src/env/workflow/decorators/workflow.ts +54 -0
- package/src/env/workflow/engine/dag-manager.test.ts +570 -0
- package/src/env/workflow/engine/dag-manager.ts +594 -0
- package/src/env/workflow/engine/engine.ts +1422 -0
- package/src/env/workflow/engine/event-bus.test.ts +359 -0
- package/src/env/workflow/engine/event-bus.ts +156 -0
- package/src/env/workflow/engine/executor-agent-session.test.ts +84 -0
- package/src/env/workflow/engine/executor.test.ts +619 -0
- package/src/env/workflow/engine/executor.ts +593 -0
- package/src/env/workflow/engine/index.ts +24 -0
- package/src/env/workflow/engine/node-registry.test.ts +560 -0
- package/src/env/workflow/engine/node-registry.ts +289 -0
- package/src/env/workflow/engine/resume-removed.test.ts +22 -0
- package/src/env/workflow/engine/scheduler.test.ts +715 -0
- package/src/env/workflow/engine/scheduler.ts +318 -0
- package/src/env/workflow/engine/workflow-engine.test.ts +815 -0
- package/src/env/workflow/extractor/workflow-converter.ts +306 -0
- package/src/env/workflow/fixtures.ts +380 -0
- package/src/env/workflow/index.ts +38 -0
- package/src/env/workflow/integration/run-resume-unified.test.ts +186 -0
- package/src/env/workflow/integration/service-integration.test.ts +267 -0
- package/src/env/workflow/metadata/keys.ts +12 -0
- package/src/env/workflow/nodes/agent-component-adapter.test.ts +318 -0
- package/src/env/workflow/nodes/agent-component-adapter.ts +448 -0
- package/src/env/workflow/nodes/agent-node.test.ts +371 -0
- package/src/env/workflow/nodes/agent-node.ts +598 -0
- package/src/env/workflow/nodes/ask-user-node.ts +113 -0
- package/src/env/workflow/nodes/condition-node.ts +200 -0
- package/src/env/workflow/nodes/index.ts +9 -0
- package/src/env/workflow/nodes/merge-node.ts +141 -0
- package/src/env/workflow/nodes/skill-node.test.ts +253 -0
- package/src/env/workflow/nodes/skill-node.ts +393 -0
- package/src/env/workflow/nodes/tool-node.test.ts +251 -0
- package/src/env/workflow/nodes/tool-node.ts +493 -0
- package/src/env/workflow/nodes/workflow-llm-history.test.ts +455 -0
- package/src/env/workflow/nodes/workflow-node.test.ts +315 -0
- package/src/env/workflow/nodes/workflow-node.ts +311 -0
- package/src/env/workflow/service/index.ts +27 -0
- package/src/env/workflow/service/registry.test.ts +133 -0
- package/src/env/workflow/service/registry.ts +71 -0
- package/src/env/workflow/service/workflow-service.test.ts +310 -0
- package/src/env/workflow/service/workflow-service.ts +393 -0
- package/src/env/workflow/storage/index.ts +28 -0
- package/src/env/workflow/storage/mock-repositories.ts +385 -0
- package/src/env/workflow/storage/sqlite.test.ts +179 -0
- package/src/env/workflow/storage/sqlite.ts +163 -0
- package/src/env/workflow/storage/workflow-repo.test.ts +780 -0
- package/src/env/workflow/storage/workflow-repo.ts +342 -0
- package/src/env/workflow/tools/ask-user-tool.ts +82 -0
- package/src/env/workflow/tools/index.ts +26 -0
- package/src/env/workflow/tools/run-workflow.test.ts +352 -0
- package/src/env/workflow/tools/run-workflow.ts +214 -0
- package/src/env/workflow/types/context.ts +18 -0
- package/src/env/workflow/types/decorators-types.ts +198 -0
- package/src/env/workflow/types/event.test.ts +515 -0
- package/src/env/workflow/types/event.ts +193 -0
- package/src/env/workflow/types/index.ts +49 -0
- package/src/env/workflow/types/run.test.ts +437 -0
- package/src/env/workflow/types/run.ts +173 -0
- package/src/env/workflow/types/workflow-hil.ts +114 -0
- package/src/env/workflow/types/workflow-message.test.ts +138 -0
- package/src/env/workflow/types/workflow-message.ts +196 -0
- package/src/env/workflow/types/workflow-session.test.ts +95 -0
- package/src/env/workflow/types/workflow-session.ts +59 -0
- package/src/env/workflow/types/workflow.test.ts +495 -0
- package/src/env/workflow/types/workflow.ts +195 -0
- package/src/env/workflow/types_compat.ts +51 -0
- package/src/env/workflow/utils/create-workflow.ts +47 -0
- package/src/env/workflow/utils/execution-state.ts +245 -0
- package/src/env/workflow/utils/index.ts +18 -0
- package/src/env/workflow/utils/node-registry-helper.ts +58 -0
- package/src/env/workflow/utils/recovery-validator.test.ts +460 -0
- package/src/env/workflow/utils/recovery-validator.ts +377 -0
- package/src/env/workflow/utils/session-parser.test.ts +111 -0
- package/src/env/workflow/utils/session-parser.ts +94 -0
- package/src/env/workflow/utils/session-recovery.test.ts +334 -0
- package/src/env/workflow/utils/session-recovery.ts +188 -0
- package/src/env/workflow/utils/template-resolver.test.ts +258 -0
- package/src/env/workflow/utils/template-resolver.ts +436 -0
- package/src/env/workflow/utils/validation-rules.ts +149 -0
- package/src/env/workflow/workflow-component.ts +544 -0
- package/src/index.ts +422 -0
- package/src/utils/id.ts +21 -0
|
@@ -0,0 +1,1711 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AgentComponent - Agent执行核心组件
|
|
3
|
+
*
|
|
4
|
+
* 基于 roy-agent docs/agent-component-design.md
|
|
5
|
+
* 参考 agent-core Agent.run 实现
|
|
6
|
+
*
|
|
7
|
+
* 核心功能:
|
|
8
|
+
* - ReAct 循环编排
|
|
9
|
+
* - Hook 机制和 Plugin 扩展
|
|
10
|
+
* - 多 Agent 管理
|
|
11
|
+
* - 工具调用
|
|
12
|
+
*
|
|
13
|
+
* 依赖:
|
|
14
|
+
* - ../llm - LLMComponent 用于 LLM 调用
|
|
15
|
+
* - ../log-trace/logger - createLogger 用于日志
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { BaseComponent, type ComponentConfig } from "../component";
|
|
19
|
+
import { createLogger } from "../log-trace/logger";
|
|
20
|
+
import type {
|
|
21
|
+
AgentInstance,
|
|
22
|
+
AgentInstanceConfig,
|
|
23
|
+
AgentRunResult,
|
|
24
|
+
AgentContext,
|
|
25
|
+
Plugin,
|
|
26
|
+
HookContext,
|
|
27
|
+
HookPoint,
|
|
28
|
+
Tool,
|
|
29
|
+
ToolCallResult,
|
|
30
|
+
AgentLLMOutput,
|
|
31
|
+
HookActionType,
|
|
32
|
+
} from "./types";
|
|
33
|
+
import type { EnvEvent } from "../types";
|
|
34
|
+
import type { ModelMessage } from "ai";
|
|
35
|
+
import type { LLMMessage } from "../llm/types";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
|
|
38
|
+
// LLMComponent 和 ToolComponent 类型
|
|
39
|
+
import type { LLMComponent } from "../llm/llm";
|
|
40
|
+
import type { ToolComponent } from "../tool/tool-component";
|
|
41
|
+
import type { ToolContext, ToolExecuteRequest } from "../tool/types";
|
|
42
|
+
import type { ConfigComponent } from "../../config/config-component";
|
|
43
|
+
import { envKeyToConfigKey } from "../../config/env-key";
|
|
44
|
+
import { AGENT_CONFIG_REGISTRATION, AGENT_DEFAULTS } from "./agent-config-registration";
|
|
45
|
+
import { TracedAs } from "../log-trace/decorator";
|
|
46
|
+
// SessionComponent 类型
|
|
47
|
+
import type { SessionComponent, SessionMessage } from "../session/session-component";
|
|
48
|
+
|
|
49
|
+
// Session Message 转换器
|
|
50
|
+
import { SessionMessageConverter } from "../session/session-message-converter";
|
|
51
|
+
|
|
52
|
+
// AskUserError - 用于工作流暂停的特定错误
|
|
53
|
+
import { AskUserError } from "../workflow/types/workflow-hil";
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Logger
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const logger = createLogger("agent:component");
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Type Conversion Utilities
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 将 ModelMessage 转换为 LLMMessage
|
|
67
|
+
*
|
|
68
|
+
* ModelMessage 来自 ai SDK,LLMMessage 是内部格式
|
|
69
|
+
*/
|
|
70
|
+
function toLLMMessage(msg: ModelMessage): LLMMessage {
|
|
71
|
+
// 处理 content 字段
|
|
72
|
+
let content: string;
|
|
73
|
+
let toolCallId: string | undefined;
|
|
74
|
+
let toolName: string | undefined;
|
|
75
|
+
// 使用 any 类型避免 ai SDK 和内部 ToolCall 类型不兼容
|
|
76
|
+
let toolCalls: any[] | undefined;
|
|
77
|
+
|
|
78
|
+
if (typeof msg.content === "string") {
|
|
79
|
+
content = msg.content;
|
|
80
|
+
// 对于 tool 消息,提取 toolCallId 和 toolName
|
|
81
|
+
if (msg.role === "tool") {
|
|
82
|
+
toolCallId = (msg as any).toolCallId;
|
|
83
|
+
toolName = (msg as any).toolName;
|
|
84
|
+
}
|
|
85
|
+
// assistant 消息可能包含 toolCalls(ai SDK v6 格式)
|
|
86
|
+
if (msg.role === "assistant" && (msg as any).toolCalls) {
|
|
87
|
+
toolCalls = (msg as any).toolCalls;
|
|
88
|
+
}
|
|
89
|
+
} else if (Array.isArray(msg.content)) {
|
|
90
|
+
// 处理 tool 消息(AI SDK v6 格式)
|
|
91
|
+
if (msg.role === "tool") {
|
|
92
|
+
// 提取 tool-result 的 output
|
|
93
|
+
const toolResult = msg.content.find((part: any) => part.type === "tool-result") as any;
|
|
94
|
+
// output 可能是 { type: 'text', value: '...' } 或纯字符串
|
|
95
|
+
const output = toolResult?.output;
|
|
96
|
+
if (typeof output === "string") {
|
|
97
|
+
content = output;
|
|
98
|
+
} else if (output && typeof output === "object" && "value" in output) {
|
|
99
|
+
content = output.value?.toString() || "";
|
|
100
|
+
} else {
|
|
101
|
+
content = JSON.stringify(output) || "";
|
|
102
|
+
}
|
|
103
|
+
toolCallId = toolResult?.toolCallId;
|
|
104
|
+
toolName = toolResult?.toolName;
|
|
105
|
+
} else {
|
|
106
|
+
// 处理多模态内容,提取文本和 toolCalls
|
|
107
|
+
const textParts = msg.content.filter((part: any) => part.type === "text");
|
|
108
|
+
const toolCallParts = msg.content.filter((part: any) => part.type === "tool-call");
|
|
109
|
+
|
|
110
|
+
// 提取文本内容
|
|
111
|
+
content = textParts.map((part: any) => part.text || "").join("");
|
|
112
|
+
|
|
113
|
+
// 对于 assistant 消息,从 tool-call part 中提取 toolCalls
|
|
114
|
+
// 注意:toModelMessage 生成的格式是扁平的 { toolCallId, toolName, input }
|
|
115
|
+
// 不是嵌套的 { toolCall: { name, arguments } }
|
|
116
|
+
if (msg.role === "assistant" && toolCallParts.length > 0) {
|
|
117
|
+
toolCalls = toolCallParts.map((part: any) => {
|
|
118
|
+
// input 是已解析的参数对象,需要转回字符串给 convertToSDKMessages
|
|
119
|
+
const argsString = typeof part.input === "string"
|
|
120
|
+
? part.input
|
|
121
|
+
: JSON.stringify(part.input || {});
|
|
122
|
+
const toolName = part.toolName || "unknown";
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id: part.toolCallId,
|
|
126
|
+
name: toolName,
|
|
127
|
+
arguments: argsString,
|
|
128
|
+
function: {
|
|
129
|
+
name: toolName,
|
|
130
|
+
arguments: argsString,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
content = "";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
role: msg.role as "system" | "user" | "assistant" | "tool",
|
|
143
|
+
content,
|
|
144
|
+
...(toolCallId ? { toolCallId } : {}),
|
|
145
|
+
...(toolName ? { name: toolName } : {}),
|
|
146
|
+
...(toolCalls && toolCalls.length > 0 ? { toolCalls } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Config Schema
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* AgentComponent 默认配置
|
|
156
|
+
*/
|
|
157
|
+
const DEFAULT_AGENT_CONFIG = {
|
|
158
|
+
type: "primary" as const,
|
|
159
|
+
maxIterations: 200,
|
|
160
|
+
maxErrorRetries: 3,
|
|
161
|
+
doomLoopThreshold: 5,
|
|
162
|
+
toolTimeout: 60000,
|
|
163
|
+
toolRetries: 3,
|
|
164
|
+
/** 是否过滤 history 中的 tool 消息(默认不过滤,保留完整上下文) */
|
|
165
|
+
filterHistory: false,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Agent 实例配置 Schema
|
|
170
|
+
*/
|
|
171
|
+
const AgentInstanceSchema = z.object({
|
|
172
|
+
type: z.enum(["primary", "sub"]).default("primary"),
|
|
173
|
+
systemPrompt: z.string().optional(),
|
|
174
|
+
behaviorSpecId: z.string().optional(),
|
|
175
|
+
model: z.string().optional(),
|
|
176
|
+
maxIterations: z.number().default(200),
|
|
177
|
+
maxErrorRetries: z.number().default(3),
|
|
178
|
+
doomLoopThreshold: z.number().default(5),
|
|
179
|
+
allowedTools: z.array(z.string()).optional(),
|
|
180
|
+
deniedTools: z.array(z.string()).optional(),
|
|
181
|
+
toolTimeout: z.number().default(60000),
|
|
182
|
+
toolRetries: z.number().default(3),
|
|
183
|
+
/** 是否过滤 history 中的 tool 消息(默认不过滤,保留完整上下文) */
|
|
184
|
+
filterHistory: z.boolean().default(false),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* AgentComponent 配置 Schema
|
|
189
|
+
*/
|
|
190
|
+
export const AgentComponentConfigSchema = z.object({
|
|
191
|
+
defaultAgent: AgentInstanceSchema,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export type AgentComponentConfig = z.infer<typeof AgentComponentConfigSchema>;
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// AgentComponentOptions (统一配置机制)
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* AgentComponent 配置选项(通过 options 传递)
|
|
202
|
+
*
|
|
203
|
+
* 配置加载顺序(优先级从低到高):
|
|
204
|
+
* 1. File - 配置文件(通过 configPath 指定)
|
|
205
|
+
* 2. Env - 环境变量(通过 envPrefix 配置前缀)
|
|
206
|
+
* 3. Object - 直接传入的 config 对象(最高优先级)
|
|
207
|
+
*/
|
|
208
|
+
export interface AgentComponentOptions {
|
|
209
|
+
/** ConfigComponent 实例(必填) */
|
|
210
|
+
configComponent: ConfigComponent;
|
|
211
|
+
|
|
212
|
+
/** 配置文件相对路径(可选,基于 XDG_DATA_HOME) */
|
|
213
|
+
configPath?: string;
|
|
214
|
+
|
|
215
|
+
/** 环境变量前缀(可选,默认 "AGENT") */
|
|
216
|
+
envPrefix?: string;
|
|
217
|
+
|
|
218
|
+
/** 直接传入的配置对象(可选,优先级最高) */
|
|
219
|
+
config?: Partial<AgentComponentConfig>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// AgentComponent
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* AgentComponent
|
|
228
|
+
*
|
|
229
|
+
* 提供 Agent 执行能力和 Hook 扩展机制
|
|
230
|
+
*/
|
|
231
|
+
export class AgentComponent extends BaseComponent {
|
|
232
|
+
readonly name = "agent";
|
|
233
|
+
readonly version = "1.0.0";
|
|
234
|
+
|
|
235
|
+
private agents: Map<string, AgentInstance> = new Map();
|
|
236
|
+
private config: AgentComponentConfig;
|
|
237
|
+
private llmComponent: LLMComponent | null = null;
|
|
238
|
+
private toolComponent: ToolComponent | null = null;
|
|
239
|
+
private aborted: Map<string, boolean> = new Map();
|
|
240
|
+
private abortControllers: Map<string, AbortController> = new Map(); // Agent Run 的 AbortController,使用 runId 作为 key
|
|
241
|
+
private defaultTools: Tool[] = [];
|
|
242
|
+
|
|
243
|
+
// Doom loop detection cache (per-agent for isolation)
|
|
244
|
+
private doomLoopCaches: Map<string, Map<string, number>> = new Map();
|
|
245
|
+
|
|
246
|
+
/** ConfigComponent 用于配置管理 */
|
|
247
|
+
private configComponent?: ConfigComponent;
|
|
248
|
+
|
|
249
|
+
/** 配置变更 watcher 清理函数 */
|
|
250
|
+
private configWatcher?: () => void;
|
|
251
|
+
|
|
252
|
+
/** Session 消息转换器 */
|
|
253
|
+
private messageConverter = new SessionMessageConverter();
|
|
254
|
+
|
|
255
|
+
/** 构造函数传入的配置(最高优先级) */
|
|
256
|
+
private _constructorConfig?: { defaultAgent?: Partial<AgentComponentConfig["defaultAgent"]> };
|
|
257
|
+
|
|
258
|
+
/** Run ID 计数器(用于生成唯一的 runId) */
|
|
259
|
+
private runCounter = 0;
|
|
260
|
+
|
|
261
|
+
constructor(options: {
|
|
262
|
+
config?: Partial<AgentComponentConfig>;
|
|
263
|
+
} = {}) {
|
|
264
|
+
super();
|
|
265
|
+
|
|
266
|
+
// 保存构造函数传入的配置(最高优先级)
|
|
267
|
+
this._constructorConfig = options.config;
|
|
268
|
+
|
|
269
|
+
this.config = {
|
|
270
|
+
defaultAgent: {
|
|
271
|
+
...DEFAULT_AGENT_CONFIG,
|
|
272
|
+
...options.config?.defaultAgent,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 初始化组件
|
|
279
|
+
*
|
|
280
|
+
* 配置加载优先级(从高到低):
|
|
281
|
+
* 1. Object - 直接传入的 config 对象
|
|
282
|
+
* 2. Env - 环境变量(通过 envPrefix 配置前缀)
|
|
283
|
+
* 3. File - 配置文件(通过 configPath 指定)
|
|
284
|
+
*/
|
|
285
|
+
async init(config: ComponentConfig): Promise<void> {
|
|
286
|
+
// 调用基类 init,注入 env
|
|
287
|
+
await super.init(config);
|
|
288
|
+
|
|
289
|
+
// 从 options 获取 ConfigComponent
|
|
290
|
+
const options = config.options as unknown as AgentComponentOptions | undefined;
|
|
291
|
+
if (!options?.configComponent) {
|
|
292
|
+
throw new Error("ConfigComponent is required for AgentComponent initialization");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.configComponent = options.configComponent;
|
|
296
|
+
await this.registerConfig(options);
|
|
297
|
+
|
|
298
|
+
this.setStatus("running");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 注册配置到 ConfigComponent
|
|
303
|
+
*
|
|
304
|
+
* 配置加载顺序(优先级从低到高):
|
|
305
|
+
* 1. Defaults - 默认值(最低)
|
|
306
|
+
* 2. FileSource - 从配置文件加载
|
|
307
|
+
* 3. EnvSource - 从环境变量加载
|
|
308
|
+
* 4. config 对象 - 直接传入的配置(最高)
|
|
309
|
+
*/
|
|
310
|
+
private async registerConfig(options: AgentComponentOptions): Promise<void> {
|
|
311
|
+
const configComponent = options.configComponent;
|
|
312
|
+
if (!configComponent) return;
|
|
313
|
+
|
|
314
|
+
const { configPath, envPrefix, config } = options;
|
|
315
|
+
const prefix = envPrefix ?? "AGENT";
|
|
316
|
+
|
|
317
|
+
// 1. 使用 registerComponent 注册配置结构(keys 和 sources,不含 defaults)
|
|
318
|
+
configComponent.registerComponent(AGENT_CONFIG_REGISTRATION);
|
|
319
|
+
|
|
320
|
+
// 2. 注册 FileSource(如果提供了 configPath)
|
|
321
|
+
if (configPath) {
|
|
322
|
+
configComponent.registerSource({
|
|
323
|
+
type: "file",
|
|
324
|
+
relativePath: configPath,
|
|
325
|
+
optional: true,
|
|
326
|
+
watch: false
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 3. 注册 EnvSource
|
|
331
|
+
configComponent.registerSource({
|
|
332
|
+
type: "env",
|
|
333
|
+
envPrefix: prefix,
|
|
334
|
+
priority: 20,
|
|
335
|
+
watch: false
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// 4. 从 sources 加载配置(FileSource 和 EnvSource)
|
|
339
|
+
await configComponent.load("agent");
|
|
340
|
+
|
|
341
|
+
// 5. 后备方案:直接读取环境变量(确保所有环境变量都被处理)
|
|
342
|
+
for (const envKey of Object.keys(process.env)) {
|
|
343
|
+
const configKey = envKeyToConfigKey(envKey, prefix, "agent");
|
|
344
|
+
if (!configKey) continue;
|
|
345
|
+
const value = process.env[envKey];
|
|
346
|
+
if (value !== undefined) {
|
|
347
|
+
await configComponent.set(configKey, value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 6. 设置默认值(只有当配置不存在时)
|
|
352
|
+
for (const [key, value] of Object.entries(AGENT_DEFAULTS)) {
|
|
353
|
+
if (configComponent.get(key) === undefined) {
|
|
354
|
+
await configComponent.set(key, value);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 7. 直接配置对象(最高优先级)
|
|
359
|
+
if (config) {
|
|
360
|
+
const flatConfig = this.flattenConfig(config);
|
|
361
|
+
for (const [key, value] of Object.entries(flatConfig)) {
|
|
362
|
+
await configComponent.set(key, value);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 8. 从 ConfigComponent 同步配置到 this.config
|
|
367
|
+
// 重要:配置文件/环境变量的值必须同步回实例配置,供 registerAgent() 使用
|
|
368
|
+
// 注意:构造函数传入的配置优先级最高
|
|
369
|
+
const constructorDefaultAgent = this._constructorConfig?.defaultAgent;
|
|
370
|
+
|
|
371
|
+
// 从 ConfigComponent 读取值,并设置默认值
|
|
372
|
+
// 构造函数传入的值优先级最高,会覆盖从 ConfigComponent 读取的值
|
|
373
|
+
this.config.defaultAgent = {
|
|
374
|
+
maxIterations: constructorDefaultAgent?.maxIterations
|
|
375
|
+
?? (configComponent.get("agent.defaultAgent.maxIterations") as number)
|
|
376
|
+
?? 200,
|
|
377
|
+
maxErrorRetries: constructorDefaultAgent?.maxErrorRetries
|
|
378
|
+
?? (configComponent.get("agent.defaultAgent.maxErrorRetries") as number)
|
|
379
|
+
?? 3,
|
|
380
|
+
doomLoopThreshold: constructorDefaultAgent?.doomLoopThreshold
|
|
381
|
+
?? (configComponent.get("agent.defaultAgent.doomLoopThreshold") as number)
|
|
382
|
+
?? 5,
|
|
383
|
+
toolTimeout: constructorDefaultAgent?.toolTimeout
|
|
384
|
+
?? (configComponent.get("agent.defaultAgent.toolTimeout") as number)
|
|
385
|
+
?? 60000,
|
|
386
|
+
toolRetries: constructorDefaultAgent?.toolRetries
|
|
387
|
+
?? (configComponent.get("agent.defaultAgent.toolRetries") as number)
|
|
388
|
+
?? 3,
|
|
389
|
+
filterHistory: constructorDefaultAgent?.filterHistory
|
|
390
|
+
?? (configComponent.get("agent.defaultAgent.filterHistory") as boolean)
|
|
391
|
+
?? false,
|
|
392
|
+
type: constructorDefaultAgent?.type
|
|
393
|
+
?? (configComponent.get("agent.defaultAgent.type") as "primary" | "sub")
|
|
394
|
+
?? "primary",
|
|
395
|
+
systemPrompt: constructorDefaultAgent?.systemPrompt
|
|
396
|
+
?? (configComponent.get("agent.defaultAgent.systemPrompt") as string | undefined),
|
|
397
|
+
model: constructorDefaultAgent?.model
|
|
398
|
+
?? (configComponent.get("agent.defaultAgent.model") as string | undefined),
|
|
399
|
+
behaviorSpecId: constructorDefaultAgent?.behaviorSpecId
|
|
400
|
+
?? (configComponent.get("agent.defaultAgent.behaviorSpecId") as string | undefined),
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
logger.info(`[registerConfig] Synced config from ConfigComponent: maxIterations=${this.config.defaultAgent.maxIterations}`);
|
|
404
|
+
|
|
405
|
+
// 9. 注册配置热更新监听
|
|
406
|
+
this.registerConfigWatcher(configComponent);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 将配置对象展平为点号路径
|
|
411
|
+
*/
|
|
412
|
+
private flattenConfig(
|
|
413
|
+
obj: Record<string, unknown>,
|
|
414
|
+
prefix = "agent"
|
|
415
|
+
): Record<string, unknown> {
|
|
416
|
+
const result: Record<string, unknown> = {};
|
|
417
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
418
|
+
const fullKey = `${prefix}.${key}`;
|
|
419
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
420
|
+
Object.assign(
|
|
421
|
+
result,
|
|
422
|
+
this.flattenConfig(value as Record<string, unknown>, fullKey)
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
result[fullKey] = value;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 注册配置热更新监听
|
|
433
|
+
*/
|
|
434
|
+
private registerConfigWatcher(configComponent: ConfigComponent): void {
|
|
435
|
+
if (typeof configComponent.watch !== "function") {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.configWatcher = configComponent.watch("agent.*", (event) => {
|
|
440
|
+
this.onConfigChange(event);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 处理配置变更
|
|
446
|
+
*/
|
|
447
|
+
protected onConfigChange(event: { key: string; oldValue?: unknown; newValue?: unknown }): void {
|
|
448
|
+
logger.info(`Agent config changed: ${event.key}`, {
|
|
449
|
+
oldValue: event.oldValue,
|
|
450
|
+
newValue: event.newValue
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 组件停止时的清理逻辑
|
|
456
|
+
*
|
|
457
|
+
* 清理内容:
|
|
458
|
+
* 1. 终止所有正在运行的 Agent(调用 abort() 以中断 LLM 调用)
|
|
459
|
+
* 2. 清空 AbortController 注册表
|
|
460
|
+
* 3. 清空 Doom Loop 缓存
|
|
461
|
+
* 4. 取消配置变更监听
|
|
462
|
+
* 5. 清空 Agent 注册表
|
|
463
|
+
*/
|
|
464
|
+
protected async onStop(): Promise<void> {
|
|
465
|
+
logger.info("[AgentComponent] Stopping and cleaning up resources...");
|
|
466
|
+
|
|
467
|
+
// 1. 终止所有正在运行的 Agent(调用 abort() 以中断 LLM 调用)
|
|
468
|
+
for (const [name, agent] of this.agents) {
|
|
469
|
+
if (agent.status === "running" || agent.status === "idle") {
|
|
470
|
+
// 调用 abort() 方法会触发 AbortController.abort(),从而中断正在进行的 LLM 调用
|
|
471
|
+
this.abort(name);
|
|
472
|
+
logger.debug(`[AgentComponent] Aborted agent: ${name}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 2. 清空 AbortController 注册表
|
|
477
|
+
this.abortControllers.clear();
|
|
478
|
+
|
|
479
|
+
// 3. 清空 Doom Loop 缓存
|
|
480
|
+
this.doomLoopCaches.clear();
|
|
481
|
+
|
|
482
|
+
// 4. 取消配置变更监听
|
|
483
|
+
if (this.configWatcher) {
|
|
484
|
+
this.configWatcher();
|
|
485
|
+
this.configWatcher = undefined;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 5. 清空 Agent 注册表
|
|
489
|
+
this.agents.clear();
|
|
490
|
+
|
|
491
|
+
this.setStatus("stopped");
|
|
492
|
+
logger.info("[AgentComponent] Cleanup completed");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 刷新依赖组件引用
|
|
497
|
+
*
|
|
498
|
+
* 当组件被添加到 Environment 后调用,以确保获取到最新的组件引用
|
|
499
|
+
*/
|
|
500
|
+
refreshDependencies(): void {
|
|
501
|
+
if (this.env) {
|
|
502
|
+
const llm = this.env.getComponent("llm");
|
|
503
|
+
const tool = this.env.getComponent("tool");
|
|
504
|
+
this.llmComponent = llm ? llm as unknown as LLMComponent : null;
|
|
505
|
+
this.toolComponent = tool ? tool as unknown as ToolComponent : null;
|
|
506
|
+
|
|
507
|
+
logger.debug("[refreshDependencies] AgentComponent refreshed", {
|
|
508
|
+
hasLLM: !!this.llmComponent,
|
|
509
|
+
hasTool: !!this.toolComponent,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 获取 Agent 实例
|
|
516
|
+
*/
|
|
517
|
+
getAgent(agentName: string): AgentInstance | undefined {
|
|
518
|
+
return this.agents.get(agentName);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 获取所有 Agent 实例
|
|
523
|
+
*/
|
|
524
|
+
listAgents(): AgentInstance[] {
|
|
525
|
+
return Array.from(this.agents.values());
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Add placeholder tool results for remaining unprocessed tool calls
|
|
530
|
+
*
|
|
531
|
+
* This ensures that the message history maintains a 1:1 correspondence
|
|
532
|
+
* between tool-calls and tool-results, which is required by AI SDK v6.
|
|
533
|
+
*
|
|
534
|
+
* @param ctx - Hook context containing messages
|
|
535
|
+
* @param allToolCalls - All tool calls from the LLM response
|
|
536
|
+
* @param processedCount - Number of tool calls that have been processed
|
|
537
|
+
* @param reason - Reason for the abort/interruption
|
|
538
|
+
*/
|
|
539
|
+
private addRemainingToolResults(
|
|
540
|
+
ctx: HookContext,
|
|
541
|
+
allToolCalls: Array<{ id: string; name?: string; function?: { name?: string; arguments?: string | unknown } }>,
|
|
542
|
+
processedCount: number,
|
|
543
|
+
reason: string
|
|
544
|
+
): void {
|
|
545
|
+
const remainingToolCalls = allToolCalls.slice(processedCount);
|
|
546
|
+
|
|
547
|
+
for (const tc of remainingToolCalls) {
|
|
548
|
+
const tcName = tc.function?.name || tc.name || "unknown";
|
|
549
|
+
|
|
550
|
+
this.pushMessage(ctx, {
|
|
551
|
+
role: "tool",
|
|
552
|
+
content: [{
|
|
553
|
+
type: "tool-result" as const,
|
|
554
|
+
toolCallId: tc.id,
|
|
555
|
+
toolName: tcName,
|
|
556
|
+
output: JSON.stringify({ error: reason }),
|
|
557
|
+
}],
|
|
558
|
+
} as unknown as ModelMessage);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 注册 Agent
|
|
564
|
+
*
|
|
565
|
+
* 配置优先级(从高到低):
|
|
566
|
+
* 1. registerAgent 传入的 config(来自 PromptComponent)
|
|
567
|
+
* 2. defaultAgent 配置(来自 ConfigComponent,作为 fallback)
|
|
568
|
+
*/
|
|
569
|
+
registerAgent(name: string, config: Partial<AgentInstanceConfig>): AgentInstance {
|
|
570
|
+
// 合并配置:defaultAgent 填充默认值,传入的 config 覆盖
|
|
571
|
+
// 注意:需要显式设置每个字段,避免 spread 操作导致默认值被覆盖的问题
|
|
572
|
+
const mergedConfig = {
|
|
573
|
+
// 基础字段
|
|
574
|
+
type: config.type ?? this.config.defaultAgent.type,
|
|
575
|
+
name,
|
|
576
|
+
// 数值类型字段(显式设置以避免被覆盖)
|
|
577
|
+
maxIterations: config.maxIterations ?? this.config.defaultAgent.maxIterations,
|
|
578
|
+
maxErrorRetries: config.maxErrorRetries ?? this.config.defaultAgent.maxErrorRetries,
|
|
579
|
+
doomLoopThreshold: config.doomLoopThreshold ?? this.config.defaultAgent.doomLoopThreshold,
|
|
580
|
+
toolTimeout: config.toolTimeout ?? this.config.defaultAgent.toolTimeout,
|
|
581
|
+
toolRetries: config.toolRetries ?? this.config.defaultAgent.toolRetries,
|
|
582
|
+
// 字符串类型字段
|
|
583
|
+
systemPrompt: config.systemPrompt ?? this.config.defaultAgent.systemPrompt,
|
|
584
|
+
model: config.model ?? this.config.defaultAgent.model,
|
|
585
|
+
behaviorSpecId: config.behaviorSpecId ?? this.config.defaultAgent.behaviorSpecId,
|
|
586
|
+
// 工具过滤字段
|
|
587
|
+
allowedTools: config.allowedTools ?? this.config.defaultAgent.allowedTools,
|
|
588
|
+
deniedTools: config.deniedTools ?? this.config.defaultAgent.deniedTools,
|
|
589
|
+
// History 过滤字段
|
|
590
|
+
filterHistory: config.filterHistory ?? this.config.defaultAgent.filterHistory,
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const instance: AgentInstance = {
|
|
594
|
+
name,
|
|
595
|
+
config: mergedConfig as AgentInstance["config"],
|
|
596
|
+
status: "idle",
|
|
597
|
+
plugins: new Map(),
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
this.agents.set(name, instance);
|
|
601
|
+
logger.info(`Agent registered: ${name}`, { type: instance.config.type });
|
|
602
|
+
return instance;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* 注销 Agent
|
|
607
|
+
*/
|
|
608
|
+
unregisterAgent(name: string): boolean {
|
|
609
|
+
const deleted = this.agents.delete(name);
|
|
610
|
+
if (deleted) {
|
|
611
|
+
logger.info(`Agent unregistered: ${name}`);
|
|
612
|
+
}
|
|
613
|
+
return deleted;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 注册 Plugin 到指定 Agent
|
|
618
|
+
*/
|
|
619
|
+
registerPlugin(agentName: string, plugin: Plugin): void {
|
|
620
|
+
const agent = this.agents.get(agentName);
|
|
621
|
+
if (!agent) {
|
|
622
|
+
throw new Error(`Agent not found: ${agentName}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
agent.plugins.set(plugin.name, plugin);
|
|
626
|
+
logger.info(`Plugin registered: ${plugin.name} to ${agentName}`, {
|
|
627
|
+
hooks: plugin.hooks.map(h => h.point),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* 注销 Plugin
|
|
633
|
+
*/
|
|
634
|
+
unregisterPlugin(agentName: string, pluginName: string): boolean {
|
|
635
|
+
const agent = this.agents.get(agentName);
|
|
636
|
+
if (!agent) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const deleted = agent.plugins.delete(pluginName);
|
|
641
|
+
if (deleted) {
|
|
642
|
+
logger.info(`Plugin unregistered: ${pluginName} from ${agentName}`);
|
|
643
|
+
}
|
|
644
|
+
return deleted;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 设置默认工具列表
|
|
649
|
+
*/
|
|
650
|
+
setDefaultTools(tools: Tool[]): void {
|
|
651
|
+
this.defaultTools = tools;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 获取可用工具
|
|
656
|
+
*/
|
|
657
|
+
getAvailableTools(agent: AgentInstance, context?: AgentContext): Tool[] {
|
|
658
|
+
// 首先添加 defaultTools(来自 setDefaultTools 的 Memory Agent tools)
|
|
659
|
+
let tools: Tool[] = [...this.defaultTools];
|
|
660
|
+
|
|
661
|
+
logger.debug(`[getAvailableTools] After defaultTools: ${tools.length} tools`, {
|
|
662
|
+
toolNames: tools.map(t => t.name)
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// 如果有 ToolComponent,从组件获取
|
|
666
|
+
if (this.toolComponent?.listTools) {
|
|
667
|
+
// 类型转换:ToolComponent 的 Tool 与 AgentComponent 的 Tool 兼容
|
|
668
|
+
const componentTools = this.toolComponent.listTools() as unknown as Tool[];
|
|
669
|
+
tools = [...tools, ...componentTools];
|
|
670
|
+
logger.debug(`[getAvailableTools] After adding componentTools: ${tools.length} tools`, {
|
|
671
|
+
componentToolNames: componentTools.map(t => t.name)
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 获取工具过滤配置(context 优先于 agent 配置)
|
|
676
|
+
const allowedTools = context?.allowedTools ?? agent.config.allowedTools;
|
|
677
|
+
const deniedTools = context?.deniedTools ?? agent.config.deniedTools;
|
|
678
|
+
|
|
679
|
+
logger.debug(`[getAvailableTools] Filtering: allowed=${allowedTools}, denied=${deniedTools}`);
|
|
680
|
+
|
|
681
|
+
// 应用 allowedTools 过滤
|
|
682
|
+
if (allowedTools?.length) {
|
|
683
|
+
tools = tools.filter(t => allowedTools.includes(t.name));
|
|
684
|
+
logger.debug(`[getAvailableTools] After allowedTools filter: ${tools.length} tools`, {
|
|
685
|
+
toolNames: tools.map(t => t.name)
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 应用 deniedTools 过滤
|
|
690
|
+
if (deniedTools?.length) {
|
|
691
|
+
tools = tools.filter(t => !deniedTools.includes(t.name));
|
|
692
|
+
logger.debug(`[getAvailableTools] After deniedTools filter: ${tools.length} tools`, {
|
|
693
|
+
toolNames: tools.map(t => t.name)
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
logger.debug(`[getAvailableTools] Final tools: ${tools.length}`, {
|
|
698
|
+
toolNames: tools.map(t => t.name)
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
return tools;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* 执行 Agent
|
|
706
|
+
*
|
|
707
|
+
* @param agentName Agent 名称
|
|
708
|
+
* @param query 用户查询
|
|
709
|
+
* @param context 执行上下文
|
|
710
|
+
* @returns Agent 执行结果
|
|
711
|
+
*/
|
|
712
|
+
async run(
|
|
713
|
+
agentName: string,
|
|
714
|
+
query: string,
|
|
715
|
+
context?: AgentContext
|
|
716
|
+
): Promise<AgentRunResult> {
|
|
717
|
+
return this._run(agentName, query, context);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private pushMessage(hookCtx: HookContext, message: ModelMessage){
|
|
721
|
+
hookCtx.messages.push(message);
|
|
722
|
+
this.notifyMessageAdded(message);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* 执行 Agent(核心方法,内部实现)
|
|
727
|
+
*/
|
|
728
|
+
@TracedAs("agent.component.run", { recordParams: true, recordResult: true, log: true })
|
|
729
|
+
private async _run(
|
|
730
|
+
agentName: string,
|
|
731
|
+
query: string,
|
|
732
|
+
context?: AgentContext
|
|
733
|
+
): Promise<AgentRunResult> {
|
|
734
|
+
// 刷新依赖组件引用(确保获取到最新的组件)
|
|
735
|
+
this.refreshDependencies();
|
|
736
|
+
|
|
737
|
+
const agent = this.getAgent(agentName);
|
|
738
|
+
if (!agent) {
|
|
739
|
+
throw new Error(`Agent not found: ${agentName}`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 生成唯一的 runId(支持并发调用同一个 agent)
|
|
743
|
+
const runId = `${agentName}:${++this.runCounter}`;
|
|
744
|
+
logger.info(`Starting agent run: ${agentName} (runId=${runId})`, { query: query.substring(0, 100) });
|
|
745
|
+
|
|
746
|
+
// Mark agent as running
|
|
747
|
+
agent.status = "running";
|
|
748
|
+
this.aborted.set(runId, false);
|
|
749
|
+
|
|
750
|
+
// Create AbortController for this agent run
|
|
751
|
+
const abortController = new AbortController();
|
|
752
|
+
this.abortControllers.set(runId, abortController);
|
|
753
|
+
|
|
754
|
+
// Initialize doom loop cache for this agent
|
|
755
|
+
if (!this.doomLoopCaches.has(runId)) {
|
|
756
|
+
this.doomLoopCaches.set(runId, new Map());
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Merge context
|
|
760
|
+
const effectiveContext: AgentContext = {
|
|
761
|
+
...context,
|
|
762
|
+
agentType: agent.config.type,
|
|
763
|
+
model: agent.config.model,
|
|
764
|
+
// 设置 abort signal(支持通过 abort() 方法中断 LLM 调用)
|
|
765
|
+
abort: abortController.signal,
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Initialize result
|
|
769
|
+
const result: AgentRunResult = {
|
|
770
|
+
iterations: 0,
|
|
771
|
+
toolCalls: [],
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// Create HookContext
|
|
775
|
+
const hookCtx: HookContext = {
|
|
776
|
+
agent,
|
|
777
|
+
iteration: 0,
|
|
778
|
+
maxIterations: agent.config.maxIterations,
|
|
779
|
+
messages: [],
|
|
780
|
+
tools: this.getAvailableTools(agent, effectiveContext),
|
|
781
|
+
systemPrompt: agent.config.systemPrompt || "You are a helpful assistant.",
|
|
782
|
+
context: effectiveContext,
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// Load session history messages
|
|
786
|
+
let historyMessageCount = 0;
|
|
787
|
+
if (effectiveContext.sessionId) {
|
|
788
|
+
try {
|
|
789
|
+
const sessionComponent = this.env.getComponent<SessionComponent>("session");
|
|
790
|
+
if (sessionComponent) {
|
|
791
|
+
// Get conversation history starting from the N-th latest user message
|
|
792
|
+
// This ensures we have complete conversation context
|
|
793
|
+
// Determine filterHistory: context > agent config > default (false)
|
|
794
|
+
const filterHistory = effectiveContext.filterHistory ?? agent.config.filterHistory ?? false;
|
|
795
|
+
const messages = await this.getConversationHistory(
|
|
796
|
+
sessionComponent,
|
|
797
|
+
effectiveContext.sessionId,
|
|
798
|
+
{ minUserMessages: 100, filterHistory }
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
hookCtx.messages = messages.map(msg => this.messageConverter.toModelMessage(msg)) as unknown as ModelMessage[];
|
|
802
|
+
historyMessageCount = hookCtx.messages.length;
|
|
803
|
+
}
|
|
804
|
+
} catch (err) {
|
|
805
|
+
logger.warn(`Failed to load session history: ${err}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Send start event via env
|
|
810
|
+
this.pushEnvEvent({
|
|
811
|
+
type: "agent.start",
|
|
812
|
+
payload: {
|
|
813
|
+
sessionId: effectiveContext.sessionId,
|
|
814
|
+
model: effectiveContext.model,
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// Always add user query to messages
|
|
819
|
+
// History messages are already loaded above, this is the new user input
|
|
820
|
+
this.pushMessage(hookCtx, {
|
|
821
|
+
role: "user",
|
|
822
|
+
content: query,
|
|
823
|
+
} as unknown as ModelMessage);
|
|
824
|
+
|
|
825
|
+
// before.start hook
|
|
826
|
+
await this.executePluginHooks(agent, "agent:before.start", hookCtx);
|
|
827
|
+
if (hookCtx._stopped) {
|
|
828
|
+
return this.finalizeResult(result, hookCtx);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ReAct Loop
|
|
832
|
+
let iteration = 0;
|
|
833
|
+
let finalText: string | undefined;
|
|
834
|
+
|
|
835
|
+
while (iteration < agent.config.maxIterations) {
|
|
836
|
+
// Check _stopped state (may be set by previous iteration's hooks)
|
|
837
|
+
if (hookCtx._stopped) {
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Check abort
|
|
842
|
+
if (this.aborted.get(runId)) {
|
|
843
|
+
hookCtx._stopped = true;
|
|
844
|
+
hookCtx._stopReason = "aborted";
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Check abort signal
|
|
849
|
+
if (effectiveContext.abort?.aborted) {
|
|
850
|
+
hookCtx._stopped = true;
|
|
851
|
+
hookCtx._stopReason = "abort_signal";
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
iteration++;
|
|
856
|
+
hookCtx.iteration = iteration;
|
|
857
|
+
result.iterations = iteration;
|
|
858
|
+
|
|
859
|
+
logger.debug(`Iteration ${iteration} started`);
|
|
860
|
+
|
|
861
|
+
// Reset doom loop cache at the start of each iteration
|
|
862
|
+
// Doom loop detection should only apply within a single LLM response cycle
|
|
863
|
+
this.doomLoopCaches.set(runId, new Map());
|
|
864
|
+
|
|
865
|
+
// 设置迭代信息,供 before.llm hook 使用(如 ReminderPlugin)
|
|
866
|
+
hookCtx.maxIterations = agent.config.maxIterations;
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
|
|
870
|
+
// 记录消息构建结果
|
|
871
|
+
logger.debug(`[ReAct] Iteration ${iteration} buildMessages result: ${hookCtx.messages.length} messages`);
|
|
872
|
+
|
|
873
|
+
// before.llm hook
|
|
874
|
+
await this.executePluginHooks(agent, "agent:before.llm", hookCtx);
|
|
875
|
+
if (hookCtx._stopped) break;
|
|
876
|
+
|
|
877
|
+
// Call LLM
|
|
878
|
+
const llmOutput = await this.invokeLLM(hookCtx);
|
|
879
|
+
hookCtx.llmOutput = llmOutput;
|
|
880
|
+
|
|
881
|
+
// Check abort after LLM call (important: abort might have interrupted the call)
|
|
882
|
+
if (this.aborted.get(runId) || effectiveContext.abort?.aborted) {
|
|
883
|
+
hookCtx._stopped = true;
|
|
884
|
+
hookCtx._stopReason = "aborted";
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// after.llm hook
|
|
889
|
+
await this.executePluginHooks(agent, "agent:after.llm", hookCtx);
|
|
890
|
+
if (hookCtx._stopped) break;
|
|
891
|
+
|
|
892
|
+
// Process LLM output
|
|
893
|
+
if (llmOutput.content && !llmOutput.toolCalls?.length) {
|
|
894
|
+
// Text response - complete
|
|
895
|
+
// Check abort before adding message and breaking
|
|
896
|
+
if (this.aborted.get(runId) || effectiveContext.abort?.aborted) {
|
|
897
|
+
hookCtx._stopped = true;
|
|
898
|
+
hookCtx._stopReason = "aborted";
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
finalText = llmOutput.content;
|
|
902
|
+
this.pushMessage(hookCtx, {
|
|
903
|
+
role: "assistant",
|
|
904
|
+
content: llmOutput.content,
|
|
905
|
+
} as unknown as ModelMessage);
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (llmOutput.toolCalls?.length) {
|
|
910
|
+
// Add assistant message with tool calls to messages (CRITICAL: must be before tool results)
|
|
911
|
+
// AI SDK v6 format: assistant message with content array containing text/reasoning/tool-call parts
|
|
912
|
+
|
|
913
|
+
// Build assistant message content parts: text/reasoning + tool-call parts
|
|
914
|
+
const assistantParts: any[] = [];
|
|
915
|
+
|
|
916
|
+
// 1. Add text content if present
|
|
917
|
+
if (llmOutput.content) {
|
|
918
|
+
assistantParts.push({
|
|
919
|
+
type: "text" as const,
|
|
920
|
+
text: llmOutput.content,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 2. Add reasoning content if present (for reasoning models)
|
|
925
|
+
if (llmOutput.reasoning) {
|
|
926
|
+
assistantParts.push({
|
|
927
|
+
type: "reasoning" as const,
|
|
928
|
+
text: llmOutput.reasoning,
|
|
929
|
+
reasoningType: "core",
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// 3. Add tool-call parts
|
|
934
|
+
for (const tc of llmOutput.toolCalls) {
|
|
935
|
+
let argsStr = "";
|
|
936
|
+
let argsObj: Record<string, unknown> = {};
|
|
937
|
+
|
|
938
|
+
// 提取参数:优先从 function.arguments 获取
|
|
939
|
+
if (tc.function?.arguments) {
|
|
940
|
+
if (typeof tc.function.arguments === "string") {
|
|
941
|
+
argsStr = tc.function.arguments;
|
|
942
|
+
try {
|
|
943
|
+
argsObj = JSON.parse(argsStr);
|
|
944
|
+
} catch {
|
|
945
|
+
argsObj = { _raw: argsStr };
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
argsObj = tc.function.arguments as Record<string, unknown>;
|
|
949
|
+
argsStr = JSON.stringify(argsObj);
|
|
950
|
+
}
|
|
951
|
+
} else if (tc.arguments) {
|
|
952
|
+
if (typeof tc.arguments === "string") {
|
|
953
|
+
argsStr = tc.arguments;
|
|
954
|
+
try {
|
|
955
|
+
argsObj = JSON.parse(argsStr);
|
|
956
|
+
} catch {
|
|
957
|
+
argsObj = { _raw: argsStr };
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
argsObj = tc.arguments as Record<string, unknown>;
|
|
961
|
+
argsStr = JSON.stringify(argsObj);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
assistantParts.push({
|
|
966
|
+
type: "tool-call" as const,
|
|
967
|
+
toolCallId: tc.id,
|
|
968
|
+
toolName: tc.function?.name || tc.name || "unknown",
|
|
969
|
+
// 关键修复:添加 input 字段供 session-message-converter 读取和存储
|
|
970
|
+
input: argsObj,
|
|
971
|
+
// 保留 toolCall 以兼容某些依赖此结构的代码
|
|
972
|
+
toolCall: {
|
|
973
|
+
name: tc.function?.name || tc.name || "unknown",
|
|
974
|
+
arguments: argsStr,
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// 记录添加 assistant 消息
|
|
980
|
+
const assistantContent = llmOutput.content?.substring(0, 80) || "(no text)";
|
|
981
|
+
const toolCallNames = llmOutput.toolCalls?.map(tc => tc.function?.name || tc.name).join(", ") || "none";
|
|
982
|
+
logger.debug(`[ReAct] Iteration ${iteration} Adding assistant message: text="${assistantContent}" toolCalls=[${toolCallNames}]`);
|
|
983
|
+
|
|
984
|
+
this.pushMessage(hookCtx, {
|
|
985
|
+
role: "assistant",
|
|
986
|
+
content: assistantParts,
|
|
987
|
+
} as unknown as ModelMessage);
|
|
988
|
+
|
|
989
|
+
// Tool calls
|
|
990
|
+
const allToolCalls = llmOutput.toolCalls;
|
|
991
|
+
let processedCount = 0; // Track number of successfully processed tool calls
|
|
992
|
+
|
|
993
|
+
for (const toolCall of allToolCalls) {
|
|
994
|
+
// Check abort before tool execution
|
|
995
|
+
if (this.aborted.get(runId) || effectiveContext.abort?.aborted) {
|
|
996
|
+
hookCtx._stopped = true;
|
|
997
|
+
hookCtx._stopReason = "aborted";
|
|
998
|
+
// Add placeholder results for remaining unprocessed tool calls
|
|
999
|
+
this.addRemainingToolResults(hookCtx, allToolCalls, processedCount, "Execution aborted");
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// 优先使用 function.name,兼容旧格式
|
|
1004
|
+
const func = toolCall.function;
|
|
1005
|
+
const tcName = func?.name || toolCall.name || "unknown";
|
|
1006
|
+
const tcArgs = func?.arguments
|
|
1007
|
+
? (typeof func.arguments === "string" ? JSON.parse(func.arguments) : func.arguments)
|
|
1008
|
+
: toolCall.arguments;
|
|
1009
|
+
|
|
1010
|
+
hookCtx.currentToolCall = {
|
|
1011
|
+
id: toolCall.id,
|
|
1012
|
+
name: tcName,
|
|
1013
|
+
arguments: tcArgs as Record<string, unknown>,
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// Doom loop detection
|
|
1017
|
+
const doomKey = `${tcName}:${JSON.stringify(tcArgs)}`;
|
|
1018
|
+
const agentCache = this.doomLoopCaches.get(runId) || new Map();
|
|
1019
|
+
const doomCount = agentCache.get(doomKey) || 0;
|
|
1020
|
+
if (doomCount >= agent.config.doomLoopThreshold) {
|
|
1021
|
+
hookCtx.error = new Error(`Doom loop detected: ${tcName}`);
|
|
1022
|
+
await this.executePluginHooks(agent, "on.error", hookCtx);
|
|
1023
|
+
result.error = hookCtx.error.message;
|
|
1024
|
+
// Add placeholder results for remaining unprocessed tool calls
|
|
1025
|
+
this.addRemainingToolResults(hookCtx, allToolCalls, processedCount, "Execution aborted: Doom loop detected");
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
agentCache.set(doomKey, doomCount + 1);
|
|
1029
|
+
this.doomLoopCaches.set(runId, agentCache);
|
|
1030
|
+
|
|
1031
|
+
// before.tool hook
|
|
1032
|
+
await this.executePluginHooks(agent, "agent:before.tool", hookCtx);
|
|
1033
|
+
if (hookCtx._stopped) {
|
|
1034
|
+
// Add placeholder results for remaining unprocessed tool calls
|
|
1035
|
+
this.addRemainingToolResults(hookCtx, allToolCalls, processedCount, "Execution aborted by plugin hook");
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Execute tool
|
|
1040
|
+
const toolResult = await this.executeTool(hookCtx);
|
|
1041
|
+
hookCtx.toolResult = toolResult;
|
|
1042
|
+
|
|
1043
|
+
// after.tool hook
|
|
1044
|
+
await this.executePluginHooks(agent, "agent:after.tool", hookCtx);
|
|
1045
|
+
if (hookCtx._stopped) {
|
|
1046
|
+
// Add placeholder results for remaining unprocessed tool calls
|
|
1047
|
+
this.addRemainingToolResults(hookCtx, allToolCalls, processedCount + 1, "Execution aborted by plugin hook");
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Add tool result to messages (AI SDK v6 format)
|
|
1052
|
+
const toolOutput = toolResult.success ? toolResult.result.output || "" : toolResult.result.error || "Unknown error";
|
|
1053
|
+
const toolOutputPreview = typeof toolOutput === "string" ? toolOutput.substring(0, 50) : JSON.stringify(toolOutput).substring(0, 50);
|
|
1054
|
+
logger.debug(`[ReAct] Iteration ${iteration} Adding tool message: tool=${toolResult.name} output="${toolOutputPreview}..."`);
|
|
1055
|
+
|
|
1056
|
+
this.pushMessage(hookCtx, {
|
|
1057
|
+
role: "tool",
|
|
1058
|
+
content: [{
|
|
1059
|
+
type: "tool-result",
|
|
1060
|
+
toolCallId: hookCtx.currentToolCall!.id,
|
|
1061
|
+
toolName: toolResult.name,
|
|
1062
|
+
output: toolOutput,
|
|
1063
|
+
}],
|
|
1064
|
+
} as unknown as ModelMessage);
|
|
1065
|
+
|
|
1066
|
+
// Record tool call
|
|
1067
|
+
result.toolCalls.push(hookCtx.currentToolCall);
|
|
1068
|
+
processedCount++;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// on.iteration hook
|
|
1072
|
+
await this.executePluginHooks(agent, "agent:on.iteration", hookCtx);
|
|
1073
|
+
}
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
logger.error(`Iteration ${iteration} error`, { error });
|
|
1076
|
+
hookCtx.error = error instanceof Error ? error : new Error(String(error));
|
|
1077
|
+
await this.executePluginHooks(agent, "agent:on.error", hookCtx);
|
|
1078
|
+
|
|
1079
|
+
// 设置 _stopped 标志(如果还没有被设置)
|
|
1080
|
+
if (!hookCtx._stopped) {
|
|
1081
|
+
hookCtx._stopped = true;
|
|
1082
|
+
// 如果是 AskUserError,特殊处理
|
|
1083
|
+
if (error instanceof AskUserError) {
|
|
1084
|
+
hookCtx._stopReason = "ask-user";
|
|
1085
|
+
|
|
1086
|
+
// ✅ 关键修复:当 ask_user 抛出异常时,也需要添加 tool result 消息
|
|
1087
|
+
// 这样可以保证 session 中有完整的 tool-call + tool-result 配对
|
|
1088
|
+
// Resume 时可以用用户响应替换这个 tool result
|
|
1089
|
+
if (hookCtx.currentToolCall) {
|
|
1090
|
+
this.pushMessage(hookCtx, {
|
|
1091
|
+
role: "tool",
|
|
1092
|
+
content: [{
|
|
1093
|
+
type: "tool-result",
|
|
1094
|
+
toolCallId: hookCtx.currentToolCall.id,
|
|
1095
|
+
toolName: "ask_user",
|
|
1096
|
+
output: JSON.stringify({
|
|
1097
|
+
error: "AskUserError",
|
|
1098
|
+
query: error.query,
|
|
1099
|
+
message: error.message,
|
|
1100
|
+
}),
|
|
1101
|
+
}],
|
|
1102
|
+
} as unknown as ModelMessage);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// 将 AskUserError 信息编码到 result.error 中,供 AgentComponentAdapter 检测和恢复抛出
|
|
1106
|
+
// 使用 JSON 编码避免 query 中包含冒号导致解析错误
|
|
1107
|
+
const errorInfo = {
|
|
1108
|
+
query: error.query,
|
|
1109
|
+
sessionId: error.sessionId,
|
|
1110
|
+
nodeId: error.nodeId,
|
|
1111
|
+
nodeType: error.nodeType,
|
|
1112
|
+
};
|
|
1113
|
+
result.error = `__ASK_USER_ERROR__:${JSON.stringify(errorInfo)}`;
|
|
1114
|
+
} else {
|
|
1115
|
+
// 其他错误 - 也需要添加 tool result 消息保持配对完整
|
|
1116
|
+
result.error = hookCtx.error.message;
|
|
1117
|
+
// 如果是 AbortError,保留 abort 相关的 reason
|
|
1118
|
+
if ((error as any)?.name === "AbortError") {
|
|
1119
|
+
hookCtx._stopReason = hookCtx._stopReason || "aborted";
|
|
1120
|
+
} else {
|
|
1121
|
+
hookCtx._stopReason = "error";
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ✅ 添加 tool result 消息(包含错误信息)
|
|
1125
|
+
if (hookCtx.currentToolCall) {
|
|
1126
|
+
this.pushMessage(hookCtx, {
|
|
1127
|
+
role: "tool",
|
|
1128
|
+
content: [{
|
|
1129
|
+
type: "tool-result",
|
|
1130
|
+
toolCallId: hookCtx.currentToolCall.id,
|
|
1131
|
+
toolName: hookCtx.currentToolCall.name,
|
|
1132
|
+
output: JSON.stringify({ error: hookCtx.error.message }),
|
|
1133
|
+
}],
|
|
1134
|
+
} as unknown as ModelMessage);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} else {
|
|
1138
|
+
// 如果 _stopped 已经被设置为 true(例如在 executeTool 中),保留错误信息
|
|
1139
|
+
result.error = hookCtx.error.message;
|
|
1140
|
+
|
|
1141
|
+
// ✅ 即使 _stopped 已为 true,也需要添加 tool result 消息
|
|
1142
|
+
// 这确保即使在 abort/doom-loop 等场景下,消息配对也是完整的
|
|
1143
|
+
if (hookCtx.currentToolCall) {
|
|
1144
|
+
const errorMsg = hookCtx.error?.message || "Execution interrupted";
|
|
1145
|
+
this.pushMessage(hookCtx, {
|
|
1146
|
+
role: "tool",
|
|
1147
|
+
content: [{
|
|
1148
|
+
type: "tool-result",
|
|
1149
|
+
toolCallId: hookCtx.currentToolCall.id,
|
|
1150
|
+
toolName: hookCtx.currentToolCall.name,
|
|
1151
|
+
output: JSON.stringify({ error: errorMsg }),
|
|
1152
|
+
}],
|
|
1153
|
+
} as unknown as ModelMessage);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Update result
|
|
1161
|
+
result.finalText = finalText;
|
|
1162
|
+
if (iteration >= agent.config.maxIterations && !result.finalText) {
|
|
1163
|
+
result.finalText = "Maximum iterations reached.";
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ✅ agent:after.react hook - React 循环结束后,用于记忆提取
|
|
1167
|
+
// 传递 reactContext 包含 messages, sessionId, summary
|
|
1168
|
+
const reactContext = {
|
|
1169
|
+
messages: hookCtx.messages,
|
|
1170
|
+
sessionId: effectiveContext.sessionId,
|
|
1171
|
+
summary: result.finalText,
|
|
1172
|
+
};
|
|
1173
|
+
await this.executePluginHooks(agent, "agent:after.react", {
|
|
1174
|
+
...hookCtx,
|
|
1175
|
+
...reactContext,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// after.complete hook
|
|
1179
|
+
await this.executePluginHooks(agent, "agent:after.complete", hookCtx);
|
|
1180
|
+
|
|
1181
|
+
// Finalize - 根据 abort 状态设置 agent 状态
|
|
1182
|
+
if (this.aborted.get(runId) === true || hookCtx._stopped) {
|
|
1183
|
+
agent.status = "stopped";
|
|
1184
|
+
} else {
|
|
1185
|
+
agent.status = "idle";
|
|
1186
|
+
}
|
|
1187
|
+
this.aborted.delete(runId);
|
|
1188
|
+
// 清理 AbortController
|
|
1189
|
+
this.abortControllers.delete(runId);
|
|
1190
|
+
|
|
1191
|
+
// 发送 agent.completed 或 agent.error 事件
|
|
1192
|
+
if (result.error) {
|
|
1193
|
+
this.pushEnvEvent({
|
|
1194
|
+
type: "agent.error",
|
|
1195
|
+
payload: {
|
|
1196
|
+
error: result.error,
|
|
1197
|
+
iterations: result.iterations,
|
|
1198
|
+
stopReason: result.stopReason,
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
} else {
|
|
1202
|
+
this.pushEnvEvent({
|
|
1203
|
+
type: "agent.completed",
|
|
1204
|
+
payload: {
|
|
1205
|
+
finalText: result.finalText,
|
|
1206
|
+
iterations: result.iterations,
|
|
1207
|
+
stopReason: result.stopReason,
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
let newMessages = hookCtx.messages.slice(historyMessageCount);
|
|
1213
|
+
logger.info(`Agent run completed: ${agentName}`, {
|
|
1214
|
+
iterations: result.iterations,
|
|
1215
|
+
hasError: !!result.error,
|
|
1216
|
+
hookCtxMessages: hookCtx.messages.length,
|
|
1217
|
+
historyMessageCount: historyMessageCount,
|
|
1218
|
+
sessionId: effectiveContext.sessionId,
|
|
1219
|
+
});
|
|
1220
|
+
await this.recordSessionMessages(
|
|
1221
|
+
effectiveContext.sessionId,
|
|
1222
|
+
newMessages
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
return this.finalizeResult(result, hookCtx);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* 中止 Agent
|
|
1230
|
+
*
|
|
1231
|
+
* 调用此方法会:
|
|
1232
|
+
* 1. 设置 aborted 标志(用于 ReAct 循环检查)
|
|
1233
|
+
* 2. 触发 AbortController.abort()(用于中断正在进行的 LLM 调用)
|
|
1234
|
+
*
|
|
1235
|
+
* 注意:不直接修改 agent.status,由 _run() 的 finally 统一处理
|
|
1236
|
+
*/
|
|
1237
|
+
abort(agentName: string): void {
|
|
1238
|
+
// 设置 aborted 标志 - 遍历所有该 agent 的 runId
|
|
1239
|
+
for (const key of this.aborted.keys()) {
|
|
1240
|
+
if (key.startsWith(agentName + ":")) {
|
|
1241
|
+
this.aborted.set(key, true);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// 触发 AbortController 中断正在进行的 LLM 调用
|
|
1246
|
+
for (const [key, controller] of this.abortControllers) {
|
|
1247
|
+
if (key.startsWith(agentName + ":")) {
|
|
1248
|
+
controller.abort();
|
|
1249
|
+
logger.debug(`[abort] AbortController.abort() called for run: ${key}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
logger.info(`Agent aborted: ${agentName}`);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* 构建消息列表
|
|
1258
|
+
*
|
|
1259
|
+
* 注意:ctx.messages 已经包含了历史消息(包括 system prompt 和 user query)
|
|
1260
|
+
* 这里只需要构建当前请求的消息,不重复添加
|
|
1261
|
+
*/
|
|
1262
|
+
private async buildMessages(
|
|
1263
|
+
ctx: HookContext,
|
|
1264
|
+
): Promise<ModelMessage[]> {
|
|
1265
|
+
const messages: ModelMessage[] = [];
|
|
1266
|
+
|
|
1267
|
+
if (ctx.systemPrompt) {
|
|
1268
|
+
messages.push({
|
|
1269
|
+
role: "system",
|
|
1270
|
+
content: ctx.systemPrompt,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Addition info (temporary context) - 也需要检查是否已存在
|
|
1275
|
+
if (ctx.additionInfo) {
|
|
1276
|
+
messages.push({
|
|
1277
|
+
role: "user",
|
|
1278
|
+
content: `额外信息:\n${ctx.additionInfo}`,
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// History messages(包含之前的所有消息)
|
|
1283
|
+
messages.push(...ctx.messages);
|
|
1284
|
+
|
|
1285
|
+
return messages;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* 调用 LLM
|
|
1290
|
+
*
|
|
1291
|
+
* 流式事件由 LLMComponent 通过 env.pushEnvEvent() 发布
|
|
1292
|
+
*/
|
|
1293
|
+
@TracedAs("agent.component.invokeLLM", { recordParams: true, recordResult: true, log: true })
|
|
1294
|
+
private async invokeLLM(ctx: HookContext): Promise<AgentLLMOutput> {
|
|
1295
|
+
logger.debug("[invokeLLM] Starting LLM invocation");
|
|
1296
|
+
|
|
1297
|
+
// 如果有 LLMComponent,使用它
|
|
1298
|
+
if (this.llmComponent?.invoke) {
|
|
1299
|
+
const messages = await this.buildMessages(ctx);
|
|
1300
|
+
const convertedMessages = messages.map(toLLMMessage);
|
|
1301
|
+
|
|
1302
|
+
logger.debug(`[invokeLLM] Calling LLMComponent.invoke with ${convertedMessages.length} messages`);
|
|
1303
|
+
|
|
1304
|
+
try {
|
|
1305
|
+
const result = await this.llmComponent.invoke({
|
|
1306
|
+
// 类型转换:ModelMessage[] -> LLMMessage[]
|
|
1307
|
+
messages: convertedMessages,
|
|
1308
|
+
// 类型转换:Tool[] -> ToolInfo[]
|
|
1309
|
+
// 注意:Tool.parameters 是 ZodType,需要提取其 JSON schema
|
|
1310
|
+
tools: ctx.tools.map(t => ({
|
|
1311
|
+
name: t.name,
|
|
1312
|
+
description: t.description || "",
|
|
1313
|
+
parameters: (t.parameters as unknown as Record<string, unknown>) ?? {},
|
|
1314
|
+
})),
|
|
1315
|
+
model: ctx.context.model,
|
|
1316
|
+
// 传递上下文用于事件元数据
|
|
1317
|
+
context: {
|
|
1318
|
+
sessionId: ctx.context.sessionId,
|
|
1319
|
+
messageId: ctx.context.messageId,
|
|
1320
|
+
},
|
|
1321
|
+
// 传递 abort signal 以支持 Agent 的优雅终止
|
|
1322
|
+
abortSignal: ctx.context.abort,
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
logger.debug("[invokeLLM] LLMComponent.invoke returned successfully");
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
content: result.output?.content || "",
|
|
1329
|
+
reasoning: result.output?.reasoning,
|
|
1330
|
+
toolCalls: result.output?.toolCalls?.map((tc) => ({
|
|
1331
|
+
id: tc.id,
|
|
1332
|
+
function: tc.function ? {
|
|
1333
|
+
name: tc.function.name,
|
|
1334
|
+
arguments: typeof tc.function.arguments === "string"
|
|
1335
|
+
? tc.function.arguments
|
|
1336
|
+
: JSON.stringify(tc.function.arguments),
|
|
1337
|
+
} : undefined,
|
|
1338
|
+
name: tc.name,
|
|
1339
|
+
arguments: tc.arguments,
|
|
1340
|
+
})),
|
|
1341
|
+
finishReason: result.output?.finishReason,
|
|
1342
|
+
};
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
logger.error("[invokeLLM] LLMComponent.invoke threw an error:", error);
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Fallback: Mock response for testing
|
|
1350
|
+
logger.warn("[invokeLLM] No LLMComponent available, using mock");
|
|
1351
|
+
|
|
1352
|
+
return {
|
|
1353
|
+
content: "Mock response: Hello!",
|
|
1354
|
+
finishReason: "stop",
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* 执行工具
|
|
1360
|
+
*/
|
|
1361
|
+
@TracedAs("agent.component.executeTool", { recordParams: true, recordResult: true, log: true })
|
|
1362
|
+
private async executeTool(ctx: HookContext): Promise<ToolCallResult> {
|
|
1363
|
+
const toolCall = ctx.currentToolCall!;
|
|
1364
|
+
|
|
1365
|
+
// 获取工具名称:优先使用 function.name(AI SDK v6 格式),兼容 toolCall.name
|
|
1366
|
+
// AI SDK v6 格式: { id, name, function: { name, arguments } }
|
|
1367
|
+
const toolName = (toolCall as any).function?.name || toolCall.name || "unknown";
|
|
1368
|
+
|
|
1369
|
+
// Debug: 打印工具查找信息
|
|
1370
|
+
logger.debug(`[executeTool] Looking for tool: "${toolName}"`);
|
|
1371
|
+
logger.debug(`[executeTool] Available tools: [${ctx.tools.map(t => t.name).join(", ")}]`);
|
|
1372
|
+
|
|
1373
|
+
// Find tool
|
|
1374
|
+
const tool = ctx.tools.find(t => t.name === toolName);
|
|
1375
|
+
if (!tool) {
|
|
1376
|
+
// 发送错误事件
|
|
1377
|
+
this.pushEnvEvent({
|
|
1378
|
+
type: "tool.error",
|
|
1379
|
+
payload: {
|
|
1380
|
+
error: `Tool not found: ${toolName}`,
|
|
1381
|
+
toolName: toolName,
|
|
1382
|
+
},
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
return {
|
|
1386
|
+
id: toolCall.id,
|
|
1387
|
+
name: toolName,
|
|
1388
|
+
success: false,
|
|
1389
|
+
result: {
|
|
1390
|
+
success: false,
|
|
1391
|
+
output: "",
|
|
1392
|
+
error: `Tool not found: ${toolName}`,
|
|
1393
|
+
},
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
// Convert AgentContext to ToolContext format
|
|
1399
|
+
// AgentContext uses sessionId (camelCase), ToolContext uses session_id (snake_case)
|
|
1400
|
+
const toolContext: ToolContext = {
|
|
1401
|
+
session_id: ctx.context.sessionId,
|
|
1402
|
+
message_id: ctx.context.messageId,
|
|
1403
|
+
abort: ctx.context.abort,
|
|
1404
|
+
workdir: ctx.context.metadata?.workdir as string | undefined,
|
|
1405
|
+
metadata: ctx.context.metadata,
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// Build ToolExecuteRequest
|
|
1409
|
+
const request: ToolExecuteRequest = {
|
|
1410
|
+
name: toolName,
|
|
1411
|
+
args: toolCall.arguments,
|
|
1412
|
+
context: toolContext,
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
// Use ToolComponent.execute() to trigger tool hooks
|
|
1416
|
+
// Check if we have a ToolComponent reference (via refreshDependencies)
|
|
1417
|
+
let result;
|
|
1418
|
+
if (this.toolComponent?.execute) {
|
|
1419
|
+
result = await this.toolComponent.execute(request);
|
|
1420
|
+
} else {
|
|
1421
|
+
// Fallback to direct tool execution (for backwards compatibility / testing)
|
|
1422
|
+
logger.debug(`[executeTool] No ToolComponent available, executing tool directly`);
|
|
1423
|
+
result = await tool.execute(toolCall.arguments, toolContext);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// 发送工具结果事件
|
|
1427
|
+
this.pushEnvEvent({
|
|
1428
|
+
type: "tool.result",
|
|
1429
|
+
payload: {
|
|
1430
|
+
id: toolCall.id,
|
|
1431
|
+
name: toolName,
|
|
1432
|
+
result: result,
|
|
1433
|
+
success: result.success,
|
|
1434
|
+
},
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
return {
|
|
1438
|
+
id: toolCall.id,
|
|
1439
|
+
name: toolName,
|
|
1440
|
+
success: result.success,
|
|
1441
|
+
result,
|
|
1442
|
+
};
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
// 检测 AskUserError - 这是用于工作流暂停的特殊错误,需要向上传播
|
|
1445
|
+
if (error instanceof AskUserError) {
|
|
1446
|
+
throw error; // 重新抛出,让调用栈上层(AgentComponentAdapter)处理
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// 发送错误事件
|
|
1450
|
+
this.pushEnvEvent({
|
|
1451
|
+
type: "tool.error",
|
|
1452
|
+
payload: {
|
|
1453
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1454
|
+
toolName: toolName,
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
return {
|
|
1459
|
+
id: toolCall.id,
|
|
1460
|
+
name: toolName,
|
|
1461
|
+
success: false,
|
|
1462
|
+
result: {
|
|
1463
|
+
success: false,
|
|
1464
|
+
output: "",
|
|
1465
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1466
|
+
},
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* 执行 Plugin Hooks (Agent 特有的 Hook 执行机制)
|
|
1473
|
+
*/
|
|
1474
|
+
private async executePluginHooks(
|
|
1475
|
+
agent: AgentInstance,
|
|
1476
|
+
point: HookPoint,
|
|
1477
|
+
ctx: HookContext
|
|
1478
|
+
): Promise<void> {
|
|
1479
|
+
// Collect all hooks for this point
|
|
1480
|
+
const hooks: Array<{ plugin: Plugin; priority: number; execute: Function }> = [];
|
|
1481
|
+
|
|
1482
|
+
for (const [name, plugin] of agent.plugins) {
|
|
1483
|
+
for (const hook of plugin.hooks) {
|
|
1484
|
+
if (hook.point === point) {
|
|
1485
|
+
hooks.push({
|
|
1486
|
+
plugin,
|
|
1487
|
+
priority: hook.priority ?? 0,
|
|
1488
|
+
execute: plugin.execute,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Sort by priority (higher first)
|
|
1495
|
+
hooks.sort((a, b) => b.priority - a.priority);
|
|
1496
|
+
|
|
1497
|
+
// Execute hooks
|
|
1498
|
+
for (const { plugin, execute } of hooks) {
|
|
1499
|
+
try {
|
|
1500
|
+
const result = await execute(ctx);
|
|
1501
|
+
if (!result.continue) {
|
|
1502
|
+
ctx._stopped = true;
|
|
1503
|
+
ctx._stopReason = `Plugin ${plugin.name} stopped execution`;
|
|
1504
|
+
logger.info(`Hook ${point} stopped by plugin ${plugin.name}`);
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Handle actions
|
|
1509
|
+
if (result.action) {
|
|
1510
|
+
await this.handleHookAction(result.action, ctx);
|
|
1511
|
+
}
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
logger.error(`Hook ${point} error in plugin ${plugin.name}`, { error });
|
|
1514
|
+
ctx._errors = ctx._errors || [];
|
|
1515
|
+
ctx._errors.push({
|
|
1516
|
+
plugin: plugin.name,
|
|
1517
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* 处理 Hook 动作
|
|
1525
|
+
*/
|
|
1526
|
+
private async handleHookAction(
|
|
1527
|
+
action: { type: string; payload?: unknown },
|
|
1528
|
+
ctx: HookContext
|
|
1529
|
+
): Promise<void> {
|
|
1530
|
+
switch (action.type) {
|
|
1531
|
+
case "stop":
|
|
1532
|
+
ctx._stopped = true;
|
|
1533
|
+
ctx._stopReason = "stopped_by_hook";
|
|
1534
|
+
break;
|
|
1535
|
+
case "inject_message":
|
|
1536
|
+
if (action.payload && typeof action.payload === "object") {
|
|
1537
|
+
const msg = action.payload as ModelMessage;
|
|
1538
|
+
ctx.messages.push(msg);
|
|
1539
|
+
}
|
|
1540
|
+
break;
|
|
1541
|
+
case "skip_tool":
|
|
1542
|
+
// Skip current tool execution
|
|
1543
|
+
ctx._pendingAction = action as { type: HookActionType; payload?: unknown };
|
|
1544
|
+
break;
|
|
1545
|
+
// Other actions can be extended
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* 通知消息添加(扩展点)
|
|
1551
|
+
*/
|
|
1552
|
+
protected notifyMessageAdded(message: ModelMessage): void {
|
|
1553
|
+
// 可被子类或插件覆盖
|
|
1554
|
+
logger.debug(`Message added: ${message.role}`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* 结束结果
|
|
1559
|
+
*/
|
|
1560
|
+
private finalizeResult(result: AgentRunResult, ctx: HookContext): AgentRunResult {
|
|
1561
|
+
return {
|
|
1562
|
+
...result,
|
|
1563
|
+
stopped: ctx._stopped,
|
|
1564
|
+
stopReason: ctx._stopReason,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Record messages to session
|
|
1570
|
+
*
|
|
1571
|
+
* 存储策略:
|
|
1572
|
+
* 1. 存储 allMessages 中的所有 user、assistant、tool 消息
|
|
1573
|
+
* 2. 跳过 system 消息
|
|
1574
|
+
* 3. 跳过空内容的 assistant/tool 消息
|
|
1575
|
+
*/
|
|
1576
|
+
@TracedAs("agent.component.recordSessionMessages", { recordParams: true, recordResult: true, log: true })
|
|
1577
|
+
private async recordSessionMessages(
|
|
1578
|
+
sessionId: string | undefined,
|
|
1579
|
+
allMessages: ModelMessage[] = []
|
|
1580
|
+
): Promise<void> {
|
|
1581
|
+
if (!sessionId) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
try {
|
|
1586
|
+
const sessionComponent = this.env.getComponent<SessionComponent>("session");
|
|
1587
|
+
if (!sessionComponent) {
|
|
1588
|
+
logger.warn("SessionComponent not found, skipping session recording");
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
for (const msg of allMessages) {
|
|
1593
|
+
// 跳过 system 消息
|
|
1594
|
+
if (msg.role === "system") {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// 跳过空内容的 assistant/tool 消息
|
|
1599
|
+
if (msg.role === "assistant" || msg.role === "tool") {
|
|
1600
|
+
if (this.isEmptyMessage(msg.content)) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
await sessionComponent.addMessage(sessionId,
|
|
1606
|
+
this.messageConverter.fromModelMessage(msg as unknown as ModelMessage, { sessionID: sessionId })
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
logger.warn(`Failed to record session messages: ${err}`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* 检查消息内容是否为空
|
|
1617
|
+
*
|
|
1618
|
+
* 空内容定义:
|
|
1619
|
+
* - 空字符串 ""
|
|
1620
|
+
* - 空数组 []
|
|
1621
|
+
* - null
|
|
1622
|
+
* - undefined
|
|
1623
|
+
*
|
|
1624
|
+
* @param content - 消息内容
|
|
1625
|
+
* @returns 是否为空
|
|
1626
|
+
*/
|
|
1627
|
+
private isEmptyMessage(content: unknown): boolean {
|
|
1628
|
+
if (content === null || content === undefined) {
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof content === "string" && content.trim() === "") {
|
|
1632
|
+
return true;
|
|
1633
|
+
}
|
|
1634
|
+
if (Array.isArray(content) && content.length === 0) {
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* 通过 Environment 发送事件
|
|
1642
|
+
*/
|
|
1643
|
+
private pushEnvEvent(event: { type: string; payload: Record<string, unknown> }): void {
|
|
1644
|
+
if (this.env && "pushEnvEvent" in this.env) {
|
|
1645
|
+
try {
|
|
1646
|
+
const fullEvent: EnvEvent = {
|
|
1647
|
+
id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1648
|
+
type: event.type,
|
|
1649
|
+
timestamp: Date.now(),
|
|
1650
|
+
metadata: {
|
|
1651
|
+
source: "agent",
|
|
1652
|
+
agent_name: this.name,
|
|
1653
|
+
},
|
|
1654
|
+
payload: event.payload,
|
|
1655
|
+
};
|
|
1656
|
+
this.env?.pushEnvEvent(fullEvent);
|
|
1657
|
+
} catch (err) {
|
|
1658
|
+
logger.warn(`[pushEnvEvent] Failed to push event: ${err}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* 获取会话对话历史
|
|
1665
|
+
*
|
|
1666
|
+
* 核心逻辑:
|
|
1667
|
+
* 1. 使用 sessionComponent.getContext() 获取消息(自动从 checkpoint 开始)
|
|
1668
|
+
* 2. 根据 messageLimit 限制消息数量(向后兼容 minUserMessages)
|
|
1669
|
+
* 3. 过滤掉 tool_call/tool_result 消息(如果 filterHistory=true)
|
|
1670
|
+
*
|
|
1671
|
+
* 关键改进:利用 checkpoint 概念,避免重复获取已压缩的历史消息,
|
|
1672
|
+
* 从而减少重复触发压缩的问题。
|
|
1673
|
+
*
|
|
1674
|
+
* @param sessionComponent - SessionComponent 实例
|
|
1675
|
+
* @param sessionId - 会话 ID
|
|
1676
|
+
* @param options - 配置选项
|
|
1677
|
+
* @param options.minUserMessages - 最少包含的 user 消息数量(默认 100)
|
|
1678
|
+
* @param options.filterHistory - 是否过滤 tool 消息(默认 true)
|
|
1679
|
+
* @returns 过滤后的会话消息列表
|
|
1680
|
+
*/
|
|
1681
|
+
@TracedAs("agent.component.getConversationHistory", { recordParams: true, recordResult: true, log: true })
|
|
1682
|
+
private async getConversationHistory(
|
|
1683
|
+
sessionComponent: SessionComponent,
|
|
1684
|
+
sessionId: string,
|
|
1685
|
+
options: { minUserMessages?: number; filterHistory?: boolean } = {}
|
|
1686
|
+
): Promise<SessionMessage[]> {
|
|
1687
|
+
const minUserMessages = options.minUserMessages ?? 100;
|
|
1688
|
+
const filterHistory = options.filterHistory ?? false;
|
|
1689
|
+
|
|
1690
|
+
// 使用 getContext 获取消息,自动从最新 checkpoint 开始
|
|
1691
|
+
// 且确保从 user 消息边界截取,避免 tool_call/tool_result 配对被截断
|
|
1692
|
+
const ctx = await sessionComponent.getContext(sessionId, {
|
|
1693
|
+
fullHistory: false,
|
|
1694
|
+
minUserMessages,
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
let messages = ctx.messages;
|
|
1698
|
+
|
|
1699
|
+
// 根据 filterHistory 配置决定是否过滤 tool 消息
|
|
1700
|
+
if (filterHistory) {
|
|
1701
|
+
messages = messages.filter(msg => {
|
|
1702
|
+
if (msg.metadata?.type === "tool_call" || msg.metadata?.type === "tool_result") {
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
return msg.role === "user" || msg.role === "assistant";
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
return messages;
|
|
1710
|
+
}
|
|
1711
|
+
}
|