mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-17 04:35:50 +00:00
refactor(agent-core-v2): rework DI test services and decompose turn domain
- rework createServices around ServiceRegistration groups (base + additionalServices) and add per-domain register*Services stubs - extract loopRunner, toolCallExecutor and turnEvents out of turnService; switch turn lifecycle to typed events - add AsyncEmitter / IWaitUntil / handleVetos to _base/event for interceptable onWill and veto events - document DI x scope (docs/di.md, docs/service-design.md, scope-domain diagram) and refresh di-testing.md - ignore plan/
This commit is contained in:
parent
b21d8820d1
commit
67810a822f
67 changed files with 2672 additions and 816 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -24,3 +24,4 @@ docker-compose.yml
|
|||
docs/superpowers/
|
||||
reports/
|
||||
.superpowers/
|
||||
plan/
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ Barrel (`src/session/index.ts`):
|
|||
|
||||
Per-domain references live in `docs/`.
|
||||
|
||||
- [`docs/di.md`](docs/di.md) — Read **before adding any business capability**: a scenario-driven walkthrough of the DI × Scope black box, from "add a global service" through dependency injection, scope selection, disposal, delayed/eager instantiation, `invokeFunction`, `createInstance`, child scopes, and cycles — introducing each concept only as the scenario needs it.
|
||||
- [`docs/service-design.md`](docs/service-design.md) — Read **before designing a new Service**: first-principles rules for choosing a scope, splitting a domain Multi-Scope, picking a calling style (direct call vs event vs hook), and directing dependencies — the design companion to `docs/di.md`.
|
||||
- [`docs/flag.md`](docs/flag.md) — Read **before gating behavior behind a feature flag**: defining/registering a flag in `FLAG_DEFINITIONS`, checking `IFlagService.enabled(id)`, wiring the `[experimental]` config section, or deciding whether a flag is Core-scope vs. per-session.
|
||||
- [`docs/errors.md`](docs/errors.md) — Read **before raising errors from a domain**: defining a co-located `XxxError`, registering a code in `ErrorCodes`/`ERROR_INFO`, translating external errors (provider/HTTP, fs, MCP) at the boundary, or (de)serializing errors across RPC/SDK with `toErrorPayload`/`fromErrorPayload`.
|
||||
- [`docs/di-testing.md`](docs/di-testing.md) — Read **before writing or touching any DI/Scope test**: picking the right harness (`InstantiationService` vs `TestInstantiationService` vs `createScopedTestHost`), declaring deps with `@IService`, stubbing collaborators, and teardown via `DisposableStore`.
|
||||
- [`docs/di-scope-domains.puml`](docs/di-scope-domains.puml) — DI Scope × Domain dependency map (node color = `LifecycleScope`; solid edges = constructor DI injection, dashed edges = `wireRecord` / event-driven). **When adding a Service or changing the dependency relationships between Services, update this puml and regenerate `docs/di-scope-domains.svg`**.
|
||||
|
|
|
|||
310
packages/agent-core-v2/docs/di-scope-domains.puml
Normal file
310
packages/agent-core-v2/docs/di-scope-domains.puml
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
@startuml agent-core-v2-domains
|
||||
top to bottom direction
|
||||
skinparam shadowing false
|
||||
skinparam nodesep 28
|
||||
skinparam ranksep 55
|
||||
|
||||
legend right
|
||||
<b>Scope = node color</b>
|
||||
<back:#D6EAF8> </back> Core (process-wide, L0)
|
||||
<back:#D5F5E3> </back> Session (per session, L1)
|
||||
<back:#FDEBD0> </back> Agent (per agent, L2)
|
||||
<back:#FADBD8> </back> Turn (per turn, L3)
|
||||
<back:#E8DAEF> </back> Multi-scope (spans layers)
|
||||
<back:#F4F6F7> </back> WIP: contract only
|
||||
== Edges ==
|
||||
<color:#34495E>solid</color> : DI injection (ctor @IX)
|
||||
<color:#16A085>dashed</color> : event-driven (subscribe/emit)
|
||||
direction: consumer ---> provider
|
||||
endlegend
|
||||
|
||||
package "Core scope (process-wide)" #EAF3FB {
|
||||
rectangle "<b>environment</b>\n<size:9><i>Core</i></size>\n IEnvironmentService" as environment #D6EAF8
|
||||
rectangle "<b>log</b>\n<size:9><i>Core</i></size>\n ILogService\n ILogSink" as log #D6EAF8
|
||||
rectangle "<b>telemetry</b>\n<size:9><i>Core</i></size>\n ITelemetryService" as telemetry #D6EAF8
|
||||
rectangle "<b>event</b>\n<size:9><i>Core</i></size>\n IEventService" as event #D6EAF8
|
||||
rectangle "<b>gateway</b>\n<size:9><i>Core</i></size>\n IScopeRegistry\n IRestGateway\n IWSGateway\n IWSBroadcastService" as gateway #D6EAF8
|
||||
rectangle "<b>workspace</b>\n<size:9><i>Core</i></size>\n IWorkspaceRegistry\n IWorkspaceFsService" as workspace #D6EAF8
|
||||
rectangle "<b>filestore</b>\n<size:9><i>Core</i></size>\n IFileStore" as filestore #D6EAF8
|
||||
rectangle "<b>auth</b>\n<size:9><i>Core</i></size>\n IOAuthService\n IAuthSummaryService" as auth #D6EAF8
|
||||
rectangle "<b>flag</b>\n<size:9><i>Core</i></size>\n IFlagService" as flag #D6EAF8
|
||||
}
|
||||
|
||||
package "Session scope (per session)" #EAFAF1 {
|
||||
rectangle "<b>session</b>\n<size:9><i>Session</i></size>\n ISessionService" as session #D5F5E3
|
||||
rectangle "<b>fs</b>\n<size:9><i>Session</i></size>\n IFsService\n IFsSearchService\n IFsGitService\n IFsWatcher" as fs #D5F5E3
|
||||
rectangle "<b>approval</b>\n<size:9><i>Session</i></size>\n IApprovalService" as approval #D5F5E3
|
||||
rectangle "<b>question</b>\n<size:9><i>Session</i></size>\n IQuestionService" as question #D5F5E3
|
||||
rectangle "<b>session-activity</b>\n<size:9><i>Session</i></size>\n ISessionActivity" as session_activity #D5F5E3
|
||||
rectangle "<b>subagentHost</b>\n<size:9><i>Session</i></size>\n ISubagentHost" as subagentHost #D5F5E3
|
||||
rectangle "<b>terminal</b>\n<size:9><i>Session</i></size>\n ITerminalService" as terminal #D5F5E3
|
||||
rectangle "<b>agent-lifecycle</b>\n<size:9><i>Session</i></size>\n IAgentLifecycleService" as agent_lifecycle #D5F5E3
|
||||
rectangle "<b>session-context</b>\n<size:9><i>Session</i></size>\n ISessionContext (seed)" as session_context #D5F5E3
|
||||
}
|
||||
|
||||
package "Agent scope (per agent)" #FDF5E6 {
|
||||
rectangle "<b>permissionMode</b>\n<size:9><i>Agent</i></size>\n IPermissionModeService" as permissionMode #FDEBD0
|
||||
rectangle "<b>prompt</b>\n<size:9><i>Agent</i></size>\n IPromptService" as prompt #FDEBD0
|
||||
rectangle "<b>userTool</b>\n<size:9><i>Agent</i></size>\n IUserToolService" as userTool #FDEBD0
|
||||
rectangle "<b>llmRequestLog</b>\n<size:9><i>Agent</i></size>\n ILLMRequestLogService" as llmRequestLog #FDEBD0
|
||||
rectangle "<b>dynamicInjector</b>\n<size:9><i>Agent</i></size>\n IDynamicInjector" as dynamicInjector #FDEBD0
|
||||
rectangle "<b>replayBuilder</b>\n<size:9><i>Agent</i></size>\n IReplayBuilderService" as replayBuilder #FDEBD0
|
||||
rectangle "<b>profile</b>\n<size:9><i>Agent</i></size>\n IProfileService" as profile #FDEBD0
|
||||
rectangle "<b>rpc</b>\n<size:9><i>Agent</i></size>\n IAgentRPCService" as rpc #FDEBD0
|
||||
rectangle "<b>contextMemory</b>\n<size:9><i>Agent</i></size>\n IContextMemory" as contextMemory #FDEBD0
|
||||
rectangle "<b>toolExecutor</b>\n<size:9><i>Agent</i></size>\n IToolExecutor" as toolExecutor #FDEBD0
|
||||
rectangle "<b>permission</b>\n<size:9><i>Agent</i></size>\n IPermissionService" as permission #FDEBD0
|
||||
rectangle "<b>cron</b>\n<size:9><i>Agent</i></size>\n ICronService" as cron #FDEBD0
|
||||
rectangle "<b>llmRequester</b>\n<size:9><i>Agent</i></size>\n ILLMRequester" as llmRequester #FDEBD0
|
||||
rectangle "<b>fullCompaction</b>\n<size:9><i>Agent</i></size>\n IFullCompaction" as fullCompaction #FDEBD0
|
||||
rectangle "<b>permissionRules</b>\n<size:9><i>Agent</i></size>\n IPermissionRulesService" as permissionRules #FDEBD0
|
||||
rectangle "<b>skill</b>\n<size:9><i>Agent</i></size>\n IAgentSkillService" as skill #FDEBD0
|
||||
rectangle "<b>goal</b>\n<size:9><i>Agent</i></size>\n IGoalService" as goal #FDEBD0
|
||||
rectangle "<b>microCompaction</b>\n<size:9><i>Agent</i></size>\n IMicroCompactionService" as microCompaction #FDEBD0
|
||||
rectangle "<b>externalHooks</b>\n<size:9><i>Agent</i></size>\n IExternalHooksService" as externalHooks #FDEBD0
|
||||
rectangle "<b>swarm</b>\n<size:9><i>Agent</i></size>\n ISwarmService" as swarm #FDEBD0
|
||||
rectangle "<b>background</b>\n<size:9><i>Agent</i></size>\n IBackgroundService" as background #FDEBD0
|
||||
rectangle "<b>toolStore</b>\n<size:9><i>Agent</i></size>\n IToolStoreService" as toolStore #FDEBD0
|
||||
rectangle "<b>wireRecord</b>\n<size:9><i>Agent</i></size>\n IWireRecord (event hub)" as wireRecord #FDEBD0
|
||||
rectangle "<b>eventBus</b>\n<size:9><i>Agent</i></size>\n IEventBus" as eventBus #FDEBD0
|
||||
rectangle "<b>toolRegistry</b>\n<size:9><i>Agent</i></size>\n IToolRegistry" as toolRegistry #FDEBD0
|
||||
rectangle "<b>usage</b>\n<size:9><i>Agent</i></size>\n IUsageService" as usage #FDEBD0
|
||||
rectangle "<b>message</b>\n<size:9><i>Agent</i></size>\n IMessageService" as message #FDEBD0
|
||||
rectangle "<b>mcp</b>\n<size:9><i>Agent</i></size>\n IMcpService" as mcp #FDEBD0
|
||||
rectangle "<b>contextSize</b>\n<size:9><i>Agent</i></size>\n IContextSizeService" as contextSize #FDEBD0
|
||||
rectangle "<b>permissionPolicy</b>\n<size:9><i>Agent</i></size>\n IPermissionPolicyService" as permissionPolicy #FDEBD0
|
||||
rectangle "<b>contextProjector</b>\n<size:9><i>Agent</i></size>\n IContextProjector" as contextProjector #FDEBD0
|
||||
rectangle "<b>todoList</b>\n<size:9><i>Agent</i></size>\n ITodoListService" as todoList #FDEBD0
|
||||
rectangle "<b>loop</b>\n<size:9><i>Agent</i></size>\n ILoopService" as loop #FDEBD0
|
||||
rectangle "<b>turn</b>\n<size:9><i>Agent</i></size>\n ITurnService\n ITurnEvents\n ILoopRunner(T)\n IToolCallExecutor(T)" as turn #FDEBD0
|
||||
rectangle "<b>plan</b>\n<size:9><i>Agent</i></size>\n IPlanService" as plan #FDEBD0
|
||||
rectangle "<b>blobStore</b>\n<size:9><i>Agent</i></size>\n IBlobStoreService" as blobStore #FDEBD0
|
||||
}
|
||||
|
||||
package "Turn scope (per turn)" #FDEDEC {
|
||||
rectangle "<b>tooldedup</b>\n<size:9><i>Turn</i></size>\n IToolDedupService" as tooldedup #FADBD8
|
||||
}
|
||||
|
||||
package "Multi-scope" #F5EEF8 {
|
||||
rectangle "<b>config</b>\n<size:9><i>Multi</i></size>\n IConfigRegistry(C)\n IConfigService(C)\n IAgentConfigService(A)" as config #E8DAEF
|
||||
rectangle "<b>kosong</b>\n<size:9><i>Multi</i></size>\n IModelCatalogService(C)\n IProviderManager(S)\n ILLMService(A)" as kosong #E8DAEF
|
||||
rectangle "<b>records</b>\n<size:9><i>Multi</i></size>\n ISessionStore(C)\n ISessionMetaStore(S)\n IAgentRecords(A)" as records #E8DAEF
|
||||
rectangle "<b>tool</b>\n<size:9><i>Multi</i></size>\n IToolDefinitionRegistry(C)\n IToolService(A)" as tool #E8DAEF
|
||||
rectangle "<b>kaos</b>\n<size:9><i>Multi</i></size>\n IKaosFactory(C)\n ISessionKaosService(S)\n IKaosService / IAgentKaos(A)" as kaos #E8DAEF
|
||||
}
|
||||
|
||||
package "WIP (contract only)" #FBFCFC {
|
||||
rectangle "<b>context</b>\n<size:9><i>WIP</i></size>\n IContextService (WIP)" as context #F4F6F7
|
||||
rectangle "<b>injection</b>\n<size:9><i>WIP</i></size>\n IInjectionService (WIP)" as injection #F4F6F7
|
||||
rectangle "<b>turnRunner</b>\n<size:9><i>WIP</i></size>\n ITurnRunner (WIP)" as turnRunner #F4F6F7
|
||||
}
|
||||
|
||||
' ---- DI injection (solid) ----
|
||||
gateway --> event #34495E
|
||||
workspace --> kaos #34495E
|
||||
workspace --> log #34495E
|
||||
filestore --> kaos #34495E
|
||||
auth --> config #34495E
|
||||
auth --> environment #34495E
|
||||
auth --> telemetry #34495E
|
||||
flag --> config #34495E
|
||||
config --> environment #34495E
|
||||
config --> log #34495E
|
||||
config --> records #34495E
|
||||
config --> kaos #34495E
|
||||
kosong --> config #34495E
|
||||
kosong --> environment #34495E
|
||||
records --> kaos #34495E
|
||||
records --> log #34495E
|
||||
tool --> config #34495E
|
||||
tool --> records #34495E
|
||||
tool --> kaos #34495E
|
||||
tool --> permission #34495E
|
||||
tool --> kosong #34495E
|
||||
kaos --> environment #34495E
|
||||
kaos --> log #34495E
|
||||
session --> records #34495E
|
||||
session --> agent_lifecycle #34495E
|
||||
session --> session_activity #34495E
|
||||
session --> event #34495E
|
||||
fs --> kaos #34495E
|
||||
fs --> log #34495E
|
||||
session_activity --> agent_lifecycle #34495E
|
||||
terminal --> log #34495E
|
||||
terminal --> kaos #34495E
|
||||
agent_lifecycle --> session_context #34495E
|
||||
agent_lifecycle --> records #34495E
|
||||
permissionMode --> wireRecord #34495E
|
||||
permissionMode --> eventBus #34495E
|
||||
permissionMode --> replayBuilder #34495E
|
||||
permissionMode --> dynamicInjector #34495E
|
||||
prompt --> contextMemory #34495E
|
||||
prompt --> turn #34495E
|
||||
prompt --> wireRecord #34495E
|
||||
prompt --> eventBus #34495E
|
||||
userTool --> toolRegistry #34495E
|
||||
userTool --> profile #34495E
|
||||
userTool --> wireRecord #34495E
|
||||
llmRequestLog --> log #34495E
|
||||
dynamicInjector --> contextMemory #34495E
|
||||
dynamicInjector --> turn #34495E
|
||||
replayBuilder --> wireRecord #34495E
|
||||
profile --> wireRecord #34495E
|
||||
profile --> eventBus #34495E
|
||||
profile --> replayBuilder #34495E
|
||||
profile --> telemetry #34495E
|
||||
rpc --> prompt #34495E
|
||||
rpc --> turn #34495E
|
||||
rpc --> profile #34495E
|
||||
rpc --> permissionMode #34495E
|
||||
rpc --> permission #34495E
|
||||
rpc --> plan #34495E
|
||||
rpc --> swarm #34495E
|
||||
rpc --> fullCompaction #34495E
|
||||
rpc --> userTool #34495E
|
||||
rpc --> toolRegistry #34495E
|
||||
rpc --> background #34495E
|
||||
rpc --> contextMemory #34495E
|
||||
rpc --> contextSize #34495E
|
||||
rpc --> skill #34495E
|
||||
rpc --> subagentHost #34495E
|
||||
rpc --> usage #34495E
|
||||
rpc --> telemetry #34495E
|
||||
rpc --> goal #34495E
|
||||
contextMemory --> wireRecord #34495E
|
||||
contextMemory --> replayBuilder #34495E
|
||||
permission --> permissionMode #34495E
|
||||
permission --> permissionRules #34495E
|
||||
permission --> permissionPolicy #34495E
|
||||
permission --> externalHooks #34495E
|
||||
permission --> telemetry #34495E
|
||||
cron --> prompt #34495E
|
||||
cron --> eventBus #34495E
|
||||
cron --> wireRecord #34495E
|
||||
cron --> turnRunner #34495E
|
||||
cron --> telemetry #34495E
|
||||
cron --> toolRegistry #34495E
|
||||
llmRequester --> contextMemory #34495E
|
||||
llmRequester --> contextProjector #34495E
|
||||
llmRequester --> toolRegistry #34495E
|
||||
llmRequester --> profile #34495E
|
||||
llmRequester --> llmRequestLog #34495E
|
||||
fullCompaction --> contextMemory #34495E
|
||||
fullCompaction --> contextProjector #34495E
|
||||
fullCompaction --> contextSize #34495E
|
||||
fullCompaction --> llmRequester #34495E
|
||||
fullCompaction --> profile #34495E
|
||||
fullCompaction --> toolStore #34495E
|
||||
fullCompaction --> telemetry #34495E
|
||||
fullCompaction --> usage #34495E
|
||||
fullCompaction --> wireRecord #34495E
|
||||
fullCompaction --> eventBus #34495E
|
||||
fullCompaction --> replayBuilder #34495E
|
||||
fullCompaction --> externalHooks #34495E
|
||||
fullCompaction --> turnRunner #34495E
|
||||
permissionRules --> wireRecord #34495E
|
||||
permissionRules --> replayBuilder #34495E
|
||||
skill --> prompt #34495E
|
||||
skill --> eventBus #34495E
|
||||
skill --> wireRecord #34495E
|
||||
skill --> telemetry #34495E
|
||||
goal --> wireRecord #34495E
|
||||
goal --> eventBus #34495E
|
||||
goal --> contextMemory #34495E
|
||||
goal --> replayBuilder #34495E
|
||||
goal --> telemetry #34495E
|
||||
goal --> dynamicInjector #34495E
|
||||
microCompaction --> contextMemory #34495E
|
||||
microCompaction --> contextSize #34495E
|
||||
microCompaction --> wireRecord #34495E
|
||||
microCompaction --> profile #34495E
|
||||
microCompaction --> telemetry #34495E
|
||||
microCompaction --> turn #34495E
|
||||
swarm --> contextMemory #34495E
|
||||
swarm --> wireRecord #34495E
|
||||
swarm --> eventBus #34495E
|
||||
swarm --> turn #34495E
|
||||
swarm --> toolRegistry #34495E
|
||||
swarm --> subagentHost #34495E
|
||||
background --> eventBus #34495E
|
||||
background --> wireRecord #34495E
|
||||
background --> telemetry #34495E
|
||||
background --> prompt #34495E
|
||||
background --> externalHooks #34495E
|
||||
background --> contextMemory #34495E
|
||||
toolStore --> wireRecord #34495E
|
||||
wireRecord --> blobStore #34495E
|
||||
eventBus --> wireRecord #34495E
|
||||
usage --> wireRecord #34495E
|
||||
usage --> eventBus #34495E
|
||||
message --> context #34495E
|
||||
mcp --> toolRegistry #34495E
|
||||
mcp --> eventBus #34495E
|
||||
contextSize --> contextMemory #34495E
|
||||
contextSize --> eventBus #34495E
|
||||
contextSize --> profile #34495E
|
||||
contextSize --> wireRecord #34495E
|
||||
todoList --> contextMemory #34495E
|
||||
todoList --> profile #34495E
|
||||
todoList --> toolStore #34495E
|
||||
todoList --> toolRegistry #34495E
|
||||
todoList --> dynamicInjector #34495E
|
||||
loop --> contextMemory #34495E
|
||||
loop --> contextProjector #34495E
|
||||
loop --> contextSize #34495E
|
||||
loop --> llmRequester #34495E
|
||||
loop --> eventBus #34495E
|
||||
loop --> toolRegistry #34495E
|
||||
loop --> toolExecutor #34495E
|
||||
loop --> usage #34495E
|
||||
loop --> profile #34495E
|
||||
loop --> telemetry #34495E
|
||||
loop --> wireRecord #34495E
|
||||
loop --> mcp #34495E
|
||||
loop --> externalHooks #34495E
|
||||
turn --> context #34495E
|
||||
turn --> kosong #34495E
|
||||
turn --> injection #34495E
|
||||
turn --> usage #34495E
|
||||
turn --> telemetry #34495E
|
||||
turn --> log #34495E
|
||||
turn --> agent_lifecycle #34495E
|
||||
turn --> tool #34495E
|
||||
plan --> contextMemory #34495E
|
||||
plan --> wireRecord #34495E
|
||||
plan --> eventBus #34495E
|
||||
plan --> kaos #34495E
|
||||
plan --> profile #34495E
|
||||
plan --> replayBuilder #34495E
|
||||
plan --> toolRegistry #34495E
|
||||
plan --> dynamicInjector #34495E
|
||||
plan --> telemetry #34495E
|
||||
tooldedup --> telemetry #34495E
|
||||
|
||||
' ---- event-driven (dashed) ----
|
||||
gateway ..> event #16A085 : subscribe
|
||||
flag ..> config #16A085 : onDidChange
|
||||
agent_lifecycle ..> turn #16A085 : onWillExecuteTool
|
||||
permissionMode ..> wireRecord #16A085 : permission.set_mode
|
||||
userTool ..> wireRecord #16A085 : tools.register_/unregister_user_tool
|
||||
profile ..> wireRecord #16A085 : config.update / tools.set_active_tools
|
||||
contextMemory ..> wireRecord #16A085 : context.splice
|
||||
cron ..> wireRecord #16A085 : cron.add / delete / cursor
|
||||
fullCompaction ..> wireRecord #16A085 : full_compaction.begin/cancel/complete
|
||||
permissionRules ..> wireRecord #16A085 : permission.rules.add / record_approval_result
|
||||
skill ..> wireRecord #16A085 : skill.activate
|
||||
goal ..> wireRecord #16A085 : goal.create/update/clear
|
||||
microCompaction ..> wireRecord #16A085 : micro_compaction.apply / full_compaction.complete
|
||||
swarm ..> wireRecord #16A085 : swarm_mode.enter/exit
|
||||
swarm ..> turn #16A085 : hooks.onEnded
|
||||
background ..> wireRecord #16A085 : background.task.started/terminated
|
||||
background ..> contextMemory #16A085 : hooks.onSpliced
|
||||
toolStore ..> wireRecord #16A085 : tools.update_store
|
||||
usage ..> wireRecord #16A085 : usage.record
|
||||
contextSize ..> contextMemory #16A085 : hooks.onSpliced
|
||||
contextSize ..> wireRecord #16A085 : context_size.measured
|
||||
loop ..> contextMemory #16A085 : hooks.onSpliced
|
||||
loop ..> wireRecord #16A085 : hooks.onResumeEnded
|
||||
plan ..> wireRecord #16A085 : plan_mode.enter/cancel/exit
|
||||
|
||||
@enduml
|
||||
1
packages/agent-core-v2/docs/di-scope-domains.svg
Normal file
1
packages/agent-core-v2/docs/di-scope-domains.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 197 KiB |
|
|
@ -65,9 +65,10 @@ Reference: [`test/message/message.test.ts`](../test/message/message.test.ts).
|
|||
|
||||
```ts
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('XxxService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -75,14 +76,15 @@ describe('XxxService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
|
||||
// 1. Stub the collaborators, by interface.
|
||||
ix.stub(IAgentRecords, stubAgentRecords());
|
||||
ix.set(IContextService, new SyncDescriptor(ContextService)); // real dep
|
||||
|
||||
// 2. Register the system under test, by interface.
|
||||
ix.set(IXxxService, new SyncDescriptor(XxxService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
// 1. Real collaborator, registered by interface.
|
||||
reg.define(IContextService, ContextService);
|
||||
// 2. System under test, registered by interface.
|
||||
reg.define(IXxxService, XxxService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -94,7 +96,11 @@ describe('XxxService', () => {
|
|||
});
|
||||
```
|
||||
|
||||
Stubbing:
|
||||
`createServices` builds the container from domain **service groups** plus
|
||||
per-test overrides (see [Service groups](#service-groups)). Reach for
|
||||
`ix.stub(...)` / `ix.set(...)` directly only inside an `it` when a single test
|
||||
needs to swap a registration (for example, to inject a spy or a second
|
||||
instance). Stubbing:
|
||||
|
||||
- whole service, partial object: `ix.stub(IId, { method() { return … } })`;
|
||||
- single method: `ix.stub(IId, 'method', value)` returns a sinon stub;
|
||||
|
|
@ -198,6 +204,66 @@ Conventions:
|
|||
If a stub is needed by two test files, it belongs in that domain's
|
||||
`test/<domain>/stubs.ts`.
|
||||
|
||||
## Service groups
|
||||
|
||||
Most unit tests stub the same handful of collaborators (`ILogService`,
|
||||
`IAgentRecords`, `IConfigService`, `ITelemetryService`, …). Rather than repeat
|
||||
`ix.stub(...)` lines in every `beforeEach`, each domain exports a
|
||||
`register*Services` function from its `stubs.ts` that registers the default test
|
||||
doubles for that domain:
|
||||
|
||||
```ts
|
||||
// test/log/stubs.ts
|
||||
export function registerLogServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(ILogService, stubLog());
|
||||
}
|
||||
```
|
||||
|
||||
`createServices(disposables, { base, additionalServices })` composes them:
|
||||
|
||||
- `base` — an ordered list of service groups. Each group's registrations are
|
||||
deduped (first writer wins), so groups supply safe defaults without
|
||||
clobbering each other.
|
||||
- `additionalServices` — applied after `base`. Registrations here **overwrite**
|
||||
any base default, so a test can swap a stub for a spy, register the system
|
||||
under test, or supply a one-off collaborator.
|
||||
|
||||
```ts
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices, registerConfigServices, registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IAgentKaos, {}); // one-off collaborator
|
||||
reg.define(IAgentRecords, spyRecords); // override a base default
|
||||
reg.define(IXxxService, XxxService); // system under test
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`ServiceRegistration` offers three verbs:
|
||||
|
||||
- `define(id, Ctor)` — lazy `SyncDescriptor`; the service is instantiated on
|
||||
first resolve. Use for real collaborators and the system under test.
|
||||
- `defineInstance(id, instance)` — a fully-built instance (a fake such as
|
||||
`stubLog()`, or `new ConfigRegistry()`).
|
||||
- `definePartialInstance(id, { ... })` — a partial mock; only the supplied
|
||||
members are provided. Use for collaborators the test does not exercise.
|
||||
|
||||
Conventions:
|
||||
|
||||
- a group registers the domain's services **as dependencies** (a fake, or a `{}`
|
||||
partial when no fake exists yet). When a service is the system under test,
|
||||
the test registers the real implementation via `additionalServices` and does
|
||||
not rely on the group's default for it;
|
||||
- keep groups small and domain-local. A service that is almost always the
|
||||
system under test, or that every consumer configures differently, should not
|
||||
have a group — register it inline via `additionalServices`;
|
||||
- import groups with a **relative path** (`../<domain>/stubs`), never from
|
||||
`#/…`.
|
||||
|
||||
`createServices` defaults to `strict: false` (missing dependencies warn rather
|
||||
than throw), matching `new TestInstantiationService()`. Pass `strict: true` to
|
||||
surface unregistered `@IService` dependencies.
|
||||
|
||||
## Declaring dependencies
|
||||
|
||||
Always use `@IService` constructor decorators — in fixtures and in production
|
||||
|
|
@ -291,12 +357,14 @@ Most legacy tests build the SUT with `ix.createInstance(Impl)`. Converting one
|
|||
is mechanical:
|
||||
|
||||
1. import the interface (`IX`) and the descriptor;
|
||||
2. add `ix.set(IX, new SyncDescriptor(Impl))` to `beforeEach`;
|
||||
2. register the SUT by interface — `reg.define(IX, Impl)` inside
|
||||
`additionalServices` (or `ix.set(IX, new SyncDescriptor(Impl))`);
|
||||
3. replace `ix.createInstance(Impl)` with `ix.get(IX)`;
|
||||
4. drop the `disposables.add(...)` wrapper around the SUT and any trailing
|
||||
`svc.dispose()` — the container disposes it;
|
||||
5. replace any hand-rolled collaborator object with the domain's shared stub
|
||||
(or add one to `test/<domain>/stubs.ts` if it does not exist);
|
||||
or service group (or add one to `test/<domain>/stubs.ts` if it does not
|
||||
exist);
|
||||
6. delete now-unused imports.
|
||||
|
||||
Before / after:
|
||||
|
|
@ -305,7 +373,8 @@ Before / after:
|
|||
// before
|
||||
const svc = ix.createInstance(MessageService);
|
||||
|
||||
// after
|
||||
ix.set(IMessageService, new SyncDescriptor(MessageService)); // in beforeEach
|
||||
// after — registration in beforeEach additionalServices
|
||||
reg.define(IMessageService, MessageService);
|
||||
// after — resolution in the test body
|
||||
const svc = ix.get(IMessageService);
|
||||
```
|
||||
|
|
|
|||
391
packages/agent-core-v2/docs/di.md
Normal file
391
packages/agent-core-v2/docs/di.md
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
# DI(依赖注入)与 Scope — 场景化指南
|
||||
|
||||
> 本文按「给 agent-core-v2 加业务功能」会遇到的场景,从最简单到最复杂,逐个引入 DI 的概念。
|
||||
> 源码位于 [`src/_base/di/`](../src/_base/di/);测试约定见 [`docs/di-testing.md`](di-testing.md)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 先把 DI 当成黑盒子
|
||||
|
||||
写业务代码时,你只需要向这个黑盒子声明三件事:
|
||||
|
||||
- **我是谁** —— 一个能当 key 又能当类型的「身份」。
|
||||
- **我需要谁** —— 我的依赖由谁提供。
|
||||
- **我活多久** —— 我属于哪一层生命周期。
|
||||
|
||||
剩下的事(何时创建、是不是同一份、谁先谁后、何时销毁)都由容器负责。类只跟接口打交道,从不关心实现怎么 new。
|
||||
|
||||
下面每个场景只引入它所需要的那一块 DI。跟着场景走,概念会逐步叠加。
|
||||
|
||||
---
|
||||
|
||||
## 场景 1:加一个全局服务(不依赖任何人)
|
||||
|
||||
> 你要做的:进程级只有一个、谁都能用的基础能力,比如日志、遥测。参考 [`log`](../src/log/log.ts)。
|
||||
|
||||
这一步引入四块:**接口 / 身份 / 实现 / 注册**。
|
||||
|
||||
### 1.1 写接口,带上 `_serviceBrand`
|
||||
|
||||
```ts
|
||||
// greet/greet.ts
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IGreeter {
|
||||
readonly _serviceBrand: undefined; // 类型记号:告诉 DI「这是一个服务」
|
||||
hello(): string;
|
||||
}
|
||||
|
||||
export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('greeter');
|
||||
```
|
||||
|
||||
`createDecorator(name)` 造出的 `ServiceIdentifier` 一身二任:运行时是 key 和参数装饰器,编译时携带 `IGreeter` 类型。
|
||||
|
||||
> ⚠️ **约束:身份名字全局唯一。** `createDecorator` 按 `name` 缓存,同名返回同一个身份。两个域用了同一个字符串就会碰撞、共享一个身份。
|
||||
|
||||
### 1.2 写实现类
|
||||
|
||||
```ts
|
||||
// greet/greetService.ts
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IGreeter } from './greet';
|
||||
|
||||
export class Greeter implements IGreeter {
|
||||
declare readonly _serviceBrand: undefined; // 与接口的 _serviceBrand 对应
|
||||
hello(): string { return 'hi'; }
|
||||
}
|
||||
```
|
||||
|
||||
实现类用 `declare readonly _serviceBrand: undefined;` 对应接口上的类型记号。
|
||||
|
||||
### 1.3 注册到一层生命周期
|
||||
|
||||
```ts
|
||||
// greet/greetService.ts(文件顶层,import 时执行)
|
||||
registerScopedService(
|
||||
LifecycleScope.Core, // 活多久:进程级
|
||||
IGreeter, // 身份
|
||||
Greeter, // 实现
|
||||
InstantiationType.Eager, // 创建时机:立刻
|
||||
'greet', // 域名(用于排错)
|
||||
);
|
||||
```
|
||||
|
||||
绑定在哪一层是这个类的**固有属性**,在注册点决定,不在调用点决定。
|
||||
|
||||
### 1.4 通过 barrel 导出,让注册生效
|
||||
|
||||
```ts
|
||||
// greet/index.ts
|
||||
export * from './greet';
|
||||
export * from './greetService'; // import 这一行即触发上面的 registerScopedService
|
||||
```
|
||||
|
||||
再在包入口 [`src/index.ts`](../src/index.ts) 加一行:
|
||||
|
||||
```ts
|
||||
export * from './greet/index';
|
||||
```
|
||||
|
||||
于是「import 这个包」=「加载全部注册」。**没有中心装配文件**:绑定散落在各自域的实现文件里,靠 import 副作用收集。
|
||||
|
||||
至此,任何人都能 `accessor.get(IGreeter)` 拿到这个全局唯一的服务。
|
||||
|
||||
---
|
||||
|
||||
## 场景 2:你的服务要用别人的服务
|
||||
|
||||
> 你要做的:你的服务需要调用别的域的能力。参考 [`sessionService.ts`](../src/session/sessionService.ts)。
|
||||
|
||||
这一步引入:**构造器注入** 与 **按接口解析**。
|
||||
|
||||
### 2.1 用 `@IX` 在构造器上声明依赖
|
||||
|
||||
```ts
|
||||
export class SessionService implements ISessionService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IAgentLifecycleService private readonly agentLifecycle: IAgentLifecycleService,
|
||||
@IEventService private readonly event: IEventService,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
`@IAgentLifecycleService` 只做一件事:把「第 0 个参数需要 `IAgentLifecycleService`」记到类的元数据上。容器 new 这个类时读元数据,把依赖填好。
|
||||
|
||||
### 2.2 三条不可破的约束
|
||||
|
||||
1. **不要 `new` 带 `@IService` 依赖的类。** `new` 会绕过容器:绕过注册、绕过 scope、绕过单例缓存。要用就 `@IX` 注入,或 `accessor.get(IX)`。
|
||||
2. **`@IX` 只能装饰构造器参数。** 装饰到字段/方法上会在运行时抛错。
|
||||
3. **服务参数排在静态参数之后**(静态参数见场景 7)。
|
||||
|
||||
### 2.3 消费方按接口取,看不到实现
|
||||
|
||||
```ts
|
||||
const session = accessor.get(ISessionService); // 类型是 ISessionService
|
||||
```
|
||||
|
||||
消费方只 import **接口** 和 **`IX` 身份**,从不 import 实现类。这是 DI 把「接口 → 实现」的替换权完全握在容器手里的关键。
|
||||
|
||||
> 如果你需要的不是「一个服务」而是「一份配置」,通常做法是把它也做成一个服务注入进来(如 `IConfigService`);如果是「每轮一个、带参数的非单例对象」,见场景 7。
|
||||
|
||||
---
|
||||
|
||||
## 场景 3:你的服务不是全局一份
|
||||
|
||||
> 你要做的:每个会话一份、每个 agent 一份、或每轮对话一份。参考 [`turn`](../src/turn/turn.ts)、[`session`](../src/session/session.ts)。
|
||||
|
||||
这一步引入:**`LifecycleScope` 四层生命周期** 与 **父子 scope 的可见性**。
|
||||
|
||||
### 3.1 四层,按寿命从长到短
|
||||
|
||||
```ts
|
||||
export enum LifecycleScope {
|
||||
Core = 0, // 进程级,全局一份
|
||||
Session = 1, // 一次会话
|
||||
Agent = 2, // 一个 agent
|
||||
Turn = 3, // 一轮对话
|
||||
}
|
||||
```
|
||||
|
||||
数值越大,寿命越短、越靠叶子。注册时把 `scope` 换成对应层即可:
|
||||
|
||||
```ts
|
||||
registerScopedService(LifecycleScope.Session, ISessionService, SessionService, InstantiationType.Delayed, 'session');
|
||||
```
|
||||
|
||||
「单例」的粒度是**每个 scope 一份**:Core 的 `ILogService` 全局只有一份;每个 Session scope 各有自己的 `ISessionService`。
|
||||
|
||||
### 3.2 子 scope 看得见父 scope,反之不行
|
||||
|
||||
Scope 是一棵树,`kind` 必须沿父子方向**严格递增**:
|
||||
|
||||
```
|
||||
Core (0)
|
||||
└── Session (1)
|
||||
└── Agent (2)
|
||||
└── Turn (3)
|
||||
```
|
||||
|
||||
解析服务时,容器先看自己这一层,没有就**递归问父 scope**。所以一条铁律:
|
||||
|
||||
> **短寿命的服务可以注入长寿命的服务,反过来不行。**
|
||||
|
||||
- ✅ Turn 服务注入 Session / Core 服务(往上找,找得到)。
|
||||
- ❌ Core 服务注入 Session 服务(Core 创建时 Session 还不存在,且父不会往下找)。
|
||||
|
||||
这条规则由树的结构强制保证,不靠纪律维持。
|
||||
|
||||
---
|
||||
|
||||
## 场景 4:你的服务要释放资源
|
||||
|
||||
> 你要做的:服务里订阅了事件、开了定时器、持有了句柄,scope 销毁时要释放。参考 `WSBroadcastService`([`gatewayService.ts`](../src/gateway/gatewayService.ts))。
|
||||
|
||||
这一步引入:**`Disposable` / `IDisposable` 生命周期**。
|
||||
|
||||
```ts
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
|
||||
export class WSBroadcastService extends Disposable implements IWSBroadcastService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(@IEventService event: IEventService) {
|
||||
super();
|
||||
this._register(event.subscribe(() => { /* … */ })); // 收集子资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 继承 `Disposable`,用 `this._register(d)` 收集任何 `IDisposable`(事件订阅、`toDisposable(fn)` 等)。
|
||||
- 容器在销毁这个服务时会自动调它的 `dispose()`,它注册过的子资源随之释放。
|
||||
|
||||
销毁顺序是确定的(见场景 3 的树):**子 scope 先死,同 scope 内按构造逆序释放**(后 new 的先释放)。业务代码只声明「我活在哪一层」,从不手动释放。
|
||||
|
||||
---
|
||||
|
||||
## 场景 5:你的服务很重,想延迟初始化
|
||||
|
||||
> 你要做的:服务依赖多、创建贵,不想在 scope 创建时就 new。
|
||||
|
||||
这一步引入:**`InstantiationType.Eager` vs `Delayed`**。
|
||||
|
||||
```ts
|
||||
// Eager:scope 创建时立刻 new
|
||||
registerScopedService(LifecycleScope.Core, ILogService, LogService, InstantiationType.Eager, 'log');
|
||||
|
||||
// Delayed:第一次被 get 时才 new
|
||||
registerScopedService(LifecycleScope.Core, IScopeRegistry, ScopeRegistry, InstantiationType.Delayed, 'gateway');
|
||||
```
|
||||
|
||||
Delayed 服务返回的是一个 **Proxy**:在首次访问任意属性时才真正构造。即便还没构造好,别人提前订阅它的 `onDid…` / `onWill…` 事件也不会丢——容器会先记下监听器,实例真正出来后再回放订阅。
|
||||
|
||||
> 经验:无依赖、被频繁使用、或有「尽早初始化副作用」的服务用 `Eager`(如 `ILogService`);其余默认 `Delayed`。
|
||||
|
||||
---
|
||||
|
||||
## 场景 6:在普通函数里临时用服务
|
||||
|
||||
> 你要做的:你不想写一个新类,只是在一个函数里临时拿一个服务用一下。或你要给外部提供一个 `ServicesAccessor`。参考 [`gatewayService.ts`](../src/gateway/gatewayService.ts)。
|
||||
|
||||
这一步引入:**`IInstantiationService.invokeFunction`** 与 **`ServicesAccessor`**。
|
||||
|
||||
```ts
|
||||
const accessor: ServicesAccessor = {
|
||||
get: <T>(id: ServiceIdentifier<T>): T => instantiation.invokeFunction((a) => a.get(id)),
|
||||
};
|
||||
```
|
||||
|
||||
`invokeFunction(fn)` 会给 `fn` 一个**只在这次调用期间有效**的 `ServicesAccessor`。
|
||||
|
||||
> ⚠️ **约束:accessor 只在调用期间有效。** `invokeFunction` 返回后再 `accessor.get()` 会抛 `"service accessor is only valid during the invocation"`。不要把 accessor 存起来异步用——要长期持有服务,就在构造器里注入(场景 2)。
|
||||
|
||||
---
|
||||
|
||||
## 场景 7:创建带依赖、但不是单例的对象
|
||||
|
||||
> 你要做的:每轮对话都要 new 一个新对象,但它也有 `@IService` 依赖。比如一个 per-turn 的执行器。
|
||||
|
||||
这一步引入:**`IInstantiationService.createInstance`** 与 **静态参数**。
|
||||
|
||||
```ts
|
||||
class TurnRunner {
|
||||
constructor(
|
||||
private readonly input: string, // 静态参数:调用时传
|
||||
private readonly turn: number, // 静态参数:调用时传
|
||||
@ILogService private readonly log: ILogService, // 服务参数:容器注入
|
||||
) {}
|
||||
}
|
||||
|
||||
// 调用时:静态参数你传,服务参数容器填
|
||||
const runner = instantiation.createInstance(TurnRunner, 'hello', 1);
|
||||
```
|
||||
|
||||
容器把静态参数放前面、服务参数接在后面,再 `Reflect.construct` 出实例。这个对象**不会**被放进任何 scope 的单例缓存——每次都是新实例。
|
||||
|
||||
> 这就是「服务参数必须排在静态参数之后」的原因:容器按 `@IX` 记录的参数位置排序后依次注入。`_serviceBrand` 让编译器能在类型上区分这两类参数。
|
||||
|
||||
---
|
||||
|
||||
## 场景 8:你的服务要派生子容器 / 子 scope
|
||||
|
||||
> 你要做的:你的服务负责「拉起一个新会话 / 新 agent」,需要为它造一个子 scope。参考 `ScopeRegistry`([`gatewayService.ts`](../src/gateway/gatewayService.ts))。
|
||||
|
||||
这一步引入:**注入 `IInstantiationService` 本身** 与 **`createChild`**。
|
||||
|
||||
每个容器都把自己绑定成 `IInstantiationService`,所以你可以像注入别的服务一样注入它:
|
||||
|
||||
```ts
|
||||
export class ScopeRegistry implements IScopeRegistry {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(@IInstantiationService private readonly instantiation: IInstantiationService) {}
|
||||
|
||||
createSession(opts: CreateSessionOptions): Promise<IScopeHandle> {
|
||||
const collection = new ServiceCollection();
|
||||
for (const entry of getScopedServiceDescriptors(LifecycleScope.Session)) {
|
||||
collection.set(entry.id, entry.descriptor); // 收集 Session 这一层的描述符
|
||||
}
|
||||
const child = this.instantiation.createChild(collection); // 派生子容器
|
||||
const accessor: ServicesAccessor = {
|
||||
get: <T>(id: ServiceIdentifier<T>): T => child.invokeFunction((a) => a.get(id)),
|
||||
};
|
||||
const handle: IScopeHandle = { id: opts.sessionId, kind: LifecycleScope.Session, accessor };
|
||||
this.sessions.set(opts.sessionId, handle);
|
||||
return Promise.resolve(handle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键点:
|
||||
|
||||
- `getScopedServiceDescriptors(scope)` 能拿回注册在某一层的所有描述符,装进一个 `ServiceCollection`。
|
||||
- `instantiation.createChild(collection)` 造一个子容器,它的父指针指向当前容器——于是子容器能向上解析到 Core 的服务(场景 3 的可见性规则)。
|
||||
- 给外部暴露时,用 `invokeFunction` 把子容器包成 `ServicesAccessor`(场景 6)。
|
||||
|
||||
> 更高层通常直接用 [`Scope.createChild(kind, id)`](../src/_base/di/scope.ts)(它帮你做了「筛描述符 + 建子容器」);只有需要手动控制 `ServiceCollection` 时才像上面这样写。
|
||||
|
||||
---
|
||||
|
||||
## 场景 9:撞上循环依赖(不允许,要重构)
|
||||
|
||||
> 业务规则:**不允许循环依赖。** 容器会拒绝它;撞上时的正确处理是重构,不是让它跑通。
|
||||
|
||||
### 9.1 容器会拒绝同步成环
|
||||
|
||||
A 创建中要 B,B 创建中又要 A——容器会抛 `CyclicDependencyError`,`path` 形如 `['A', 'B', 'A']`。自环(A 依赖自己)同样会被拒绝。这不是 bug,是保护机制:它在告诉你「这两个服务的职责划错了」。
|
||||
|
||||
### 9.2 为什么不允许
|
||||
|
||||
- scope 分层让正常依赖天然是 DAG(Turn → Agent → Session → Core 向上找),一个环几乎总是设计味道。
|
||||
- 靠「让环刚好能跑」会把构造顺序变成隐式约定,难调试、难排错。
|
||||
|
||||
所以 v2 的立场是:**依赖图必须是无环的。**
|
||||
|
||||
### 9.3 撞上时怎么重构
|
||||
|
||||
按优先级考虑:
|
||||
|
||||
1. **抽出第三个服务 C。** 把 A、B 互相需要的那部分逻辑提到 C,让 A、B 都依赖 C,而不是互相依赖。这是最常见的解。
|
||||
2. **用事件解耦。** 如果 A 只是想知道 B 的某个变化,让 B 通过 `IEventService` 发事件、A 订阅,而不是 A 直接持有 B 的引用。
|
||||
3. **重新划分 scope。** 也许其中一个本不该在这一层——它其实该更短或更长寿命,移动后环自然消失。
|
||||
|
||||
### 9.4 关于 Delayed 破环(遗留逃生舱,禁用)
|
||||
|
||||
容器里有一个遗留机制:当环里的某一边注册为 `Delayed`(场景 5)时,Proxy 能让这个「软循环」不同步炸开。**业务上禁止使用它来绕过循环依赖**——它存在是为了兼容历史代码,不是给你的设计兜底的。撞上 `CyclicDependencyError` 时,按 9.3 重构。
|
||||
|
||||
---
|
||||
|
||||
## 场景 10:给服务写测试
|
||||
|
||||
> 你要做的:让测试走和生产一样的路径——按接口解析、依赖由容器注入。
|
||||
|
||||
这一步引入:**两个测试 harness**。详见 [`docs/di-testing.md`](di-testing.md),这里只给选择标准:
|
||||
|
||||
| 测什么 | 用哪个 harness | 怎么取 SUT |
|
||||
|---|---|---|
|
||||
| 单个服务的行为(单元) | `TestInstantiationService`(扁平容器) | `ix.set(ISut, new SyncDescriptor(Sut))` 后 `ix.get(ISut)` |
|
||||
| 跨 scope 接线 / 服务活在哪一层 | `createScopedTestHost`(scope 树) | `host.<scope>.accessor.get(ISut)` |
|
||||
|
||||
核心规则:**按接口解析被测对象,绝不 `new` 带 `@IService` 依赖的实现类**——否则 `registerScopedService(IX → Impl)` 这条绑定在测试里根本没跑过。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:接口速查
|
||||
|
||||
| 接口 | 出现场景 | 作用 |
|
||||
|---|---|---|
|
||||
| `createDecorator<T>(name)` → `ServiceIdentifier<T>` | 1 | 造身份(运行时 key + 编译时类型 + 参数装饰器) |
|
||||
| `@IService` | 2, 7 | 在构造器参数上声明依赖 |
|
||||
| `registerScopedService(scope, id, ctor, type, domain)` | 1, 3, 5 | 把实现绑定到一层生命周期 |
|
||||
| `ServicesAccessor.get(IX)` | 2, 6 | 按接口解析实例 |
|
||||
| `IInstantiationService.invokeFunction(fn, …)` | 6, 8 | 在函数里临时拿到 accessor |
|
||||
| `IInstantiationService.createInstance(ctor, …args)` | 7 | 创建非单例对象并注入依赖 |
|
||||
| `IInstantiationService.createChild(collection)` | 8 | 派生子容器 |
|
||||
| `getScopedServiceDescriptors(scope)` | 8 | 取回注册在某一层的所有描述符 |
|
||||
| `Disposable` / `DisposableStore` / `IDisposable` | 4 | 资源管理与销毁 |
|
||||
| `Scope` / `LifecycleScope` | 3, 8 | 生命周期树 |
|
||||
| `SyncDescriptor` | (测试/底层) | 把「构造器 + 静态参数」打包成待 new 描述符 |
|
||||
|
||||
> 遗留导出(v2 不用,知道即可):`registerSingleton` / `getSingletonServiceDescriptors` / `refineServiceDecorator` 是 VS Code 遗留的全局单例注册器,v2 的 src/test 零引用,统一走 `registerScopedService`。
|
||||
|
||||
## 附录 B:红线汇总
|
||||
|
||||
1. 不 `new` 带 `@IService` 依赖的类——用 `@IX` 注入或 `accessor.get(IX)`。
|
||||
2. `@IX` 只能装饰构造器参数;服务参数排在静态参数之后。
|
||||
3. 接口和实现都带 `_serviceBrand`。
|
||||
4. 身份名字全局唯一。
|
||||
5. 父 scope 的服务不依赖子 scope 的服务(运行时也解析不到)。
|
||||
6. **不写循环依赖**——容器会抛 `CyclicDependencyError`;撞上时按场景 9 重构,不用 Delayed 绕过。
|
||||
7. `ServicesAccessor` 只在 `invokeFunction` 调用期间有效,不存起来异步用。
|
||||
8. 注册写在实现文件顶层;测试里用 `_clearScopedRegistryForTests()` 后显式重注册,不依赖生产 import 顺序。
|
||||
|
||||
## 附录 C:新增一个服务的标准动作
|
||||
|
||||
1. **契约**:`src/<domain>/<domain>.ts` 写接口(带 `_serviceBrand`)+ `createDecorator` 身份。
|
||||
2. **实现**:`src/<domain>/<domain>Service.ts` 写类,`@IX` 声明依赖,文件顶层 `registerScopedService(scope, IX, Impl, type, '<domain>')`。
|
||||
3. **barrel**:`src/<domain>/index.ts` re-export 契约和实现。
|
||||
4. **入口**:`src/index.ts` 加一行 `export * from './<domain>/index';`。
|
||||
5. **测试**:`test/<domain>/` 用 `TestInstantiationService` 或 `createScopedTestHost`,按接口解析。
|
||||
283
packages/agent-core-v2/docs/service-design.md
Normal file
283
packages/agent-core-v2/docs/service-design.md
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
# Service Design Principles
|
||||
|
||||
> First-principles guide for designing a new Service in agent-core-v2: how to pick its
|
||||
> **scope**, when to **split it across scopes**, how to **call** other Services, and which
|
||||
> direction dependencies should point.
|
||||
>
|
||||
> This complements [`docs/di.md`](di.md). `di.md` explains the DI/Scope machinery
|
||||
> ("how the container works"); this doc explains the **design rules** ("where to put things
|
||||
> and why"). Read `di.md` first if you have not.
|
||||
|
||||
---
|
||||
|
||||
## 1. What a Service is
|
||||
|
||||
Before discussing scope or calling style, define the object.
|
||||
|
||||
**A Service = a bundle of state + a set of behaviors, bound to a lifetime.**
|
||||
|
||||
Of these three:
|
||||
|
||||
- **Behavior** is almost *free* — the same logic runs anywhere, so it does not by itself
|
||||
decide a scope.
|
||||
- **State** is what pins a Service to a scope. State has an **identity** (what it is keyed
|
||||
by) and a **lifetime** (when it is born, when it dies).
|
||||
- **Dependencies / calling style** answer a different question: **who controls whom, and who
|
||||
knows whom**.
|
||||
|
||||
Every principle below derives from two root questions:
|
||||
|
||||
1. **What is the identity of the state it owns?** → decides the **Scope**.
|
||||
2. **Who owns the decision, and who needs the result?** → decides the **calling style** and
|
||||
the **dependency direction**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Choosing a Scope
|
||||
|
||||
**First principle: Scope = the identity + lifetime of the owned state.**
|
||||
|
||||
`Core` / `Session` / `Agent` / `Turn` are four tiers of identity + lifetime:
|
||||
|
||||
| Scope | State identity (keyed by) | Lifetime |
|
||||
|---|---|---|
|
||||
| `Core` | none (single global instance) | the process |
|
||||
| `Session` | `sessionId` | one session |
|
||||
| `Agent` | `agentId` | one agent |
|
||||
| `Turn` | `turnId` | one turn |
|
||||
|
||||
### Decision tree
|
||||
|
||||
**Q1. Does it own mutable state?**
|
||||
|
||||
- **No (pure behavior)** → jump to Q3.
|
||||
- **Yes** → Q2.
|
||||
|
||||
**Q2. What is the identity of that state?**
|
||||
|
||||
- one global instance → **`Core`**
|
||||
- one per session → **`Session`**
|
||||
- one per agent → **`Agent`**
|
||||
- one per turn → **`Turn`**
|
||||
- a mix (a global registry *and* per-instance state) → **do not put it in one Service;
|
||||
split it** (see §3 Multi-Scope).
|
||||
|
||||
**Q3 (stateless). What is the shortest-lived dependency it must inject?**
|
||||
|
||||
A stateless Service is pulled *down* by its shortest-lived dependency: if it injects an
|
||||
`Agent`-scoped Service, it cannot be `Core`. Among the scopes that still satisfy every
|
||||
dependency, **default to the longest-lived one** (usually `Core`) to maximize reuse and
|
||||
singleton sharing. Push it down only when:
|
||||
|
||||
1. it must inject a shorter-lived Service (enforced by the container); or
|
||||
2. you want to limit its visibility (it conceptually belongs to one agent and should not be
|
||||
globally exposed).
|
||||
|
||||
### The core anti-pattern (a litmus test)
|
||||
|
||||
> **Do not store per-session state in a `Map<sessionId, …>` inside a `Core` Service.**
|
||||
|
||||
This is the tell-tale sign of "this should have been `Session`-scoped but was lazily parked
|
||||
at `Core`". Consequences:
|
||||
|
||||
- nobody cleans the entry up when the session ends → **leak**;
|
||||
- every consumer threads `sessionId` around → **loss of type safety**;
|
||||
- it cannot inject `Session`/`Agent`-scoped collaborators.
|
||||
|
||||
### One-sentence self-check
|
||||
|
||||
> **"When this scope is disposed, should this state disappear with it?"**
|
||||
>
|
||||
> - Yes → the scope is right.
|
||||
> - It must outlive the scope → the scope is too short; move up one tier.
|
||||
> - It should be one-per-unit but is being shared → the scope is too long; move down one tier.
|
||||
|
||||
---
|
||||
|
||||
## 3. Multi-Scope splitting
|
||||
|
||||
**First principle: one Service owns state at exactly one identity / lifetime. If a domain
|
||||
owns state at several lifetimes, split it along those lifetime boundaries — one Service per
|
||||
lifetime.**
|
||||
|
||||
This is not layered-architecture aesthetics; it is forced by state identity. A class that
|
||||
holds both "a global registry" and "per-session instances" will either leak (the global part
|
||||
keeps per-session entries alive) or get pinned to an awkward scope where it can do neither
|
||||
job well.
|
||||
|
||||
### The standard split: "global registry / factory" + "per-instance"
|
||||
|
||||
| Tier | Role | Naming tends to |
|
||||
|---|---|---|
|
||||
| `Core` | **global registry / catalog / factory** — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` |
|
||||
| `Session` / `Agent` | **one instance** — only the state of "this one" | `XxxService` / `ISessionXxx` / `IAgentXxx` |
|
||||
|
||||
This pattern recurs throughout the codebase and confirms the rule:
|
||||
|
||||
- **`records`** — `ISessionStore` (`Core`, index of all sessions) + `ISessionMetaStore`
|
||||
(`Session`, this session's metadata) + `IAgentRecords` (`Agent`, this agent's record
|
||||
stream).
|
||||
- **`config`** — `IConfigRegistry` / `IConfigService` (`Core`, global config) +
|
||||
`IAgentConfigService` (`Agent`, this agent's config view).
|
||||
- **`kosong`** — `IModelCatalogService` (`Core`, model catalog) + `IProviderManager`
|
||||
(`Session`) + `ILLMService` (`Agent`, this agent's generation).
|
||||
- **`tool`** — `IToolDefinitionRegistry` (`Core`, tool-definition registry) + `IToolService`
|
||||
(`Agent`, this agent's execution).
|
||||
|
||||
### When to split and when not to
|
||||
|
||||
- **Split** when the domain genuinely has both a global view and per-instance state.
|
||||
- **Do not split** when the domain has state at only one lifetime (e.g. purely `Core` like
|
||||
`log` / `telemetry`; purely `Agent` like `prompt`). **Do not pre-split for symmetry.**
|
||||
|
||||
### Dependency direction after the split
|
||||
|
||||
The `Core` Service usually plays the **factory**: it knows how to create or locate the
|
||||
per-instance one. Most consumers inject the **per-instance** Service, because it serves the
|
||||
current session/agent directly without threading an id. Inject the `Core` factory only when
|
||||
you genuinely need cross-instance management.
|
||||
|
||||
---
|
||||
|
||||
## 4. Choosing a calling style
|
||||
|
||||
There are three ways for one Service to make another act: a **direct call** (DI injection),
|
||||
an **event**, or a **hook**. From first principles, they answer three different questions.
|
||||
|
||||
**First principle: the choice depends on "who owns the decision" + "is a result needed" +
|
||||
"how many consumers".**
|
||||
|
||||
### What the three mechanisms mean
|
||||
|
||||
| Mechanism | Nature | Coupling | Returns a value? | Consumers |
|
||||
|---|---|---|---|---|
|
||||
| **Direct call** | command: A tells B to do | A → B | yes | one (known) |
|
||||
| **Event** | fact: A announces "X happened" | both depend only on the bus | no | zero / one / many (unknown) |
|
||||
| **Hook** (`onWill` / `onDid`, `OrderedHookSlot`) | participation: observers step into an operation, in order | both depend only on the bus | can observe / veto | many, but ordered |
|
||||
|
||||
### Decision tree
|
||||
|
||||
**Q1. Does A need a return value from B?**
|
||||
|
||||
- Yes → **direct call**. Events cannot return a value (doing request/reply over events is an
|
||||
anti-pattern).
|
||||
|
||||
**Q2. Is B's reaction part of A's responsibility, or B's own concern?**
|
||||
|
||||
- A's responsibility *includes* B's behavior (A orchestrates B) → **direct call**. E.g.
|
||||
`session` drives `agent-lifecycle`; `loop` drives `llmRequester` / `toolExecutor` — that
|
||||
*is* their job.
|
||||
- B's reaction is B's own concern, and A is merely **stating a fact** → **event**. E.g.
|
||||
`flag` reacts to `config.onDidChange`; `config` does not know who is listening.
|
||||
|
||||
**Q3. How many consumers?**
|
||||
|
||||
- exactly one, and known → **direct call**.
|
||||
- zero / one / many, and the producer should not know how many → **event**.
|
||||
|
||||
**Q4. Would a direct A→B call create a cycle or violate the scope direction?**
|
||||
|
||||
- This is a **consequence check**, not a primary reason. Decide by Q1–Q3 first; if the
|
||||
semantics already call for an event, the decoupling comes for free. Do not turn a genuine
|
||||
direct call into an event just to break a cycle.
|
||||
|
||||
**Q5. Is this fact part of the durable record / replay / cross-agent projection?**
|
||||
|
||||
- Yes → **emit it on the wire** (`wireRecord`). This is a system-specific but strong reason:
|
||||
state changes that must be recorded, replayed, or synchronized across agents have to be
|
||||
projected onto the wire, not handled by a direct call alone. `permission.set_mode`,
|
||||
`goal.create/update/clear`, and `plan_mode.enter/exit` are all in this category.
|
||||
Note that the wire is the *durable record*, not the live notification channel: a live
|
||||
`spliceHistory(...)` call appends a `context.splice` record to the wire *and* applies it,
|
||||
and `contextMemory` then fires `hooks.onSpliced`, which `contextSize` / `loop` /
|
||||
`background` / `microCompaction` / `dynamicInjector` actually subscribe to. Those listeners
|
||||
react to the **hook**, not the wire — the wire is what makes the splice replayable.
|
||||
|
||||
### One-sentence rule
|
||||
|
||||
> **"I am telling you to do this, and I may need the result" → direct call.**
|
||||
> **"I am announcing that something happened; react if you care" → event.**
|
||||
> **"I am announcing something, and you may step in, in order, possibly to veto" → hook.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Dependency direction
|
||||
|
||||
Two distinct layers are involved, and they differ in *hardness*:
|
||||
|
||||
- **Scope direction**: short-lived → long-lived, **enforced by the container** (already
|
||||
covered in [`docs/di.md`](di.md)).
|
||||
- **Domain direction**: which domain may depend on which, **a matter of judgment** — the
|
||||
container does not enforce it.
|
||||
|
||||
### First principle: dependency direction = the direction of "needs to know"
|
||||
|
||||
> **A depends on B iff A needs B's data or behavior to do its own job.**
|
||||
|
||||
That is the whole rule. `prompt` depending on `turn` (as it does today) is legitimate —
|
||||
the prompt needs the turn's information to be built. `loop` depending on many capabilities
|
||||
is legitimate — orchestration *is* its job.
|
||||
|
||||
This rule alone is not enough; add one anti-rot heuristic to keep the graph from collapsing
|
||||
into a clique:
|
||||
|
||||
> **Do not let a more foundational / more-reused Service come to know a more specific /
|
||||
> more-upstream one.**
|
||||
|
||||
Reason: reuse gets inverted — once a foundational component knows about an upstream
|
||||
scenario, it can no longer be reused by other scenarios, and it will almost always create a
|
||||
cycle.
|
||||
|
||||
### The natural layers of this repo
|
||||
|
||||
Derived from "what is more foundational", roughly (lower is depended on by higher, never the
|
||||
reverse):
|
||||
|
||||
1. **Root (depend on no business domain)**: `_base`, `log`, `environment`, `event`,
|
||||
`telemetry`, `kaos`.
|
||||
2. **Data / state**: `records`, `filestore`, `workspace`, `blobStore`, `config`.
|
||||
3. **Capabilities**: `tool`, `permission`, `prompt`, `contextMemory`, `kosong`, `skill`, …
|
||||
4. **Orchestrators**: `session`, `agent-lifecycle`, `loop`, `turn`, `swarm`.
|
||||
5. **Edge**: `gateway`, `rpc`.
|
||||
|
||||
**Red lines:**
|
||||
|
||||
- Layer 1 (root) **never** depends on any business domain.
|
||||
- Business logic does **not** depend on layer 5 (edge) — business code should not know REST /
|
||||
WebSocket exist.
|
||||
- A cycle means knowledge was placed the wrong way around. Fix it (consistent with `di.md`
|
||||
scenario 9): extract a third, more foundational Service, or invert the "notification" half
|
||||
into an event.
|
||||
|
||||
> Note: capability → orchestrator (e.g. `prompt → turn`) is **allowed and present** in this
|
||||
> repo; do not treat it as a red line. The real red line is *inverted reuse* — a
|
||||
> foundational / lower Service depending on a specific / upper one.
|
||||
|
||||
---
|
||||
|
||||
## 6. Putting it together
|
||||
|
||||
The complete checklist for a new `IXxxService`:
|
||||
|
||||
1. **What does it remember, and what is the state's identity?** → pick the scope (§2).
|
||||
2. **What is the shortest-lived dependency it must inject?** → the scope cannot be longer
|
||||
than that.
|
||||
3. **Does it own state at both a global and a per-instance lifetime?** → if yes, split it
|
||||
Multi-Scope (§3).
|
||||
4. **For each collaborator: am I commanding it, notifying it, or letting it participate?**
|
||||
→ pick the calling style (§4).
|
||||
5. **Does each dependency arrow make a more foundational thing know a more specific thing?**
|
||||
→ if yes, invert it (§5).
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary
|
||||
|
||||
- **Scope**: the **identity** of the state fixes the scope; do not fake per-instance state
|
||||
at `Core` with a `Map<id, …>`.
|
||||
- **Multi-Scope**: a domain with state at several lifetimes → split into "a `Core` registry
|
||||
+ per-instance Services".
|
||||
- **Calling style**: need a result / I orchestrate → direct call; stating a fact / react if
|
||||
you care → event; ordered participation / may veto → hook.
|
||||
- **Dependency direction**: arrows follow "needs to know", but never let a foundational layer
|
||||
know an upstream one; a cycle means knowledge is placed backwards.
|
||||
|
|
@ -6,7 +6,11 @@ export {
|
|||
createServices,
|
||||
TestInstantiationService,
|
||||
} from './testInstantiationService';
|
||||
export type { ServiceIdCtorPair } from './testInstantiationService';
|
||||
export type {
|
||||
CreateServicesOptions,
|
||||
ServiceGroup,
|
||||
ServiceRegistration,
|
||||
} from './testInstantiationService';
|
||||
|
||||
import { type ServiceIdentifier } from './instantiation';
|
||||
import { createCoreScope, LifecycleScope, Scope, type ScopeSeed } from './scope';
|
||||
|
|
|
|||
|
|
@ -292,45 +292,110 @@ interface SinonOptions {
|
|||
stub?: boolean;
|
||||
}
|
||||
|
||||
export type ServiceIdCtorPair<T> = [
|
||||
id: ServiceIdentifier<T>,
|
||||
ctorOrInstance: T | AnyConstructor<T>,
|
||||
];
|
||||
/**
|
||||
* Registration surface handed to a {@link ServiceGroup} or to
|
||||
* `CreateServicesOptions.additionalServices`. Mirrors the three ways a test
|
||||
* supplies a service: a lazy constructor, a full instance, or a partial mock.
|
||||
*/
|
||||
export interface ServiceRegistration {
|
||||
/**
|
||||
* Register a lazy `SyncDescriptor` for a service constructor. The service is
|
||||
* instantiated only when first resolved from the container.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
define<T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T): void;
|
||||
/** Register a fully-constructed instance. */
|
||||
defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;
|
||||
/**
|
||||
* Register a partial instance (a mock). Only the supplied members need to be
|
||||
* provided; the container returns it typed as `T`.
|
||||
*/
|
||||
definePartialInstance<T>(id: ServiceIdentifier<T>, instance: Partial<T>): void;
|
||||
}
|
||||
|
||||
/** A bundle of service registrations, typically one per domain. */
|
||||
export type ServiceGroup = (reg: ServiceRegistration) => void;
|
||||
|
||||
export interface CreateServicesOptions {
|
||||
/**
|
||||
* Base service groups applied first, in order. Registrations are deduped
|
||||
* (first writer wins) so groups can supply safe defaults without clobbering
|
||||
* each other.
|
||||
*/
|
||||
readonly base?: readonly ServiceGroup[];
|
||||
/**
|
||||
* Applied after `base`. Registrations here overwrite any base default, so a
|
||||
* test can swap a stub for a spy, register the system under test, or supply a
|
||||
* one-off collaborator.
|
||||
*/
|
||||
readonly additionalServices?: (reg: ServiceRegistration) => void;
|
||||
/**
|
||||
* When `true`, resolving an unregistered service throws. Defaults to `false`
|
||||
* to match `new TestInstantiationService()` (missing deps only warn), keeping
|
||||
* migrated tests behavior-preserving.
|
||||
*/
|
||||
readonly strict?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `TestInstantiationService` from domain service groups plus per-test
|
||||
* overrides. The container is added to `disposables`; directly-registered
|
||||
* instances are disposed with it.
|
||||
*/
|
||||
export function createServices(
|
||||
disposables: DisposableStore,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
services: ServiceIdCtorPair<any>[],
|
||||
options: CreateServicesOptions = {},
|
||||
): TestInstantiationService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const serviceIdentifiers: ServiceIdentifier<any>[] = [];
|
||||
const serviceCollection = new ServiceCollection();
|
||||
// Directly-registered instances are not constructed by the container, so the
|
||||
// container will not dispose them — track their ids and dispose them below.
|
||||
// Descriptor-created services are disposed by the container itself and are
|
||||
// intentionally not tracked here (disposing them again would double-dispose).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const instanceIds = new Set<ServiceIdentifier<any>>();
|
||||
|
||||
const define = <T>(
|
||||
const register = <T>(
|
||||
id: ServiceIdentifier<T>,
|
||||
ctorOrInstance: T | AnyConstructor<T>,
|
||||
value: T | Partial<T> | SyncDescriptor<T>,
|
||||
isInstance: boolean,
|
||||
overwrite: boolean,
|
||||
): void => {
|
||||
if (!serviceCollection.has(id)) {
|
||||
if (typeof ctorOrInstance === 'function') {
|
||||
serviceCollection.set(id, new SyncDescriptor(ctorOrInstance as AnyConstructor<T>));
|
||||
} else {
|
||||
serviceCollection.set(id, ctorOrInstance);
|
||||
}
|
||||
if (overwrite || !serviceCollection.has(id)) {
|
||||
serviceCollection.set(id, value as T | SyncDescriptor<T>);
|
||||
}
|
||||
if (isInstance) {
|
||||
instanceIds.add(id);
|
||||
}
|
||||
serviceIdentifiers.push(id);
|
||||
};
|
||||
|
||||
for (const [id, ctorOrInstance] of services) {
|
||||
define(id, ctorOrInstance);
|
||||
const baseReg: ServiceRegistration = {
|
||||
define: (id, ctor) => register(id, new SyncDescriptor(ctor), false, false),
|
||||
defineInstance: (id, instance) => register(id, instance, true, false),
|
||||
definePartialInstance: (id, instance) => register(id, instance, true, false),
|
||||
};
|
||||
|
||||
for (const group of options.base ?? []) {
|
||||
group(baseReg);
|
||||
}
|
||||
|
||||
const instantiationService = disposables.add(new TestInstantiationService(serviceCollection, true));
|
||||
if (options.additionalServices) {
|
||||
const overrideReg: ServiceRegistration = {
|
||||
define: (id, ctor) => register(id, new SyncDescriptor(ctor), false, true),
|
||||
defineInstance: (id, instance) => register(id, instance, true, true),
|
||||
definePartialInstance: (id, instance) => register(id, instance, true, true),
|
||||
};
|
||||
options.additionalServices(overrideReg);
|
||||
}
|
||||
|
||||
const instantiationService = disposables.add(
|
||||
new TestInstantiationService(serviceCollection, options.strict ?? false),
|
||||
);
|
||||
disposables.add(toDisposable(() => {
|
||||
const serviceDisposables: IDisposable[] = [];
|
||||
for (const id of serviceIdentifiers) {
|
||||
const instanceOrDescriptor = serviceCollection.get(id);
|
||||
if (isDisposable(instanceOrDescriptor)) {
|
||||
serviceDisposables.push(instanceOrDescriptor);
|
||||
for (const id of instanceIds) {
|
||||
const instance = serviceCollection.get(id);
|
||||
if (isDisposable(instance)) {
|
||||
serviceDisposables.push(instance);
|
||||
}
|
||||
}
|
||||
dispose(serviceDisposables);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,15 @@ export class LinkedList<E> {
|
|||
};
|
||||
}
|
||||
|
||||
shift(): E | undefined {
|
||||
if (this._first === Node.Undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const node = this._first as Node<E>;
|
||||
this._remove(node);
|
||||
return node.element;
|
||||
}
|
||||
|
||||
private _remove(node: Node<E>): void {
|
||||
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
|
||||
const anchor = node.prev as Node<E>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
/**
|
||||
* `event` domain (L0) — `Event` / `Emitter` primitives and event combinators (`once` / `map` / `filter` / `any`).
|
||||
* `event` domain (L0) — `Event` / `Emitter` primitives, the async
|
||||
* `AsyncEmitter` / `IWaitUntil` participation primitive (for interceptable
|
||||
* `onWill` events whose listeners register work via `waitUntil`), the
|
||||
* `handleVetos` helper (for `onBefore*` veto events whose listeners answer
|
||||
* with `veto(value, id)`), and event combinators (`once` / `map` / `filter`
|
||||
* / `any`).
|
||||
*/
|
||||
|
||||
import { onUnexpectedError, safelyCallListener } from './errors/unexpectedError';
|
||||
|
|
@ -9,6 +14,7 @@ import {
|
|||
combinedDisposable,
|
||||
type IDisposable,
|
||||
} from './di/lifecycle';
|
||||
import { LinkedList } from './di/util/linkedList';
|
||||
|
||||
export interface Event<T> {
|
||||
(
|
||||
|
|
@ -24,7 +30,7 @@ interface ListenerEntry<T> {
|
|||
}
|
||||
|
||||
export class Emitter<T> {
|
||||
private _listeners: Set<ListenerEntry<T>> | undefined;
|
||||
protected _listeners: Set<ListenerEntry<T>> | undefined;
|
||||
private _disposed = false;
|
||||
private _event: Event<T> | undefined;
|
||||
|
||||
|
|
@ -85,6 +91,100 @@ export class Emitter<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export interface IWaitUntil {
|
||||
readonly signal: AbortSignal;
|
||||
waitUntil(thenable: Promise<unknown>): void;
|
||||
}
|
||||
|
||||
export type IWaitUntilData<T> = Omit<T, 'waitUntil' | 'signal'>;
|
||||
|
||||
export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
|
||||
private _asyncDeliveryQueue?: LinkedList<[(event: T) => void, IWaitUntilData<T>]>;
|
||||
|
||||
async fireAsync(data: IWaitUntilData<T>, signal: AbortSignal): Promise<void> {
|
||||
if (this.isDisposed || this._listeners === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._asyncDeliveryQueue ??= new LinkedList();
|
||||
for (const entry of this._listeners) {
|
||||
this._asyncDeliveryQueue.push([
|
||||
(event) => {
|
||||
entry.listener.call(entry.thisArg, event);
|
||||
},
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
while (this._asyncDeliveryQueue.size > 0 && !signal.aborted) {
|
||||
const [deliver, eventData] = this._asyncDeliveryQueue.shift()!;
|
||||
const thenables: Promise<unknown>[] = [];
|
||||
|
||||
const event = {
|
||||
...eventData,
|
||||
signal,
|
||||
waitUntil: (p: Promise<unknown>): void => {
|
||||
if (Object.isFrozen(thenables)) {
|
||||
throw new Error('waitUntil can NOT be called asynchronously');
|
||||
}
|
||||
thenables.push(p);
|
||||
},
|
||||
} as T;
|
||||
|
||||
try {
|
||||
deliver(event);
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.freeze(thenables);
|
||||
const settled = await Promise.allSettled(thenables);
|
||||
for (const result of settled) {
|
||||
if (result.status === 'rejected') {
|
||||
onUnexpectedError(result.reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleVetos(
|
||||
vetos: (boolean | Promise<boolean>)[],
|
||||
onError: (error: unknown) => void,
|
||||
): Promise<boolean> {
|
||||
if (vetos.length === 0) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
let lazyValue = false;
|
||||
|
||||
for (const valueOrPromise of vetos) {
|
||||
if (valueOrPromise === true) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (typeof valueOrPromise === 'boolean') {
|
||||
continue;
|
||||
}
|
||||
promises.push(
|
||||
valueOrPromise.then(
|
||||
(value) => {
|
||||
if (value) {
|
||||
lazyValue = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
onError(error);
|
||||
lazyValue = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.allSettled(promises).then(() => lazyValue);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Event {
|
||||
export const None: Event<unknown> = () => Disposable.None;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
/**
|
||||
* `agent-lifecycle` domain (L6) — `IAgentLifecycleService` implementation.
|
||||
*
|
||||
* Creates and tracks the session's agents as child scopes; persists records
|
||||
* through `records` and reads session context through `session-context`. Bound
|
||||
* at Session scope.
|
||||
* Creates and tracks the session's agents as child scopes, acting as the agent
|
||||
* composition root: wires the tool-call permission veto through `turn` and
|
||||
* `permission`, persists records through `records`, and reads session context
|
||||
* through `session-context`. Bound at Session scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import {
|
||||
createScopedChildHandle,
|
||||
type IScopeHandle,
|
||||
LifecycleScope,
|
||||
getScopedServiceDescriptors,
|
||||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { ISessionMetaStore } from '#/records';
|
||||
import {
|
||||
IInstantiationService,
|
||||
type ServiceIdentifier,
|
||||
type ServicesAccessor,
|
||||
} from '#/_base/di/instantiation';
|
||||
import { ServiceCollection } from '#/_base/di/serviceCollection';
|
||||
import { IPermissionService } from '#/permission/permission';
|
||||
import { ISessionMetaStore } from '#/records/records';
|
||||
import { ISessionContext } from '#/session-context/sessionContext';
|
||||
import { ITurnEvents } from '#/turn/turn';
|
||||
|
||||
import { type CreateAgentOptions, IAgentLifecycleService } from './agentLifecycle';
|
||||
|
||||
|
|
@ -36,15 +44,35 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
|
||||
create(opts: CreateAgentOptions): Promise<IScopeHandle> {
|
||||
const agentId = opts.agentId ?? `agent-${nextAgentId++}`;
|
||||
const handle = createScopedChildHandle(
|
||||
this.instantiation,
|
||||
LifecycleScope.Agent,
|
||||
agentId,
|
||||
);
|
||||
const collection = new ServiceCollection();
|
||||
for (const entry of getScopedServiceDescriptors(LifecycleScope.Agent)) {
|
||||
collection.set(entry.id, entry.descriptor);
|
||||
}
|
||||
const child = this.instantiation.createChild(collection);
|
||||
this.wireToolCallPermissionVeto(child);
|
||||
const accessor: ServicesAccessor = {
|
||||
get: <T>(id: ServiceIdentifier<T>): T => child.invokeFunction((a) => a.get(id)),
|
||||
};
|
||||
const handle: IScopeHandle = { id: agentId, kind: LifecycleScope.Agent, accessor };
|
||||
this.handles.set(agentId, handle);
|
||||
return Promise.resolve(handle);
|
||||
}
|
||||
|
||||
private wireToolCallPermissionVeto(instantiation: IInstantiationService): void {
|
||||
instantiation.invokeFunction((accessor) => {
|
||||
const turnEvents = accessor.get(ITurnEvents);
|
||||
const permission = accessor.get(IPermissionService);
|
||||
turnEvents.onWillExecuteTool((event) => {
|
||||
event.veto(
|
||||
Promise.resolve(
|
||||
permission.beforeToolCall({ toolName: event.toolName, args: event.args }),
|
||||
).then((decision) => decision === 'deny'),
|
||||
'permission',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createMain(): Promise<IScopeHandle> {
|
||||
return this.create({ agentId: 'main' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { createDecorator } from "#/_base/di";
|
||||
import { createDecorator } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IKaosService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
|
@ -9,3 +9,30 @@ export interface IKaosService {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IKaosService = createDecorator<IKaosService>('agentKaosService');
|
||||
|
||||
export type KaosFactoryOptions =
|
||||
| { readonly kind: 'local'; readonly cwd?: string }
|
||||
| { readonly kind: 'ssh'; readonly host: string; readonly cwd?: string };
|
||||
|
||||
export interface IKaosFactory {
|
||||
readonly _serviceBrand: undefined;
|
||||
create(options: KaosFactoryOptions): Promise<Kaos>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const IKaosFactory = createDecorator<IKaosFactory>('kaosFactory');
|
||||
|
||||
export interface ISessionKaosService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly toolKaos: Kaos;
|
||||
readonly persistenceKaos: Kaos;
|
||||
readonly systemContextKaos: Kaos;
|
||||
readonly additionalDirs: readonly string[];
|
||||
setToolKaos(kaos: Kaos): void;
|
||||
setPersistenceKaos(kaos: Kaos): void;
|
||||
addAdditionalDir(dir: string): void;
|
||||
removeAdditionalDir(dir: string): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ISessionKaosService = createDecorator<ISessionKaosService>('sessionKaosService');
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { type Kaos, LocalKaos } from '@moonshot-ai/kaos';
|
|||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { ILogService } from '#/log';
|
||||
import { IEnvironmentService } from '#/environment/environment';
|
||||
import { ILogService } from '#/log/log';
|
||||
|
||||
import { type KaosFactoryOptions, IKaosFactory } from './kaos';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { Kaos } from '@moonshot-ai/kaos';
|
|||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { ILogService } from '#/log';
|
||||
import { ILogService } from '#/log/log';
|
||||
|
||||
import { ISessionKaosService } from './kaos';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
/**
|
||||
* `turnRunner` domain barrel - re-exports the turnRunner service contract and implementation.
|
||||
* `turn` domain barrel — re-exports the turn contract (`turn`) and its scoped
|
||||
* services (`turnService`, `turnEvents`, `loopRunner`, `toolCallExecutor`).
|
||||
* Importing this barrel registers the `ITurnService`, `ITurnEvents`,
|
||||
* `ILoopRunner`, and `IToolCallExecutor` bindings into the scope registry.
|
||||
*/
|
||||
|
||||
export * from './turn';
|
||||
export * from './turnService';
|
||||
export * from './turnEvents';
|
||||
export * from './loopRunner';
|
||||
export * from './toolCallExecutor';
|
||||
|
|
|
|||
19
packages/agent-core-v2/src/turn/loopRunner.ts
Normal file
19
packages/agent-core-v2/src/turn/loopRunner.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* `turn` domain (L4) — `ILoopRunner` implementation.
|
||||
*
|
||||
* Runs the per-turn loop. Bound at Turn scope.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
|
||||
import { ILoopRunner } from './turn';
|
||||
|
||||
export class LoopRunner implements ILoopRunner {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
run(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(LifecycleScope.Turn, ILoopRunner, LoopRunner, InstantiationType.Delayed, 'turn');
|
||||
70
packages/agent-core-v2/src/turn/toolCallExecutor.ts
Normal file
70
packages/agent-core-v2/src/turn/toolCallExecutor.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* `turn` domain (L4) — `IToolCallExecutor` implementation.
|
||||
*
|
||||
* Runs a single tool call through its generic veto gate and fires the
|
||||
* surrounding turn events; executes tools through `tool`. The veto gate is
|
||||
* policy-agnostic — participants such as permission are subscribed by the
|
||||
* composition root, not hard-wired here. Bound at Turn scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { handleVetos } from '#/_base/event';
|
||||
import { onUnexpectedError } from '#/_base/errors/unexpectedError';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IToolService } from '#/tool/tool';
|
||||
|
||||
import {
|
||||
type ToolCallOutcome,
|
||||
IToolCallExecutor,
|
||||
ITurnContext,
|
||||
ITurnEvents,
|
||||
} from './turn';
|
||||
|
||||
type VetoEntry = { readonly value: boolean | Promise<boolean>; readonly id?: string };
|
||||
|
||||
export class ToolCallExecutor extends Disposable implements IToolCallExecutor {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ITurnEvents private readonly turnEvents: ITurnEvents,
|
||||
@IToolService private readonly tool: IToolService,
|
||||
@ITurnContext private readonly turnContext: ITurnContext,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async execute(toolCallId: string, toolName: string, args: unknown): Promise<ToolCallOutcome> {
|
||||
const vetos: VetoEntry[] = [];
|
||||
const { turnId } = this.turnContext;
|
||||
|
||||
this.turnEvents.fireWillExecuteTool({
|
||||
turnId,
|
||||
toolCallId,
|
||||
toolName,
|
||||
args,
|
||||
veto: (value, id) => {
|
||||
vetos.push({ value, id });
|
||||
},
|
||||
});
|
||||
|
||||
if (await handleVetos(vetos.map((entry) => entry.value), onUnexpectedError)) {
|
||||
return { vetoed: true, reason: await this.resolveVetoReason(vetos) };
|
||||
}
|
||||
|
||||
const result = await this.tool.execute(toolName, args);
|
||||
this.turnEvents.fireDidFinalizeTool({ turnId, toolCallId, toolName });
|
||||
return { vetoed: false, result };
|
||||
}
|
||||
|
||||
private async resolveVetoReason(vetos: readonly VetoEntry[]): Promise<string> {
|
||||
for (const { value, id } of vetos) {
|
||||
if (await Promise.resolve(value)) {
|
||||
return id ?? 'vetoed';
|
||||
}
|
||||
}
|
||||
return 'vetoed';
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(LifecycleScope.Turn, IToolCallExecutor, ToolCallExecutor, InstantiationType.Delayed, 'turn');
|
||||
|
|
@ -1,50 +1,102 @@
|
|||
import { createDecorator } from "#/_base/di";
|
||||
import type { ContextMessage, PromptOrigin } from '#/contextMemory';
|
||||
import type { Hooks } from '#/hooks';
|
||||
/**
|
||||
* `turn` domain (L4) — drives the turn lifecycle.
|
||||
*
|
||||
* Defines the public contract of a turn: the `ITurnService` used by upper layers
|
||||
* to start, steer, retry, and cancel a turn and to observe its events, the
|
||||
* `ITurnEvents` dispatcher that owns the turn's event emitters (subscribed to at
|
||||
* Agent scope, fired from both the Agent-scope controller and the per-turn
|
||||
* loop), the `IToolCallExecutor` that runs a single tool call through its
|
||||
* veto/permission gate, the per-turn `ITurnContext`, and the `ILoopRunner` that
|
||||
* runs the turn loop. `ITurnService` / `ITurnEvents` are Agent-scoped;
|
||||
* `IToolCallExecutor` / `ILoopRunner` / `ITurnContext` are Turn-scoped.
|
||||
*/
|
||||
|
||||
import type { Event } from '#/_base/event';
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
import type { ToolCallResult } from '#/tool/tool';
|
||||
|
||||
export interface TurnResult {
|
||||
readonly reason: 'completed' | 'cancelled' | 'failed' | 'filtered';
|
||||
readonly error?: unknown;
|
||||
export interface TurnStartEvent {
|
||||
readonly turnId: string;
|
||||
}
|
||||
|
||||
export interface Turn {
|
||||
readonly id: number;
|
||||
readonly abortController: AbortController;
|
||||
readonly ready: Promise<void>;
|
||||
readonly result: Promise<TurnResult>;
|
||||
export interface TurnWillExecuteToolEvent {
|
||||
readonly turnId: string;
|
||||
readonly toolCallId: string;
|
||||
readonly toolName: string;
|
||||
readonly args: unknown;
|
||||
veto(value: boolean | Promise<boolean>, id?: string): void;
|
||||
}
|
||||
|
||||
export interface TurnStepContext {
|
||||
readonly turn: Turn;
|
||||
continueTurn: boolean;
|
||||
export interface TurnToolEvent {
|
||||
readonly turnId: string;
|
||||
readonly toolCallId: string;
|
||||
readonly toolName: string;
|
||||
}
|
||||
|
||||
export interface TurnRunContext {
|
||||
readonly turn: Turn;
|
||||
readonly origin: PromptOrigin;
|
||||
readonly promptMessage?: ContextMessage;
|
||||
result?: TurnResult;
|
||||
export type ToolCallOutcome =
|
||||
| { readonly vetoed: true; readonly reason: string }
|
||||
| { readonly vetoed: false; readonly result: ToolCallResult };
|
||||
export interface TurnStepEvent {
|
||||
readonly turnId: string;
|
||||
readonly step: number;
|
||||
}
|
||||
|
||||
export interface TurnEndedContext {
|
||||
readonly turn: Turn;
|
||||
readonly result: TurnResult;
|
||||
export interface TurnEndEvent {
|
||||
readonly turnId: string;
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ITurnService {
|
||||
launch(origin: PromptOrigin): Turn;
|
||||
getActiveTurn(): Turn | undefined;
|
||||
cancel(turnId?: number, reason?: unknown): void;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onLaunched: { turn: Turn };
|
||||
onEnded: TurnEndedContext;
|
||||
beforeStep: TurnStepContext;
|
||||
afterStep: TurnStepContext;
|
||||
}>;
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onWillStartTurn: Event<TurnStartEvent>;
|
||||
readonly onWillExecuteTool: Event<TurnWillExecuteToolEvent>;
|
||||
readonly onDidFinalizeTool: Event<TurnToolEvent>;
|
||||
readonly onDidEndStep: Event<TurnStepEvent>;
|
||||
readonly onDidEndTurn: Event<TurnEndEvent>;
|
||||
readonly hasActiveTurn: boolean;
|
||||
readonly currentId: string | undefined;
|
||||
prompt(input: string): Promise<void>;
|
||||
steer(content: string, origin?: string): void;
|
||||
retry(): Promise<void>;
|
||||
cancel(reason?: string): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ITurnService = createDecorator<ITurnService>('turnService');
|
||||
export const ITurnService: ServiceIdentifier<ITurnService> =
|
||||
createDecorator<ITurnService>('turnService');
|
||||
|
||||
export interface ITurnEvents {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onWillStartTurn: Event<TurnStartEvent>;
|
||||
readonly onWillExecuteTool: Event<TurnWillExecuteToolEvent>;
|
||||
readonly onDidFinalizeTool: Event<TurnToolEvent>;
|
||||
readonly onDidEndStep: Event<TurnStepEvent>;
|
||||
readonly onDidEndTurn: Event<TurnEndEvent>;
|
||||
fireWillStartTurn(event: TurnStartEvent): void;
|
||||
fireWillExecuteTool(event: TurnWillExecuteToolEvent): void;
|
||||
fireDidFinalizeTool(event: TurnToolEvent): void;
|
||||
fireDidEndStep(event: TurnStepEvent): void;
|
||||
fireDidEndTurn(event: TurnEndEvent): void;
|
||||
}
|
||||
|
||||
export const ITurnEvents: ServiceIdentifier<ITurnEvents> =
|
||||
createDecorator<ITurnEvents>('turnEvents');
|
||||
|
||||
export interface IToolCallExecutor {
|
||||
readonly _serviceBrand: undefined;
|
||||
execute(toolCallId: string, toolName: string, args: unknown): Promise<ToolCallOutcome>;
|
||||
}
|
||||
|
||||
export const IToolCallExecutor: ServiceIdentifier<IToolCallExecutor> =
|
||||
createDecorator<IToolCallExecutor>('toolCallExecutor');
|
||||
|
||||
export interface ITurnContext {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly turnId: string;
|
||||
}
|
||||
|
||||
export const ITurnContext: ServiceIdentifier<ITurnContext> =
|
||||
createDecorator<ITurnContext>('turnContext');
|
||||
|
||||
export interface ILoopRunner {
|
||||
readonly _serviceBrand: undefined;
|
||||
run(): Promise<void>;
|
||||
}
|
||||
|
||||
export const ILoopRunner: ServiceIdentifier<ILoopRunner> =
|
||||
createDecorator<ILoopRunner>('loopRunner');
|
||||
|
|
|
|||
54
packages/agent-core-v2/src/turn/turnEvents.ts
Normal file
54
packages/agent-core-v2/src/turn/turnEvents.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* `turn` domain (L4) — `ITurnEvents` implementation.
|
||||
*
|
||||
* Owns the turn's event emitters and exposes both the subscribe surfaces and
|
||||
* the fire methods, so the Agent-scope lifecycle controller and the per-turn
|
||||
* loop / tool executor can drive the same event stream. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { Emitter, type Event } from '#/_base/event';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
|
||||
import {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
type TurnStepEvent,
|
||||
type TurnToolEvent,
|
||||
type TurnWillExecuteToolEvent,
|
||||
ITurnEvents,
|
||||
} from './turn';
|
||||
|
||||
export class TurnEvents extends Disposable implements ITurnEvents {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onWillStartTurn = this._register(new Emitter<TurnStartEvent>());
|
||||
readonly onWillStartTurn: Event<TurnStartEvent> = this._onWillStartTurn.event;
|
||||
private readonly _onWillExecuteTool = this._register(new Emitter<TurnWillExecuteToolEvent>());
|
||||
readonly onWillExecuteTool: Event<TurnWillExecuteToolEvent> = this._onWillExecuteTool.event;
|
||||
private readonly _onDidFinalizeTool = this._register(new Emitter<TurnToolEvent>());
|
||||
readonly onDidFinalizeTool: Event<TurnToolEvent> = this._onDidFinalizeTool.event;
|
||||
private readonly _onDidEndStep = this._register(new Emitter<TurnStepEvent>());
|
||||
readonly onDidEndStep: Event<TurnStepEvent> = this._onDidEndStep.event;
|
||||
private readonly _onDidEndTurn = this._register(new Emitter<TurnEndEvent>());
|
||||
readonly onDidEndTurn: Event<TurnEndEvent> = this._onDidEndTurn.event;
|
||||
|
||||
fireWillStartTurn(event: TurnStartEvent): void {
|
||||
this._onWillStartTurn.fire(event);
|
||||
}
|
||||
fireWillExecuteTool(event: TurnWillExecuteToolEvent): void {
|
||||
this._onWillExecuteTool.fire(event);
|
||||
}
|
||||
fireDidFinalizeTool(event: TurnToolEvent): void {
|
||||
this._onDidFinalizeTool.fire(event);
|
||||
}
|
||||
fireDidEndStep(event: TurnStepEvent): void {
|
||||
this._onDidEndStep.fire(event);
|
||||
}
|
||||
fireDidEndTurn(event: TurnEndEvent): void {
|
||||
this._onDidEndTurn.fire(event);
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(LifecycleScope.Agent, ITurnEvents, TurnEvents, InstantiationType.Delayed, 'turn');
|
||||
|
|
@ -1,329 +1,117 @@
|
|||
import {
|
||||
IInstantiationService,
|
||||
} from "#/_base/di";
|
||||
/**
|
||||
* `turn` domain (L4) — `ITurnService` implementation.
|
||||
*
|
||||
* Drives the turn lifecycle and emits its events through the turn event
|
||||
* dispatcher; runs the turn loop through `loopRunner`, drives agent lifecycle
|
||||
* through `agent-lifecycle`, reads history through `context`, enqueues
|
||||
* follow-up through `injection`, drives LLM generation through `kosong`, logs
|
||||
* through `log`, reports telemetry through `telemetry`, and checks usage
|
||||
* through `usage`. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { type Event } from '#/_base/event';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { toKimiErrorPayload, type KimiErrorPayload } from "#/_base/errors";
|
||||
import { isUserCancellation, userCancellationReason } from "#/_base/utils/abort";
|
||||
import type { ContextMessage, PromptOrigin } from '#/contextMemory';
|
||||
import { IContextMemory, USER_PROMPT_ORIGIN } from '#/contextMemory';
|
||||
import { IEventBus } from '#/eventBus';
|
||||
import { IExternalHooksService } from '#/externalHooks';
|
||||
import { OrderedHookSlot } from '#/hooks';
|
||||
import { ILoopService } from '#/loop';
|
||||
import { IPlanService } from '#/plan';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { IUsageService } from '#/usage';
|
||||
import { IWireRecord } from '#/wireRecord';
|
||||
import type {
|
||||
Turn,
|
||||
TurnEndedContext,
|
||||
TurnResult,
|
||||
TurnStepContext,
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { IContextService } from '#/context/context';
|
||||
import { IInjectionService } from '#/injection/injection';
|
||||
import { ILLMService } from '#/kosong/kosong';
|
||||
import { ILogService } from '#/log/log';
|
||||
import { ITelemetryService } from '#/telemetry/telemetry';
|
||||
import { IUsageService } from '#/usage/usage';
|
||||
|
||||
import {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
type TurnStepEvent,
|
||||
type TurnToolEvent,
|
||||
type TurnWillExecuteToolEvent,
|
||||
ILoopRunner,
|
||||
ITurnEvents,
|
||||
ITurnService,
|
||||
} from './turn';
|
||||
import { ITurnService } from './turn';
|
||||
|
||||
declare module '#/wireRecord' {
|
||||
interface WireRecordMap {
|
||||
'turn.launch': {
|
||||
turnId: number;
|
||||
origin: PromptOrigin;
|
||||
};
|
||||
}
|
||||
}
|
||||
let nextTurnId = 0;
|
||||
|
||||
export class TurnRunnerService implements ITurnService {
|
||||
private nextTurnId = 0;
|
||||
private activeTurn: Turn | undefined;
|
||||
private readonly readyControllers = new WeakMap<Turn, ControlledPromise<void>>();
|
||||
private readonly readySettled = new WeakSet<Turn>();
|
||||
private readonly currentStepByTurn = new Map<number, number>();
|
||||
private readonly interruptedTelemetryTurnIds = new Set<number>();
|
||||
private readonly telemetryModeByTurn = new Map<number, 'agent' | 'plan'>();
|
||||
export class TurnService extends Disposable implements ITurnService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
readonly hooks = {
|
||||
onLaunched: new OrderedHookSlot<{ turn: Turn }>(),
|
||||
onEnded: new OrderedHookSlot<TurnEndedContext>(),
|
||||
beforeStep: new OrderedHookSlot<TurnStepContext>(),
|
||||
afterStep: new OrderedHookSlot<TurnStepContext>(),
|
||||
};
|
||||
readonly onWillStartTurn: Event<TurnStartEvent>;
|
||||
readonly onWillExecuteTool: Event<TurnWillExecuteToolEvent>;
|
||||
readonly onDidFinalizeTool: Event<TurnToolEvent>;
|
||||
readonly onDidEndStep: Event<TurnStepEvent>;
|
||||
readonly onDidEndTurn: Event<TurnEndEvent>;
|
||||
|
||||
private active: { readonly turnId: string; cancelled: boolean } | undefined;
|
||||
private readonly steerBuffer: { content: string; origin?: string }[] = [];
|
||||
|
||||
constructor(
|
||||
@ILoopService private readonly loop: ILoopService,
|
||||
@IUsageService private readonly usage: IUsageService,
|
||||
@IEventBus private readonly events: IEventBus,
|
||||
@IWireRecord private readonly wireRecord: IWireRecord,
|
||||
@IContextMemory private readonly context: IContextMemory,
|
||||
@IExternalHooksService private readonly externalHooks: IExternalHooksService,
|
||||
@IInstantiationService private readonly instantiation: IInstantiationService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IContextService _context: IContextService,
|
||||
@ILLMService _llm: ILLMService,
|
||||
@IInjectionService _injection: IInjectionService,
|
||||
@IUsageService _usage: IUsageService,
|
||||
@ITelemetryService _telemetry: ITelemetryService,
|
||||
@ILogService _log: ILogService,
|
||||
@IAgentLifecycleService _agentLifecycle: IAgentLifecycleService,
|
||||
@ILoopRunner private readonly loopRunner: ILoopRunner,
|
||||
@ITurnEvents private readonly turnEvents: ITurnEvents,
|
||||
) {
|
||||
wireRecord.register('turn.launch', (record) => {
|
||||
this.restoreLaunch(record.turnId);
|
||||
});
|
||||
this.hooks.beforeStep.register('turn-before-step-event', async (ctx, next) => {
|
||||
await next();
|
||||
this.resolveReady(ctx.turn);
|
||||
});
|
||||
this.events.on((event) => {
|
||||
if (event.type === 'turn.step.started') {
|
||||
this.currentStepByTurn.set(event.turnId, event.step);
|
||||
return;
|
||||
}
|
||||
if (event.type === 'turn.step.interrupted') {
|
||||
this.trackTurnInterrupted(event.turnId, event.step);
|
||||
}
|
||||
});
|
||||
super();
|
||||
|
||||
this.onWillStartTurn = this.turnEvents.onWillStartTurn;
|
||||
this.onWillExecuteTool = this.turnEvents.onWillExecuteTool;
|
||||
this.onDidFinalizeTool = this.turnEvents.onDidFinalizeTool;
|
||||
this.onDidEndStep = this.turnEvents.onDidEndStep;
|
||||
this.onDidEndTurn = this.turnEvents.onDidEndTurn;
|
||||
}
|
||||
|
||||
launch(origin: PromptOrigin): Turn {
|
||||
if (this.activeTurn !== undefined) {
|
||||
throw new Error(`Cannot launch a new turn while turn ${this.activeTurn.id} is active`);
|
||||
get hasActiveTurn(): boolean {
|
||||
return this.active !== undefined;
|
||||
}
|
||||
get currentId(): string | undefined {
|
||||
return this.active?.turnId;
|
||||
}
|
||||
|
||||
async prompt(input: string): Promise<void> {
|
||||
if (this.active !== undefined) {
|
||||
this.steer(input);
|
||||
return;
|
||||
}
|
||||
|
||||
const turnId = this.nextTurnId;
|
||||
this.wireRecord.append({ type: 'turn.launch', turnId, origin });
|
||||
this.restoreLaunch(turnId);
|
||||
const abortController = new AbortController();
|
||||
const ready = createControlledPromise<void>();
|
||||
const turn: MutableTurn = {
|
||||
id: turnId,
|
||||
abortController,
|
||||
ready: ready.promise,
|
||||
result: Promise.resolve({ reason: 'failed' }),
|
||||
};
|
||||
this.readyControllers.set(turn, ready);
|
||||
void ready.promise.catch(() => undefined);
|
||||
this.activeTurn = turn;
|
||||
turn.result = this.runTurn(turn, origin);
|
||||
void this.hooks.onLaunched.run({ turn });
|
||||
return turn;
|
||||
await this.launch(input);
|
||||
}
|
||||
|
||||
getActiveTurn(): Turn | undefined {
|
||||
return this.activeTurn;
|
||||
steer(content: string, origin?: string): void {
|
||||
this.steerBuffer.push({ content, origin });
|
||||
}
|
||||
|
||||
cancel(turnId?: number, reason?: unknown): void {
|
||||
const turn = this.activeTurn;
|
||||
if (turn === undefined) return;
|
||||
if (turnId !== undefined && turn.id !== turnId) return;
|
||||
turn.abortController.abort(reason ?? userCancellationReason());
|
||||
retry(): Promise<void> {
|
||||
throw new Error('TODO: TurnService.retry');
|
||||
}
|
||||
|
||||
private async runTurn(turn: Turn, origin: PromptOrigin): Promise<TurnResult> {
|
||||
const startedAt = Date.now();
|
||||
const telemetryMode = this.telemetryMode();
|
||||
this.telemetryModeByTurn.set(turn.id, telemetryMode);
|
||||
let result: TurnResult | undefined;
|
||||
cancel(reason?: string): void {
|
||||
if (this.active === undefined) return;
|
||||
this.active.cancelled = true;
|
||||
const turnId = this.active.turnId;
|
||||
this.active = undefined;
|
||||
this.turnEvents.fireDidEndTurn({ turnId, reason: reason ?? 'cancelled' });
|
||||
}
|
||||
|
||||
private async launch(input: string): Promise<void> {
|
||||
const turnId = `turn-${nextTurnId++}`;
|
||||
this.active = { turnId, cancelled: false };
|
||||
this.turnEvents.fireWillStartTurn({ turnId });
|
||||
try {
|
||||
this.usage.beginTurn();
|
||||
this.telemetry.track('turn_started', { mode: telemetryMode });
|
||||
this.events.emit({ type: 'turn.started', turnId: turn.id, origin });
|
||||
const promptHookResult = await this.applyUserPromptHook(turn, origin);
|
||||
if (promptHookResult !== undefined) {
|
||||
result = promptHookResult;
|
||||
return result;
|
||||
}
|
||||
result = await this.loop.runTurn(turn, {
|
||||
beforeStep: this.hooks.beforeStep,
|
||||
afterStep: this.hooks.afterStep,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (turn.abortController.signal.aborted) {
|
||||
result = { reason: 'cancelled', error: turn.abortController.signal.reason };
|
||||
this.rejectReady(turn, turn.abortController.signal.reason);
|
||||
return result;
|
||||
}
|
||||
this.externalHooks.triggerStopFailure(error, turn.abortController.signal);
|
||||
this.rejectReady(turn, error);
|
||||
result = { reason: 'failed', error };
|
||||
return result;
|
||||
await this.loopRunner.run();
|
||||
this.turnEvents.fireDidEndStep({ turnId, step: 0 });
|
||||
} finally {
|
||||
if (result !== undefined) {
|
||||
this.rejectReady(turn, result);
|
||||
if (this.active?.turnId === turnId) {
|
||||
this.active = undefined;
|
||||
this.turnEvents.fireDidEndTurn({ turnId, reason: 'completed' });
|
||||
}
|
||||
this.usage.endTurn();
|
||||
if (this.activeTurn === turn) {
|
||||
this.activeTurn = undefined;
|
||||
}
|
||||
if (result !== undefined) {
|
||||
const ended = toTurnEndedEvent(turn, result, Date.now() - startedAt);
|
||||
if (
|
||||
ended.reason === 'cancelled' &&
|
||||
isUserCancellation(turn.abortController.signal.reason)
|
||||
) {
|
||||
this.externalHooks.triggerInterrupt({ turnId: turn.id, reason: 'cancelled' });
|
||||
}
|
||||
this.events.emit(ended);
|
||||
if (ended.error !== undefined) {
|
||||
this.events.emit({ type: 'error', ...ended.error });
|
||||
}
|
||||
if (ended.reason !== 'completed') {
|
||||
this.trackTurnInterrupted(turn.id, this.currentStepByTurn.get(turn.id) ?? 0);
|
||||
}
|
||||
}
|
||||
if (result !== undefined) {
|
||||
await this.hooks.onEnded.run({ turn, result });
|
||||
}
|
||||
this.currentStepByTurn.delete(turn.id);
|
||||
this.interruptedTelemetryTurnIds.delete(turn.id);
|
||||
this.telemetryModeByTurn.delete(turn.id);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveReady(turn: Turn): void {
|
||||
if (this.readySettled.has(turn)) return;
|
||||
this.readySettled.add(turn);
|
||||
this.readyControllers.get(turn)?.resolve();
|
||||
}
|
||||
|
||||
private restoreLaunch(turnId: number): void {
|
||||
if (Number.isInteger(turnId) && turnId >= this.nextTurnId) {
|
||||
this.nextTurnId = turnId + 1;
|
||||
}
|
||||
}
|
||||
|
||||
private async applyUserPromptHook(
|
||||
turn: Turn,
|
||||
origin: PromptOrigin,
|
||||
): Promise<TurnResult | undefined> {
|
||||
if (origin.kind !== 'user') return undefined;
|
||||
const promptMessage = this.context.getHistory().at(-1);
|
||||
if (!shouldRunUserPromptHook(promptMessage)) return undefined;
|
||||
|
||||
const hookResult = await this.externalHooks.triggerUserPromptSubmit(
|
||||
promptMessage.content,
|
||||
turn.abortController.signal,
|
||||
);
|
||||
if (hookResult?.action === 'block') {
|
||||
this.append({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: hookResult.text }],
|
||||
toolCalls: [],
|
||||
origin: { kind: 'hook_result', event: hookResult.event, blocked: true },
|
||||
});
|
||||
this.events.emit({
|
||||
type: 'hook.result',
|
||||
turnId: turn.id,
|
||||
hookEvent: hookResult.event,
|
||||
content: hookResult.message,
|
||||
blocked: true,
|
||||
});
|
||||
return { reason: 'completed' };
|
||||
}
|
||||
|
||||
if (hookResult?.action === 'append') {
|
||||
this.append({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: hookResult.text }],
|
||||
toolCalls: [],
|
||||
origin: { kind: 'hook_result', event: hookResult.event },
|
||||
});
|
||||
this.events.emit({
|
||||
type: 'hook.result',
|
||||
turnId: turn.id,
|
||||
hookEvent: hookResult.event,
|
||||
content: hookResult.message,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private append(...messages: ContextMessage[]): void {
|
||||
if (messages.length === 0) return;
|
||||
this.context.spliceHistory(this.context.getHistory().length, 0, messages);
|
||||
}
|
||||
|
||||
private rejectReady(turn: Turn, reason: unknown): void {
|
||||
if (this.readySettled.has(turn)) return;
|
||||
this.readySettled.add(turn);
|
||||
this.readyControllers.get(turn)?.reject(reason);
|
||||
}
|
||||
|
||||
private trackTurnInterrupted(turnId: number, atStep: number): void {
|
||||
if (this.interruptedTelemetryTurnIds.has(turnId)) return;
|
||||
this.interruptedTelemetryTurnIds.add(turnId);
|
||||
this.telemetry.track('turn_interrupted', {
|
||||
mode: this.telemetryModeByTurn.get(turnId) ?? this.telemetryMode(),
|
||||
at_step: atStep,
|
||||
});
|
||||
}
|
||||
|
||||
private telemetryMode(): 'agent' | 'plan' {
|
||||
const planMode = this.instantiation.invokeFunction((accessor) =>
|
||||
accessor.get(IPlanService),
|
||||
);
|
||||
return planMode.isActive ? 'plan' : 'agent';
|
||||
void input;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRunUserPromptHook(message: ContextMessage | undefined): message is ContextMessage {
|
||||
if (message === undefined || message.role !== 'user') return false;
|
||||
return (message.origin ?? USER_PROMPT_ORIGIN).kind === 'user';
|
||||
}
|
||||
|
||||
function toTurnEndedEvent(
|
||||
turn: Turn,
|
||||
result: TurnResult,
|
||||
durationMs: number,
|
||||
): {
|
||||
type: 'turn.ended';
|
||||
turnId: number;
|
||||
reason: TurnResult['reason'];
|
||||
error?: KimiErrorPayload;
|
||||
durationMs: number;
|
||||
} {
|
||||
if (result.reason !== 'failed' || result.error === undefined) {
|
||||
return { type: 'turn.ended', turnId: turn.id, reason: result.reason, durationMs };
|
||||
}
|
||||
return {
|
||||
type: 'turn.ended',
|
||||
turnId: turn.id,
|
||||
reason: result.reason,
|
||||
error: summarizeTurnError(result.error, turn.id),
|
||||
durationMs,
|
||||
};
|
||||
}
|
||||
|
||||
const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login';
|
||||
|
||||
function summarizeTurnError(error: unknown, turnId: number): KimiErrorPayload {
|
||||
const payload = toKimiErrorPayload(error);
|
||||
const details = { ...payload.details, turnId };
|
||||
// Substitute a friendlier, login-aware message for model-not-configured. The
|
||||
// raw "Model not set" / "Provider not set" text is not actionable.
|
||||
if (payload.code === 'model.not_configured') {
|
||||
return { ...payload, message: LLM_NOT_SET_MESSAGE, details };
|
||||
}
|
||||
return { ...payload, details };
|
||||
}
|
||||
|
||||
interface ControlledPromise<T> {
|
||||
readonly promise: Promise<T>;
|
||||
resolve(value: T | PromiseLike<T>): void;
|
||||
reject(reason?: unknown): void;
|
||||
}
|
||||
|
||||
type MutableTurn = {
|
||||
-readonly [K in keyof Turn]: Turn[K];
|
||||
};
|
||||
|
||||
function createControlledPromise<T>(): ControlledPromise<T> {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
ITurnService,
|
||||
TurnRunnerService,
|
||||
InstantiationType.Delayed,
|
||||
'turn',
|
||||
);
|
||||
registerScopedService(LifecycleScope.Agent, ITurnService, TurnService, InstantiationType.Delayed, 'turn');
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { AgentLifecycleService } from '#/agent-lifecycle/agentLifecycleService';
|
||||
import { ISessionMetaStore } from '#/records';
|
||||
import { ISessionContext } from '#/session-context/sessionContext';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerSessionContextServices } from '../session-context/stubs';
|
||||
|
||||
describe('AgentLifecycleService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -14,10 +14,12 @@ describe('AgentLifecycleService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ISessionContext, {});
|
||||
ix.stub(ISessionMetaStore, {});
|
||||
ix.set(IAgentLifecycleService, new SyncDescriptor(AgentLifecycleService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerSessionContextServices, registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IAgentLifecycleService, AgentLifecycleService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
19
packages/agent-core-v2/test/agent-lifecycle/stubs.ts
Normal file
19
packages/agent-core-v2/test/agent-lifecycle/stubs.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* `agent-lifecycle` test stubs — shared `IAgentLifecycleService` placeholder.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or
|
||||
* `../agent-lifecycle/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
|
||||
/**
|
||||
* Register an empty `IAgentLifecycleService` placeholder. Tests that need a
|
||||
* lifecycle service with handles should register a custom fake via
|
||||
* `additionalServices` instead.
|
||||
*/
|
||||
export function registerAgentLifecycleServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(IAgentLifecycleService, {});
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IApprovalService } from '#/approval';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IApprovalService } from '#/approval/approval';
|
||||
import { ApprovalService } from '#/approval/approvalService';
|
||||
|
||||
describe('ApprovalService', () => {
|
||||
|
|
@ -12,8 +12,11 @@ describe('ApprovalService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.set(IApprovalService, new SyncDescriptor(ApprovalService));
|
||||
ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IApprovalService, ApprovalService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IOAuthService } from '#/auth';
|
||||
import { IConfigService } from '#/config';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IOAuthService } from '#/auth/auth';
|
||||
|
||||
import { OAuthService } from '#/auth/authService';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerEnvironmentServices } from '../environment/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
|
||||
describe('OAuthService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -16,11 +16,16 @@ describe('OAuthService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigService, {});
|
||||
ix.stub(IEnvironmentService, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.set(IOAuthService, new SyncDescriptor(OAuthService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerEnvironmentServices,
|
||||
registerTelemetryServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IOAuthService, OAuthService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { IBackgroundService } from '#/background';
|
||||
import { IKaosService } from '#/kaos';
|
||||
import { ILogService } from '#/log';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IBackgroundService } from '#/background/background';
|
||||
import { IKaosService } from '#/kaos/kaos';
|
||||
|
||||
import { BackgroundService } from '#/background/backgroundService';
|
||||
import { registerAgentLifecycleServices } from '../agent-lifecycle/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
|
||||
describe('BackgroundService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -18,13 +18,18 @@ describe('BackgroundService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IKaosService, {});
|
||||
ix.stub(IAgentRecords, {});
|
||||
ix.stub(ILogService, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.stub(IAgentLifecycleService, {});
|
||||
ix.set(IBackgroundService, new SyncDescriptor(BackgroundService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerLogServices,
|
||||
registerTelemetryServices,
|
||||
registerRecordsServices,
|
||||
registerAgentLifecycleServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IKaosService, {});
|
||||
reg.define(IBackgroundService, BackgroundService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { CompactionService } from '#/compaction/compactionService';
|
||||
import { IAgentConfigService } from '#/config';
|
||||
import { IContextService } from '#/context';
|
||||
import { IContextService } from '#/context/context';
|
||||
import { ContextService } from '#/context/contextService';
|
||||
import { IInjectionService } from '#/injection';
|
||||
import { IInjectionService } from '#/injection/injection';
|
||||
import { InjectionService } from '#/injection/injectionService';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { stubAgentRecords } from '../records/stubs';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { ITurnService } from '#/turn';
|
||||
import { stubTurn } from '../turn/stubs';
|
||||
import { ITurnService } from '#/turn/turn';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
import { registerTurnServices } from '../turn/stubs';
|
||||
|
||||
describe('CompactionService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -21,13 +20,18 @@ describe('CompactionService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentRecords, stubAgentRecords());
|
||||
ix.stub(IAgentConfigService, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.stub(ITurnService, stubTurn());
|
||||
ix.set(IContextService, new SyncDescriptor(ContextService));
|
||||
ix.set(IInjectionService, new SyncDescriptor(InjectionService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerRecordsServices,
|
||||
registerConfigServices,
|
||||
registerTelemetryServices,
|
||||
registerTurnServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IContextService, ContextService);
|
||||
reg.define(IInjectionService, InjectionService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { IKaosService } from '#/kaos';
|
||||
import { IKaosService } from '#/kaos/kaos';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { stubEnvironment } from '../environment/stubs';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { stubAgentRecords } from '../records/stubs';
|
||||
import { IAgentConfigService, IConfigRegistry, IConfigService } from '#/config';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentConfigService, IConfigService } from '#/config/config';
|
||||
|
||||
import { AgentConfigService, ConfigRegistry, ConfigService } from '#/config/configService';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerEnvironmentServices } from '../environment/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('ConfigRegistry', () => {
|
||||
it('registers and retrieves a section', () => {
|
||||
|
|
@ -45,11 +43,16 @@ describe('ConfigService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigRegistry, new ConfigRegistry());
|
||||
ix.stub(IEnvironmentService, stubEnvironment());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IConfigService, new SyncDescriptor(ConfigService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerEnvironmentServices,
|
||||
registerLogServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IConfigService, ConfigService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -86,12 +89,15 @@ describe('AgentConfigService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
agentSection = {};
|
||||
ix.stub(IConfigService, { get: <T>() => agentSection as T });
|
||||
ix.stub(IAgentRecords, stubAgentRecords());
|
||||
ix.stub(IKaosService, agentKaos);
|
||||
ix.set(IAgentConfigService, new SyncDescriptor(AgentConfigService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IConfigService, { get: <T>() => agentSection as T });
|
||||
reg.defineInstance(IKaosService, agentKaos);
|
||||
reg.define(IAgentConfigService, AgentConfigService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
22
packages/agent-core-v2/test/config/stubs.ts
Normal file
22
packages/agent-core-v2/test/config/stubs.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* `config` test stubs — shared config collaborators for unit tests.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or `../config/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IAgentConfigService, IConfigRegistry, IConfigService } from '#/config/config';
|
||||
import { ConfigRegistry } from '#/config/configService';
|
||||
|
||||
/**
|
||||
* Register the default config collaborators: a real `ConfigRegistry` plus empty
|
||||
* `IConfigService` / `IAgentConfigService` placeholders. Tests exercising the
|
||||
* real `ConfigService` / `AgentConfigService` should override the placeholder
|
||||
* via `additionalServices`.
|
||||
*/
|
||||
export function registerConfigServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(IConfigRegistry, new ConfigRegistry());
|
||||
reg.definePartialInstance(IConfigService, {});
|
||||
reg.definePartialInstance(IAgentConfigService, {});
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IContextService } from '#/context';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IContextService } from '#/context/context';
|
||||
import { ContextService } from '#/context/contextService';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('ContextService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -13,9 +13,12 @@ describe('ContextService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentRecords, { _serviceBrand: undefined });
|
||||
ix.set(IContextService, new SyncDescriptor(ContextService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IContextService, ContextService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
17
packages/agent-core-v2/test/context/stubs.ts
Normal file
17
packages/agent-core-v2/test/context/stubs.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* `context` test stubs — shared `IContextService` placeholder.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or `../context/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IContextService } from '#/context/context';
|
||||
|
||||
/**
|
||||
* Register an empty `IContextService` placeholder. Tests exercising the real
|
||||
* `ContextService` should override it via `additionalServices`.
|
||||
*/
|
||||
export function registerContextServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(IContextService, {});
|
||||
}
|
||||
|
|
@ -1,21 +1,19 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { ICronFireCoordinator, ICronService } from '#/cron';
|
||||
import { ICronFireCoordinator, ICronService } from '#/cron/cron';
|
||||
import { CronFireCoordinator, CronService } from '#/cron/cronService';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { stubEnvironment } from '../environment/stubs';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { ISessionMetaStore } from '#/records';
|
||||
import { ISessionActivity } from '#/session-activity/sessionActivity';
|
||||
import { ISessionContext } from '#/session-context/sessionContext';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { registerEnvironmentServices } from '../environment/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerSessionContextServices } from '../session-context/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
import { stubTurn } from '../turn/stubs';
|
||||
|
||||
function activity(idle: boolean): ISessionActivity {
|
||||
|
|
@ -28,13 +26,18 @@ describe('CronService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ISessionContext, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.stub(IEnvironmentService, stubEnvironment());
|
||||
ix.stub(ISessionMetaStore, {});
|
||||
ix.set(ICronService, new SyncDescriptor(CronService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerSessionContextServices,
|
||||
registerTelemetryServices,
|
||||
registerLogServices,
|
||||
registerEnvironmentServices,
|
||||
registerRecordsServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ICronService, CronService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -76,15 +79,20 @@ describe('CronService', () => {
|
|||
describe('CronFireCoordinator', () => {
|
||||
it('steers the main agent on fire', async () => {
|
||||
const disposables = new DisposableStore();
|
||||
const ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ISessionContext, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.stub(IEnvironmentService, stubEnvironment());
|
||||
ix.stub(ISessionMetaStore, {});
|
||||
ix.stub(ISessionActivity, activity(true));
|
||||
ix.set(ICronService, new SyncDescriptor(CronService));
|
||||
ix.set(ICronFireCoordinator, new SyncDescriptor(CronFireCoordinator));
|
||||
const ix = createServices(disposables, {
|
||||
base: [
|
||||
registerSessionContextServices,
|
||||
registerTelemetryServices,
|
||||
registerLogServices,
|
||||
registerEnvironmentServices,
|
||||
registerRecordsServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.defineInstance(ISessionActivity, activity(true));
|
||||
reg.define(ICronService, CronService);
|
||||
reg.define(ICronFireCoordinator, CronFireCoordinator);
|
||||
},
|
||||
});
|
||||
|
||||
const turn = stubTurn();
|
||||
const handle: IScopeHandle = {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
* `../environment/stubs`).
|
||||
*/
|
||||
|
||||
import type { IEnvironmentService } from '#/environment';
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IEnvironmentService } from '#/environment/environment';
|
||||
|
||||
/**
|
||||
* An `IEnvironmentService` rooted at the given home dir. `detect()` rejects
|
||||
|
|
@ -21,3 +22,8 @@ export function stubEnvironment(homeDir = '/tmp/kimi-home'): IEnvironmentService
|
|||
detect: () => Promise.reject(new Error('unused in test')),
|
||||
};
|
||||
}
|
||||
|
||||
/** Register the default `IEnvironmentService` rooted at `/tmp/kimi-home`. */
|
||||
export function registerEnvironmentServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(IEnvironmentService, stubEnvironment());
|
||||
}
|
||||
|
|
|
|||
140
packages/agent-core-v2/test/event/emitter.test.ts
Normal file
140
packages/agent-core-v2/test/event/emitter.test.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { AsyncEmitter, handleVetos, type IWaitUntil, type IWaitUntilData } from '#/_base/event';
|
||||
|
||||
interface TestEvent extends IWaitUntil {
|
||||
readonly value: number;
|
||||
}
|
||||
|
||||
function fire(
|
||||
emitter: AsyncEmitter<TestEvent>,
|
||||
data: IWaitUntilData<TestEvent>,
|
||||
signal = new AbortController().signal,
|
||||
): Promise<void> {
|
||||
return emitter.fireAsync(data, signal);
|
||||
}
|
||||
|
||||
describe('AsyncEmitter', () => {
|
||||
it('resolves without delivering when there are no listeners', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
await expect(fire(emitter, { value: 1 })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('awaits every waitUntil promise before resolving', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
const order: string[] = [];
|
||||
emitter.event((e) => {
|
||||
e.waitUntil(
|
||||
new Promise((resolve) => setTimeout(resolve, 10)).then(() => {
|
||||
order.push('a');
|
||||
}),
|
||||
);
|
||||
e.waitUntil(
|
||||
Promise.resolve().then(() => {
|
||||
order.push('b');
|
||||
}),
|
||||
);
|
||||
});
|
||||
await fire(emitter, { value: 1 });
|
||||
expect(order.sort()).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('delivers to listeners sequentially in registration order', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
const order: string[] = [];
|
||||
emitter.event((e) => {
|
||||
e.waitUntil(
|
||||
new Promise((resolve) => setTimeout(resolve, 10)).then(() => {
|
||||
order.push('first-done');
|
||||
}),
|
||||
);
|
||||
order.push('first');
|
||||
});
|
||||
emitter.event(() => {
|
||||
order.push('second');
|
||||
});
|
||||
await fire(emitter, { value: 1 });
|
||||
expect(order).toEqual(['first', 'first-done', 'second']);
|
||||
});
|
||||
|
||||
it('exposes the abort signal and data fields on the event', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
const ac = new AbortController();
|
||||
let seen: { value: number; aborted: boolean } | undefined;
|
||||
emitter.event((e) => {
|
||||
seen = { value: e.value, aborted: e.signal.aborted };
|
||||
});
|
||||
await fire(emitter, { value: 42 }, ac.signal);
|
||||
expect(seen).toEqual({ value: 42, aborted: false });
|
||||
});
|
||||
|
||||
it('rejects waitUntil calls made after the synchronous delivery phase', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
let captured: TestEvent | undefined;
|
||||
emitter.event((e) => {
|
||||
captured = e;
|
||||
});
|
||||
await fire(emitter, { value: 1 });
|
||||
expect(captured).toBeDefined();
|
||||
expect(() => captured!.waitUntil(Promise.resolve())).toThrow(/asynchronously/);
|
||||
});
|
||||
|
||||
it('stops delivering once the signal is aborted', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
const ac = new AbortController();
|
||||
const seen: string[] = [];
|
||||
emitter.event((e) => {
|
||||
seen.push('first');
|
||||
e.waitUntil(
|
||||
Promise.resolve().then(() => {
|
||||
ac.abort();
|
||||
}),
|
||||
);
|
||||
});
|
||||
emitter.event(() => {
|
||||
seen.push('second');
|
||||
});
|
||||
await fire(emitter, { value: 1 }, ac.signal);
|
||||
expect(seen).toEqual(['first']);
|
||||
});
|
||||
|
||||
it('isolates a throwing listener and still delivers to the rest', async () => {
|
||||
const emitter = new AsyncEmitter<TestEvent>();
|
||||
const seen: string[] = [];
|
||||
emitter.event(() => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
emitter.event(() => {
|
||||
seen.push('ok');
|
||||
});
|
||||
await fire(emitter, { value: 1 });
|
||||
expect(seen).toEqual(['ok']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleVetos', () => {
|
||||
const noop = (): void => {};
|
||||
|
||||
it('returns false when there are no vetos', async () => {
|
||||
await expect(handleVetos([], noop)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('short-circuits to true on a synchronous veto', async () => {
|
||||
await expect(handleVetos([false, true, Promise.resolve(true)], noop)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when all vetos are false', async () => {
|
||||
await expect(handleVetos([false, Promise.resolve(false)], noop)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when a promise veto resolves to true', async () => {
|
||||
await expect(handleVetos([false, Promise.resolve(true)], noop)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('treats a rejected promise veto as a veto and reports the error', async () => {
|
||||
const errors: unknown[] = [];
|
||||
const result = await handleVetos([Promise.reject(new Error('boom'))], (e) => errors.push(e));
|
||||
expect(result).toBe(true);
|
||||
expect(errors).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IFileStore } from '#/filestore';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IFileStore } from '#/filestore/filestore';
|
||||
import { FileStore } from '#/filestore/fileStoreService';
|
||||
import { IKaosFactory } from '#/kaos';
|
||||
import { IKaosFactory } from '#/kaos/kaos';
|
||||
|
||||
describe('FileStore', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -13,9 +13,12 @@ describe('FileStore', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IKaosFactory, { _serviceBrand: undefined });
|
||||
ix.set(IFileStore, new SyncDescriptor(FileStore));
|
||||
ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IKaosFactory, {});
|
||||
reg.define(IFileStore, FileStore);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IConfigRegistry, IConfigService } from '#/config';
|
||||
import { ConfigRegistry, ConfigService } from '#/config/configService';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { stubEnvironment } from '../environment/stubs';
|
||||
import { IFlagService } from '#/flag';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IConfigRegistry, IConfigService } from '#/config/config';
|
||||
import { ConfigService } from '#/config/configService';
|
||||
import { IFlagService } from '#/flag/flag';
|
||||
import {
|
||||
EXPERIMENTAL_SECTION,
|
||||
FlagService,
|
||||
MASTER_ENV,
|
||||
} from '#/flag/flagService';
|
||||
import { FlagRegistry } from '#/flag/registry';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerEnvironmentServices } from '../environment/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('FlagRegistry', () => {
|
||||
it('lists registered definitions and resolves by id', () => {
|
||||
|
|
@ -37,12 +36,17 @@ describe('FlagService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IEnvironmentService, stubEnvironment());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IConfigRegistry, new SyncDescriptor(ConfigRegistry));
|
||||
ix.set(IConfigService, new SyncDescriptor(ConfigService));
|
||||
ix.set(IFlagService, new SyncDescriptor(FlagService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerEnvironmentServices,
|
||||
registerLogServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IConfigService, ConfigService);
|
||||
reg.define(IFlagService, FlagService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,14 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
|
||||
import { LocalKaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IFsService } from '#/fs';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IFsService } from '#/fs/fs';
|
||||
import { FsService } from '#/fs/fsService';
|
||||
import { ISessionKaosService } from '#/kaos';
|
||||
import { ISessionKaosService } from '#/kaos/kaos';
|
||||
import { SessionKaosService } from '#/kaos/sessionKaosService';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('FsService', () => {
|
||||
let dir: string;
|
||||
|
|
@ -26,10 +25,13 @@ describe('FsService', () => {
|
|||
dir = await mkdtemp(join(tmpdir(), 'fs-test-'));
|
||||
const base = await LocalKaos.create();
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(ISessionKaosService, new SyncDescriptor(SessionKaosService));
|
||||
ix.set(IFsService, new SyncDescriptor(FsService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionKaosService, SessionKaosService);
|
||||
reg.define(IFsService, FsService);
|
||||
},
|
||||
});
|
||||
const sessionKaos = ix.get(ISessionKaosService);
|
||||
sessionKaos.setToolKaos(base.withCwd(dir));
|
||||
fs = ix.get(IFsService);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { IRestGateway, IScopeRegistry } from '#/gateway';
|
||||
import { RestGateway, ScopeRegistry } from '#/gateway/gatewayService';
|
||||
|
|
@ -16,8 +16,11 @@ describe('ScopeRegistry', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.set(IScopeRegistry, new SyncDescriptor(ScopeRegistry));
|
||||
ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IScopeRegistry, ScopeRegistry);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -34,7 +37,11 @@ describe('ScopeRegistry', () => {
|
|||
describe('RestGateway', () => {
|
||||
it('routes prompt to the agent turn service', async () => {
|
||||
const disposables = new DisposableStore();
|
||||
const ix = disposables.add(new TestInstantiationService());
|
||||
const ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IRestGateway, RestGateway);
|
||||
},
|
||||
});
|
||||
|
||||
const turn = stubTurn();
|
||||
const agentHandle: IScopeHandle = {
|
||||
|
|
@ -61,7 +68,6 @@ describe('RestGateway', () => {
|
|||
get: (id) => (id === 's1' ? sessionHandle : undefined),
|
||||
close: () => Promise.resolve(),
|
||||
});
|
||||
ix.set(IRestGateway, new SyncDescriptor(RestGateway));
|
||||
|
||||
const gw = ix.get(IRestGateway);
|
||||
await gw.prompt('s1', 'main', 'hello');
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IGoalService } from '#/goal';
|
||||
import { IInjectionService } from '#/injection';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { ITurnService } from '#/turn';
|
||||
import { stubTurn } from '../turn/stubs';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IGoalService } from '#/goal/goal';
|
||||
|
||||
import { GoalService } from '#/goal/goalService';
|
||||
import { registerInjectionServices } from '../injection/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerTurnServices } from '../turn/stubs';
|
||||
|
||||
describe('GoalService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -17,11 +16,12 @@ describe('GoalService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentRecords, {});
|
||||
ix.stub(ITurnService, stubTurn());
|
||||
ix.stub(IInjectionService, {});
|
||||
ix.set(IGoalService, new SyncDescriptor(GoalService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices, registerTurnServices, registerInjectionServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IGoalService, GoalService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { IConfigService } from '#/config';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IHookEngine } from '#/hooks';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IHookEngine } from '#/hooks/hooks';
|
||||
import { HookEngine } from '#/hooks/hookEngine';
|
||||
import { ILogService } from '#/log';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('HookEngine', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -14,10 +14,12 @@ describe('HookEngine', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigService, { _serviceBrand: undefined });
|
||||
ix.stub(ILogService, { _serviceBrand: undefined });
|
||||
ix.set(IHookEngine, new SyncDescriptor(HookEngine));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerConfigServices, registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IHookEngine, HookEngine);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { IContextService } from '#/context';
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IInjectionQueue, IInjectionService } from '#/injection';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IInjectionQueue, IInjectionService } from '#/injection/injection';
|
||||
import { InjectionQueue, InjectionService } from '#/injection/injectionService';
|
||||
import { registerContextServices } from '../context/stubs';
|
||||
|
||||
describe('InjectionService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -13,9 +13,12 @@ describe('InjectionService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IContextService, { _serviceBrand: undefined });
|
||||
ix.set(IInjectionService, new SyncDescriptor(InjectionService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerContextServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IInjectionService, InjectionService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -37,8 +40,11 @@ describe('InjectionQueue', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.set(IInjectionQueue, new SyncDescriptor(InjectionQueue));
|
||||
ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IInjectionQueue, InjectionQueue);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
18
packages/agent-core-v2/test/injection/stubs.ts
Normal file
18
packages/agent-core-v2/test/injection/stubs.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* `injection` test stubs — shared `IInjectionService` placeholder.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or
|
||||
* `../injection/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IInjectionService } from '#/injection/injection';
|
||||
|
||||
/**
|
||||
* Register an empty `IInjectionService` placeholder. Tests exercising the real
|
||||
* `InjectionService` should override it via `additionalServices`.
|
||||
*/
|
||||
export function registerInjectionServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(IInjectionService, {});
|
||||
}
|
||||
|
|
@ -2,17 +2,16 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
|
||||
import { LocalKaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
|
||||
import { IKaosService, IKaosFactory, ISessionKaosService } from '#/kaos';
|
||||
import { IKaosService, IKaosFactory, ISessionKaosService } from '#/kaos/kaos';
|
||||
import { AgentKaos } from '#/kaos/agentKaos';
|
||||
import { KaosFactory } from '#/kaos/kaosFactory';
|
||||
import { SessionKaosService } from '#/kaos/sessionKaosService';
|
||||
import { registerEnvironmentServices } from '../environment/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('KaosFactory', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -20,10 +19,12 @@ describe('KaosFactory', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IEnvironmentService, {});
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IKaosFactory, new SyncDescriptor(KaosFactory));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerEnvironmentServices, registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IKaosFactory, KaosFactory);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -53,9 +54,12 @@ describe('SessionKaosService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(ISessionKaosService, new SyncDescriptor(SessionKaosService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionKaosService, SessionKaosService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -107,10 +111,13 @@ describe('AgentKaos', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(ISessionKaosService, new SyncDescriptor(SessionKaosService));
|
||||
ix.set(IKaosService, new SyncDescriptor(AgentKaos));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionKaosService, SessionKaosService);
|
||||
reg.define(IKaosService, AgentKaos);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IConfigRegistry, IConfigService } from '#/config';
|
||||
import { IEnvironmentService } from '#/environment';
|
||||
import { stubEnvironment } from '../environment/stubs';
|
||||
import { IModelCatalogService, IProviderManager } from '#/kosong';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IConfigService } from '#/config/config';
|
||||
import { IModelCatalogService, IProviderManager } from '#/kosong/kosong';
|
||||
|
||||
import { ConfigRegistry, ConfigService } from '#/config/configService';
|
||||
import { ConfigService } from '#/config/configService';
|
||||
import { ModelCatalogService, ProviderManager } from '#/kosong/kosongService';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerEnvironmentServices } from '../environment/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('ModelCatalogService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -20,12 +19,17 @@ describe('ModelCatalogService', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigRegistry, new ConfigRegistry());
|
||||
ix.stub(IEnvironmentService, stubEnvironment());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IConfigService, new SyncDescriptor(ConfigService));
|
||||
ix.set(IModelCatalogService, new SyncDescriptor(ModelCatalogService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerEnvironmentServices,
|
||||
registerLogServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IConfigService, ConfigService);
|
||||
reg.define(IModelCatalogService, ModelCatalogService);
|
||||
},
|
||||
});
|
||||
const config = ix.get(IConfigService);
|
||||
await config.set('kosong', {
|
||||
providers: [
|
||||
|
|
@ -63,13 +67,18 @@ describe('ProviderManager', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigRegistry, new ConfigRegistry());
|
||||
ix.stub(IEnvironmentService, stubEnvironment());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IConfigService, new SyncDescriptor(ConfigService));
|
||||
ix.set(IModelCatalogService, new SyncDescriptor(ModelCatalogService));
|
||||
ix.set(IProviderManager, new SyncDescriptor(ProviderManager));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerEnvironmentServices,
|
||||
registerLogServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IConfigService, ConfigService);
|
||||
reg.define(IModelCatalogService, ModelCatalogService);
|
||||
reg.define(IProviderManager, ProviderManager);
|
||||
},
|
||||
});
|
||||
config = ix.get(IConfigService);
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
|
|
|||
14
packages/agent-core-v2/test/kosong/stubs.ts
Normal file
14
packages/agent-core-v2/test/kosong/stubs.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* `kosong` test stubs — shared kosong collaborators for unit tests.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or `../kosong/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { ILLMService } from '#/kosong/kosong';
|
||||
|
||||
/** Register an empty `ILLMService` placeholder. */
|
||||
export function registerKosongServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(ILLMService, {});
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
* production tree. Import from a relative path (`./stubs` or `../log/stubs`).
|
||||
*/
|
||||
|
||||
import type { ILogger, ILogService } from '#/log';
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { ILogService } from '#/log/log';
|
||||
import type { ILogger } from '#/log/log';
|
||||
|
||||
/** A no-op `ILogger`: every method is a no-op, `child()` returns itself. */
|
||||
export function stubLogger(): ILogger {
|
||||
|
|
@ -28,3 +30,8 @@ export function stubLog(): ILogService {
|
|||
setLevel: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Register the default no-op `ILogService`. */
|
||||
export function registerLogServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(ILogService, stubLog());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IOAuthService } from '#/auth';
|
||||
import { IConfigService } from '#/config';
|
||||
import { ILogService } from '#/log';
|
||||
import { IMcpService } from '#/mcp';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IOAuthService } from '#/auth/auth';
|
||||
import { IMcpService } from '#/mcp/mcp';
|
||||
|
||||
import { McpService } from '#/mcp/mcpService';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
|
||||
describe('McpService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -17,12 +17,17 @@ describe('McpService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigService, {});
|
||||
ix.stub(ILogService, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.stub(IOAuthService, {});
|
||||
ix.set(IMcpService, new SyncDescriptor(McpService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerLogServices,
|
||||
registerTelemetryServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IOAuthService, {});
|
||||
reg.define(IMcpService, McpService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IContextService } from '#/context';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IContextService } from '#/context/context';
|
||||
import { ContextService } from '#/context/contextService';
|
||||
import { IMessageService } from '#/message';
|
||||
import { IMessageService } from '#/message/message';
|
||||
import { MessageService } from '#/message/messageService';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { stubAgentRecords } from '../records/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('MessageService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -16,12 +15,15 @@ describe('MessageService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
// Dependencies: real ContextService (itself backed by a stubbed IAgentRecords).
|
||||
ix.stub(IAgentRecords, stubAgentRecords());
|
||||
ix.set(IContextService, new SyncDescriptor(ContextService));
|
||||
// System under test, registered by interface so the binding is exercised.
|
||||
ix.set(IMessageService, new SyncDescriptor(MessageService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
// Dependencies: real ContextService (itself backed by a stubbed IAgentRecords).
|
||||
reg.define(IContextService, ContextService);
|
||||
// System under test, registered by interface so the binding is exercised.
|
||||
reg.define(IMessageService, MessageService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IApprovalService } from '#/approval';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IApprovalService } from '#/approval/approval';
|
||||
import { ApprovalService } from '#/approval/approvalService';
|
||||
import { IAgentConfigService } from '#/config';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import {
|
||||
IPermissionPolicyRegistry,
|
||||
IPermissionService,
|
||||
} from '#/permission';
|
||||
} from '#/permission/permission';
|
||||
import {
|
||||
PermissionPolicyRegistry,
|
||||
PermissionService,
|
||||
} from '#/permission/permissionService';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { stubAgentRecords } from '../records/stubs';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('PermissionPolicyRegistry', () => {
|
||||
it('returns the first non-undefined decision', () => {
|
||||
|
|
@ -40,13 +38,14 @@ describe('PermissionService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentConfigService, {});
|
||||
ix.stub(IAgentRecords, stubAgentRecords());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IPermissionPolicyRegistry, new SyncDescriptor(PermissionPolicyRegistry));
|
||||
ix.set(IApprovalService, new SyncDescriptor(ApprovalService));
|
||||
ix.set(IPermissionService, new SyncDescriptor(PermissionService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerConfigServices, registerRecordsServices, registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IPermissionPolicyRegistry, PermissionPolicyRegistry);
|
||||
reg.define(IApprovalService, ApprovalService);
|
||||
reg.define(IPermissionService, PermissionService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
18
packages/agent-core-v2/test/permission/stubs.ts
Normal file
18
packages/agent-core-v2/test/permission/stubs.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* `permission` test stubs — shared `IPermissionService` placeholder.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or
|
||||
* `../permission/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IPermissionService } from '#/permission/permission';
|
||||
|
||||
/**
|
||||
* Register an empty `IPermissionService` placeholder. Tests exercising the real
|
||||
* `PermissionService` should override it via `additionalServices`.
|
||||
*/
|
||||
export function registerPermissionServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(IPermissionService, {});
|
||||
}
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentConfigService } from '#/config';
|
||||
import { IContextService } from '#/context';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IContextService } from '#/context/context';
|
||||
import { ContextService } from '#/context/contextService';
|
||||
import { IInjectionService } from '#/injection';
|
||||
import { IInjectionService } from '#/injection/injection';
|
||||
import { InjectionService } from '#/injection/injectionService';
|
||||
import { IKaosService } from '#/kaos';
|
||||
import { IPlanService } from '#/plan';
|
||||
import { IAgentKaos } from '#/kaos/kaos';
|
||||
import { IPlanService } from '#/plan/plan';
|
||||
import { PlanService } from '#/plan/planService';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { stubAgentRecords } from '../records/stubs';
|
||||
import { ITurnService } from '#/turn';
|
||||
import { stubTurn } from '../turn/stubs';
|
||||
import { ITurnService } from '#/turn/turn';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerTurnServices } from '../turn/stubs';
|
||||
|
||||
describe('PlanService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -22,14 +21,15 @@ describe('PlanService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentRecords, stubAgentRecords());
|
||||
ix.stub(IKaosService, {});
|
||||
ix.stub(IAgentConfigService, {});
|
||||
ix.stub(ITurnService, stubTurn());
|
||||
ix.set(IContextService, new SyncDescriptor(ContextService));
|
||||
ix.set(IInjectionService, new SyncDescriptor(InjectionService));
|
||||
ix.set(IPlanService, new SyncDescriptor(PlanService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices, registerConfigServices, registerTurnServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IAgentKaos, {});
|
||||
reg.define(IContextService, ContextService);
|
||||
reg.define(IInjectionService, InjectionService);
|
||||
reg.define(IPlanService, PlanService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IQuestionService } from '#/question';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IQuestionService } from '#/question/question';
|
||||
import { QuestionService } from '#/question/questionService';
|
||||
|
||||
describe('QuestionService', () => {
|
||||
|
|
@ -12,8 +12,11 @@ describe('QuestionService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.set(IQuestionService, new SyncDescriptor(QuestionService));
|
||||
ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IQuestionService, QuestionService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -6,23 +6,22 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
|
||||
import { LocalKaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { AgentKaos } from '#/kaos/agentKaos';
|
||||
import { IKaosService, ISessionKaosService } from '#/kaos';
|
||||
import { IKaosService, ISessionKaosService } from '#/kaos/kaos';
|
||||
import { SessionKaosService } from '#/kaos/sessionKaosService';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import {
|
||||
IAgentRecords,
|
||||
ISessionMetaStore,
|
||||
} from '#/records';
|
||||
} from '#/records/records';
|
||||
import {
|
||||
AgentRecords,
|
||||
SessionMetaStore,
|
||||
encodeWorkDirKey,
|
||||
} from '#/records/recordsService';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('encodeWorkDirKey', () => {
|
||||
it('is deterministic and path-sensitive', () => {
|
||||
|
|
@ -44,10 +43,13 @@ describe('SessionMetaStore', () => {
|
|||
dir = await mkdtemp(join(tmpdir(), 'records-test-'));
|
||||
const base = await LocalKaos.create();
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(ISessionKaosService, new SyncDescriptor(SessionKaosService));
|
||||
ix.set(ISessionMetaStore, new SyncDescriptor(SessionMetaStore));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionKaosService, SessionKaosService);
|
||||
reg.define(ISessionMetaStore, SessionMetaStore);
|
||||
},
|
||||
});
|
||||
const sessionKaos = ix.get(ISessionKaosService);
|
||||
sessionKaos.setToolKaos(base.withCwd(dir));
|
||||
});
|
||||
|
|
@ -84,11 +86,14 @@ describe('AgentRecords', () => {
|
|||
dir = await mkdtemp(join(tmpdir(), 'records-test-'));
|
||||
const base = await LocalKaos.create();
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(ISessionKaosService, new SyncDescriptor(SessionKaosService));
|
||||
ix.set(IKaosService, new SyncDescriptor(AgentKaos));
|
||||
ix.set(IAgentRecords, new SyncDescriptor(AgentRecords));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionKaosService, SessionKaosService);
|
||||
reg.define(IKaosService, AgentKaos);
|
||||
reg.define(IAgentRecords, AgentRecords);
|
||||
},
|
||||
});
|
||||
const sessionKaos = ix.get(ISessionKaosService);
|
||||
sessionKaos.setToolKaos(base.withCwd(dir));
|
||||
records = ix.get(IAgentRecords);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
* production tree. Import from a relative path (`./stubs` or `../records/stubs`).
|
||||
*/
|
||||
|
||||
import type { IAgentRecords } from '#/records';
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { IAgentRecords, ISessionMetaStore } from '#/records/records';
|
||||
|
||||
/**
|
||||
* A no-op `IAgentRecords`: writes vanish, replay yields nothing, restore is a
|
||||
|
|
@ -23,3 +24,12 @@ export function stubAgentRecords(): IAgentRecords {
|
|||
restore: () => Promise.resolve(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default records collaborators: a no-op `IAgentRecords` and an
|
||||
* empty `ISessionMetaStore` placeholder.
|
||||
*/
|
||||
export function registerRecordsServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(IAgentRecords, stubAgentRecords());
|
||||
reg.definePartialInstance(ISessionMetaStore, {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { ISessionActivity } from '#/session-activity/sessionActivity';
|
||||
import { SessionActivity } from '#/session-activity/sessionActivityService';
|
||||
|
|
@ -33,8 +33,11 @@ describe('SessionActivity', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.set(ISessionActivity, new SyncDescriptor(SessionActivity));
|
||||
ix = createServices(disposables, {
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionActivity, SessionActivity);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
15
packages/agent-core-v2/test/session-context/stubs.ts
Normal file
15
packages/agent-core-v2/test/session-context/stubs.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* `session-context` test stubs — shared `ISessionContext` placeholder.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or
|
||||
* `../session-context/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { ISessionContext } from '#/session-context/sessionContext';
|
||||
|
||||
/** Register an empty `ISessionContext` placeholder. */
|
||||
export function registerSessionContextServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(ISessionContext, {});
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { IEventService } from '#/event';
|
||||
import { ISessionMetaStore } from '#/records';
|
||||
import { IEventService } from '#/event/event';
|
||||
import { ISessionActivity } from '#/session-activity/sessionActivity';
|
||||
import { ISessionService } from '#/session';
|
||||
import { ISessionService } from '#/session/session';
|
||||
import { SessionService } from '#/session/sessionService';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
const handle: IScopeHandle = {
|
||||
id: 'main',
|
||||
|
|
@ -24,10 +24,13 @@ describe('SessionService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ISessionMetaStore, {});
|
||||
ix.stub(IEventService, {});
|
||||
ix.set(ISessionService, new SyncDescriptor(SessionService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IEventService, {});
|
||||
reg.define(ISessionService, SessionService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IConfigService } from '#/config';
|
||||
import { ILogService } from '#/log';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { ISkillRegistry, ISkillService } from '#/skill';
|
||||
import { ITurnService } from '#/turn';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { ISkillRegistry, ISkillService } from '#/skill/skill';
|
||||
import { ITurnService } from '#/turn/turn';
|
||||
import { stubTurn } from '../turn/stubs';
|
||||
|
||||
import { SkillRegistry, SkillService } from '#/skill/skillService';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('SkillRegistry', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -18,10 +18,12 @@ describe('SkillRegistry', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigService, {});
|
||||
ix.stub(ILogService, {});
|
||||
ix.set(ISkillRegistry, new SyncDescriptor(SkillRegistry));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerConfigServices, registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISkillRegistry, SkillRegistry);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -40,12 +42,17 @@ describe('SkillService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IConfigService, {});
|
||||
ix.stub(ILogService, {});
|
||||
ix.stub(IAgentRecords, {});
|
||||
ix.set(ISkillRegistry, new SyncDescriptor(SkillRegistry));
|
||||
ix.set(ISkillService, new SyncDescriptor(SkillService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerLogServices,
|
||||
registerRecordsServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISkillRegistry, SkillRegistry);
|
||||
reg.define(ISkillService, SkillService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { IPermissionService } from '#/permission';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { ISwarmService } from '#/swarm';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { ISwarmService } from '#/swarm/swarm';
|
||||
import { SwarmService } from '#/swarm/swarmService';
|
||||
import { registerAgentLifecycleServices } from '../agent-lifecycle/stubs';
|
||||
import { registerPermissionServices } from '../permission/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
describe('SwarmService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -15,11 +15,16 @@ describe('SwarmService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentRecords, {});
|
||||
ix.stub(IAgentLifecycleService, {});
|
||||
ix.stub(IPermissionService, {});
|
||||
ix.set(ISwarmService, new SyncDescriptor(SwarmService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerRecordsServices,
|
||||
registerAgentLifecycleServices,
|
||||
registerPermissionServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISwarmService, SwarmService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
18
packages/agent-core-v2/test/telemetry/stubs.ts
Normal file
18
packages/agent-core-v2/test/telemetry/stubs.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* `telemetry` test stubs — shared `ITelemetryService` placeholder for unit tests.
|
||||
*
|
||||
* Lives under `test/` (not `src/`) so test-support code stays out of the
|
||||
* production tree. Import from a relative path (`./stubs` or
|
||||
* `../telemetry/stubs`).
|
||||
*/
|
||||
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { ITelemetryService } from '#/telemetry/telemetry';
|
||||
|
||||
/**
|
||||
* Register an empty `ITelemetryService` placeholder. Tests that assert on
|
||||
* telemetry should register a spy via `additionalServices` instead.
|
||||
*/
|
||||
export function registerTelemetryServices(reg: ServiceRegistration): void {
|
||||
reg.definePartialInstance(ITelemetryService, {});
|
||||
}
|
||||
|
|
@ -6,15 +6,14 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
|
||||
import { LocalKaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { ISessionKaosService } from '#/kaos';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { ISessionKaosService } from '#/kaos/kaos';
|
||||
import { SessionKaosService } from '#/kaos/sessionKaosService';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { ITerminalService } from '#/terminal';
|
||||
import { ITerminalService } from '#/terminal/terminal';
|
||||
import { TerminalService } from '#/terminal/terminalService';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('TerminalService', () => {
|
||||
let dir: string;
|
||||
|
|
@ -26,10 +25,13 @@ describe('TerminalService', () => {
|
|||
dir = await mkdtemp(join(tmpdir(), 'term-test-'));
|
||||
const base = await LocalKaos.create();
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(ISessionKaosService, new SyncDescriptor(SessionKaosService));
|
||||
ix.set(ITerminalService, new SyncDescriptor(TerminalService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(ISessionKaosService, SessionKaosService);
|
||||
reg.define(ITerminalService, TerminalService);
|
||||
},
|
||||
});
|
||||
const sessionKaos = ix.get(ISessionKaosService);
|
||||
sessionKaos.setToolKaos(base.withCwd(dir));
|
||||
terminal = ix.get(ITerminalService);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentConfigService } from '#/config';
|
||||
import { IKaosService } from '#/kaos';
|
||||
import { ILLMService } from '#/kosong';
|
||||
import { IPermissionService } from '#/permission';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IKaosService } from '#/kaos/kaos';
|
||||
import {
|
||||
IToolDefinitionRegistry,
|
||||
IToolService,
|
||||
type ToolCallResult,
|
||||
type ToolDefinition,
|
||||
} from '#/tool';
|
||||
} from '#/tool/tool';
|
||||
import { ToolDefinitionRegistry, ToolService } from '#/tool/toolService';
|
||||
import { registerConfigServices } from '../config/stubs';
|
||||
import { registerKosongServices } from '../kosong/stubs';
|
||||
import { registerPermissionServices } from '../permission/stubs';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
|
||||
const echoDef: ToolDefinition = {
|
||||
name: 'echo',
|
||||
|
|
@ -41,16 +41,21 @@ describe('ToolService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
reg = new ToolDefinitionRegistry();
|
||||
reg.register(echoDef);
|
||||
ix.set(IToolDefinitionRegistry, reg);
|
||||
ix.stub(IAgentConfigService, {});
|
||||
ix.stub(IAgentRecords, {});
|
||||
ix.stub(IKaosService, {});
|
||||
ix.stub(IPermissionService, {});
|
||||
ix.stub(ILLMService, {});
|
||||
ix.set(IToolService, new SyncDescriptor(ToolService));
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerConfigServices,
|
||||
registerRecordsServices,
|
||||
registerPermissionServices,
|
||||
registerKosongServices,
|
||||
],
|
||||
additionalServices: (r) => {
|
||||
r.defineInstance(IToolDefinitionRegistry, reg);
|
||||
r.definePartialInstance(IKaosService, {});
|
||||
r.define(IToolService, ToolService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import { Emitter, type Event } from '#/_base/event';
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { ITurnService } from '#/turn/turn';
|
||||
import type {
|
||||
ITurnService,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
TurnStepEvent,
|
||||
TurnToolEvent,
|
||||
} from '#/turn';
|
||||
TurnWillExecuteToolEvent,
|
||||
} from '#/turn/turn';
|
||||
|
||||
const noneEvent = (<T>(): Event<T> => () => ({ dispose: () => {} }))();
|
||||
|
||||
|
|
@ -43,7 +45,7 @@ export function stubTurn(options: StubTurnOptions = {}): StubTurn {
|
|||
return {
|
||||
_serviceBrand: undefined,
|
||||
onWillStartTurn: noneEvent as Event<TurnStartEvent>,
|
||||
onWillExecuteTool: noneEvent as Event<TurnToolEvent>,
|
||||
onWillExecuteTool: noneEvent as Event<TurnWillExecuteToolEvent>,
|
||||
onDidFinalizeTool: noneEvent as Event<TurnToolEvent>,
|
||||
onDidEndStep: endStep.event,
|
||||
onDidEndTurn: endTurn.event,
|
||||
|
|
@ -70,3 +72,12 @@ export function stubTurn(options: StubTurnOptions = {}): StubTurn {
|
|||
steered,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default `ITurnService` stub. Tests that need to inspect
|
||||
* `prompts` / `steered` should create their own `stubTurn()` and register it
|
||||
* via `additionalServices` instead.
|
||||
*/
|
||||
export function registerTurnServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(ITurnService, stubTurn());
|
||||
}
|
||||
|
|
|
|||
134
packages/agent-core-v2/test/turn/toolCallExecutor.test.ts
Normal file
134
packages/agent-core-v2/test/turn/toolCallExecutor.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { type Decision, IPermissionService, type PermissionContext } from '#/permission/permission';
|
||||
import { type ToolCallResult, type ToolDefinition, IToolService } from '#/tool/tool';
|
||||
import { ITurnContext, ITurnEvents } from '#/turn/turn';
|
||||
|
||||
import { TurnEvents } from '#/turn/turnEvents';
|
||||
import { ToolCallExecutor } from '#/turn/toolCallExecutor';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('ToolCallExecutor', () => {
|
||||
let disposables: DisposableStore;
|
||||
let ix: TestInstantiationService;
|
||||
let turnEvents: TurnEvents;
|
||||
let executed: string[];
|
||||
let decision: Decision;
|
||||
let permission: IPermissionService;
|
||||
|
||||
function toolService(): IToolService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
execute: (name: string): Promise<ToolCallResult> => {
|
||||
executed.push(name);
|
||||
return Promise.resolve({ output: `ran:${name}` });
|
||||
},
|
||||
list: (): readonly ToolDefinition[] => [],
|
||||
registerUserTool: () => {},
|
||||
registerMcpTools: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function permissionService(): IPermissionService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
beforeToolCall: (_ctx: PermissionContext): Promise<Decision> => Promise.resolve(decision),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
executed = [];
|
||||
decision = 'allow';
|
||||
permission = permissionService();
|
||||
turnEvents = new TurnEvents();
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.defineInstance(ITurnEvents, turnEvents);
|
||||
reg.defineInstance(IToolService, toolService());
|
||||
reg.definePartialInstance(ITurnContext, { turnId: 'turn-0' });
|
||||
},
|
||||
});
|
||||
|
||||
turnEvents.onWillExecuteTool((event) => {
|
||||
event.veto(
|
||||
Promise.resolve(
|
||||
permission.beforeToolCall({ toolName: event.toolName, args: event.args }),
|
||||
).then((value) => value === 'deny'),
|
||||
'permission',
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
it('runs the tool and fires onDidFinalizeTool when permission allows', async () => {
|
||||
const executor = ix.createInstance(ToolCallExecutor);
|
||||
const finalized: string[] = [];
|
||||
turnEvents.onDidFinalizeTool((e) => finalized.push(e.toolName));
|
||||
|
||||
const outcome = await executor.execute('call-1', 'echo', { text: 'hi' });
|
||||
|
||||
expect(outcome).toEqual({ vetoed: false, result: { output: 'ran:echo' } });
|
||||
expect(executed).toEqual(['echo']);
|
||||
expect(finalized).toEqual(['echo']);
|
||||
});
|
||||
|
||||
it('vetoes the tool call when permission denies, skipping execution', async () => {
|
||||
decision = 'deny';
|
||||
permission = permissionService();
|
||||
const executor = ix.createInstance(ToolCallExecutor);
|
||||
const finalized: string[] = [];
|
||||
turnEvents.onDidFinalizeTool((e) => finalized.push(e.toolName));
|
||||
|
||||
const outcome = await executor.execute('call-1', 'rm', {});
|
||||
|
||||
expect(outcome.vetoed).toBe(true);
|
||||
if (outcome.vetoed) {
|
||||
expect(outcome.reason).toBe('permission');
|
||||
}
|
||||
expect(executed).toEqual([]);
|
||||
expect(finalized).toEqual([]);
|
||||
});
|
||||
|
||||
it('awaits an asynchronous permission decision before vetoing', async () => {
|
||||
permission = {
|
||||
_serviceBrand: undefined,
|
||||
beforeToolCall: (): Promise<Decision> =>
|
||||
new Promise((resolve) => setTimeout(() => resolve('deny'), 10)),
|
||||
};
|
||||
const executor = ix.createInstance(ToolCallExecutor);
|
||||
|
||||
const outcome = await executor.execute('call-1', 'rm', {});
|
||||
|
||||
expect(outcome.vetoed).toBe(true);
|
||||
expect(executed).toEqual([]);
|
||||
});
|
||||
|
||||
it('lets an external onWillExecuteTool listener veto the call', async () => {
|
||||
const executor = ix.createInstance(ToolCallExecutor);
|
||||
turnEvents.onWillExecuteTool((e) => {
|
||||
e.veto(true, 'manual block');
|
||||
});
|
||||
|
||||
const outcome = await executor.execute('call-1', 'echo', {});
|
||||
|
||||
expect(outcome).toEqual({ vetoed: true, reason: 'manual block' });
|
||||
expect(executed).toEqual([]);
|
||||
});
|
||||
|
||||
it('delivers tool name and args to onWillExecuteTool listeners', async () => {
|
||||
const executor = ix.createInstance(ToolCallExecutor);
|
||||
const seen: { toolName: string; args: unknown }[] = [];
|
||||
turnEvents.onWillExecuteTool((e) => {
|
||||
seen.push({ toolName: e.toolName, args: e.args });
|
||||
});
|
||||
|
||||
await executor.execute('call-1', 'echo', { text: 'hi' });
|
||||
|
||||
expect(seen).toEqual([{ toolName: 'echo', args: { text: 'hi' } }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/agent-lifecycle/agentLifecycle';
|
||||
import { IContextService } from '#/context';
|
||||
import { IInjectionService } from '#/injection';
|
||||
import { ILLMService } from '#/kosong';
|
||||
import { ILogService } from '#/log';
|
||||
import { IPermissionService } from '#/permission';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { IToolService } from '#/tool';
|
||||
import { ILoopRunner } from '#/turn';
|
||||
import { IUsageService } from '#/usage';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { ILoopRunner, ITurnEvents } from '#/turn/turn';
|
||||
import { IUsageService } from '#/usage/usage';
|
||||
|
||||
import { LoopRunner } from '#/turn/loopRunner';
|
||||
import { TurnEvents } from '#/turn/turnEvents';
|
||||
import { TurnService } from '#/turn/turnService';
|
||||
import { registerAgentLifecycleServices } from '../agent-lifecycle/stubs';
|
||||
import { registerContextServices } from '../context/stubs';
|
||||
import { registerInjectionServices } from '../injection/stubs';
|
||||
import { registerKosongServices } from '../kosong/stubs';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
import { registerPermissionServices } from '../permission/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
|
||||
describe('TurnService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -22,17 +23,22 @@ describe('TurnService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IContextService, {});
|
||||
ix.stub(IToolService, {});
|
||||
ix.stub(IPermissionService, {});
|
||||
ix.stub(ILLMService, {});
|
||||
ix.stub(IInjectionService, {});
|
||||
ix.stub(IUsageService, {});
|
||||
ix.stub(ITelemetryService, {});
|
||||
ix.stub(ILogService, {});
|
||||
ix.stub(IAgentLifecycleService, {});
|
||||
ix.set(ILoopRunner, new LoopRunner());
|
||||
ix = createServices(disposables, {
|
||||
base: [
|
||||
registerLogServices,
|
||||
registerTelemetryServices,
|
||||
registerAgentLifecycleServices,
|
||||
registerPermissionServices,
|
||||
registerContextServices,
|
||||
registerInjectionServices,
|
||||
registerKosongServices,
|
||||
],
|
||||
additionalServices: (reg) => {
|
||||
reg.defineInstance(ITurnEvents, new TurnEvents());
|
||||
reg.definePartialInstance(IUsageService, {});
|
||||
reg.defineInstance(ILoopRunner, new LoopRunner());
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentRecords } from '#/records';
|
||||
import { ITelemetryService } from '#/telemetry';
|
||||
import { IUsageService } from '#/usage';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IUsageService } from '#/usage/usage';
|
||||
import { UsageService } from '#/usage/usageService';
|
||||
import { registerRecordsServices } from '../records/stubs';
|
||||
import { registerTelemetryServices } from '../telemetry/stubs';
|
||||
|
||||
describe('UsageService', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -14,10 +14,12 @@ describe('UsageService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentRecords, { _serviceBrand: undefined });
|
||||
ix.stub(ITelemetryService, { _serviceBrand: undefined });
|
||||
ix.set(IUsageService, new SyncDescriptor(UsageService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerTelemetryServices, registerRecordsServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IUsageService, UsageService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IKaosFactory } from '#/kaos';
|
||||
import { ILogService } from '#/log';
|
||||
import { stubLog } from '../log/stubs';
|
||||
import { IWorkspaceFsService, IWorkspaceRegistry } from '#/workspace';
|
||||
import { createServices } from '#/_base/di/test';
|
||||
import type { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IKaosFactory } from '#/kaos/kaos';
|
||||
import { IWorkspaceFsService, IWorkspaceRegistry } from '#/workspace/workspace';
|
||||
import { WorkspaceFsService, WorkspaceRegistry } from '#/workspace/workspaceService';
|
||||
import { registerLogServices } from '../log/stubs';
|
||||
|
||||
describe('WorkspaceRegistry', () => {
|
||||
let disposables: DisposableStore;
|
||||
|
|
@ -15,10 +14,13 @@ describe('WorkspaceRegistry', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IKaosFactory, {});
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IWorkspaceRegistry, new SyncDescriptor(WorkspaceRegistry));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IKaosFactory, {});
|
||||
reg.define(IWorkspaceRegistry, WorkspaceRegistry);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
@ -37,11 +39,14 @@ describe('WorkspaceFsService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IKaosFactory, {});
|
||||
ix.stub(ILogService, stubLog());
|
||||
ix.set(IWorkspaceRegistry, new SyncDescriptor(WorkspaceRegistry));
|
||||
ix.set(IWorkspaceFsService, new SyncDescriptor(WorkspaceFsService));
|
||||
ix = createServices(disposables, {
|
||||
base: [registerLogServices],
|
||||
additionalServices: (reg) => {
|
||||
reg.definePartialInstance(IKaosFactory, {});
|
||||
reg.define(IWorkspaceRegistry, WorkspaceRegistry);
|
||||
reg.define(IWorkspaceFsService, WorkspaceFsService);
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => disposables.dispose());
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue