@google/gemini-cli-core 0.42.0 → 0.43.0-preview.1
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/docs/changelogs/index.md +14 -0
- package/dist/docs/changelogs/latest.md +108 -166
- package/dist/docs/changelogs/preview.md +227 -103
- package/dist/docs/cli/auto-memory.md +60 -38
- package/dist/docs/cli/settings.md +1 -1
- package/dist/docs/cli/tutorials/memory-management.md +1 -1
- package/dist/docs/extensions/releasing.md +58 -24
- package/dist/docs/reference/configuration.md +14 -1
- package/dist/docs/reference/keyboard-shortcuts.md +23 -0
- package/dist/google-gemini-cli-core-0.43.0-preview.0.tgz +0 -0
- package/dist/src/agent/content-utils.js +2 -0
- package/dist/src/agent/content-utils.js.map +1 -1
- package/dist/src/agent/content-utils.test.js +5 -1
- package/dist/src/agent/content-utils.test.js.map +1 -1
- package/dist/src/agent/event-translator.js +7 -6
- package/dist/src/agent/event-translator.js.map +1 -1
- package/dist/src/agent/legacy-agent-session.js +4 -0
- package/dist/src/agent/legacy-agent-session.js.map +1 -1
- package/dist/src/agent/legacy-agent-session.test.js +9 -1
- package/dist/src/agent/legacy-agent-session.test.js.map +1 -1
- package/dist/src/agent/tool-display-utils.d.ts +3 -2
- package/dist/src/agent/tool-display-utils.js +3 -2
- package/dist/src/agent/tool-display-utils.js.map +1 -1
- package/dist/src/agent/types.d.ts +33 -3
- package/dist/src/agents/a2aUtils.d.ts +1 -1
- package/dist/src/agents/a2aUtils.js +4 -3
- package/dist/src/agents/a2aUtils.js.map +1 -1
- package/dist/src/agents/agentLoader.d.ts +2 -2
- package/dist/src/agents/browser/browserAgentInvocation.js +24 -19
- package/dist/src/agents/browser/browserAgentInvocation.js.map +1 -1
- package/dist/src/agents/local-executor.d.ts +1 -0
- package/dist/src/agents/local-executor.js +46 -32
- package/dist/src/agents/local-executor.js.map +1 -1
- package/dist/src/agents/local-executor.test.js +127 -0
- package/dist/src/agents/local-executor.test.js.map +1 -1
- package/dist/src/agents/local-invocation.js +24 -20
- package/dist/src/agents/local-invocation.js.map +1 -1
- package/dist/src/agents/local-invocation.test.js +9 -9
- package/dist/src/agents/local-invocation.test.js.map +1 -1
- package/dist/src/agents/local-subagent-protocol.d.ts +18 -0
- package/dist/src/agents/local-subagent-protocol.js +357 -0
- package/dist/src/agents/local-subagent-protocol.js.map +1 -0
- package/dist/src/agents/local-subagent-protocol.test.d.ts +6 -0
- package/dist/src/agents/local-subagent-protocol.test.js +676 -0
- package/dist/src/agents/local-subagent-protocol.test.js.map +1 -0
- package/dist/src/agents/remote-invocation.js +6 -6
- package/dist/src/agents/remote-invocation.js.map +1 -1
- package/dist/src/agents/remote-invocation.test.js +23 -12
- package/dist/src/agents/remote-invocation.test.js.map +1 -1
- package/dist/src/agents/remote-subagent-protocol.d.ts +31 -0
- package/dist/src/agents/remote-subagent-protocol.js +330 -0
- package/dist/src/agents/remote-subagent-protocol.js.map +1 -0
- package/dist/src/agents/remote-subagent-protocol.test.d.ts +6 -0
- package/dist/src/agents/remote-subagent-protocol.test.js +652 -0
- package/dist/src/agents/remote-subagent-protocol.test.js.map +1 -0
- package/dist/src/agents/skill-extraction-agent.js +1 -0
- package/dist/src/agents/skill-extraction-agent.js.map +1 -1
- package/dist/src/agents/skill-extraction-agent.test.js +1 -0
- package/dist/src/agents/skill-extraction-agent.test.js.map +1 -1
- package/dist/src/agents/types.d.ts +13 -2
- package/dist/src/agents/types.js +7 -0
- package/dist/src/agents/types.js.map +1 -1
- package/dist/src/availability/modelAvailabilityService.d.ts +6 -6
- package/dist/src/availability/modelAvailabilityService.js +14 -7
- package/dist/src/availability/modelAvailabilityService.js.map +1 -1
- package/dist/src/availability/modelAvailabilityService.test.js +34 -0
- package/dist/src/availability/modelAvailabilityService.test.js.map +1 -1
- package/dist/src/availability/policyHelpers.js +24 -12
- package/dist/src/availability/policyHelpers.js.map +1 -1
- package/dist/src/availability/policyHelpers.test.js +3 -2
- package/dist/src/availability/policyHelpers.test.js.map +1 -1
- package/dist/src/code_assist/oauth2.js +3 -0
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/setup.d.ts +3 -0
- package/dist/src/code_assist/setup.js +9 -0
- package/dist/src/code_assist/setup.js.map +1 -1
- package/dist/src/code_assist/setup.test.js +9 -1
- package/dist/src/code_assist/setup.test.js.map +1 -1
- package/dist/src/commands/memory.d.ts +5 -14
- package/dist/src/commands/memory.js +19 -141
- package/dist/src/commands/memory.js.map +1 -1
- package/dist/src/commands/memory.test.js +62 -0
- package/dist/src/commands/memory.test.js.map +1 -1
- package/dist/src/config/config.d.ts +6 -2
- package/dist/src/config/config.js +63 -8
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +48 -0
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/defaultModelConfigs.js +13 -0
- package/dist/src/config/defaultModelConfigs.js.map +1 -1
- package/dist/src/config/models.js +1 -1
- package/dist/src/config/models.js.map +1 -1
- package/dist/src/config/projectRegistry.d.ts +1 -0
- package/dist/src/config/projectRegistry.js +13 -2
- package/dist/src/config/projectRegistry.js.map +1 -1
- package/dist/src/config/projectRegistry.test.js +43 -0
- package/dist/src/config/projectRegistry.test.js.map +1 -1
- package/dist/src/context/config/profiles.js +4 -0
- package/dist/src/context/config/profiles.js.map +1 -1
- package/dist/src/context/contextManager.barrier.test.js +4 -3
- package/dist/src/context/contextManager.barrier.test.js.map +1 -1
- package/dist/src/context/contextManager.d.ts +9 -2
- package/dist/src/context/contextManager.hotstart.test.d.ts +6 -0
- package/dist/src/context/contextManager.hotstart.test.js +61 -0
- package/dist/src/context/contextManager.hotstart.test.js.map +1 -0
- package/dist/src/context/contextManager.js +96 -21
- package/dist/src/context/contextManager.js.map +1 -1
- package/dist/src/context/eventBus.d.ts +6 -0
- package/dist/src/context/eventBus.js +6 -0
- package/dist/src/context/eventBus.js.map +1 -1
- package/dist/src/context/graph/render.d.ts +3 -1
- package/dist/src/context/graph/render.js +33 -9
- package/dist/src/context/graph/render.js.map +1 -1
- package/dist/src/context/graph/render.test.d.ts +6 -0
- package/dist/src/context/graph/render.test.js +203 -0
- package/dist/src/context/graph/render.test.js.map +1 -0
- package/dist/src/context/graph/toGraph.js +3 -3
- package/dist/src/context/graph/toGraph.js.map +1 -1
- package/dist/src/context/graph/toGraph.test.d.ts +6 -0
- package/dist/src/context/graph/toGraph.test.js +35 -0
- package/dist/src/context/graph/toGraph.test.js.map +1 -0
- package/dist/src/context/initializer.js +11 -2
- package/dist/src/context/initializer.js.map +1 -1
- package/dist/src/context/pipeline/contextWorkingBuffer.d.ts +5 -7
- package/dist/src/context/pipeline/contextWorkingBuffer.js +105 -29
- package/dist/src/context/pipeline/contextWorkingBuffer.js.map +1 -1
- package/dist/src/context/pipeline/contextWorkingBuffer.test.js +67 -0
- package/dist/src/context/pipeline/contextWorkingBuffer.test.js.map +1 -1
- package/dist/src/context/pipeline/environment.d.ts +4 -0
- package/dist/src/context/pipeline/environmentImpl.d.ts +6 -5
- package/dist/src/context/pipeline/environmentImpl.js +6 -8
- package/dist/src/context/pipeline/environmentImpl.js.map +1 -1
- package/dist/src/context/pipeline/environmentImpl.test.js +5 -1
- package/dist/src/context/pipeline/environmentImpl.test.js.map +1 -1
- package/dist/src/context/pipeline/orchestrator.d.ts +1 -0
- package/dist/src/context/pipeline/orchestrator.js +59 -24
- package/dist/src/context/pipeline/orchestrator.js.map +1 -1
- package/dist/src/context/processors/blobDegradationProcessor.test.js +2 -2
- package/dist/src/context/processors/rollingSummaryProcessor.js +2 -15
- package/dist/src/context/processors/rollingSummaryProcessor.js.map +1 -1
- package/dist/src/context/processors/stateSnapshotAsyncProcessor.d.ts +7 -0
- package/dist/src/context/processors/stateSnapshotAsyncProcessor.js +33 -21
- package/dist/src/context/processors/stateSnapshotAsyncProcessor.js.map +1 -1
- package/dist/src/context/processors/stateSnapshotAsyncProcessor.test.js +31 -7
- package/dist/src/context/processors/stateSnapshotAsyncProcessor.test.js.map +1 -1
- package/dist/src/context/processors/stateSnapshotProcessor.d.ts +2 -0
- package/dist/src/context/processors/stateSnapshotProcessor.js +28 -4
- package/dist/src/context/processors/stateSnapshotProcessor.js.map +1 -1
- package/dist/src/context/processors/stateSnapshotProcessor.test.js +40 -1
- package/dist/src/context/processors/stateSnapshotProcessor.test.js.map +1 -1
- package/dist/src/context/system-tests/hysteresis.test.d.ts +6 -0
- package/dist/src/context/system-tests/hysteresis.test.js +98 -0
- package/dist/src/context/system-tests/hysteresis.test.js.map +1 -0
- package/dist/src/context/system-tests/lifecycle.golden.test.js +90 -69
- package/dist/src/context/system-tests/lifecycle.golden.test.js.map +1 -1
- package/dist/src/context/system-tests/simulationHarness.d.ts +1 -4
- package/dist/src/context/system-tests/simulationHarness.js +16 -30
- package/dist/src/context/system-tests/simulationHarness.js.map +1 -1
- package/dist/src/context/testing/contextTestUtils.d.ts +1 -0
- package/dist/src/context/testing/contextTestUtils.js +48 -25
- package/dist/src/context/testing/contextTestUtils.js.map +1 -1
- package/dist/src/context/utils/adaptiveTokenCalculator.d.ts +61 -0
- package/dist/src/context/utils/adaptiveTokenCalculator.js +116 -0
- package/dist/src/context/utils/adaptiveTokenCalculator.js.map +1 -0
- package/dist/src/context/utils/adaptiveTokenCalculator.test.d.ts +6 -0
- package/dist/src/context/utils/adaptiveTokenCalculator.test.js +85 -0
- package/dist/src/context/utils/adaptiveTokenCalculator.test.js.map +1 -0
- package/dist/src/context/utils/contextTokenCalculator.d.ts +47 -1
- package/dist/src/context/utils/contextTokenCalculator.js +20 -3
- package/dist/src/context/utils/contextTokenCalculator.js.map +1 -1
- package/dist/src/context/utils/contextTokenCalculator.test.js +8 -8
- package/dist/src/context/utils/contextTokenCalculator.test.js.map +1 -1
- package/dist/src/context/utils/formatNodesForLlm.d.ts +21 -0
- package/dist/src/context/utils/formatNodesForLlm.js +69 -0
- package/dist/src/context/utils/formatNodesForLlm.js.map +1 -0
- package/dist/src/context/utils/formatNodesForLlm.test.d.ts +6 -0
- package/dist/src/context/utils/formatNodesForLlm.test.js +110 -0
- package/dist/src/context/utils/formatNodesForLlm.test.js.map +1 -0
- package/dist/src/context/utils/snapshotGenerator.d.ts +23 -1
- package/dist/src/context/utils/snapshotGenerator.js +249 -31
- package/dist/src/context/utils/snapshotGenerator.js.map +1 -1
- package/dist/src/context/utils/snapshotGenerator.test.d.ts +6 -0
- package/dist/src/context/utils/snapshotGenerator.test.js +255 -0
- package/dist/src/context/utils/snapshotGenerator.test.js.map +1 -0
- package/dist/src/context/utils/tokenCalibration.d.ts +9 -0
- package/dist/src/context/utils/tokenCalibration.js +30 -0
- package/dist/src/context/utils/tokenCalibration.js.map +1 -0
- package/dist/src/core/baseLlmClient.d.ts +8 -0
- package/dist/src/core/baseLlmClient.js +14 -0
- package/dist/src/core/baseLlmClient.js.map +1 -1
- package/dist/src/core/client.js +12 -1
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +4 -4
- package/dist/src/core/contentGenerator.js +12 -10
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +2 -0
- package/dist/src/core/geminiChat.js +60 -5
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +195 -2
- package/dist/src/core/geminiChat.test.js.map +1 -1
- package/dist/src/core/turn.js +30 -2
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/core/turn.test.js +13 -8
- package/dist/src/core/turn.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/hooks/hookEventHandler.js +3 -2
- package/dist/src/hooks/hookEventHandler.js.map +1 -1
- package/dist/src/hooks/hookEventHandler.test.js +80 -0
- package/dist/src/hooks/hookEventHandler.test.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/prompts/snippets.js +1 -1
- package/dist/src/prompts/snippets.js.map +1 -1
- package/dist/src/routing/strategies/approvalModeStrategy.js +5 -3
- package/dist/src/routing/strategies/approvalModeStrategy.js.map +1 -1
- package/dist/src/routing/strategies/approvalModeStrategy.test.js +2 -0
- package/dist/src/routing/strategies/approvalModeStrategy.test.js.map +1 -1
- package/dist/src/routing/strategies/classifierStrategy.js +8 -1
- package/dist/src/routing/strategies/classifierStrategy.js.map +1 -1
- package/dist/src/routing/strategies/classifierStrategy.test.js +4 -0
- package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -1
- package/dist/src/routing/strategies/gemmaClassifierStrategy.js +7 -1
- package/dist/src/routing/strategies/gemmaClassifierStrategy.js.map +1 -1
- package/dist/src/routing/strategies/gemmaClassifierStrategy.test.js +4 -0
- package/dist/src/routing/strategies/gemmaClassifierStrategy.test.js.map +1 -1
- package/dist/src/routing/strategies/numericalClassifierStrategy.d.ts +1 -0
- package/dist/src/routing/strategies/numericalClassifierStrategy.js +22 -3
- package/dist/src/routing/strategies/numericalClassifierStrategy.js.map +1 -1
- package/dist/src/routing/strategies/numericalClassifierStrategy.test.js +168 -23
- package/dist/src/routing/strategies/numericalClassifierStrategy.test.js.map +1 -1
- package/dist/src/scheduler/scheduler.js +11 -0
- package/dist/src/scheduler/scheduler.js.map +1 -1
- package/dist/src/scheduler/scheduler.test.js +1 -1
- package/dist/src/scheduler/scheduler.test.js.map +1 -1
- package/dist/src/scheduler/state-manager.js +5 -1
- package/dist/src/scheduler/state-manager.js.map +1 -1
- package/dist/src/scheduler/tool-executor.js +7 -4
- package/dist/src/scheduler/tool-executor.js.map +1 -1
- package/dist/src/scheduler/types.d.ts +5 -1
- package/dist/src/scheduler/types.js.map +1 -1
- package/dist/src/services/fileDiscoveryService.js +2 -1
- package/dist/src/services/fileDiscoveryService.js.map +1 -1
- package/dist/src/services/fileDiscoveryService.test.js +36 -0
- package/dist/src/services/fileDiscoveryService.test.js.map +1 -1
- package/dist/src/services/gitService.js +8 -1
- package/dist/src/services/gitService.js.map +1 -1
- package/dist/src/services/gitService.test.js +104 -0
- package/dist/src/services/gitService.test.js.map +1 -1
- package/dist/src/services/keychainService.js +14 -5
- package/dist/src/services/keychainService.js.map +1 -1
- package/dist/src/services/memoryPatchUtils.d.ts +66 -4
- package/dist/src/services/memoryPatchUtils.js +267 -5
- package/dist/src/services/memoryPatchUtils.js.map +1 -1
- package/dist/src/services/memoryService.js +25 -1
- package/dist/src/services/memoryService.js.map +1 -1
- package/dist/src/services/memoryService.test.js +61 -1
- package/dist/src/services/memoryService.test.js.map +1 -1
- package/dist/src/services/test-data/resolved-aliases-retry.golden.json +11 -0
- package/dist/src/services/test-data/resolved-aliases.golden.json +11 -0
- package/dist/src/telemetry/gcp-exporters.d.ts +2 -0
- package/dist/src/telemetry/gcp-exporters.js +69 -5
- package/dist/src/telemetry/gcp-exporters.js.map +1 -1
- package/dist/src/telemetry/gcp-exporters.test.js +52 -0
- package/dist/src/telemetry/gcp-exporters.test.js.map +1 -1
- package/dist/src/telemetry/metrics.js +13 -2
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/metrics.test.js +61 -1
- package/dist/src/telemetry/metrics.test.js.map +1 -1
- package/dist/src/tools/definitions/model-family-sets/default-legacy.js +5 -2
- package/dist/src/tools/definitions/model-family-sets/default-legacy.js.map +1 -1
- package/dist/src/tools/definitions/model-family-sets/gemini-3.js +7 -4
- package/dist/src/tools/definitions/model-family-sets/gemini-3.js.map +1 -1
- package/dist/src/tools/edit.js +19 -0
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +9 -0
- package/dist/src/tools/edit.test.js.map +1 -1
- package/dist/src/tools/grep.js +13 -1
- package/dist/src/tools/grep.js.map +1 -1
- package/dist/src/tools/ls.js +5 -0
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client.js +17 -3
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +24 -0
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/read-file.js +11 -6
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-file.test.js +20 -8
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/ripGrep.js +13 -1
- package/dist/src/tools/ripGrep.js.map +1 -1
- package/dist/src/tools/shell.js +14 -0
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +5 -0
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/tools.d.ts +6 -0
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/topicTool.js +5 -0
- package/dist/src/tools/topicTool.js.map +1 -1
- package/dist/src/tools/write-file.js +13 -0
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/tools/write-file.test.js +8 -0
- package/dist/src/tools/write-file.test.js.map +1 -1
- package/dist/src/utils/errors.js +3 -8
- package/dist/src/utils/errors.js.map +1 -1
- package/dist/src/utils/filesearch/ignore.js +4 -1
- package/dist/src/utils/filesearch/ignore.js.map +1 -1
- package/dist/src/utils/ignoreFileParser.js +1 -1
- package/dist/src/utils/ignoreFileParser.js.map +1 -1
- package/dist/src/utils/ignorePatterns.js +2 -0
- package/dist/src/utils/ignorePatterns.js.map +1 -1
- package/dist/src/utils/ignorePatterns.test.js +1 -0
- package/dist/src/utils/ignorePatterns.test.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.js +55 -40
- package/dist/src/utils/memoryDiscovery.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.test.js +77 -9
- package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
- package/dist/src/utils/modelUtils.d.ts +14 -0
- package/dist/src/utils/modelUtils.js +17 -0
- package/dist/src/utils/modelUtils.js.map +1 -0
- package/dist/src/utils/modelUtils.test.d.ts +6 -0
- package/dist/src/utils/modelUtils.test.js +23 -0
- package/dist/src/utils/modelUtils.test.js.map +1 -0
- package/dist/src/utils/paths.d.ts +15 -1
- package/dist/src/utils/paths.js +22 -7
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/src/utils/paths.test.js +25 -1
- package/dist/src/utils/paths.test.js.map +1 -1
- package/dist/src/utils/quotaErrorDetection.js +23 -12
- package/dist/src/utils/quotaErrorDetection.js.map +1 -1
- package/dist/src/utils/shell-utils.js +9 -1
- package/dist/src/utils/shell-utils.js.map +1 -1
- package/dist/src/utils/shell-utils.test.js +7 -3
- package/dist/src/utils/shell-utils.test.js.map +1 -1
- package/dist/src/utils/tokenCalculation.js +2 -1
- package/dist/src/utils/tokenCalculation.js.map +1 -1
- package/dist/src/utils/tokenCalculation.test.js +15 -0
- package/dist/src/utils/tokenCalculation.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { LocalSubagentSession } from './local-subagent-protocol.js';
|
|
8
|
+
import { LocalAgentExecutor } from './local-executor.js';
|
|
9
|
+
import { AgentTerminateMode, } from './types.js';
|
|
10
|
+
import { makeFakeConfig } from '../test-utils/config.js';
|
|
11
|
+
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
|
|
12
|
+
vi.mock('./local-executor.js');
|
|
13
|
+
const MockLocalAgentExecutor = vi.mocked(LocalAgentExecutor);
|
|
14
|
+
// Captures the onActivity callback passed to LocalAgentExecutor.create().
|
|
15
|
+
// Set via create.mockImplementation in beforeEach to avoid mock.calls index fragility.
|
|
16
|
+
let capturedOnActivity;
|
|
17
|
+
const testDefinition = {
|
|
18
|
+
kind: 'local',
|
|
19
|
+
name: 'TestProtocolAgent',
|
|
20
|
+
description: 'A test agent for protocol tests.',
|
|
21
|
+
inputConfig: {
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
task: { type: 'string' },
|
|
26
|
+
priority: { type: 'number' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
modelConfig: { model: 'test', generateContentConfig: {} },
|
|
31
|
+
runConfig: { maxTimeMinutes: 1 },
|
|
32
|
+
promptConfig: { systemPrompt: 'test' },
|
|
33
|
+
};
|
|
34
|
+
const GOAL_OUTPUT = {
|
|
35
|
+
result: 'Analysis complete.',
|
|
36
|
+
terminate_reason: AgentTerminateMode.GOAL,
|
|
37
|
+
};
|
|
38
|
+
describe('LocalSubagentSession (protocol)', () => {
|
|
39
|
+
let mockContext;
|
|
40
|
+
let mockMessageBus;
|
|
41
|
+
let mockExecutorInstance;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
capturedOnActivity = undefined;
|
|
45
|
+
mockContext = makeFakeConfig();
|
|
46
|
+
mockMessageBus = createMockMessageBus();
|
|
47
|
+
mockExecutorInstance = {
|
|
48
|
+
run: vi.fn().mockResolvedValue(GOAL_OUTPUT),
|
|
49
|
+
definition: testDefinition,
|
|
50
|
+
};
|
|
51
|
+
// Use mockImplementation (not mockResolvedValue) so we can capture onActivity.
|
|
52
|
+
MockLocalAgentExecutor.create.mockImplementation(
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
async (_def, _ctx, onActivity) => {
|
|
55
|
+
capturedOnActivity = onActivity;
|
|
56
|
+
return mockExecutorInstance;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.restoreAllMocks();
|
|
61
|
+
});
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Lifecycle events
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
describe('lifecycle events', () => {
|
|
66
|
+
it('emits agent_start then agent_end(completed) for a GOAL run', async () => {
|
|
67
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
68
|
+
const events = [];
|
|
69
|
+
session.subscribe((e) => events.push(e));
|
|
70
|
+
await session.send({
|
|
71
|
+
message: { content: [{ type: 'text', text: 'query' }] },
|
|
72
|
+
});
|
|
73
|
+
await session.getResult();
|
|
74
|
+
expect(events[0].type).toBe('agent_start');
|
|
75
|
+
expect(events[events.length - 1].type).toBe('agent_end');
|
|
76
|
+
const endEvent = events[events.length - 1];
|
|
77
|
+
if (endEvent.type === 'agent_end') {
|
|
78
|
+
expect(endEvent.reason).toBe('completed');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it('emits agent_start exactly once even if ensureAgentStart called twice internally', async () => {
|
|
82
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
83
|
+
const events = [];
|
|
84
|
+
session.subscribe((e) => events.push(e));
|
|
85
|
+
await session.send({
|
|
86
|
+
message: { content: [{ type: 'text', text: 'query' }] },
|
|
87
|
+
});
|
|
88
|
+
await session.getResult();
|
|
89
|
+
const startEvents = events.filter((e) => e.type === 'agent_start');
|
|
90
|
+
expect(startEvents).toHaveLength(1);
|
|
91
|
+
});
|
|
92
|
+
it('emits agent_end exactly once on error path', async () => {
|
|
93
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
94
|
+
mockExecutorInstance.run.mockRejectedValue(new Error('executor failed'));
|
|
95
|
+
const events = [];
|
|
96
|
+
session.subscribe((e) => events.push(e));
|
|
97
|
+
await session.send({
|
|
98
|
+
message: { content: [{ type: 'text', text: 'query' }] },
|
|
99
|
+
});
|
|
100
|
+
await expect(session.getResult()).rejects.toThrow('executor failed');
|
|
101
|
+
const endEvents = events.filter((e) => e.type === 'agent_end');
|
|
102
|
+
expect(endEvents).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
it('all events share the same streamId', async () => {
|
|
105
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
106
|
+
const events = [];
|
|
107
|
+
session.subscribe((e) => events.push(e));
|
|
108
|
+
await session.send({
|
|
109
|
+
message: { content: [{ type: 'text', text: 'query' }] },
|
|
110
|
+
});
|
|
111
|
+
await session.getResult();
|
|
112
|
+
const streamIds = new Set(events.map((e) => e.streamId));
|
|
113
|
+
expect(streamIds.size).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Config buffering (update + message pattern)
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
describe('config buffering', () => {
|
|
120
|
+
it('merges buffered config with message query', async () => {
|
|
121
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
122
|
+
await session.send({
|
|
123
|
+
update: { config: { task: 'analyze', priority: 5 } },
|
|
124
|
+
});
|
|
125
|
+
await session.send({
|
|
126
|
+
message: { content: [{ type: 'text', text: 'my query' }] },
|
|
127
|
+
});
|
|
128
|
+
await session.getResult();
|
|
129
|
+
expect(mockExecutorInstance.run).toHaveBeenCalledWith({ task: 'analyze', priority: 5, query: 'my query' }, expect.any(AbortSignal));
|
|
130
|
+
});
|
|
131
|
+
it('omits query key when message text is empty', async () => {
|
|
132
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
133
|
+
await session.send({ update: { config: { task: 'no-query-task' } } });
|
|
134
|
+
await session.send({
|
|
135
|
+
message: { content: [{ type: 'text', text: '' }] },
|
|
136
|
+
});
|
|
137
|
+
await session.getResult();
|
|
138
|
+
const callArgs = mockExecutorInstance.run.mock.calls[0][0];
|
|
139
|
+
expect(callArgs).not.toHaveProperty('query');
|
|
140
|
+
expect(callArgs).toEqual({ task: 'no-query-task' });
|
|
141
|
+
});
|
|
142
|
+
it('sends only query when no prior update', async () => {
|
|
143
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
144
|
+
await session.send({
|
|
145
|
+
message: { content: [{ type: 'text', text: 'just a query' }] },
|
|
146
|
+
});
|
|
147
|
+
await session.getResult();
|
|
148
|
+
expect(mockExecutorInstance.run).toHaveBeenCalledWith({ query: 'just a query' }, expect.any(AbortSignal));
|
|
149
|
+
});
|
|
150
|
+
it('multiple update calls are merged', async () => {
|
|
151
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
152
|
+
await session.send({ update: { config: { field1: 'a' } } });
|
|
153
|
+
await session.send({ update: { config: { field2: 'b' } } });
|
|
154
|
+
await session.send({
|
|
155
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
156
|
+
});
|
|
157
|
+
await session.getResult();
|
|
158
|
+
expect(mockExecutorInstance.run).toHaveBeenCalledWith({ field1: 'a', field2: 'b', query: 'q' }, expect.any(AbortSignal));
|
|
159
|
+
});
|
|
160
|
+
it('update returns streamId: null; message returns a streamId', async () => {
|
|
161
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
162
|
+
const updateResult = await session.send({ update: { config: {} } });
|
|
163
|
+
expect(updateResult.streamId).toBeNull();
|
|
164
|
+
const messageResult = await session.send({
|
|
165
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
166
|
+
});
|
|
167
|
+
expect(messageResult.streamId).not.toBeNull();
|
|
168
|
+
expect(typeof messageResult.streamId).toBe('string');
|
|
169
|
+
// Await completion to prevent dangling execution affecting subsequent tests
|
|
170
|
+
await session.getResult();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Activity translation
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
describe('activity translation', () => {
|
|
177
|
+
function makeSession() {
|
|
178
|
+
const activityEvents = [];
|
|
179
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
180
|
+
return { session, activityEvents };
|
|
181
|
+
}
|
|
182
|
+
async function runWithActivities(session, activities) {
|
|
183
|
+
mockExecutorInstance.run.mockImplementation(async () => {
|
|
184
|
+
// capturedOnActivity is set by the create.mockImplementation in beforeEach
|
|
185
|
+
// and updated whenever create() is called. By the time run() is called,
|
|
186
|
+
// capturedOnActivity holds the onActivity closure for the most-recently
|
|
187
|
+
// created executor — which is the one associated with this session.
|
|
188
|
+
for (const act of activities) {
|
|
189
|
+
capturedOnActivity?.(act);
|
|
190
|
+
}
|
|
191
|
+
return GOAL_OUTPUT;
|
|
192
|
+
});
|
|
193
|
+
const events = [];
|
|
194
|
+
session.subscribe((e) => events.push(e));
|
|
195
|
+
await session.send({
|
|
196
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
197
|
+
});
|
|
198
|
+
await session.getResult();
|
|
199
|
+
return events;
|
|
200
|
+
}
|
|
201
|
+
it('THOUGHT_CHUNK → message event with thought content', async () => {
|
|
202
|
+
const { session } = makeSession();
|
|
203
|
+
const events = await runWithActivities(session, [
|
|
204
|
+
{
|
|
205
|
+
isSubagentActivityEvent: true,
|
|
206
|
+
agentName: 'TestProtocolAgent',
|
|
207
|
+
type: 'THOUGHT_CHUNK',
|
|
208
|
+
data: { text: 'I am thinking...' },
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
const msgEvent = events.find((e) => e.type === 'message');
|
|
212
|
+
expect(msgEvent).toBeDefined();
|
|
213
|
+
if (msgEvent?.type === 'message') {
|
|
214
|
+
expect(msgEvent.role).toBe('agent');
|
|
215
|
+
expect(msgEvent.content).toContainEqual({
|
|
216
|
+
type: 'thought',
|
|
217
|
+
thought: 'I am thinking...',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
it('TOOL_CALL_START → tool_request event', async () => {
|
|
222
|
+
const { session } = makeSession();
|
|
223
|
+
const events = await runWithActivities(session, [
|
|
224
|
+
{
|
|
225
|
+
isSubagentActivityEvent: true,
|
|
226
|
+
agentName: 'TestProtocolAgent',
|
|
227
|
+
type: 'TOOL_CALL_START',
|
|
228
|
+
data: { callId: 'call-123', name: 'read_file', args: { path: '/a' } },
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
const reqEvent = events.find((e) => e.type === 'tool_request');
|
|
232
|
+
expect(reqEvent).toBeDefined();
|
|
233
|
+
if (reqEvent?.type === 'tool_request') {
|
|
234
|
+
expect(reqEvent.requestId).toBe('call-123');
|
|
235
|
+
expect(reqEvent.name).toBe('read_file');
|
|
236
|
+
expect(reqEvent.args).toEqual({ path: '/a' });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
it('TOOL_CALL_END → tool_response event', async () => {
|
|
240
|
+
const { session } = makeSession();
|
|
241
|
+
const events = await runWithActivities(session, [
|
|
242
|
+
{
|
|
243
|
+
isSubagentActivityEvent: true,
|
|
244
|
+
agentName: 'TestProtocolAgent',
|
|
245
|
+
type: 'TOOL_CALL_END',
|
|
246
|
+
data: { id: 'call-123', name: 'read_file', output: 'file contents' },
|
|
247
|
+
},
|
|
248
|
+
]);
|
|
249
|
+
const respEvent = events.find((e) => e.type === 'tool_response');
|
|
250
|
+
expect(respEvent).toBeDefined();
|
|
251
|
+
if (respEvent?.type === 'tool_response') {
|
|
252
|
+
expect(respEvent.requestId).toBe('call-123');
|
|
253
|
+
expect(respEvent.name).toBe('read_file');
|
|
254
|
+
expect(respEvent.content).toContainEqual({
|
|
255
|
+
type: 'text',
|
|
256
|
+
text: 'file contents',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
it('ERROR activity → error event with INTERNAL status, fatal: false', async () => {
|
|
261
|
+
const { session } = makeSession();
|
|
262
|
+
const events = await runWithActivities(session, [
|
|
263
|
+
{
|
|
264
|
+
isSubagentActivityEvent: true,
|
|
265
|
+
agentName: 'TestProtocolAgent',
|
|
266
|
+
type: 'ERROR',
|
|
267
|
+
data: { error: 'something went wrong' },
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
const errEvent = events.find((e) => e.type === 'error');
|
|
271
|
+
expect(errEvent).toBeDefined();
|
|
272
|
+
if (errEvent?.type === 'error') {
|
|
273
|
+
expect(errEvent.status).toBe('INTERNAL');
|
|
274
|
+
expect(errEvent.message).toBe('something went wrong');
|
|
275
|
+
expect(errEvent.fatal).toBe(false);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
it('unknown activity type → no events emitted', async () => {
|
|
279
|
+
const { session } = makeSession();
|
|
280
|
+
const events = await runWithActivities(session, [
|
|
281
|
+
{
|
|
282
|
+
isSubagentActivityEvent: true,
|
|
283
|
+
agentName: 'TestProtocolAgent',
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
|
+
type: 'UNKNOWN_TYPE',
|
|
286
|
+
data: {},
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
// Only agent_start and agent_end should be present
|
|
290
|
+
const nonLifecycle = events.filter((e) => e.type !== 'agent_start' && e.type !== 'agent_end');
|
|
291
|
+
expect(nonLifecycle).toHaveLength(0);
|
|
292
|
+
});
|
|
293
|
+
it('TOOL_CALL_START with non-object args defaults to {}', async () => {
|
|
294
|
+
const { session } = makeSession();
|
|
295
|
+
const events = await runWithActivities(session, [
|
|
296
|
+
{
|
|
297
|
+
isSubagentActivityEvent: true,
|
|
298
|
+
agentName: 'TestProtocolAgent',
|
|
299
|
+
type: 'TOOL_CALL_START',
|
|
300
|
+
data: { callId: 'x', name: 'tool', args: null },
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
const reqEvent = events.find((e) => e.type === 'tool_request');
|
|
304
|
+
if (reqEvent?.type === 'tool_request') {
|
|
305
|
+
expect(reqEvent.args).toEqual({});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// getResult() promise
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
describe('getResult()', () => {
|
|
313
|
+
it('resolves with OutputObject on GOAL termination', async () => {
|
|
314
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
315
|
+
await session.send({
|
|
316
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
317
|
+
});
|
|
318
|
+
const output = await session.getResult();
|
|
319
|
+
expect(output.result).toBe('Analysis complete.');
|
|
320
|
+
expect(output.terminate_reason).toBe(AgentTerminateMode.GOAL);
|
|
321
|
+
});
|
|
322
|
+
it('rejects when executor throws', async () => {
|
|
323
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
324
|
+
mockExecutorInstance.run.mockRejectedValue(new Error('executor error'));
|
|
325
|
+
await session.send({
|
|
326
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
327
|
+
});
|
|
328
|
+
await expect(session.getResult()).rejects.toThrow('executor error');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// rawActivityCallback
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
describe('rawActivityCallback', () => {
|
|
335
|
+
it('receives raw SubagentActivityEvent before AgentEvent translation', async () => {
|
|
336
|
+
const rawActivities = [];
|
|
337
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus, (activity) => rawActivities.push(activity));
|
|
338
|
+
const thoughtActivity = {
|
|
339
|
+
isSubagentActivityEvent: true,
|
|
340
|
+
agentName: 'TestProtocolAgent',
|
|
341
|
+
type: 'THOUGHT_CHUNK',
|
|
342
|
+
data: { text: 'raw thought' },
|
|
343
|
+
};
|
|
344
|
+
mockExecutorInstance.run.mockImplementation(async () => {
|
|
345
|
+
const onActivity = MockLocalAgentExecutor.create.mock.calls[0]?.[2];
|
|
346
|
+
onActivity?.(thoughtActivity);
|
|
347
|
+
return GOAL_OUTPUT;
|
|
348
|
+
});
|
|
349
|
+
await session.send({
|
|
350
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
351
|
+
});
|
|
352
|
+
await session.getResult();
|
|
353
|
+
expect(rawActivities).toHaveLength(1);
|
|
354
|
+
expect(rawActivities[0]).toBe(thoughtActivity);
|
|
355
|
+
});
|
|
356
|
+
it('is called before AgentEvent translation (raw arrives first)', async () => {
|
|
357
|
+
const callOrder = [];
|
|
358
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus, () => callOrder.push('raw'));
|
|
359
|
+
session.subscribe((e) => {
|
|
360
|
+
if (e.type === 'message')
|
|
361
|
+
callOrder.push('translated');
|
|
362
|
+
});
|
|
363
|
+
mockExecutorInstance.run.mockImplementation(async () => {
|
|
364
|
+
const onActivity = MockLocalAgentExecutor.create.mock.calls[0]?.[2];
|
|
365
|
+
onActivity?.({
|
|
366
|
+
isSubagentActivityEvent: true,
|
|
367
|
+
agentName: 'TestProtocolAgent',
|
|
368
|
+
type: 'THOUGHT_CHUNK',
|
|
369
|
+
data: { text: 'thought' },
|
|
370
|
+
});
|
|
371
|
+
return GOAL_OUTPUT;
|
|
372
|
+
});
|
|
373
|
+
await session.send({
|
|
374
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
375
|
+
});
|
|
376
|
+
await session.getResult();
|
|
377
|
+
expect(callOrder).toEqual(['raw', 'translated']);
|
|
378
|
+
});
|
|
379
|
+
it('is optional — no callback causes no error', async () => {
|
|
380
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
381
|
+
await session.send({
|
|
382
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
383
|
+
});
|
|
384
|
+
await expect(session.getResult()).resolves.toBeDefined();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Subscription
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
describe('subscription', () => {
|
|
391
|
+
it('unsubscribe stops event delivery', async () => {
|
|
392
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
393
|
+
const received = [];
|
|
394
|
+
const unsub = session.subscribe((e) => received.push(e));
|
|
395
|
+
unsub();
|
|
396
|
+
await session.send({
|
|
397
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
398
|
+
});
|
|
399
|
+
await session.getResult();
|
|
400
|
+
expect(received).toHaveLength(0);
|
|
401
|
+
});
|
|
402
|
+
it('multiple subscribers all receive events', async () => {
|
|
403
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
404
|
+
const received1 = [];
|
|
405
|
+
const received2 = [];
|
|
406
|
+
session.subscribe((e) => received1.push(e));
|
|
407
|
+
session.subscribe((e) => received2.push(e));
|
|
408
|
+
await session.send({
|
|
409
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
410
|
+
});
|
|
411
|
+
await session.getResult();
|
|
412
|
+
expect(received1.length).toBeGreaterThan(0);
|
|
413
|
+
expect(received1).toEqual(received2);
|
|
414
|
+
});
|
|
415
|
+
it('events array accumulates all emitted events', async () => {
|
|
416
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
417
|
+
await session.send({
|
|
418
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
419
|
+
});
|
|
420
|
+
await session.getResult();
|
|
421
|
+
expect(session.events.length).toBeGreaterThanOrEqual(2); // at least agent_start + agent_end
|
|
422
|
+
expect(session.events[0].type).toBe('agent_start');
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Terminate mode mapping
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
describe('terminate mode → StreamEndReason mapping', () => {
|
|
429
|
+
const cases = [
|
|
430
|
+
[AgentTerminateMode.GOAL, 'completed'],
|
|
431
|
+
[AgentTerminateMode.TIMEOUT, 'max_time'],
|
|
432
|
+
[AgentTerminateMode.MAX_TURNS, 'max_turns'],
|
|
433
|
+
[AgentTerminateMode.ABORTED, 'aborted'],
|
|
434
|
+
[AgentTerminateMode.ERROR, 'failed'],
|
|
435
|
+
[AgentTerminateMode.ERROR_NO_COMPLETE_TASK_CALL, 'failed'],
|
|
436
|
+
];
|
|
437
|
+
for (const [terminateMode, expectedReason] of cases) {
|
|
438
|
+
it(`${terminateMode} → agent_end(reason:'${expectedReason}')`, async () => {
|
|
439
|
+
mockExecutorInstance.run.mockResolvedValue({
|
|
440
|
+
result: 'done',
|
|
441
|
+
terminate_reason: terminateMode,
|
|
442
|
+
});
|
|
443
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
444
|
+
const events = [];
|
|
445
|
+
session.subscribe((e) => events.push(e));
|
|
446
|
+
await session.send({
|
|
447
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
448
|
+
});
|
|
449
|
+
await session.getResult().catch(() => {
|
|
450
|
+
// ABORTED results in rejection — catch to let test complete
|
|
451
|
+
});
|
|
452
|
+
const endEvent = events.find((e) => e.type === 'agent_end');
|
|
453
|
+
expect(endEvent).toBeDefined();
|
|
454
|
+
if (endEvent?.type === 'agent_end') {
|
|
455
|
+
expect(endEvent.reason).toBe(expectedReason);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Abort
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
describe('abort()', () => {
|
|
464
|
+
it('abort() causes agent_end(reason:aborted)', async () => {
|
|
465
|
+
// Make run() wait until aborted
|
|
466
|
+
let abortSignal;
|
|
467
|
+
mockExecutorInstance.run.mockImplementation((_params, signal) => {
|
|
468
|
+
abortSignal = signal;
|
|
469
|
+
return new Promise((_resolve, reject) => {
|
|
470
|
+
signal.addEventListener('abort', () => {
|
|
471
|
+
const err = new Error('AbortError');
|
|
472
|
+
err.name = 'AbortError';
|
|
473
|
+
reject(err);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
478
|
+
const events = [];
|
|
479
|
+
session.subscribe((e) => events.push(e));
|
|
480
|
+
void session.send({
|
|
481
|
+
message: { content: [{ type: 'text', text: 'q' }] },
|
|
482
|
+
});
|
|
483
|
+
// Wait for executor to be created and run started
|
|
484
|
+
await vi.waitFor(() => {
|
|
485
|
+
expect(abortSignal).toBeDefined();
|
|
486
|
+
});
|
|
487
|
+
await session.abort();
|
|
488
|
+
const result = await session.getResult();
|
|
489
|
+
expect(result.result).toBe('');
|
|
490
|
+
expect(result.terminate_reason).toBe('ABORTED');
|
|
491
|
+
const endEvent = events.find((e) => e.type === 'agent_end');
|
|
492
|
+
expect(endEvent).toBeDefined();
|
|
493
|
+
if (endEvent?.type === 'agent_end') {
|
|
494
|
+
expect(endEvent.reason).toBe('aborted');
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Full event sequence
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
describe('full event sequence', () => {
|
|
502
|
+
it('emits agent_start → message(thought) → tool_request → tool_response → agent_end in order', async () => {
|
|
503
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
504
|
+
mockExecutorInstance.run.mockImplementation(async () => {
|
|
505
|
+
const onActivity = MockLocalAgentExecutor.create.mock.calls[0]?.[2];
|
|
506
|
+
onActivity?.({
|
|
507
|
+
isSubagentActivityEvent: true,
|
|
508
|
+
agentName: 'TestProtocolAgent',
|
|
509
|
+
type: 'THOUGHT_CHUNK',
|
|
510
|
+
data: { text: 'thinking' },
|
|
511
|
+
});
|
|
512
|
+
onActivity?.({
|
|
513
|
+
isSubagentActivityEvent: true,
|
|
514
|
+
agentName: 'TestProtocolAgent',
|
|
515
|
+
type: 'TOOL_CALL_START',
|
|
516
|
+
data: { callId: 'c1', name: 'tool', args: {} },
|
|
517
|
+
});
|
|
518
|
+
onActivity?.({
|
|
519
|
+
isSubagentActivityEvent: true,
|
|
520
|
+
agentName: 'TestProtocolAgent',
|
|
521
|
+
type: 'TOOL_CALL_END',
|
|
522
|
+
data: { id: 'c1', name: 'tool', output: 'result' },
|
|
523
|
+
});
|
|
524
|
+
return GOAL_OUTPUT;
|
|
525
|
+
});
|
|
526
|
+
const events = [];
|
|
527
|
+
session.subscribe((e) => events.push(e));
|
|
528
|
+
await session.send({
|
|
529
|
+
message: { content: [{ type: 'text', text: 'go' }] },
|
|
530
|
+
});
|
|
531
|
+
await session.getResult();
|
|
532
|
+
const types = events.map((e) => e.type);
|
|
533
|
+
expect(types).toEqual([
|
|
534
|
+
'agent_start',
|
|
535
|
+
'message',
|
|
536
|
+
'tool_request',
|
|
537
|
+
'tool_response',
|
|
538
|
+
'agent_end',
|
|
539
|
+
]);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Concurrent send() guard
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
describe('concurrent send() guard', () => {
|
|
546
|
+
it('calling send() while a stream is active throws', async () => {
|
|
547
|
+
let abortSignal;
|
|
548
|
+
mockExecutorInstance.run.mockImplementation((_params, signal) => {
|
|
549
|
+
abortSignal = signal;
|
|
550
|
+
return new Promise((_resolve, reject) => {
|
|
551
|
+
// Reject when aborted so getResult() can settle during cleanup
|
|
552
|
+
signal.addEventListener('abort', () => {
|
|
553
|
+
const err = new Error('AbortError');
|
|
554
|
+
err.name = 'AbortError';
|
|
555
|
+
reject(err);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
560
|
+
void session.send({
|
|
561
|
+
message: { content: [{ type: 'text', text: 'first' }] },
|
|
562
|
+
});
|
|
563
|
+
// Wait for execution to start
|
|
564
|
+
await vi.waitFor(() => {
|
|
565
|
+
expect(abortSignal).toBeDefined();
|
|
566
|
+
});
|
|
567
|
+
// Second send() while first stream is active must throw
|
|
568
|
+
await expect(session.send({
|
|
569
|
+
message: { content: [{ type: 'text', text: 'second' }] },
|
|
570
|
+
})).rejects.toThrow('cannot be called while a stream is active');
|
|
571
|
+
// Clean up: abort to unblock the hanging executor
|
|
572
|
+
await session.abort();
|
|
573
|
+
await session.getResult().catch(() => { });
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
// Multi-send support
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
describe('multi-send', () => {
|
|
580
|
+
it('supports sequential sends after stream completion', async () => {
|
|
581
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
582
|
+
// First send
|
|
583
|
+
const result1 = await session.send({
|
|
584
|
+
message: { content: [{ type: 'text', text: 'first' }] },
|
|
585
|
+
});
|
|
586
|
+
expect(result1.streamId).not.toBeNull();
|
|
587
|
+
const output1 = await session.getResult();
|
|
588
|
+
expect(output1.result).toBe('Analysis complete.');
|
|
589
|
+
// Second send — should work, not throw
|
|
590
|
+
const secondOutput = {
|
|
591
|
+
result: 'Second analysis.',
|
|
592
|
+
terminate_reason: AgentTerminateMode.GOAL,
|
|
593
|
+
};
|
|
594
|
+
mockExecutorInstance.run.mockResolvedValue(secondOutput);
|
|
595
|
+
const result2 = await session.send({
|
|
596
|
+
message: { content: [{ type: 'text', text: 'second' }] },
|
|
597
|
+
});
|
|
598
|
+
expect(result2.streamId).not.toBeNull();
|
|
599
|
+
expect(result2.streamId).not.toBe(result1.streamId);
|
|
600
|
+
const output2 = await session.getResult();
|
|
601
|
+
expect(output2.result).toBe('Second analysis.');
|
|
602
|
+
});
|
|
603
|
+
it('getResult() returns the latest stream result', async () => {
|
|
604
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
605
|
+
// First send
|
|
606
|
+
await session.send({
|
|
607
|
+
message: { content: [{ type: 'text', text: 'first' }] },
|
|
608
|
+
});
|
|
609
|
+
const result1 = await session.getResult();
|
|
610
|
+
// Second send with different output
|
|
611
|
+
const secondOutput = {
|
|
612
|
+
result: 'Different result.',
|
|
613
|
+
terminate_reason: AgentTerminateMode.GOAL,
|
|
614
|
+
};
|
|
615
|
+
mockExecutorInstance.run.mockResolvedValue(secondOutput);
|
|
616
|
+
await session.send({
|
|
617
|
+
message: { content: [{ type: 'text', text: 'second' }] },
|
|
618
|
+
});
|
|
619
|
+
const result2 = await session.getResult();
|
|
620
|
+
expect(result1.result).toBe('Analysis complete.');
|
|
621
|
+
expect(result2.result).toBe('Different result.');
|
|
622
|
+
});
|
|
623
|
+
it('buffered config does not bleed across sends', async () => {
|
|
624
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
625
|
+
// Buffer config, then send first message
|
|
626
|
+
await session.send({ update: { config: { temperature: 0.5 } } });
|
|
627
|
+
await session.send({
|
|
628
|
+
message: { content: [{ type: 'text', text: 'first' }] },
|
|
629
|
+
});
|
|
630
|
+
await session.getResult();
|
|
631
|
+
// The executor.run params include the buffered config
|
|
632
|
+
const firstRunParams = mockExecutorInstance.run.mock.calls[0]?.[0];
|
|
633
|
+
expect(firstRunParams).toHaveProperty('temperature', 0.5);
|
|
634
|
+
expect(firstRunParams).toHaveProperty('query', 'first');
|
|
635
|
+
// Second send without buffered config — temperature should be gone
|
|
636
|
+
mockExecutorInstance.run.mockResolvedValue(GOAL_OUTPUT);
|
|
637
|
+
await session.send({
|
|
638
|
+
message: { content: [{ type: 'text', text: 'second' }] },
|
|
639
|
+
});
|
|
640
|
+
await session.getResult();
|
|
641
|
+
const secondRunParams = mockExecutorInstance.run.mock.calls[1]?.[0];
|
|
642
|
+
expect(secondRunParams).not.toHaveProperty('temperature');
|
|
643
|
+
expect(secondRunParams).toHaveProperty('query', 'second');
|
|
644
|
+
});
|
|
645
|
+
it('getResult() rejects when called before any send', async () => {
|
|
646
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
647
|
+
await expect(session.getResult()).rejects.toThrow('No active or completed stream');
|
|
648
|
+
});
|
|
649
|
+
it('emits fresh agent_start/agent_end per stream', async () => {
|
|
650
|
+
const session = new LocalSubagentSession(testDefinition, mockContext, mockMessageBus);
|
|
651
|
+
const events = [];
|
|
652
|
+
session.subscribe((e) => events.push(e));
|
|
653
|
+
// First send
|
|
654
|
+
await session.send({
|
|
655
|
+
message: { content: [{ type: 'text', text: 'first' }] },
|
|
656
|
+
});
|
|
657
|
+
await session.getResult();
|
|
658
|
+
const firstStreamEvents = events.length;
|
|
659
|
+
expect(events[0]?.type).toBe('agent_start');
|
|
660
|
+
expect(events[firstStreamEvents - 1]?.type).toBe('agent_end');
|
|
661
|
+
// Second send
|
|
662
|
+
mockExecutorInstance.run.mockResolvedValue(GOAL_OUTPUT);
|
|
663
|
+
await session.send({
|
|
664
|
+
message: { content: [{ type: 'text', text: 'second' }] },
|
|
665
|
+
});
|
|
666
|
+
await session.getResult();
|
|
667
|
+
// Should have a second agent_start/agent_end pair
|
|
668
|
+
const secondStreamStart = events[firstStreamEvents];
|
|
669
|
+
const lastEvent = events[events.length - 1];
|
|
670
|
+
expect(secondStreamStart?.type).toBe('agent_start');
|
|
671
|
+
expect(lastEvent?.type).toBe('agent_end');
|
|
672
|
+
expect(secondStreamStart?.streamId).not.toBe(events[0]?.streamId);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
//# sourceMappingURL=local-subagent-protocol.test.js.map
|