606 Commits

Author SHA1 Message Date
e9eaaa24db fix(BuiltinCommand): spill session streams to log files 2026-04-20 14:37:40 +08:00
2ec2d8e096 fix(CommandExecutionService): preserve stdout and stderr outputs 2026-04-20 14:32:25 +08:00
eea72c747c fix(DirectoryWatchSupport): isolate handler failures 2026-04-19 17:31:36 +08:00
137b1ee917 fix(CommandExecutionService): avoid persistent reader executor 2026-04-19 17:30:16 +08:00
c5aa558319 fix(McpActionExecutor): handle client call failures gracefully 2026-04-19 17:27:27 +08:00
8c8b0883bb fix(BuiltinCommand): expire finished command sessions by ttl 2026-04-19 17:26:20 +08:00
dd8e20838d fix(BuiltinCommand): apply execution policy to command tools 2026-04-19 17:23:04 +08:00
15d7eb6850 fix(DynamicActionMcp): apply execution policy to tool handler 2026-04-19 17:21:37 +08:00
657023694c fix(OriginExecution): apply execution policy to process launch 2026-04-19 17:19:39 +08:00
9b97fffc5c fix(LocalRunnerClient): unregister policy listener on close 2026-04-19 17:13:40 +08:00
14df95fc59 fix(McpConfigWatcher): clean stale actions on client removal 2026-04-19 17:13:04 +08:00
864bda95e4 refactor(action): register no blocks while no actions recovered 2026-04-19 00:51:22 +08:00
bfa3562335 refactor(exception): optimize exception report behavior 2026-04-19 00:47:31 +08:00
9b24b662da chore: add slf4j log annotation on AgentModule implementations 2026-04-19 00:36:15 +08:00
41c611cb70 refactor(framework): support inject Standalone into Standalone modules 2026-04-19 00:34:45 +08:00
6ca77bdb74 feat(perceive): add SystemStatsMonitor 2026-04-18 23:51:23 +08:00
e5ce6d6722 refactor(memory): rename MemoryUpdater into MemoryRecallProfileExtractor, and create prompt 2026-04-18 23:50:48 +08:00
a7ef9bff49 refactor(memory): enhance topic-based memory runtime on recalling and indexing 2026-04-18 22:28:40 +08:00
92c8e01000 feat(memory): create prompts for submodules of memory selector 2026-04-18 19:19:05 +08:00
e0543a8966 refactor(communication): create prompts for summarizer, and optimize message structure 2026-04-17 23:16:20 +08:00
0c079c127e chore: remove useless annotations on module 2026-04-17 18:09:40 +08:00
96301dc64a refactor(action): use synchronized lock to prevent concurrent problems in executable actions executing 2026-04-17 18:05:04 +08:00
3fd90c0f5b refactor(context): sort context blocks by descending 2026-04-17 16:01:50 +08:00
8928ec9e07 feat(action): create prompts for submodules of ActionExecutor 2026-04-17 15:01:45 +08:00
503afecbe2 refactor(action): redefine ExtractorResult structure to support openai sdk better, and make failed meta-action executing should be recorded as history normally 2026-04-16 23:27:46 +08:00
281984bb05 feat(action): redefine structure of evaluator result to support json schema of openai sdk, and create system prompt for sub-modules of ActionPlanner 2026-04-16 21:38:11 +08:00
062af4b7d2 feat(communication): update prompt 2026-04-16 21:05:39 +08:00
380c674d06 feat(communication): enrich and correct system-prompt in CommunicationProducer, and support NO_REPLY answering 2026-04-16 17:44:36 +08:00
347560d979 refactor(context): rename VisibleDomain into FocusedDomain, and optimize related context usage 2026-04-15 22:16:33 +08:00
999a6a8d7e refactor(dialog): remove legacy data class 2026-04-15 17:24:22 +08:00
f510dc5a42 refactor(context): support wait a debounce delay before executing turn 2026-04-15 16:36:23 +08:00
d8ff0b5ea4 refactor(context): simplify constructor and inputs encoding of running flow context 2026-04-15 15:45:42 +08:00
dc147000ba refactor(runtime): support collect context by source and interrupt same-source running flow by module order 2026-04-15 14:32:52 +08:00
247057e100 fix(test): fix MemoryRuntimeTest 2026-04-14 15:07:32 +08:00
33ffd782c4 refactor(cognition): remove duplicate recent chat block building logic, refresh which in cognition core uniformly 2026-04-14 12:40:56 +08:00
a847f3bff8 refactor(rolling): separate dialog rolling with memory topic binding,
now support register after-rolling consumer independently
2026-04-14 11:14:37 +08:00
28d0a43ef3 refactor(log): support advice invoke or invoke with no result via different methods to avoid unnecessary nullable check 2026-04-14 10:08:31 +08:00
cb4380eb1e feat(trace): support trace context workspace changes 2026-04-13 22:48:31 +08:00
737f9d122a feat(trace): support trace input/output information on Running/Sub AgentModule 2026-04-13 22:27:11 +08:00
e65d3302c6 feat(trace): support create advice for method with no return value 2026-04-13 21:57:10 +08:00
fece67135f feat(trace): support create loggable advice and implement common trace recorder 2026-04-13 21:39:42 +08:00
d30e58ff83 refactor(framework): centralize model exception reporting in ActivateModel and remove duplicated module-level handlers 2026-04-12 22:15:08 +08:00
93304878ad refactor(memory): migrate slice/topic/date lookup to Result flow and unify MemoryLookupException reporting 2026-04-12 20:13:11 +08:00
e37a282141 fix(test): adapt ActionExecutor tests to Result-based extractor/corrector execution flow 2026-04-12 19:19:07 +08:00
04d6ad6d80 refactor(core): normalize memory unit after adding new slice before return 2026-04-12 19:08:35 +08:00
9755672750 refactor(action-core): wrap runner submit flow with Result.runCatching and centralize action error handling 2026-04-12 18:51:33 +08:00
e0f955694d refactor(framework): wrap module execution in Result and report failures via exception handler 2026-04-12 17:33:11 +08:00
c14e6f84e7 refactor(action-core): migrate action modules to Result return flow and unify exception reporting 2026-04-12 17:29:10 +08:00
19f56d11f0 refactor(framework): add Result chain APIs and align runtime exception handling 2026-04-12 16:58:36 +08:00
94d91d9746 feat(agent): support add custom configurable object in Agent launch flow 2026-04-11 21:07:50 +08:00
fac6e24e49 feat(exception): support report necessary exception info into context 2026-04-11 18:52:53 +08:00
2aae1b1f14 refactor(action-core): return Result for meta action lookups and adapt runtime callers 2026-04-11 18:30:24 +08:00
1b48e955bd refactor(core): migrate deprecated exceptions to new hierarchy and align tests 2026-04-11 16:10:34 +08:00
b8cb2afbcf refactor(framework): unify model invocation result and exception handling 2026-04-11 14:50:12 +08:00
3732555f02 refactor(framework): migrate startup chain exceptions to AgentException hierarchy 2026-04-10 22:39:59 +08:00
ec52d64e73 refactor(exception): register exception reporters on Agent launcher 2026-04-10 19:04:11 +08:00
7b963df991 refactor(exception): establish new exception system, and move the legacy into deprecated package 2026-04-10 18:41:13 +08:00
4876d621b2 将 .java 重命名为 .kt 2026-04-10 18:37:49 +08:00
663d66fdea chore: remove legacy exception handler and adjust runtime class location 2026-04-10 14:31:56 +08:00
d29dad4691 refactor(config): adjust declared config location 2026-04-09 17:28:23 +08:00
291371f8da chore: adjust reflect related util's location 2026-04-09 16:58:24 +08:00
3e5d6ebeb4 feat(runtime): support mask assigned modules; and remove legacy module record in agent context 2026-04-09 16:13:04 +08:00
56daaf0b60 refactor(agent): normalize Agent launch flow 2026-04-09 12:26:52 +08:00
d6593c10f9 refactor(agent): support register registry shutdown hooks while agent launching 2026-04-09 12:19:20 +08:00
328befecca refactor(action): add shutdown method to ActionCore to close runner client and thread pool 2026-04-09 12:00:32 +08:00
1e46149d0a refactor(agent): remove legacy ConfigLoader and related factory; refactor agent launch flow 2026-04-09 10:35:13 +08:00
427d224f65 refactor(gateway): manage gateway implementions via related registry and interface 2026-04-08 22:24:07 +08:00
0528890d60 refactor(communication): remove channel assignment in ReplyDispatcher 2026-04-08 20:49:37 +08:00
f233c5ce32 fixup! refactor(runner): manage execution policy via ConfigCenter 2026-04-08 20:48:42 +08:00
91a595d073 chore: remove legacy utils and constant class 2026-04-08 17:36:03 +08:00
6d27e55a1e refactor(runner): manage execution policy via ConfigCenter 2026-04-08 17:21:35 +08:00
2935daeffa refactor(action): support recover schedulable executable actions together, and ActionExecutor will emit a context block to explain recovering result 2026-04-08 16:52:21 +08:00
e04b2c4fe8 refactor(memory): manage state serialization via StateCenter in MemoryRuntime 2026-04-08 14:55:37 +08:00
21a9d2114f chore(core): remove legact PartnerCore 2026-04-07 23:27:31 +08:00
3640cc2108 feat(action): support continuing executable actions after state restored 2026-04-07 23:16:12 +08:00
a114044c23 refactor(action): manage state serialization via StateCenter in ActionCore 2026-04-07 22:01:46 +08:00
874488ea79 test(memory): test with new memory behavior 2026-04-07 17:23:14 +08:00
b80ff8400c refactor(memory): compute memory slice in MemoryCore 2026-04-07 17:13:51 +08:00
6fd12cd19f fix(memory): restore normalizing logic in memory core, and fix errors in MemoryCoreTest 2026-04-07 15:02:55 +08:00
2cbaccedba refactor(cognition): manage state serialization via StateCenter in CognitionCore 2026-04-07 14:44:47 +08:00
006e7c61e5 refactor(context): remove legacy attribute coreResponse in PartnerRunningFlowContext 2026-04-07 14:42:15 +08:00
2458ea4849 refactor(memory): manage state serialization via StateCenter in MemoryUnit, and support optional loading on register in StateCenter 2026-04-07 12:27:36 +08:00
eab8eec46e chore(memory): add refactor todo in MemoryUpdater 2026-04-07 10:23:55 +08:00
a242723727 refactor(memory): manage state serialization via StateCenter in MemoryCore, and normalize slice and unit building 2026-04-07 10:21:17 +08:00
57bc63c57b refactor(perceive): manage state serialization via StateCenter in PerceiveCore 2026-04-07 10:05:35 +08:00
9aa793df8e feat(state): add StateCenter and related interface 2026-04-06 22:16:50 +08:00
8c04566243 chore(framework): reorganize Partner-Framework 2026-04-06 20:16:51 +08:00
b1ba86be57 chore(build): adjust pom.xml in Partner-Framework and fix test errors 2026-04-06 16:12:13 +08:00
f79a0521b2 refactor(vector): refactor VectorClient configuration loading method 2026-04-06 15:52:30 +08:00
332792daa2 refactor(action): make LocalRunnerClient as default runner client 2026-04-05 23:23:02 +08:00
3b236286b9 refactor(action): remove problematic action tendency cache 2026-04-05 22:32:52 +08:00
50db3fa7b2 refactor(config): adjust method init and onReload to support polymorphic config loading 2026-04-04 23:34:00 +08:00
6503ec32b4 chore(git): update gitignore 2026-04-04 17:54:25 +08:00
9771aa1de5 refactor(model): manage model registry by ConfigCenter 2026-04-04 17:53:54 +08:00
660bb01440 chore(config): add config doc to WebSocketConfig 2026-04-04 14:49:06 +08:00
188b5e8b53 refactor(config): support printing more information after init failed by ConfigDoc 2026-04-04 00:33:25 +08:00
db4dc6d040 refactor(gateway): init and load config from config center for WebSocketGateway 2026-04-03 23:07:40 +08:00
ef9d177adc fix(config): correct duplicate config checking 2026-04-03 15:35:27 +08:00
5a41e02602 fixup! refactor(config): rename watching method and remove useless covariant 2026-04-02 22:59:09 +08:00
f387c36b17 refactor(config): prevent registering Configurable after watching started 2026-04-02 22:55:53 +08:00
03532d3d97 refactor(config): run init after watching started 2026-04-02 22:19:37 +08:00
f37bef57ba refactor(config): rename watching method and remove useless covariant 2026-04-02 22:12:08 +08:00
29d6546b07 feat(config): support ConfigCenter file watching and registered json reloads 2026-04-01 23:43:19 +08:00
b9fd9bcaac refactor(mcp): switch desc.json loading to fastjson2 to avoid desc.json loading error 2026-04-01 22:57:17 +08:00
4ae65b885e refactor(watch): support configurable directory watch depth 2026-04-01 22:15:00 +08:00
632e47ec13 feat(config): add config center and runtime path resolution 2026-04-01 20:42:45 +08:00
4f200cadfc refactor(model): move model APIs from chat to agent.model 2026-03-31 21:13:57 +08:00
e4df68ea5d refactor(chat): extract model activation into provider registry
- move ActivateModel and StreamChatMessageConsumer into api.chat
- replace direct OpenAI runtime construction with ModelRuntimeRegistry
- add provider config, runtime override and OpenAI-compatible provider forking
- rename OpenAiChatRuntime to OpenAiCompatibleProvider and update imports
2026-03-31 18:37:41 +08:00
81aa4b7933 feat(chat): support streaming reply in agent turn 2026-03-31 14:44:58 +08:00
b4c44c7d98 fix(perceive): fix legacy errors 2026-03-31 12:14:52 +08:00
1301a0f8b1 refactor(event): enrich necessary attributes in InteractionEvent.Reply 2026-03-31 10:28:27 +08:00
7d82ec7238 refactor(gateway): register WebsocketGateway as response channel while launch 2026-03-31 10:15:31 +08:00
d70054cd9b refactor(interaction): decouple gateway IO from runtime response flow
- replace interaction adapter/input-output DTO flow with InputData and InteractionEvent
  - introduce ResponseChannel and default LogChannel for runtime response dispatch
  - let AgentGateway parse running context directly and submit turns asynchronously
  - update WebSocketGateway to emit serialized interaction events
  - simplify cognition turn initiation to fire-and-forget semantics
  - streamline running flow context source construction and runtime module execution
2026-03-30 22:46:02 +08:00
def48fd0ce refactor(context): remove legacy module context 2026-03-30 18:01:55 +08:00
0b750f8028 refactor(action): support initiate turn after evaluation 2026-03-29 22:58:55 +08:00
6e37ed032b refactor(context): support skip modules by adding specific moduleName into runningFlowContext 2026-03-29 22:26:10 +08:00
b6c86c6640 refactor(action): adjust pending action block content and make full-expanded block as SUPPLY block 2026-03-29 22:07:29 +08:00
71956b4dce refactor(action): adjust tendency evaluation to be performed asynchronously, and pass the tendency in evaluation to communication as a block of SUPPLY type. 2026-03-29 21:55:53 +08:00
c9466f4359 refactor(executor): remove legacy InteractionThreadPoolExecutor , and change related executor calling into virtual executor and CountDownLatch 2026-03-29 20:19:09 +08:00
cb09b86b23 refactor(memory): remove legacy dialogMap in MemoryRuntime and related modules 2026-03-29 18:54:23 +08:00
d4a5c5a0ed fix(memory): trim persisted overlap from chat snapshot in MemoryUpdater 2026-03-29 18:49:44 +08:00
274d007ba1 refactor(memory): add DialogRollingService to control message rolling logic 2026-03-29 18:24:38 +08:00
c7df35beb4 refactor(memory): correct memorySlice-topicPath binding behavior and adjust slice index into [startIndex,endIndex) 2026-03-29 18:11:02 +08:00
1c995923a1 refactor(context): support copy attributes on root tag into snapshot element 2026-03-29 17:28:41 +08:00
247052e318 refactor(memory): update end index of memory slice 2026-03-29 16:15:30 +08:00
eb1723de97 refactor(memory): remove legacy activated memory slice records in memory core 2026-03-29 16:08:29 +08:00
c2fbfe751f refactor(memory): add memory recalling builtin capability meta-action 2026-03-29 16:04:06 +08:00
4a1828ed94 refactor(memory): finished the context block content produced in MemorySelector 2026-03-29 00:17:14 +08:00
db20e0ca78 refactor(memory): refactor memory selector into asynchronous module 2026-03-28 22:41:48 +08:00
09f90d8ad5 refactor(memory): refactor memory extract/evaluating logic and messages building in memory modules 2026-03-28 21:52:02 +08:00
baa6870ccf chore(action): remove unused attribute 2026-03-28 21:50:17 +08:00
fd43466dd5 refactor(runner): remove useless builtin capability meta actions 2026-03-27 15:22:34 +08:00
7628d40645 refactor(communication): rename module directory 2026-03-27 15:19:36 +08:00
d806693e08 refactor(action): simplify ActionCorrectionRecognizer input and build messages with context/task block 2026-03-27 15:01:45 +08:00
dbfd0b1fc3 refactor(action): simplify corrector input and build messages with context/task block 2026-03-27 14:53:00 +08:00
f5b9f8fc58 refactor(action): simplify extractor input and switch params extraction to context/task block messages 2026-03-26 21:43:48 +08:00
7bbb7745f4 refactor(action): remove legacy additionalContext from Action state and snapshots 2026-03-26 21:22:59 +08:00
b65f5f37fb refactor(context): rename encodeToContextMessage to encodeToMessage and update usages 2026-03-26 20:43:58 +08:00
fb9b3860af refactor(common): extract shared XML Block base and reuse it in BlockContent and TaskBlock 2026-03-26 20:43:26 +08:00
c5256cbc90 refactor(module): flatten package paths from module.modules.* to module.* and remove unused ModelConstant 2026-03-26 19:59:03 +08:00
1592c876c7 refactor(action): emit full execution lifecycle/correction blocks and carry correction reason in CorrectorResult 2026-03-26 17:32:40 +08:00
453a1cfe84 refactor(build): move kotlin sourceDirs into compile/test-compile executions 2026-03-26 16:52:29 +08:00
54320dbfde refactor(context): make block activation/rendering exposure-aware and use rendered projections in aggregation 2026-03-26 16:49:56 +08:00
201addbc64 refactor(build): centralize surefire config in parent POM and default to running tests 2026-03-26 16:44:21 +08:00
5219142b5c refactor(action): add ExecutingActionBlockManager to emit execution lifecycle ContextBlocks from action snapshots 2026-03-26 15:38:01 +08:00
750bef0fd8 refactor(action): add snapshot models and snapshot builders for Action/StateAction 2026-03-26 15:18:46 +08:00
a9b925c614 refactor(context): aggregate resolved blocks by source snapshots and centralize elapsed-time activation refresh 2026-03-25 20:13:55 +08:00
264a45c85f refactor(context): adjust fading factor of pending-action block 2026-03-24 22:55:58 +08:00
155d06df45 refactor(context): add urgency attribute/label to BlockContent 2026-03-24 22:52:02 +08:00
7879836b91 refactor(action): remove the explicit PendingAction control flow and assume the confirmation semantics through a short-term, fast-decaying ContextBlock belonging to the ACTION domain. 2026-03-24 22:28:42 +08:00
82db27484c refactor(communication): adjust domains that involved in communication 2026-03-24 14:28:51 +08:00
d11a431614 refactor(action): remove the redundant fields in the prompt construction logic and data classes left in ActionEvaluator 2026-03-24 14:24:31 +08:00
50b07488a6 refactor(action): adjust ActionPlanner and related submodules to adapt to current Agent Context acquisition 2026-03-24 11:49:29 +08:00
313cea0d3b refactor(communication): register recent chat messages as context into ContextWorkspace 2026-03-24 11:28:40 +08:00
4494d58ff9 refactor(context): add ResolvedContext as response of ContextWorkspace#resolve, integrated with Message encoding 2026-03-24 11:04:03 +08:00
d7179364a1 refactor(context): forces the context of the first domain hit to be fully expanded to adapt to the context focus needs within the module 2026-03-24 10:29:15 +08:00
b05ef8683d refactor(executor): remove legacy PhaserRecord and related methods in ActionCapability 2026-03-23 23:27:37 +08:00
556b8a5348 refactor(executor): remove legacy ActionRepairer、DynamicActionGenerator and related data class 2026-03-23 23:19:16 +08:00
027ebf860e refactor(executor): Reconstruct the execution process of ExecutableAction in ActionExecutor, remove ActionRepairer, and adjust the execution timing of ActionCorrector 2026-03-23 23:16:57 +08:00
8cd12f7379 refactor(action): add resume/interrupt methods to ExecutableAction; add related definitions to support acquiring help from user 2026-03-23 17:24:19 +08:00
617daea17c refactor(context): remove legacy ContextBlock 2026-03-22 22:18:32 +08:00
61d5270625 refactor(perceive): refactor into registering context via ContextWorkspace 2026-03-22 22:17:21 +08:00
93b0199c9e refactor(communication): route messages through cognition workspace 2026-03-22 22:01:04 +08:00
977d92881c feat(cognition): add ContextWorkspace to manage context blocks 2026-03-22 21:39:25 +08:00
6aa96c33ac refactor(cognition): remove legacy user exception 2026-03-22 13:50:18 +08:00
e85094670b refactor(cognition): rename Cognation core/capability and package references to Cognition 2026-03-22 12:31:57 +08:00
21ea6a25c8 refactor(chat): remove legacy MetaMessage POJO 2026-03-22 11:02:37 +08:00
ad65cd4c09 feat(runner): add and register DynamicAction related builtin MetaAction provider 2026-03-21 23:53:13 +08:00
ff46d97eed feat(scheduler): add cancel method to ActionScheduler 2026-03-21 22:39:18 +08:00
1a83075031 refactor(runner): inline command action JSON responses and remove command response DTOs 2026-03-21 18:14:47 +08:00
809d38bd07 refactor(action): remove outdated TODO comments in ActionScheduler 2026-03-20 23:16:57 +08:00
f7d46c8ef1 refactor(action): remove legacy interventor module and move intervention entities into core action package 2026-03-20 23:09:58 +08:00
e1ee6589ef feat(runner): add and register builtin intervention action provider with intervention meta actions 2026-03-20 23:04:32 +08:00
17108f3239 fix(runner): correct actions list provided in BuiltinCapabilityActionProvider 2026-03-20 15:02:13 +08:00
59a5e22f35 refactor(runner): extract createActionKey as an abstract method in BuiltinActionProvider 2026-03-20 15:01:22 +08:00
30373cbc02 feat(runner): register builtin capability action definitions in BuiltinActionRegistry 2026-03-19 23:20:06 +08:00
0e164115c0 feat(runner): add builtin capability action provider for memory slices, initiate turn, and interaction status 2026-03-19 23:18:32 +08:00
3cc6e8df99 refactor(perceive): return Instant in showLastInteract 2026-03-19 23:16:02 +08:00
ccb7041093 refactor(perceive): remove legacy user storage in perceive core 2026-03-19 23:14:20 +08:00
e0b20ce414 refactor(runner): adjust command action response shcema in BuiltinCommandActionProvider 2026-03-19 11:31:34 +08:00
1029624dc7 refactor(cogiaiateTurnnation): support assigning reply target in method
initiateTurn
2026-03-19 10:54:36 +08:00
5b9b9c3c09 refactor(runner): add builtin action provider interface 2026-03-19 10:39:06 +08:00
67d7fd34f8 feat(runner): register builtin command action definitions in BuiltinActionRegistry 2026-03-18 23:06:06 +08:00
12368ded53 feat(runner): implement builtin command session actions with start/inspect/read/cancel/overview 2026-03-18 23:00:56 +08:00
7d9ec976e3 feat(runner): implement builtin command execute MetaAction definition 2026-03-18 17:15:47 +08:00
d8b19ebcea refactor(runner): refactor CommandExecutionService into single instance 2026-03-18 16:52:52 +08:00
7f4b82204a refactor(runner): change stdout/stderr reading thread into virtual thread in CommandExecutionService 2026-03-18 16:18:10 +08:00
664bd5a0fb refactor(runner): add typed param helper methods in BuiltinActionRegistry 2026-03-17 22:54:55 +08:00
6474eb8dc6 refactor(runner): allow object-typed params in BuiltinActionRegistry builtin actions 2026-03-17 22:51:00 +08:00
4439e5c04b feat(runner): add BuiltinCommandActionManager and command action data models scaffold 2026-03-17 22:24:23 +08:00
ef2eb909b7 refactor(runner): use string-only params and return type for BuiltinActionRegistry builtin actions 2026-03-17 22:12:22 +08:00
fd20af3e1c test(exec): add test for CommandExecutionService 2026-03-17 14:59:48 +08:00
d30ff322a2 refactor(runner): remove unused static method definition in BuiltinActionRegistry 2026-03-17 14:32:29 +08:00
cb63bbf570 feat(runner): add builtinAction defining method to BuiltinActionRegistry 2026-03-17 14:08:50 +08:00
4da4e5f161 refactor(runner): rename method buildDefinitions in BuiltinActionRegistry 2026-03-17 13:56:25 +08:00
5a717dbdda refactor(runner): load different policy provider according to os, now support bwrap sandbox in linux only 2026-03-17 12:21:31 +08:00
a6682a7719 feat(runner): implement BubbleWrap policy provider and related test 2026-03-17 12:11:54 +08:00
1465d7687b refactor(runner): add policy listener registering function and support registering McpConfigWatcher after starting 2026-03-17 10:35:35 +08:00
d31cac70a6 refactor(runner): apply execution policy wrapping in MCP transport and reload on policy changes 2026-03-16 22:45:49 +08:00
108cf9b071 refactor(runner): rename method buildFileExecutionCommands in CommandExecutionService 2026-03-16 16:52:04 +08:00
c4b8c2a858 refactor(runner): rename exec vararg parameter to commands for clarity 2026-03-15 22:41:46 +08:00
d55b849747 refactor(runner): apply execution policy wrapping before command execution in OriginExecutionService 2026-03-15 22:41:19 +08:00
a9993299a5 refactor(runner): add exec(List<String>) overload in CommandExecutionService 2026-03-15 22:41:11 +08:00
4c47cac3a5 fix(runner): repair MetaAction related logic in McpMetaRegistry and tests 2026-03-14 21:44:27 +08:00
cba9ff4f0b refactor(action): pass launcher through meta action flow instead of inferring command by file extension 2026-03-14 21:27:38 +08:00
603b0835c5 将 .java 重命名为 .kt 2026-03-14 21:27:38 +08:00
fc0d4ef03b refactor(runner): remove legacy abstract for command execution 2026-03-13 15:22:14 +08:00
97bb897407 feat(runner): add execution policy abstract for local running 2026-03-13 14:45:50 +08:00
8463eb9dae refactor(runner): remove unused method listSystemDependencies 2026-03-12 16:07:06 +08:00
9794b39572 refactor(runner): adjust the directory organization of runner 2026-03-12 15:32:28 +08:00
0506149f5f refactor(runner): separate logic of different domain in LocalRunnerClient into different class 2026-03-12 15:04:13 +08:00
9325b84d14 refactor(ActionExecutor): remove unused ActionScheduler in executor 2026-03-12 11:59:26 +08:00
6c8a1b2636 refactor(action): support built-in actions 2026-03-12 10:41:17 +08:00
3c550af33d refactor(perceive): omit user lookup when building evaluator input 2026-03-11 14:41:43 +08:00
229c7a0edb refactor(memory): remove redundant activated slice helpers 2026-03-11 13:37:18 +08:00
a067e058fb refactor(memory): remove module context and recall tracking 2026-03-10 23:09:32 +08:00
cdfae8ab1a refactor(memory): remove postprocess executor and simplify memory updater trigger 2026-03-10 23:07:58 +08:00
c1998f61b6 refactor(perceive): drop legacy perceive updater modules and remove PreProcessExecutor 2026-03-10 22:59:29 +08:00
3f6283d12a refactor(perceive): expose last interaction tracking and refresh memory sessions 2026-03-10 22:57:59 +08:00
3d1c258944 refactor(memory): drop message cleanup before summarizing 2026-03-10 21:15:35 +08:00
36dfd65046 refactor(CommunicationProducer): drop timestamp from user message 2026-03-10 21:13:12 +08:00
ee1a033c1b refactor(memory): centralize memory recording and retrieval logic 2026-03-10 20:42:46 +08:00
027e8bddc0 refactor(memory): move memory id refresh into runtime init 2026-03-10 20:17:34 +08:00
0903b8482b test(CommunicationProducer): remove CommunicationProducerTest 2026-03-10 19:54:37 +08:00
f51401e2f2 refactor(CommunicationProducer): snapshot chat history when assembling conversation 2026-03-10 19:53:20 +08:00
5ad80d8b86 refactor(memory): decouple memory storage and runtime structures 2026-03-10 19:41:05 +08:00
760ba8300b refactor(module): remove legacy prompt scaffolding 2026-03-10 17:15:50 +08:00
0f3d4659ae chore(project): remove legacy module Partner-Test-Demo 2026-03-10 15:04:16 +08:00
331d415925 refactor(CommunicationProducer): update datetime pattern and source in communicating 2026-03-10 15:01:07 +08:00
f5f64971f3 refactor(ContextBlock): return dom nodes directly 2026-03-10 14:48:14 +08:00
1cd6ba11bb refactor(CommunicationProducer): split context and supply message assembly 2026-03-10 14:31:13 +08:00
5db533f823 refactor(chat): use Message.Character enum for roles and remove unused prompt helpers 2026-03-09 22:12:35 +08:00
1b2ccaee9c refactor(chat): replace custom client with OpenAI runtime and remove file-based module prompt loading logic, prompt will be provided by each module 2026-03-09 21:51:07 +08:00
8dc7ed080b chore(build): import OpenAI dependency 2026-03-09 16:20:10 +08:00
3348557352 refactor(Action): backfill ExecutableAction result on success/failure and add immediate-action completion watcher self-talk 2026-03-08 15:42:25 +08:00
4bb83f86a8 refactor(ActionPlanner): switch pending confirmation flow to PendingActionRecord with decision parsing and reminder/expire lifecycle scheduling 2026-03-08 14:40:53 +08:00
b256af0f58 refactor(Action): remove @JvmOverloads from SchedulableExecutableAction constructor 2026-03-08 13:32:08 +08:00
ec429db4da refactor(Core): normalize preprocess user resolution from source/additional info, add Kotlin-safe AgentConfigLoader getters, and update async summary writes by index 2026-03-08 13:31:53 +08:00
145aeed600 refactor(Module): remove unused meta component POJOs from agent factory 2026-03-08 13:31:02 +08:00
5e8ef6d66f refactor(build): add kotlin-maven-plugin setup to Partner-Core and Partner-Framework poms 2026-03-08 13:30:39 +08:00
65690c65f8 refactor(MemoryUpdater): move auto-update to ActionScheduler cron and use Cognation snapshot/drain APIs for thread-safe memory refresh 2026-03-08 13:11:20 +08:00
7df0f208b5 refactor(Module): rename PostRunningAbstractAgentModuleAbstract to PostRunningAgentModule and update updater inheritance 2026-03-08 12:04:29 +08:00
4484d4a06b refactor(Context): remove finished state from flow/module context and drop MemoryUpdater finish guard 2026-03-08 11:47:44 +08:00
25ddc6f181 refactor(ActionExecutor): enforce action timeout with cancellation and move timeout defaults into Action base model 2026-03-07 20:15:41 +08:00
d905c4ace1 refactor(Action): add @JvmOverloads constructors to SchedulableExecutableAction and StateAction 2026-03-07 18:52:36 +08:00
c3c4c88c9a refactor(ActionScheduler): add timeout attribute to Schedulable, and support reschedule Schedulable content when it's fished in ActionScheduler 2026-03-07 17:05:07 +08:00
ae1b7fc033 refactor(ActionScheduler): support receiving single data that implements Schedulable and Action in ActionScheduler 2026-03-07 15:30:25 +08:00
d9e384960f refactor(action): remove ActionExecutorInput 2026-03-07 15:15:08 +08:00
2baa3971a8 refactor(action): support executing any type of Actions with virtual thread in ActionExecutor, ActionScheduler will send actionData to executor directly 2026-03-07 15:05:51 +08:00
4ee7a52f42 refactor(Action): make attribute status available for all implementations of Action, StateAction needs to present its trigger status also 2026-03-07 14:29:11 +08:00
28400545a7 chore(ActionExecutor): remove unused Void return value 2026-03-07 14:20:07 +08:00
1ce2038ab8 fix(Communication): fix Message appending logic and add refactor todo 2026-03-07 14:01:33 +08:00
0b63ec8523 fix(MemoryUpdater): fix message cleaning logic, and add refactor todo in updater 2026-03-07 13:53:34 +08:00
28a1bf8d1f refactor(chat): migrate Message from Java POJO to Kotlin data class 2026-03-06 21:04:58 +08:00
77059f84c4 将 .java 重命名为 .kt 2026-03-06 21:04:58 +08:00
d3eb5e8ee3 refactor(config): migrate ModelConfig from Java POJO to Kotlin data class 2026-03-06 17:01:03 +08:00
01cfc04dc7 将 .java 重命名为 .kt 2026-03-06 17:01:03 +08:00
6919fe656e refactor(module): rename CoreModel into CommunicationProducer 2026-03-06 15:21:36 +08:00
df25f488fa refactor(action): remove ActionDispatcher and related empty directory 2026-03-06 15:06:32 +08:00
036fd9e051 refactor(ActionPlanner): support schedule or execute actions(or
confirmed actions) immediately
2026-03-06 14:58:13 +08:00
383a49b855 refactor(ActionExecutor): refactor into Standalone module 2026-03-06 14:49:01 +08:00
7e88b8b926 chore(action): add refactor todos in ActionScheduler 2026-03-06 14:28:52 +08:00
c6c8a83dad chore(test): remove unused non-null test for ActionScheduler 2026-03-06 14:17:04 +08:00
6635d7aca2 refactor(Action): add attribute enabled status into Action, and update related collectToTrigger logic in ActionScheduler 2026-03-06 14:15:51 +08:00
facc49a799 refactor(ActionScheduler): rename entry method into schedule 2026-03-06 14:04:15 +08:00
3c6076ee0a chore(idea): update misc.xml 2026-03-06 12:49:02 +08:00
40bd2deeba feat(cognation): add initiateTurn to execute input via AgentRuntime.submit 2026-03-05 16:49:17 +08:00
839f19f15b refactor(context): add PartnerRunningFlowContext factories for user/self source and update adapter creation 2026-03-04 20:02:56 +08:00
da1abbdc88 fix(AgentRuntime): repair exception handling logic in method executeTurn 2026-03-04 17:23:14 +08:00
e6a071fc93 refactor(context): migrate running flow context to source/status/info model, and update related modules 2026-03-04 17:21:34 +08:00
56688785ba 将 .java 重命名为 .kt 2026-03-04 17:21:34 +08:00
8a9892f039 chore(core): add TODO to use RunningFlowContext for message timestamp in CoreModel 2026-03-04 13:52:19 +08:00
06f5ae9aac refactor(core): remove global running flow context map and exception context reference 2026-03-04 13:11:42 +08:00
f8d90fbcee refactor(framework): make AgentRuntime.submit blocking and remove runBlocking from AgentInteractionAdapter 2026-03-03 21:23:52 +08:00
b02f29b1b1 refactor(framework): extract interaction execution into AgentRuntime and delegate from AgentInteractionAdapter 2026-03-03 21:14:10 +08:00
f1848fece4 refactor(framework): cache running modules in AgentInteractionAdapter and add refresh method 2026-03-03 20:29:23 +08:00
85cc5cace8 chore(framework): add message queue todo in AgentGateway 2026-03-03 11:48:59 +08:00
d462f02960 refactor(framework): mv interaction in-out flow into AgentInteractionAdapter 2026-03-03 11:48:08 +08:00
5ae8b713d7 refactor(framework): correct InteractionAdapter, support loading latest modules while running 2026-03-03 11:26:36 +08:00
cf25fce09e refactor(framework): remove coordinated capability mechanism and related manager/annotations 2026-03-02 20:34:30 +08:00
868b17b56b refactor(core): use chat message user IDs for single-user check in
`MemorySelector`
2026-03-02 20:34:16 +08:00
fe8031d9ac fix(core): remove CoordinatedManager log tag from memory insert logs 2026-03-02 20:31:22 +08:00
5847b38f2b refactor(framework): remove obsolete lifecycle TODO comment in AgentModule 2026-03-01 21:35:30 +08:00
6920bc6130 refactor(core): remove redundant @NotNull type-use annotation from async resource spec builder 2026-03-01 21:34:56 +08:00
fa9512db3b refactor(framework): migrate AgentInteractionAdapter flow execution to Kotlin coroutines and remove AgentRunningFlow 2026-03-01 21:33:31 +08:00
51d51937ed 将 .java 重命名为 .kt 2026-03-01 21:33:31 +08:00
23026d6dc8 refactor(framework): move RunningFlowContext from flow.entity to flow package 2026-03-01 21:31:04 +08:00
661dd625e3 chore(maven): move coroutines dependencies into Partner-Framework 2026-03-01 20:48:41 +08:00
33fdc61eff refactor(framework): remove module enabled-status loading and persistence from config loaders 2026-02-28 21:19:58 +08:00
0870d7bc0e fix(framework): fail module registration on duplicate moduleName in AgentContext 2026-02-28 21:05:12 +08:00
baf0b05e60 doc(framework): add KDoc for factory responsibilities and agent registration lifecycle 2026-02-28 20:44:40 +08:00
51efb55259 refactor(framework): rename ComponentInitHookExecuteFactory to ComponentInitHookExecutorFactory 2026-02-28 20:38:29 +08:00
d5095359db refactor(framework): validate and collect @Shutdown hooks during agent
registration
2026-02-28 20:38:09 +08:00
1abfc729f8 fix(framework): reject @Init methods with parameters during component validation 2026-02-28 20:25:33 +08:00
528e88f613 refactor(framework): scope shutdown-only Instances and computeInstances helpers inside installShutdownHook 2026-02-27 00:34:07 +08:00
333d087979 refactor(framework): register and execute ordered @Shutdown hooks in AgentContext via JVM shutdown hook 2026-02-27 00:28:14 +08:00
a863b43563 refactor(framework): register capability core instances per capability and store cores in AgentContext 2026-02-26 22:27:43 +08:00
dde01a6253 fix(framework): reject additional components missing @AgentComponent annotation 2026-02-26 22:18:36 +08:00
fa50f4aeb7 refactor(framework): store AgentContext additional components by type and update init/injection target collection 2026-02-26 22:17:41 +08:00
b87ede0e8b refactor(framework): add @Shutdown annotation to AgentContext for ordered shutdown hook execution 2026-02-26 22:08:33 +08:00
010860de8d refactor(framework): migrate AgentRegisterFactory to Kotlin object and preserve agent registration flow 2026-02-23 23:34:54 +08:00
379cabe042 refactor(framework): replace ModuleInitHookExecuteFactory with Kotlin ComponentInitHookExecuteFactory and drive @Init execution from validated component context 2026-02-23 23:14:45 +08:00
1b164cedf1 refactor(framework): migrate capability injection to Kotlin CapabilityInjectorFactory and inject capability instances directly into components 2026-02-23 22:59:47 +08:00
7ee698768c refactor(framework): simplify AgentContext capability storage to a single implementation per capability 2026-02-23 22:59:20 +08:00
2e29e5ca7f refactor(framework): migrate CapabilityRegisterFactory to Kotlin and rebuild capability registration from validated context 2026-02-23 22:32:32 +08:00
e0a62053b5 refactor(framework): migrate AgentRegisterContext to Kotlin and store validated capability scan results in context 2026-02-23 22:12:56 +08:00
f13e45327d refactor(framework): remove legacy CapabilityFactoryContext and ModuleFactoryContext from registration flow 2026-02-23 21:58:25 +08:00
f56ff7d719 refactor(framework): rework AgentContext capability storage to group implementations by capability and include method metadata 2026-02-23 21:53:37 +08:00
fd9b376afa refactor(framework): replace CapabilityCheckFactory with Kotlin CapabilityAnnotationValidatorFactory and update capability validation flow 2026-02-23 21:41:38 +08:00
542de84640 refactor(framework): add ComponentInjectorFactory for module/additional component injection before capability registration 2026-02-23 21:16:04 +08:00
907bb626f2 chore(gitignore): update gitignore 2026-02-22 14:15:17 +08:00
2cdeaa1c30 refactor(framework): make AgentContext capabilities non-null and infer capability type on registration 2026-02-21 22:38:56 +08:00
833fe4deb3 refactor(framework): add component annotation validation before component registration 2026-02-21 19:27:17 +08:00
3c26e77b76 refactor(framework): rename AgentComponentRegisterFactory to ComponentRegisterFactory 2026-02-21 19:19:04 +08:00
2825f7f1de refactor(framework): replace legacy module check/register/proxy flow with unified AgentComponentRegisterFactory 2026-02-21 18:56:40 +08:00
86b7e5c492 refactor(framework): rename agent.factory.module to agent.factory.component and update related imports 2026-02-21 17:57:29 +08:00
b1e4d3c2e4 fixup! refactor(framework): rename AgentConfigManager to AgentConfigLoader and update agent/core wiring 2026-02-21 17:46:16 +08:00
997616e45f refactor(framework): migrate ConfigLoaderFactory to Kotlin and simplify loader metadata registration 2026-02-21 17:42:05 +08:00
6733984843 将 .java 重命名为 .kt 2026-02-21 17:42:05 +08:00
0f2052c507 refactor(framework): migrate AgentBaseFactory from Java to Kotlin and update execute signature 2026-02-21 17:22:38 +08:00
23bfb8bac1 将 .java 重命名为 .kt 2026-02-21 17:22:38 +08:00
15c11ac500 refactor(framework): type AgentContext metadata entries and add JSON-backed metadata registration 2026-02-21 17:10:46 +08:00
deffc91dd1 refactor(framework): remove obsolete Java AgentContext class 2026-02-21 16:55:31 +08:00
b2d44668da refactor(framework): rename AgentConfigManager to AgentConfigLoader and update agent/core wiring 2026-02-21 16:51:59 +08:00
f8399d2280 refactor(framework): add additional component storage and guarded registration in AgentContext 2026-02-21 16:28:20 +08:00
2870e79f79 refactor(framework): remove obsolete agent proxy and register test stubs 2026-02-21 16:19:57 +08:00
3c9ace8e56 refactor(framework): rename CapabilityHolder to AgentComponent across factory and module hooks 2026-02-21 16:19:00 +08:00
e510725e71 refactor(framework): remove legacy running/sub module base classes and module annotations 2026-02-21 16:14:46 +08:00
f963cae4ed refactor(framework): rename Context.kt to AgentContext.kt 2026-02-21 16:06:11 +08:00
45c4e8169a refactor(framework): add mutable metadata map to Context 2026-02-21 15:43:19 +08:00
6bf4d95b05 refactor(framework): encapsulate AgentContext maps and require modelInfo in module contexts 2026-02-20 21:31:20 +08:00
00ef090d2f refactor(framework): generalize AgentContext and ModuleContextData generics with wildcard bounds 2026-02-20 20:55:56 +08:00
e62cddfe44 refactor(framework): add enabled flag to ModuleContextData.Running 2026-02-20 19:00:49 +08:00
115a8d5446 refactor(framework): redesign AgentContext to store typed module contexts and runtime metadata 2026-02-20 18:57:36 +08:00
ef5d5802a7 refactor(framework): make Standalone extend AbstractAgentModule 2026-02-20 17:46:33 +08:00
87c34cc699 refactor(modules): convert ActionScheduler to Standalone and simplify async execute signature 2026-02-20 17:29:26 +08:00
bbace28d7a refactor(project): normalize formatting and reorder class members across modules 2026-02-20 17:22:54 +08:00
c47d2b2285 refactor(modules): refactor modules base class into AbstractAgentModule and remove unused @slf4j annotation 2026-02-20 17:17:49 +08:00
38c618a222 chore(gitignore): update gitignore 2026-02-20 16:30:22 +08:00
e00441faa8 refactor(framework): make AbstractAgentModule sealed with class-based module contracts and streamline ActivateModel model access 2026-02-20 16:12:35 +08:00
c3b0a9dd25 refactor(framework): make ActivateModel companion configManager private 2026-02-20 15:39:52 +08:00
6b7c9db5b1 refactor(framework): add order() to AbstractAgentModule.Running contract 2026-02-20 15:30:55 +08:00
e2ef92ce43 refactor(framework): use moduleName as modelKey for
`AbstractAgentModule` instances in ActivateModel
2026-02-20 15:29:38 +08:00
051b6450e7 refactor(framework): mark AbstractAgentModule as capability holder and consolidate module contracts within abstracts 2026-02-20 15:24:35 +08:00
2a3d33a61e refactor(framework): correct comments for ActivateModel 2026-02-20 15:02:33 +08:00
e57c03e213 refactor(framework): migrate module abstracts/ActivateModel to Kotlin and introduce shared model/context structures 2026-02-20 15:00:14 +08:00
14e6d71ac9 将 .java 重命名为 .kt 2026-02-20 15:00:14 +08:00
dc9f9417bc refactor(framework): remove @BeforeExecute/@AfterExecute hook proxy logic and use direct module instantiation 2026-02-19 22:53:49 +08:00
5051c2f662 refactor(framework): use existing model instance in
init hook `ActivateModel#modelSettings`
2026-02-19 21:46:37 +08:00
c30ec35f85 refactor(framework): rename @AgentModule to @AgentRunningModule 2026-02-19 21:39:59 +08:00
c7f113b59a refactor(framework): rename AgentRunningModule/SubModule to AbstractAgent* and migrate module inheritance usage 2026-02-19 15:04:30 +08:00
8735660830 refactor(framework): remove log hook surrounded with method execute in AgentRunningSubModule 2026-02-19 14:37:21 +08:00
18b2bb8812 refactor(framework): reorganize Module abstracts location 2026-02-19 14:09:51 +08:00
7fccea5b91 chore(agent): add todo for PartnerOutputData 2026-02-19 14:07:39 +08:00
a9bf7ca1c2 refactor(framework): correct exception names in Partner-Framework 2026-02-19 14:01:26 +08:00
1685d148c4 chore(gitignore): update gitignore 2026-02-19 14:00:35 +08:00
73ab40416d refactor(Project): rename Partner-Api/Partner-Main modules to Partner-Framework/Partner-Core and update Maven dependencies 2026-02-19 10:39:21 +08:00
1244d59fa4 chore(ActionScheduler): remove todos 2026-02-18 15:22:02 +08:00
11ea1045f4 refactor(Action): generalize ActionScheduler to Schedulable and add StateAction trigger execution path 2026-02-18 15:20:52 +08:00
a1bc784da5 refactor(Action): rename Scheduled interfaces/classes to Schedulable 2026-02-15 23:09:35 +08:00
747d3e47d6 refactor(Action): add StateAction with scheduled trigger support for state updates/callbacks 2026-02-15 21:27:11 +08:00
5f0165fa3a refactor(Action): split ActionData into Action/ExecutableAction and unify scheduled action types 2026-02-15 21:26:17 +08:00
2b0682b9e0 chore(Action): remove todo in ActionData
Context:
SYNC is like the normal reAct execution, the thread based execution model with necessary notification can cover the demands entirely
2026-02-14 12:02:03 +08:00
16a92de377 chore(Action): add TODO notes for trigger type design and executor write-back flow 2026-02-13 21:27:23 +08:00
cbba183b60 docs(README): formatted 2026-02-10 13:52:14 +08:00
8e642b07d9 docs(README): expand Action system section with architecture and execution flow details 2026-02-10 13:50:31 +08:00
66d8a95c73 docs(README): clean up section formatting and update content 2026-02-10 13:26:31 +08:00
0850f8403d docs(README): rewrite project description and adjust planning items 2026-02-10 13:17:30 +08:00
24c29a6dc6 chore(resource): remove legacy Partner prompt and module config in resources, which will be provided in config dir or other ways 2026-02-10 13:17:05 +08:00
f703cc8157 refactor(vector): make embedding URL and model fields final 2026-02-10 13:05:09 +08:00
d52f48f132 fix(AgentConfigManager): set default INSTANCE to null instead of FileAgentConfigManager, which will be set in ConfigLoaderFactory or AgentApp 2026-02-10 13:03:58 +08:00
f6afe21b43 Merge branch 'feature/ActionModule'
# Conflicts:
#	.gitignore
2026-02-09 21:22:26 +08:00
d381a97731 refactor(ActionScheduler): add debug log for hour-change trigger scan 2026-02-09 21:12:38 +08:00
940beb2587 test(ActionScheduler): add test 2026-02-09 21:05:08 +08:00
69d9f04f11 fix(ActionScheduler): stabilize wheel tick pacing and run trigger scan before hour/day refresh 2026-02-09 21:04:48 +08:00
e2bd9eb0af fix(ActionScheduler): enqueue same-hour actions in wheel and add scheduling debug logs 2026-02-09 21:19:40 +08:00
9ec03c4c95 fix(ActionScheduler): include previous tick in trigger scan and tighten next execution filtering 2026-02-09 21:01:22 +08:00
ecbbbc9954 refactor(ActionScheduler): include tick in wheel stop debug log 2026-02-09 20:56:45 +08:00
a5d26769e8 fix(ActionScheduler): skip trigger callback when tick has no actions 2026-02-09 20:54:35 +08:00
2db1bdf3e9 refactor(ActionScheduler): add debug log for action execution 2026-02-09 20:50:36 +08:00
656d6b65e3 refactor(ActionScheduler): add debug logs for wheel start/stop, wait window, and action loading 2026-02-09 20:41:01 +08:00
7c46f1d1ff fix(ActionScheduler): remove triggered hour actions by uuid to avoid removeAll mismatch 2026-02-09 20:03:24 +08:00
406b4250aa refactor(ActionScheduler): correct actions loading logic in hour/day updating 2026-02-09 20:03:10 +08:00
eab3d00fe8 refactor(ActionScheduler): remove useless delay in TimeWheel#wheel 2026-02-09 20:02:26 +08:00
d47e9fbf95 fix(ActionScheduler): initialize wheel tick baseline before launch to avoid check-to-wheel startup drift 2026-02-09 17:29:32 +08:00
4b77f26e7b refactor(ActionScheduler): capture current hour once and reuse it for
day/hour rollover checks
2026-02-09 16:37:46 +08:00
650f9b27a1 fix(ActionScheduler): use checkThenExecute current hour consistently and trigger wheel tasks outside lock 2026-02-09 15:56:12 +08:00
9f479c5f6f fix(ActionScheduler): unify time check/loading under checkThenExecute and guard wheel loop with launch-hour consistency 2026-02-09 14:57:13 +08:00
227c735667 fix(ActionScheduler): make TimeWheel load scheduled actions dynamically instead of using init snapshot 2026-02-09 00:13:36 +08:00
b05b665960 fix(ActionScheduler): reload day/hour action buckets on time changes via checkTimeAndLoad, and reorganize functions 2026-02-09 00:03:21 +08:00
882ec43f2b fix(ActionScheduler): make scheduling thread-safe with Mutex and cancel scheduler/time wheel scopes on shutdown 2026-02-08 23:07:03 +08:00
7cb565fd1b fix(ActionScheduler): use withTimeoutOrNull when waiting for ACTIVE state, to avoid exception leading to wheel stopped 2026-02-08 21:56:35 +08:00
84b96b6645 test(ActionScheduler): remove unused actionExecutor mock 2026-02-08 21:52:16 +08:00
2169376062 test(ActionScheduler): add unit test for ActionScheduler 2026-02-08 21:51:57 +08:00
9bff74c8c7 fix(ActionScheduler): remove second offset when loading hour actions 2026-02-08 21:51:30 +08:00
76c9c27532 refactor(MetaAction): make result a read-only property 2026-02-08 17:22:47 +08:00
8524ca6f9f refactor(ActionData): use action.result.reset() when clearing action chain state 2026-02-08 17:22:10 +08:00
7dd2104689 refactor(MetaAction): migrate to Kotlin data class, merge MetaActionType/ResultStatus into nested enums, and update runner/action usages 2026-02-08 17:15:58 +08:00
6ba5784a7f 将 .java 重命名为 .kt 2026-02-08 17:15:58 +08:00
cdea8d6322 refactor(ActionData): migrate to Kotlin sealed class and data classes, update planner/scheduler usage 2026-02-08 16:27:44 +08:00
8ca2b9998d 将 .java 重命名为 .kt 2026-02-08 16:27:44 +08:00
d098b28f31 refactor(ActionExecutorInput): migrate to Kotlin data class 2026-02-08 15:12:10 +08:00
98e4d4cf1b 将 .java 重命名为 .kt 2026-02-08 15:12:10 +08:00
70489e57f7 chore(pom): add kotlinx-coroutines-test dependency 2026-02-08 14:34:19 +08:00
a43c87006e refactor(ActionCore): replace existing action with same UUID before putAction 2026-02-08 13:29:18 +08:00
be43b7eec6 refactor(ActionScheduler): implement Kotlin time-wheel scheduling and requeue scheduled actions after execution 2026-02-08 13:24:56 +08:00
3bc2ce839a 将 .java 重命名为 .kt 2026-02-08 13:24:56 +08:00
fe5a366527 refactor(ActionExecutor): remove userId from ActionExecutorInput and use source 2026-02-08 11:29:36 +08:00
9f724cee5d chore(pom): remove test scope from coroutines dependency 2026-02-08 11:22:47 +08:00
ad58b83020 refactor(ActionExecutor): rename actions variable for clarity 2026-02-08 11:22:30 +08:00
c9b64fec2a chore(pom): add cron-utils dependency 2026-02-07 15:36:45 +08:00
0eb4765235 refactor(ActionExecutor): use HistoryAction record and track scheduled history reset 2026-02-07 15:35:34 +08:00
050c39cbc7 refactor(ActionExecutor): correct input actions' type in ActionExecutor 2026-02-06 23:38:13 +08:00
08100aea8a refactor(ActionCore): replace prepared action APIs with generic list/put 2026-02-06 21:38:54 +08:00
2cd0774834 refactor(ActionCapability): rename listAvailableActions to listAvailableMetaActions 2026-02-06 21:05:10 +08:00
12df938d85 refactor(ActionCore): simplify handleInterventions to use ActionData 2026-02-06 20:41:08 +08:00
277c0d437f chore(ActionCore): update comment 2026-02-06 20:36:50 +08:00
6b861f4b77 fix(ActionExecutor): correct logic in ActionExecutor when actionChain is empty, which will skip execution 2026-02-05 20:40:02 +08:00
d33b6617c1 fix(ActionExecutor): support removing phaserRecord correctly when exception occurred in ActionCorrector 2026-02-05 19:40:28 +08:00
a1dcf4a6fa test(ActionExecutor): test with action failure 2026-02-05 17:05:53 +08:00
9c38719514 test(ActionExecutor): test with additionalContext appending 2026-02-05 16:58:22 +08:00
33df0fa017 test(ActionExecutor): test with virtual thread pool support 2026-02-05 16:53:38 +08:00
08bda84471 refactor(ActionCore): Specifies the minimum platform thread pool size
Context:
 The ActionExecutor needs at least 2 platform thread to support async  action execution,
 otherwise the current ActionExecutor logic cannot be executed
2026-02-05 16:49:58 +08:00
76da3c29f8 fix(ActionExecutorTest): repair stub in test 2026-02-05 16:13:21 +08:00
558b589830 refactor(ActionInterventor): redefine DTOs in ActionInterventor to adapt the actual intervention logic 2026-02-05 15:48:58 +08:00
80d7c283c5 refactor(ActionExecutor): update ActionChain execution, support executing and advancing correctly 2026-02-04 00:29:42 +08:00
b0bb40c5f0 test(ActionExecutor): add unit test for ActionExecutor 2026-02-01 19:49:21 +08:00
eec8f71096 fix(action): correct return type of method runnerClient() in ActionCapability 2026-02-01 16:43:12 +08:00
fbd30d1a96 build(maven): import Mockito related dependencies 2026-02-01 14:56:47 +08:00
346f925b66 chore(ActionExecutor): update comment 2026-01-31 23:35:17 +08:00
04e8d9e531 feat(ActionExecutor): support executing interventions in ActionExecutor 2026-01-30 20:58:12 +08:00
63d1552de2 refactor(ActionInterventor): remove InterventionHandler and related data class
Context:
Since last commit, the logic of interventions has been moved into ActionCapability,
the InterventionHandler is not needed.
2026-01-30 20:52:36 +08:00
77eb9b92a4 refactor(ActionCorrector): move intervention logic from InterventionHandler into ActionCapability 2026-01-30 20:10:01 +08:00
a1b4743eeb feat(ActionCorrector): complete corrector's executing logic 2026-01-30 19:19:48 +08:00
0768cddd2d fix(ActivateModel): correct modelSettings 2026-01-30 16:50:45 +08:00
75145cc547 chore(ActionRepairer): correct name of AssemblyHelper 2026-01-30 16:30:10 +08:00
d1ca1cda7d feature(ActionExecutor): complete CorrectorInput 2026-01-28 23:11:45 +08:00
fac6609d6b refactor(ActionExecutor): remove useless method getHistoryActionResults 2026-01-28 23:00:53 +08:00
dce8825e58 refactor(ActionExecutor): update type of history field in ActionData 2026-01-28 21:16:51 +08:00
cd641ac8dd fix(ActionExecutor): correct phaser block logic in method execute 2026-01-28 15:38:13 +08:00
5ffdab9e4a refactor(ActionExecutor): rework staged execution and runner submit
Context:
This refactor drops unnecessary method abstractions and cleans the action execution flow.
Additionally, method 'run' is renamed to 'submit' in RunnerClient, which better reflects that execution results are held in MetaAction.
2026-01-25 19:38:53 +08:00
830503eee4 chore(ActionExecutor): update comments 2026-01-23 19:23:07 +08:00
96e74ec877 test(LocalRunnerClient): add test for method run in RunnerClient 2026-01-17 11:28:21 +08:00
420d51af15 fix(LocalRunnerClient): harden doRun branches and add tests 2026-01-16 23:28:46 +08:00
8ead306b7b fix(RunnerClient): correct RunnerResponse's visibility 2026-01-16 22:17:15 +08:00
c793851107 fix(LocalRunnerClient): support cleaning non-existing MCP Servers' tools while MCP configuration files changed in CommonMcp 2026-01-16 21:48:17 +08:00
fb5cabc747 fix(LocalRunnerClient): support read MetaActionInfo according to desc files when an MCP Client with described tools registered by CommonMcp 2026-01-16 21:28:45 +08:00
c5f6c4e0ae fix(LocalRunnerClient): recover desc watcher after root deletion and expand DescMcp tests 2026-01-14 19:57:24 +08:00
200c0f3f13 fix(LocalRunnerClient): guard against null tool meta and ignore non-protocol MCP 2026-01-14 16:10:33 +08:00
fdf398b86e fix(LocalRunnerClient): close old MCP client while a new client's name is duplicated with the old one 2026-01-13 23:22:54 +08:00
774e2b6cd5 fix(LocalRunnerClient): correct abnormal deleting condition in CommonMcp 2026-01-13 23:13:52 +08:00
837a4c92d1 fix(LocalRunnerClient): treat missing action dir as invalid path during DELETE in DynamicMcp
Context:
Action directories may already be removed when DELETE events are handled.
Return null from loadFiles to signal invalid paths and lock behavior with DynamicAction watch tests.
2026-01-12 21:46:34 +08:00
ddd999d47b fix(LocalRunnerClient): prevent WatchService event loss caused by concurrent consumers
Context:
Shared WatchService with multiple watch threads caused WatchKey events to be consumed by mismatched processors, leading to missed file events.
Use isolated WatchService per WatchContext to restore correct semantics.
2026-01-12 19:46:45 +08:00
9694a022c7 chore(gitignore): update gitignore 2026-01-12 19:35:41 +08:00
31968c7076 chore(gitignore): create AGENTS.md for codex, and add it to .gitignore 2026-01-12 14:25:20 +08:00
abec141e4e fix(LocalRunnerClient): correct path creating logic in RunnerClient and its implementations 2026-01-11 16:47:14 +08:00
cdb6ae9d01 fix(LocalRunnerClient): correct method loadFiles in LocalWatchEventProcessor 2026-01-11 16:30:44 +08:00
dd8d86d3c4 chore(LocalRunnerClient): add logs to LocalRunnerClient 2026-01-11 16:27:14 +08:00
99b42620d0 refactor(LocalRunnerClient): repair paths registering order and support creating directories automatically 2026-01-11 15:01:19 +08:00
70b8335d49 feat(LocalRunnerClient): support atomic persist serialization in LocalRunnerClient 2026-01-11 14:24:34 +08:00
8ca475beeb feat(LocalRunnerClient): support registering CommonMcp 2026-01-08 22:28:12 +08:00
4f36c0dd2d feat(LocalRunnerClient): support deleting MCP configurations in CommonMcp 2026-01-08 22:23:08 +08:00
00993bd763 feat(LocalRunnerClient): support creating MCP configurations in CommonMcp 2026-01-08 22:09:14 +08:00
a0bca668cb refactor(LocalRunnerClient): support update existedMetaActions in method registerMcpClient 2026-01-08 21:48:30 +08:00
c6118c41b0 refactor(LocalRunnerClient): support loading primary fileMcpCache when CommonMcp launched 2026-01-08 21:33:39 +08:00
872d21170a feat(LocalRunnerClient): support modify and overflow events on mcp configurations in CommonMcp
Context:
Due to single file cannot present all mcp configurations, loading all MCPs at once is required.
This is compatible in both modify and overflow events.
2026-01-08 21:16:28 +08:00
44ab6cfac8 feat(LocalRunnerClient): support registering MCP clients in CommonMcp 2026-01-05 23:06:17 +08:00
ec30ac1922 refactor(LocalRunnerClient): remove tool change consumer in registerMcpClient
Context:
ExistedMetaActions' updating logic is covered by implementations of LocalWatchEventProcessor.
2026-01-03 16:34:04 +08:00
74b6d0c653 chore(RunnerClient): fix RunnerClient error usages in implementations 2026-01-03 15:49:54 +08:00
de462866b2 feat(LocalRunnerClient): support registering DescMcpServer watch service 2026-01-02 21:41:47 +08:00
4ea8926363 feat(LocalRunnerClient): support repairing description data while OVERFLOW event happened in DescMcpServer 2026-01-02 21:29:18 +08:00
04c98c7856 fix(LocalRunnerClient): support deleting descCache while *.desc.json is not available in DescMcpServer 2026-01-02 18:18:15 +08:00
0757856187 feat(LocalRunnerClient): support deleting *.desc.json in DescMcpServer 2026-01-02 17:20:12 +08:00
19ec93f248 feat(LocalRunnerClient): create modify *.desc.json in DescMcpServer 2026-01-02 16:45:01 +08:00
5877b9e80d feat(LocalRunnerClient): support modify *.desc.json in DescMcpServer 2026-01-02 16:42:56 +08:00
5db0b5fad1 feat(LocalRunnerClient): support load *.desc.json when DescMcpServer launched 2026-01-02 15:55:29 +08:00
623a86daab chore(LocalRunnerClient): update mcp servers' comments 2026-01-02 15:52:44 +08:00
64f24d3fc3 chore(LocalRunnerClient): adjust mcp servers' comments location 2026-01-02 13:38:43 +08:00
3097efe453 feat(LocalRunnerClient): support register DynamicActionMcp watch service 2026-01-02 13:29:00 +08:00
b58eeffd2f feat(LocalRunnerClient): support overflow event in DynamicActionMcpServer 2026-01-01 23:32:21 +08:00
62cec79005 refactor(LocalRunnerClient): extract duplicated action adding logic 2026-01-01 22:39:32 +08:00
03a5935107 fix(LocalRunnerClient): support deleting event for action directories in DynamicActionMcp 2026-01-01 21:29:30 +08:00
0ecaec0545 fix(LocalRunnerClient): repair loading logic of action subdirectories 2026-01-01 20:28:19 +08:00
74f2c6c950 fix(LocalRunnerClient): support creating and registering new action in
method buildCreate in DynamicActionMcp
2026-01-01 00:32:34 +08:00
f35a467ebc fix(LocalRunnerClient): support registering subdirectories in LocalWatchServiceBuild 2025-12-31 23:15:27 +08:00
64b907707a refactor(LocalRunnerClient): introduce WatchContext and decouple build/processor state 2025-12-31 23:11:15 +08:00
a6e33edc7a refactor(LocalRunnerClient): support remove action temporarily while action is not usable 2025-12-31 16:27:34 +08:00
94ef79c67d feat(LocalRunnerClient): support program deletion for DynamicActionMcp 2025-12-31 13:41:35 +08:00
a222015abb feat(LocalRunnerClient): support program modify and unify action load protocol
Context:
The method buildModify reuses AsyncToolSpecification building logic in buildLoad.
This feature unifies local action directory protocol, and refactors related logic in buildLoad.
New action directory protocol defines the file names of program and description files.
2025-12-30 20:52:32 +08:00
1c562f0e7b refactor(LocalRunnerClient): update action keys building source in
DynamicActionMcp

 Context:
 Building action keys by subdirector's name keeps unique identity for each local action.
2025-12-30 16:43:39 +08:00
89535a6b1c feat(LocalRunnerClient): add initial support for loading local action tools from filesystem
Context:
This feature supports DynamicActionMcpServer.

During initialization, directories containing a program file and a
.meta.json description are scanned and registered as MCP tools.
Tool execution is handled asynchronously via boundedElastic to avoid blocking server threads.
2025-12-29 20:46:26 +08:00
6e90bc8d67 refactor(LocalRunnerClient): co-locate system execution result 2025-12-29 18:53:41 +08:00
0e741802d1 refactor(LocalRunnerClient): consolidate MCP client transport params
Context:
Group HTTP and STDIO transport parameter variants under a sealed internal transport parameter hierarchy.
2025-12-29 18:48:53 +08:00
db3435fccf refactor(LocalRunnerClient): co-locate watch service builder internals
Context:
Group WatchService build interfaces and registry implementation into a
single internal structure for better cohesion.
2025-12-29 18:40:20 +08:00
e3294ec302 refactor(LocalRunnerClient): move system execution methods into SystemExecHelper 2025-12-29 18:26:30 +08:00
bf99e01b51 feat(LocalRunnerClient): introduce LocalWatchServiceHelper and internal implementations
Context:
This change introduces an internal scaffold to organize WatchEventHandler building logic.
2025-12-29 17:45:39 +08:00
1bd23b20c4 refactor(LocalRunnerClient): introduce DescMcpServer
Context:
This refactor supports creating descriptional files for common MCP Tools.
2025-12-29 17:35:03 +08:00
442dd55686 refactor(LocalRunnerClient): rename LocalWatchServiceRegistry 2025-12-29 14:27:00 +08:00
abe5dd5251 chore(idea): update misc.xml 2025-12-26 21:28:10 +08:00
1f737c0e29 refactor(action): reorganize constants in action module 2025-12-26 21:28:02 +08:00
d41074c814 refactor(LocalRunnerClient): replace ActionWatchService with unified watch service builder.
Context:
ActionWatchService was used to support SCRIPT and PLUGIN type actions loading from local FileSystem, this refactor allows register different paths to watch.
2025-12-25 15:41:49 +08:00
621441601a feat(LocalRunnerClient): correct method signature 2025-12-25 10:20:55 +08:00
e00d77f076 feat(LocalRunnerClient): add shutdown logic for dynamicActionMcpServer 2025-12-25 10:12:38 +08:00
d614ac0b15 feat(LocalRunnerClient): support initializing in-process dynamic action MCP Server 2025-12-24 21:36:39 +08:00
592e2604d9 refactor(mcp): move InProcessMcpTransport into Partner-Common module
Context:
Action modules in Partner-Main and SandboxRunner module rely on in-process MCP transport to support dynamically action generating.
2025-12-24 19:34:04 +08:00
dcbd2c6569 build(maven): introduce common module 2025-12-24 19:21:53 +08:00
476acb0641 refactor(LocalRunnerClient): rename McpServerParams into McpClientTrasnportParams 2025-12-22 15:02:07 +08:00
88a14f36b2 refactor(runner): relocate InProcessMcpTransport to experimental and move local MCP client logic into LocalRunnerClient
Context:
Recent changes blurred the responsibility boundary between RunnerClient and LocalRunnerClient.
This refactor moves local MCP client–specific logic into LocalRunnerClient and isolates InProcessMcpTransport and related code under the experimental package.
RunnerClient only defines indispensable methods and attributes.
2025-12-22 14:56:23 +08:00
05d1fff125 refactor(RunnerClient): remove unused MCP type enum class 2025-12-21 23:03:25 +08:00
49a4c9eb01 docs(RunnerClient): add architecture-location comment on RunnerClient 2025-12-21 22:05:46 +08:00
9e76c3e7ad refactor(SandboxRunnerClient): align doRun visibility with superclass 2025-12-19 23:34:17 +08:00
9762739138 refactor(action): replace HashMap with ConcurrentHashMap for thread-safe MetaAction storage 2025-12-19 23:30:27 +08:00
1f5509c17d refactor(RunnerClient): redesign existedMetaActions update strategy
Context:
Resource-change events cannot reliably represent tool changes.
The previous approach attempted to externalize descriptive content into files, but the meta attribute of McpSchema.Tool can provide this information.
2025-12-19 23:22:36 +08:00
ed042cfffa fix(action): correct params type in related DTOs 2025-12-19 22:57:34 +08:00
128592e23c chore(MetaActionInfo): remove unused type attribute 2025-12-19 22:47:06 +08:00
5ba36ed3e8 feat(LocalRunnerClient): support executing MetaActions via MCP type 2025-12-19 22:29:03 +08:00
4dea948f82 refactor(MetaAction): separate key attribute into name and location
Context:
This change adapts MetaAction locating to support different MetaAction types,
including loading from the local filesystem and from MCP tools.
2025-12-19 21:35:39 +08:00
dc4074715e chore(MetaAction): remove unused order attribute 2025-12-19 20:53:01 +08:00
225802c1a8 refactor(MetaActionInfo): remove key attribute and update related logic
Context:
MetaActionInfo was previously located via its own key attribute.
This is now redundant, as ActionCore already uses the key of existedMetaActions
as the single source of truth.
2025-12-19 20:41:07 +08:00
e851e33b2e feat(RunnerClient): support MCP type-based dynamic client/server registration
This allows implementations of RunnerClient to dynamically register different types of MCP service, and also provides a shutdown hook to close client/server properly.
2025-12-18 22:25:32 +08:00
cb28a5b068 feat(RunnerClient): add InProcessMcpTransport to support in-process MCP communication
Context:
This allows RunnerClient implementations to host local MCP servers without spawning another process.
2025-12-18 21:48:35 +08:00
ad58567ada chore(deps): introduce mcp dependencies 2025-12-18 17:52:15 +08:00
0eee12d685 refactor(MetaActionInfo): remove outdated constructor
Context:
Previously, MetaActionInfo comes from the local filesystem changes.
But now MCP Servers already provide a method to get information of MetaActions.
The pre- or post-dependencies are still required, for some MCP Tools cannot just be executed without additional context.
2025-12-18 17:49:52 +08:00
1e6ff1b30c chore(ActionCore): update outdated comment 2025-12-18 17:49:52 +08:00
0413fc281d chore(MetaAction): update outdated comment 2025-12-17 22:18:43 +08:00
8a7681ae31 chore(LocalRunnerClient): remove a redundant comment 2025-12-17 20:02:28 +08:00
1947f25ed6 feat(LocalRunnerClient): support executing origin actions
Context:
Origin actions are generated by DynamicActionGenerator and may optionally be
persistently serialized. This feature adds the basic execution flow for origin
actions within LocalRunnerClient.

Notes:
The current mapping between action files and their extensions is hardcoded. This should later be replaced with a configurable registry or loaded dynamically
during application startup.
2025-12-16 21:59:53 +08:00
488246525f chore(gitignore): exclude runtime data directory from version control 2025-12-16 21:39:11 +08:00
534dcd5ade fix(LocalRunnerClient): correctly capture stdout and stderr to avoid missing output in method exec 2025-12-16 21:30:51 +08:00
ad58c0cc7c refactor(LocalRunnerClient): allow injecting action watch path
Context:
The hardcoded action watch path made LocalRunnerClient difficult to test and
tightened it to a specific runtime layout. Injecting the watch path improves
testability and allows the runner to work in different runtime environments.
2025-12-16 21:02:29 +08:00
d546148d69 chore(test): organize experimental tests and test resources 2025-12-16 19:58:08 +08:00
bf2d5ac707 refactor(RunnerClient): restructure serialization and temp execution paths
Context:
Following the consolidation of action types into ORIGIN and MCP,
the serialization logic needs to be separated into dedicated methods.
These methods are invoked by DynamicActionGenerator.
2025-12-16 10:47:23 +08:00
628234f6e2 refactor(MetaActionType): redefine meta action types into MCP and ORIGIN
Context:
Previously, SCRIPT and PLUGIN were treated as separate action types,
but their semantics are already covered by MCP.
However, a generic execution path for locally generated actions is still
required, which is represented by ORIGIN.
2025-12-16 10:37:04 +08:00
4b852e0049 推进 ActionExecutor 下的‘行动生成与执行’部分
- 新增 RunnerClient 抽象类,并划分 SandboxRunnerClient、LocalRunnerClient两个子类(内容待完善)。前者负责对接 SandboxRunner 模块,后者直接使用本地作为执行环境(但不推荐)。
- 将 ActionWatchService 划为 LocalRunnerClient 的内部类,负责采用本地执行环境时,监听行动程序变化
- 完善 ActionRepairer 处的修复逻辑
- 调整 MetaAction 中路径获取逻辑

这提交方式真该调整一下了,这阶段推进容易攒太多,但又不好停手。或许阶段目标可以保留,但推进点应该可以细化🤔
2025-12-15 21:54:24 +08:00
6e3deced77 推进 ActionExecutor 下的 DynamicActionGenerator 子模块
- 完善了 DynamicActionGenerator 的大致逻辑,序列化逻辑待实现
- 补充了 PhaserRecord 中的阻塞逻辑,使用普通的线程sleep操作
- 调整了 MetaAction 中参数形式,由列表替换为 Map,便于执行时填写参数
- 完善了 DynamicActionGenerator 相关的数据类
2025-12-07 20:10:53 +08:00
6a351413a1 推进行动执行模块: 调整了 ActionExecutor 以支持行动链动态修复和参数提取; 完善了 ActionRepairer、ParamsExtractor 的主要逻辑; 完善了部分数据类的内容
- 在 ActionData 中新增 additionalContext 用于存储各个执行阶段临时修复生成的上下文,同样以执行阶段为键
- 调整 ActionExecutor 的输入参数,可传入用户标识,用于执行器调用 ActionRepairer 的修复过程
- 完善了 ActionExecutor 中行动单元的执行与修复逻辑,将支持正常状态推进执行、触发自对话时阻塞当前行动单元、所有修复方式失败时将整个行动数据标为 FAILED
- 完善了 ActionExecutor 中各个DTO的构建方法
- 完善了 ParamsExtractor 中的参数提取逻辑
- 在 PhaserRecord 中新增 interrupt 和 complete 方法,将用于后续行动单元的阻塞(ActionExecutor中)与恢复(InterventionHandler中)
- 完善了 ActionRepairer 中的修复逻辑,但自对话通道的暴露方式、DynamicActionGenerator 的具体逻辑待完善
2025-12-05 21:58:21 +08:00
ad973d4230 对 ActionExecutor 下子模块的功能分布、某些实体类进行了调整; 完善了 ActionExecutor 中的大致执行逻辑
- 梳理执行链路时发现 ActionRepairer 的能力明显超出可实现边界,故将其能力进行限定
- 新增 ActionCorrector 负责单组行动执行完毕后,根据意图和执行状况进行行动链修正
- 将 PhaserRecord 拆分为独立实体,未来将封装一部分流程控制逻辑
2025-12-02 22:35:53 +08:00
1d315a9b62 ActionExecutor 的执行流程规划完毕,具体逻辑待填充
- 调整了部分代码分布,移除了某些非必需的转发方法
- 新增几个 TODO 内容,后续工作已明确

这套调度方式看起来真的有些‘探索性质’了。实际上看起来有些像把 ReAct 的逻辑显式地进行了工程实现,不管是修复、依据状态选择行动单元生成还是阶段间针对行动单元的参数提取,在 ReAct Agent 中都是由一个智能体完成的。

但在这里,它要做的事情太多了,再加上 Partner 行动链的干预逻辑、幻觉参数又不可接受所以需要自对话或者用户干预,这些东西交给一个 ReAct 模块恐怕并不合适也不放心。所以这种显式模块划分应该更符合 Partner 行动模块的需求。

这点硬要说的话,应该还是在于‘ReAct 行为’并非 Partner 的全部吧。

不过谁知道呢,也许以后也会变,但这套至少现在看来是更能实现理想行为的
2025-12-01 19:25:21 +08:00
4e32129b31 优化行动链结构及相关组件、针对 ActionPlanner 相关组件做出调整
- 将existedMetaActions的实现由LinkedHashMap替换为HashMap,免去不必要的性能消耗
- 在 ActionCapability 中新增 listAvailableActions 方法用于获取当前存在的可用行动
- 将 ActionData 及相关类中的 LinkedHashMap 替换为普通Map,阶段并发将通过遍历key集合进行,而非针对原始行动链进行遍历
- 在 ActionPlanner 中完善行动链依赖修正逻辑,防止行动单元执行时的输入缺失
- 在 ActionEvaluator 中调整了 Prompt 构建方式
- 调整处理行动链相关代码,移除多余参数,简化方法签名
- 修正 EvaluatorResult 中行动链数据结构为Map,LLM将直接返回初始行动链,后续将加载行动数据并修复行动单元间的依赖关系
- 优化 InterventionHandler、ActionExecutor 等模块中对行动链Map的使用
2025-12-01 17:20:54 +08:00
3f59719e16 调整 MetaAction 的执行方式,将交给 ActionCapability、SandBoxRunnerClient 执行 2025-11-30 22:16:57 +08:00
c548cceec6 新增 SandboxRunner 项目子模块,该模块将在指定容器运行持久服务,与外部主进程通信,将用于后续执行JARSCRIPT两类行动类型 2025-11-30 18:41:42 +08:00
b3098310b4 完善了 ActionConfirmer 的遗漏逻辑 2025-11-30 15:16:57 +08:00
f48d559a7b 调整了 ActionInterventor 中数据构建方法的组织方式 2025-11-30 14:38:50 +08:00
14a57f0be6 推进行动干预模块,前置部分逻辑已基本完成
- 在`ActionData`中添加必要注释、新增`executingStage`字段表示当前执行阶段、移除了`WAITING`的状态类型
- 调整并修正了`ActionExecutor`中的`Phaser`阻塞逻辑
- 完善了`ActionInterventor`中`识别 -> 评估 -> 异步执行`的干预逻辑,并将干预结果以 Prompt 形式回写至流程上下文,作为主模块的已知内容
- 调整了干预模块内部的各个数据类的字段结构,适配干预流程
- 完善了`InterventionEvaluator`、`InterventionHandler`、`InterventionRecognizer`等必需的干预子模块
2025-11-29 20:56:29 +08:00
dff7b69b51 更新 README 2025-11-12 19:53:48 +08:00
d77ffd1db6 Merge remote-tracking branch 'origin/doc/architechture' into doc/architechture 2025-11-11 16:51:18 +08:00
264cdb09e5 推进行动干预模块; 接下来将进一步完善 InterventionHandler 的具体内容
- 调整相关目录为 interventor
-  调整了某些 ActionInterventor 的子模块用到的数据类结构
- 完善了 InterventionEvaluator 的具体逻辑
- 为 InterventionType 添加了注释,并新增了 CANCEL 干预类型
2025-11-11 16:11:09 +08:00
fea7f9c81f PerceiveSelector、PeiceiveUpdater 流程图制作完毕 2025-11-11 08:47:21 +08:00
a1520f117b 推进行动干预模块
- 完善了 ActionInterventor 中的具体逻辑以及不同情况下的prompt填充内容;
- 调整了 PreRunningModule 中的 getPromptDataMap 方法;
- 在 ActionCapability 中新增了检查 actionKey 是否存在的逻辑
2025-11-10 23:02:48 +08:00
ae5caf8475 更新 memory.md 2025-11-10 18:59:05 +08:00
980d9384d1 MemoryUpdater 流程图制作完毕 2025-11-08 17:33:05 +08:00
9ba0d1363a 创建了 action、memory、perceive 三类模块的流程文档; 完成了记忆模块中 MemorySelector 的流程图 2025-11-07 15:14:29 +08:00
f6d5cad5cd 更新 README 2025-11-07 13:51:30 +08:00
c3ca4145b8 推进行动干预模块
- 完善了大致的执行流程
- 明确并创建了评估与处理所需的数据类及干预类型
- 不同情况的Prompt处理结果、评估和处理的具体流程需要进一步完善
2025-11-06 22:07:27 +08:00
5419722c40 更新文档内容 2025-11-06 11:17:25 +08:00
31ebee3ded 制作了整体流程图 2025-11-06 11:14:37 +08:00
746fda1a5e 干预意图提取模块初步完成,Prompt 待制定; 在 ChatClient 中添加了默认的超时设定,超时时间后续可能需要调整。
另: 发现很多细节错误,比如“各个后置模块允许执行的条件”、“主模块出现异常时需要如何处理”、“模块Prompt的构建方式、采用格式不统一”等,需要后续进行修复或调整
2025-10-31 21:26:45 +08:00
ec4fbb7f19 行动干预足以抽离为新的前置模块,但仍属于‘行动’语义,大致框架已确立。后续实现时并发控制、各种干预的协调与触发时机需要注意。 2025-10-31 21:26:45 +08:00
f9c3cacfea 推进 ActionExecutor 相关的动态插拔式行动调度机制
- 移除先前构想的 SpecializedPartnerInputData 及相关类,无论是自反思、向用户求助还是用户主动干预,都应当通过语义识别来作用于对应行动事件,使用固定行动id的机制不足以支撑这种机制
- 在 ActionCore 中新增执行中行动的 phaser 管理逻辑
- 新增几个异常类,适用于行动数据加载的异常情况
- 新增 ActionIdentifier 负责行动干预意图的识别
-
2025-10-31 21:25:12 +08:00
e35e18f3b7 推进 ActionExecutor、确定动态插拔式行动调度的实现思路
- 在 ActionCore 中添加关闭hook,用于正确设置异常中断时执行中任务的状态
- 修正 actionPool 相关注释及用法
- 将 ActionData 中行动链字段调整为 LinkedHashMap 用于更好地支持分组并发及动态调度
- 重构 ActionExecutor 行动链执行逻辑,采用 Phaser 支持动态调度
- 扩展 InputData、Context 字段并调整 GateWay 格式化逻辑以适应特殊输入
2025-10-31 21:25:12 +08:00
83832d2060 推进 ActionExecutor、针对action core做出了一些调整
- 将 ActionWatchService 抽取为独立的类,使用构造参数传递所需内容
- ActionCore 中除了pendingAction外,将只维护一个行动池,通过用户键和STATUS区分类型
- 开始推进 ActionExecutor,但其中的同组并发、动态行动链、行动间参数对齐、参数重构等内容需要仔细考虑
2025-10-31 21:25:12 +08:00
4757425a15 推进 ActionDispatcher 模块、完善行动程序规范与加载逻辑
- 明确行动程序的存储形式与加载规则,分为执行程序和描述文件,前者负责逻辑,后者提供必要的描述性信息;
- 将 ActionInfo 重命名为 ActionData,更新相关接口和实现,增强代码一致性和可读性;
- 添加异常处理类以支持行动程序、描述信息的初始化和加载失败的场景;
- 实现行动程序目录的监控功能,支持行动程序的动态加载与管理;
- 明确了 ActionDispatcher 两个子模块的输入输出规范
2025-10-31 21:25:12 +08:00
21b3a0e846 开始推进 ActionDispatcher 模块
- ActionDispatcher 划分为 ActionScheduler 和 ActionExecutor 两个子模块,分别负责处理计划任务和即时任务
- 正式确定 Action 将以 ActionChain 的形式进行执行,也采用同组并发策略,按照 order 字段在 chain 中进行排序
- 调整了 ActionInfo 等类以适应当前的元行动类
- 对于行动能力的支持,或可考虑这几种方式: Agent自生成python脚本(必须经过验证,确认可执行且无风险)、MCP调用(需适配为Partner所支持的形式)、普通插件(在指定目录动态加载)
2025-10-31 21:25:12 +08:00
6bfa941c35 更新 README 2025-10-31 21:24:46 +08:00
456a7e04e8 更新 README 2025-10-24 17:29:55 +08:00
5864760f35 Action 模块语义缓存机制实现完毕,支持三种情况的语义缓存相关行为: 命中缓存且评估通过、命中缓存但评估未通过、未命中缓存但评估通过。将在评估过后步入主模块之前,进行异步更新操作(借助@AfterExecute注解,通过虚拟线程进入异步流程,在真正调用处使用平台线程加速计算) 2025-10-19 22:05:27 +08:00
aee6d879e9 推进 Action 模块语义缓存机制
- 完善缓存命中部分;
- 调整 ActionExtractor 以适配缓存逻辑
- 缓存更新大致框架待填充具体更新逻辑;
2025-10-18 21:56:50 +08:00
d1ea8dde79 推进 ActionExtractor 语义缓存机制: 移除了 VectorUtil,实现了 ollama、onnx runtime 两种向量客户端,通过 Agent 启动类暴露的后置启动任务加载并进行测试。 2025-10-17 11:20:11 +08:00
7094a8a68b 推进 ActionExtractor 语义缓存机制: 两种嵌入模型的连接方式测试完毕,在高性能主机上,可以通过ollama调用mxbai-embed-large这类模型,但放到4核8G香橙派3B就会出现推理时长过长,哪怕换成ONNX RUNTIME JAVA 也难以避免,但如果更换成 nomic-embed-text + ONNX RUNTIME JAVA ,仍能够拿到70左右ms的推理时长,远低于提取模型以及向量模型API的调用时长。预期可提供两种语义缓存所用的嵌入模型接入方式: 通过 http 调用 本地ollama接口; 指定 ONNX 格式的嵌入模型直接调用。 2025-10-16 23:04:41 +08:00
e78048f66d 推进 ActionExtractor: 新增语义向量计算工具;开始推进语义缓存相关;调整配置类格式 2025-10-16 15:39:38 +08:00
2f09c0cd71 推进 ActionExtractor: 完善大致逻辑,开始语义-行为缓存相关部分 2025-10-16 15:39:31 +08:00
8c43d6594f 推进 ActionPlanner: 新增行动确认机制,将与原‘提取-评估’流程并发执行; 将繁杂的装配逻辑封装在内部类ActionAssemblyHelper
# Conflicts:
#	Partner-Main/src/main/java/work/slhaf/partner/core/cache/CacheCapability.java
#	Partner-Main/src/main/java/work/slhaf/partner/core/memory/MemoryCore.java
#	Partner-Main/src/main/java/work/slhaf/partner/module/modules/memory/selector/MemorySelector.java
2025-10-16 15:39:16 +08:00
2d052442b1 推进 ActionPlanner: 添加行动短路机制,如果未提取到行动,则跳过评估子模块 2025-10-16 15:34:30 +08:00
84f7befb75 推进 ActionPlanner: 完成了 ActionPlanner 模块中的执行逻辑,同步调整了数据类中的字段。下一步将进行 ActionPlanner 子模块的开发。 2025-10-16 15:34:30 +08:00
85818556f8 将记忆模块的缓存逻辑迁移至 MemoryCore; 移除了 CacheCore,并将 CoordinatedManager 中原记忆模块与缓存模块中的逻辑迁移至现记忆模块中,确保语义正确 2025-10-16 15:22:19 +08:00
cb1a25e9d5 移除 ActiveData ,其逻辑回归至 CacheCore,下一步将对 CacheCore 及 CoordinateManager 中的 cognation 相关内容进行拆分 2025-10-16 11:40:55 +08:00
a10a149edb 开始推进行动模块(ActionModule); 针对框架与本体分别进行了一系列架构优化。
框架:
- 调整模块注册以及AgentRunningFlow的相关逻辑,以支持同组模块并发执行,将以@AgentModule注解中的order属性区分组间顺序先后及是否同组
- 针对@CoordinateManager注解新增了Core的自动注入处理,以便更好的协调不同Core的逻辑

本体: - 开始推进行动模块。将采取类似记忆模块的分层思路,分为ActionPlanner与ActionDispatcher两个主要模块,再各自细分子模块划分
- 将CognationCore从核心统筹的身份下降至与其他核心平级,同时将其中的序列化逻辑抽取至统一的PartnerCore父类,所有核心都将继承该类以获得序列化能力,不同core的内容将序列化至各自的memory文件
- 将SessionManager移除,相关逻辑迁移至CognationCore,统一序列化逻辑的同时又保证语义正确
- 将CognationCore中的某些缓存性质逻辑移动至CacheCore,确保语义正确
- 调整了目录结构以适应优化过的架构
2025-10-12 16:23:11 +08:00
41bf19f43e 将 .java 重命名为 .kt 2025-10-12 16:23:11 +08:00
941943f696 Partner 主体与框架适配完成! 完整逻辑已达到适配框架之前的完成度。发现并修复了不少问题,以及更新了README
框架:
- 由于`Gateway`的启动属于`Agent`启动流程的子线程,而主线程可能由于逻辑执行结束时机早于`Gateway`创建完成时机而报错,故引入`CountDownLatch`进行阻塞
- 在`AgentRunningModule`与`AgentRunningSubModule`中添加日志hook,记录模块执行的起始与截止时机
- 修复了`AgentUtil`中收集继承链时遗忘起始类的错误
- 在`CapabilityCheckFactory`中针对`CoordinateManager`无参构造方法的实现检验
- 在`CapabilityRegisterFactory`中添加了收集模块之外的CapabilityHolder的逻辑,与`@InjectCapability`的校验与注入逻辑保持一致
- 修复了‘生成模块启用配置时,多余局部变量导致无法执行流正确读取启用情况’的错误
- 在GlobalExceptionHandler中添加了对于未知异常的处理逻辑,确保不会导致程序异常终止
- 发现`ModuleProxyFactory`中使用`record`类型会导致`ByteBuddy`无法正确创建代理类,已修复,替换成普通类

本体:
- `ActiveData`由于`CognationCore`的引用,也需要实现序列化,已修复
- 修复了`MemorySelectExtractor`中由于匹配到的主题列表为空导致的空指针异常
- 将后置模块的trigger判定抽取到新的父类中,统一判断
- 修复了`WebSocketServer`如果存在过ws连接,关闭后短时间再次启动内仍提示端口占用的情况,设置允许端口重用
- 在`WebSocketGateway`新增了断开ws客户端连接的逻辑
2025-09-30 15:46:05 +08:00
a7d54349e4 进行 框架-主题 的适配测试,发现了一些问题并进行了修复
框架:
- 去除了 ActivateModel 中 modelKey() 方法的默认实现,对于特殊的 AgentModule 继承者(CoreModule)而言,直接获取注解信息不可行,如果保持,则需要另加判断逻辑。这是没有必要的
- 发现 Agent 启动流程中,由于 Gateway 的启动可能依赖配置文件的加载,故将 AgentConfigManager 与 AgentGateway 的指定替换为类型指定,在合适的时机通过反射进行实例化
- 在 AgentUtil 中新增了链式判断指定类的注解链上是否存在指定注解的方法,目前用于 CapabilityHolder 的持有实例判定
- 发现 CapabilityFactoryContext 中 cores、capabilities 未赋值导致空指针异常,已修复
- 将 AgentConfigManager 中的检验逻辑进行抽离,放到了 ConfigLoaderFactory 中,避免职责混淆
- 发现 CoreModule 的注解使用错误,`@Retention(RetentionPolicy.RUNTIME)`元注解可以使得注解在代码运行时能够被反射扫描
- 在 ModuleCheckFactory 中添加了对于 Module 与 SubModule 的注解、继承使用是否匹配的检验
- 发现对于一个类来说,无法直接通过一层反射获取到‘注解的注解’,故在 ModuleRegisterFactory 中针对 CoreModule 的注册做了特殊处理

主体:
- 发现一些类缺少必要注解,已修复
- 发现存在有些必要的类未公开化无参构造函数,已修复,并在框架部分增加校验逻辑

其他:
- 由于项目的启动流程与完整的配置文件密不可分,所以开始尝试编写启动说明,目前只写了开头
2025-09-21 23:29:45 +08:00
3c2ac32708 完成了本体与框架的适配工作,并修复了某些问题。需要进一步进行测试
- 修复了 CognationCapability 相关的注解使用错误
- 将前置模块中的 setAppendedPrompt 与 setActiveModule 方法抽取到 execute 模板方法中
- 完善了已有模块的适配工作, 并去除了不必要的单例配置
2025-09-18 16:03:59 +08:00
7f9d007f07 适配框架时发现工厂注册链上存在一些执行顺序上的错误,于是尝试修复问题,为Agent启动链添加了完整的注释,并做出了必要的修复与调整 2025-09-13 23:37:35 +08:00
c1018d6b54 进行 Partner 框架层的部分调整
- 新增 AgentSubModule 注解,用于标识子模块
- 新增 MetaSubModule 类,用于存储子模块元信息
- 支持子模块初始化和注入逻辑,不再使用单例模式为执行模块提供子模块服务
- 重构模块初始化流程,支持模块和子模块的初始化
- 优化模块注册流程,分别处理模块和子模块
2025-09-11 13:07:48 +08:00
47684c78e0 进行 Partner 本体对于框架的适配,以及框架层的部分调整
框架:

- 调整 ActivateModel 中模型初始化设置的 initHook 权重为-1(最优先)
- 为 AgentGateway 中 receive 操作提供模板方法,子类需实现发送逻辑并提供适配器
- 取消了 AgentInteractionAdapter 的单例配置
- 调整 RunningFlow 的异常处理,并在RunningFlowContext中提供错误码进行判断
- 调整模块基类
-

本体:
- 新增配置加载异常,继承自Agent启动异常
- 修改 GlobalExceptionData 获取逻辑
- 移除 MessageSender 等交互接口,适配框架的交互逻辑
- 异常处理已适配
- 配置加载逻辑已适配
- Gateway 已适配
- CoreModel 已适配
2025-09-09 20:42:28 +08:00
10fb689c83 完善了框架层的完整执行流程,待进行demo测试并适配进Partner本体。
- 调整 runtime 目录结构, flow/ 归为 interaction/ 的子包
- 新增了 AgentGateway 以及 Gateway 对应的 Adapter 抽象类,下游必须实现这两者才能启动项目
- 调整 MetaModule 中的某些字段类别, 锁定为 AgentRunningModule 相关子类, 便于在执行流中执行模块
- 修复了 ModuleCheckFactory 中对于 @AgentModule 的错误检验逻辑
- 更新 Agent 启动器为step builder模式进行逐步构建,强制实现 AgentGateway 相关内容, 其余带有默认实现的部分也可自定义实现,只需实现对应的类或接口
2025-08-13 22:03:32 +08:00
86548903a0 完善配置工厂遗留问题; 初步完善 AgentRunningFlow 流程相关。
- 修复了 ActivateModel 中 model 实例化后却未赋值的问题
- 调整 Api 包下目录结构, 新增 runtime 包用于存放运行时相关类
- 完善 AgentConfigManager 基类以及对应的默认实现类中的加载、序列化以及更新逻辑
- 将异常类型分类为‘启动时异常’与‘运行时异常’,前者将直接导致启动停止,后者可通过异常回调实现进行处理
- 新增全局异常处理类以及默认的异常回调实现
- 新增几个异常类
- 完善 Agent 链式构建流程, 实际上只是做了一些方法转发,但毕竟那些可提供自定义实现的,不管是factory还是manager、handler, 它们都过于分散了。
2025-08-11 00:19:08 +08:00
cf1578fd14 模块注解机制完成,待测试。
- 调整Api包下的目录结构
- 抽取方法‘递归收集某类的继承链上的所有类’中
- 移除 ModuleFactoryContext、ModuleRegisterFactory 中关于 Init 方法的加载逻辑,将在 ModuleInitHookExecuteFactory 中加载并执行
- 完善了 ActivateModel 接口中prompt通用加载的实现
- 移除原有的框架Demo实现,开始进行测试Demo的编写
2025-08-07 23:33:11 +08:00
35b7dc7cda 继续推进框架中的模块注册机制。
- 完善 ModuleProxyFactory 中的hook逻辑代理实现,从模块类开始,自下而上扫描继承链中每个类的hook方法, 收集完毕并排序后统一实现代理逻辑。
- 从 ModuleFactoryContext 、 ModuleRegisterFactory 中移除了原有的‘先注册hook方法’相关内容。
- 更新 README
2025-08-07 00:09:18 +08:00
b1ed79ae9d 推进框架中的模块注册机制; 完善启动逻辑流程。
- 调整 InteractionFlowContext 为抽象类,实现者的模块上下文应当继承自该类。
- 完善 Agent 启动类具体逻辑
- 取消 Agent 类中 launchRunners 中关闭虚拟线程池的行为,交由JVM等待线程关闭,而非直接停止整个进程。
- 调整原hook注解分别为 BeforeExecute 、 AfterExecute,该注解用于模块抽象类的子类时可抽取重复逻辑
- 新增 Init 注解,用于执行初始化逻辑,可通过 order 属性指定顺序,默认为 0
- ModuleRegisterFactory 中新增‘加载 Init 注解标注的方法’的相关逻辑
- ModuleCheckFactory 添加对于 Init 注解的校验
- 完善了 ModuleProxyFactory 中设置代理实例的内容,可同时添加pre、post hook逻辑
2025-08-06 00:17:10 +08:00
507917157d 推进框架中的模块注册机制。引入 ByteBuddy 完成针对模块的代理实现。
- 调整部分包结构
- 重构 AgentRegisterContext ,将不同的 Context 内容按照对应模块进行封装
- 调整了‘添加可扫描包’的添加逻辑、新增了添加外部目录的扫描逻辑
- 新增几个异常类
- 在 ModuleCheckFactory 中新增了对于执行模块‘是否实现无参构造方法’的校验逻辑
- 引入 ByteBuddy 库负责构造执行模块实例,并添加对应的hook逻辑
- 调整 ModuleRegisterFactory 的逻辑,允许注册加载Module内的Hook方法
- 调整依赖引用关系,因为Partner-Main、Partner-Demo都继承自Partner-Api包,故将通用依赖移动至Api的pom文件中
2025-08-05 01:01:42 +08:00
ca3ffca4ea 推进框架中的模块注册机制,完善了模块校验与加载,接下来应当进行对于PostHook的动态代理以及模块的实例化逻辑。
- 移除了 ActivateModel 中的 promptModule 方法,不再需要
- 添加了必要的注释
- 为 AgentRegisterFactory 添加了用于指定扫描包的方法
- 新增了几个异常类
- 新增 MetaModule 类,包含Agent执行模块的必要信息,在工厂流程中作为执行模块的上下文
- 完善了 ModuleCheckFactory 中的检查逻辑
2025-08-03 23:48:20 +08:00
3c41abbba8 完善配置加载逻辑.
- 在 AgentRegisterContext 中新增对应的配置字段,供后续与模块协同检验
- 调整 ActivateModel 接口中的 modelSettings 实现逻辑
- 可以通过 ConfigLoaderFactory 提供的静态方法设置所需的配置管理类,并提供加载逻辑实现
- 为 ModelConfigManager 提供了默认的实现,从运行目录下加载模型配置与模块对应的提示词
- 实现了 ModelConfigManager 中的初步检验,检测是否存在默认模型配置与基础提示词,以及是否存在多余的模型配置。
- 新增了两个实体类,对应从文件加载的原始配置、提示词数据。
2025-08-02 23:04:15 +08:00
64a7ed261e 新增配置加载功能并优化模型设置
- 新增 ConfigLoaderFactory 和 ModelConfigFactory 以及对应的默认实现用于加载模型配置和提示词列表
- 重构 ActivateModel 接口,支持基本提示和特定提示的加载,具体逻辑待实现,可通过ModelConfigFactory加载
- 优化模块注册和能力注入相关逻辑
- 添加了必要注释
2025-07-31 22:13:10 +08:00
ade922cbc2 推进核心服务与模块注册机制
- 完善Agent流程执行框架
- Api包下新增flow流程包,该部分对应模块的执行流程
- 明确ModuleFactory与CapabilityFactory以及ModuleHook的共同运作流程
- 调整了Hook注解名称
2025-07-25 23:46:54 +08:00
effa1df7fa 需继续为上层模块构建注册体系以适应完整的加载逻辑。
- 移除了 BaseCoordinateManager 抽象类,而是添加了 @CoordinateManager 注解
- 移除了 CapabilityHolder 抽象类,换成 @CapabilityHolder 注解
- 新增了适应新注册机制的部分类,仍需进一步推进
2025-07-22 22:04:46 +08:00
954095aa55 - 新建模块Partner-Api,推进Partner适配核心服务注册机制。
- 将原有的模块体系进一步区分,分离模型持有能力与调用能力,Model将有Module自身持有,可通过ActivateModel开启相应能力
2025-07-21 23:47:52 +08:00
c9c9b05f18 核心服务注册机制完成,Partner待适配
- 将`methodSignature`抽取至工具类中
- 新增了数个异常类,适配工厂注册时的异常处理
- 完善了核心服务的注解检测、函数路由表生成以及代理动态注入实现。
2025-07-17 19:08:13 +08:00
dd10b00fb6 推进核心服务注册机制,并调整了Partner的模块结构
- 为了方便调试,将项目分为两个子模块,demo模块中进行新机制的开发工作,core模块为原来的Partner项目;
- 新增了多个注解,用于适配新的核心服务注册机制;
- 在`CapabilityRegisterFactory`中,将首先启动`statusCheck`,检查各个注解是否正常工作,包括以下内容:
   - `CapabilityCore`核心服务与`Capability`接口是否匹配
   - 核心服务中的`CapabilityMethod`是否与`Capability`接口中的方法匹配
   - 是否存在待协调方法`ToCoordinatedMethod`以及对应的存在于`BaseCognationManager`子类实现中
2025-07-15 16:48:27 +08:00
98d830d08b 调整包结构; 新增调度模块大致框架; 尝试实现能力注册与注入机制,减轻重复逻辑,增强扩展性 2025-07-13 23:05:06 +08:00
192ae1bf5f 第一版感知模块完毕,设计了该模块的提示词,支持态度印象关系以及变化历史等层面的关系建模。
计划先推进调度模块,至于‘自我认知’‘情绪状态’等模块,或许可以划分为感知模块的子模块,等待后续补充。
2025-07-11 21:28:32 +08:00
a1d3c1e295 初步完善感知相关模块,提示词待设计:
- 修复了`Config`中生成的配置文件的模块链未加入`PostprocessExecutor`的问题
- 发现`InteractionHub`中还留有未使用的`coreModel`、`taskScheduler`,已删除
- 将`PerceiveUpdater`感知更新模块的提取逻辑下放到感知子模块`RelationExtractor`和`StaticMemoryExtractor`,感知更新主模块只负责将两个子模块的执行进行并发以及整合结果,最终提交给`PerceiveCapability`进行更新
-
2025-07-07 16:26:04 +08:00
9302417e58 Partner开发正式重启,回顾并继续推进感知模块:
- 在MemoryGraph新增用户索引,用于后续感知等模块的触发流程
- CognationManager及相关调用链中updateUser()方法的参数调整为User, 不再是PerceiveChatResult, 后者会跨越分层影响架构
- Model子类移除字段: MODEL_KEY,同时在setModel()中将不再需要传递MODEL_KEY参数,只需要实现modelKey()方法即可
2025-07-05 23:34:22 +08:00
e9053a4e88 推进感知模块相关开发,这部分倒意外地简单,现在有些基础,可能以后会有改进
- 新增 PerceiveCore 中的 updateUser 方法
- 新增了 PerceiveSelector 用于获取用户信息,提供基础的身份建模信息
- 新增 PerceiveUpdater 类用于异步更新用户身份感知
- 抽取 MemoryUpdater 中的执行判定逻辑至新增的 PostprocessExecutor 类,判定逻辑适用于多个`后处理模块`
- 重构 Model 类,改为抽象类,将modelKey定义为抽象方法,强制规范子类实现
2025-06-12 22:08:34 +08:00
f5c37f26a5 重构认知模块、着手感知模块相关开发
- 调整MemoryManager为CognationManager
- 由于CognationManager方法数量过多,根据能力分类抽取为四个主要接口CognationCapability、CacheCapability、MemoryCapability、PerceiveCapability
- 开始推进感知模块开发
2025-06-11 16:48:55 +08:00
d11d39ea81 重构拆分原‘记忆图谱’以适应后续扩展
- 拆分原`MemoryGraph`为 MemoryCore, CacheCore, GraphCore, PerceiveCore几个部分.
- MemoryCore中将不再包含操作逻辑, 由MemoryManager统一处理, 序列化逻辑仍交给MemoryCore。
- 更新README
2025-06-06 19:28:10 +08:00
407181db05 进行第二阶段调试修复: 部分InteractionContext相关类没有实现序列化,已修复 2025-06-06 10:55:34 +08:00
319 changed files with 26216 additions and 4715 deletions

38
.gitignore vendored
View File

@@ -8,6 +8,8 @@ target/
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/compiler.xml .idea/compiler.xml
.idea/libraries/ .idea/libraries/
.idea/db-forest-config.xml
.idea/markdown.xml
*.iws *.iws
*.iml *.iml
*.ipr *.ipr
@@ -36,16 +38,28 @@ build/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
/data/ /backup/data/
/config/ /backup/config/
/src/test/java/memory/test.json /Partner-Core/src/main/java/src/test/java/memory/test.json
/src/test/java/memory/result/input1.json /Partner-Core/src/main/java/src/test/java/memory/result/input1.json
/src/test/java/memory/result/input2.json /Partner-Core/src/main/java/src/test/java/memory/result/input2.json
/src/test/java/memory/result/output1.json /Partner-Core/src/main/java/src/test/java/memory/result/output1.json
/src/test/java/memory/result/output2.json /Partner-Core/src/main/java/src/test/java/memory/result/output2.json
/src/test/java/memory/result/total_input.json /Partner-Core/src/main/java/src/test/java/memory/result/total_input.json
/src/test/java/memory/result/input3.json /Partner-Core/src/main/java/src/test/java/memory/result/input3.json
/src/test/java/memory/result/input4.json /Partner-Core/src/main/java/src/test/java/memory/result/input4.json
/src/test/java/memory/result/primary_input.json /Partner-Core/src/main/java/src/test/java/memory/result/primary_input.json
/src/main/resources/prompt/module/memory/topic_extractor.json.bak /Partner-Core/src/main/java/src/main/resources/prompt/module/memory/topic_extractor.json.bak
/backup/ /backup/
/Partner-Main/src/test/java/text/test.json
/CLAUDE.md
/config/
/data/
/generated-classes/
/.idea/copilot.data.migration.ask2agent.xml
/Partner-Main/data/
/AGENTS.md
/.serena/
/Partner-Core/data/
/.ai/mcp/mcp.json
/.codex

1
.idea/.gitignore generated vendored
View File

@@ -6,3 +6,4 @@
# Datasource local storage ignored files # Datasource local storage ignored files
/dataSources/ /dataSources/
/dataSources.local.xml /dataSources.local.xml
/inspectionProfiles/Project_Default.xml

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

8
.idea/dictionaries/project.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>openai</w>
<w>zuper</w>
</words>
</dictionary>
</component>

11
.idea/encodings.xml generated
View File

@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Encoding"> <component name="Encoding">
<file url="file://$PROJECT_DIR$/Partner-Common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Framework/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Framework/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-SandboxRunner/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component> </component>

6
.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.2.0" />
</component>
</project>

36
.idea/misc.xml generated
View File

@@ -1,12 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager">
<list size="19">
<item index="0" class="java.lang.String" itemvalue="lombok.Data" />
<item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" />
<item index="2" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
<item index="3" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
<item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" />
</list>
<writeAnnotations>
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />
<writeAnnotation name="work.slhaf.partner.api.agent.factory.component.annotation.InjectModule" />
<writeAnnotation name="work.slhaf.partner.api.agent.factory.module.annotation.InjectModule" />
<writeAnnotation name="work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability" />
<writeAnnotation name="work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule" />
</writeAnnotations>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager"> <component name="MavenProjectsManager">
<option name="originalFiles"> <option name="originalFiles">
<list> <list>
<option value="$PROJECT_DIR$/pom.xml" /> <option value="$PROJECT_DIR$/pom.xml" />
<option value="$PROJECT_DIR$/PartnerExecutor/pom.xml" />
</list> </list>
</option> </option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/Partner-Test-Demo/pom.xml" />
</set>
</option>
</component> </component>
<component name="PWA"> <component name="PWA">
<option name="enabled" value="true" /> <option name="enabled" value="true" />

28
Partner-Common/pom.xml Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>work.slhaf</groupId>
<artifactId>Partner</artifactId>
<version>0.5.0</version>
</parent>
<artifactId>Partner-Common</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.modelcontextprotocol.sdk/mcp -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.17.0</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -0,0 +1,155 @@
package work.slhaf.partner.common.mcp;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.server.McpStatelessServerHandler;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
public final class InProcessMcpTransport implements McpClientTransport, McpStatelessServerTransport {
// 每个 transport 只处理一条 inbound 流
private final Sinks.Many<McpSchema.JSONRPCMessage> inbound =
Sinks.many().unicast().onBackpressureBuffer();
private final AtomicBoolean clientConnected = new AtomicBoolean(false);
private final AtomicBoolean serverConnected = new AtomicBoolean(false);
/**
* 对端
*/
private volatile InProcessMcpTransport peer;
private volatile McpStatelessServerHandler serverHandler;
public static Pair pair() {
InProcessMcpTransport client = new InProcessMcpTransport();
InProcessMcpTransport server = new InProcessMcpTransport();
client.peer = server;
server.peer = client;
return new Pair(client, server);
}
/* ======================================================
* Internal receive: peer.sendMessage -> this.receive
* ====================================================== */
private void receive(McpSchema.JSONRPCMessage message) {
if (inbound.tryEmitNext(message).isFailure()) {
throw new RuntimeException("Failed to receive message: " + message);
}
}
/* ======================================================
* Client → Server sendMessage
* ====================================================== */
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
InProcessMcpTransport p = this.peer;
if (p == null) {
return Mono.error(new IllegalStateException("Transport is not linked"));
}
return Mono.fromRunnable(() -> p.receive(message));
}
/* ======================================================
* Client connect(handler) 处理 server → client 消息
* ====================================================== */
@Override
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
if (!clientConnected.compareAndSet(false, true)) {
return Mono.error(new IllegalStateException("Client already connected"));
}
return inbound.asFlux()
.concatMap(msg ->
handler.apply(Mono.just(msg))
// handler may emit response message → send back to server
.flatMap(resp -> resp != null ? sendMessage(resp) : Mono.empty())
)
.doFinally(sig -> clientConnected.set(false))
.then();
}
@Override
public void setExceptionHandler(Consumer<Throwable> handler) {
McpClientTransport.super.setExceptionHandler(handler);
}
/* ======================================================
* Server: bind stateless handler = process client → server inbound
* ====================================================== */
@Override
public void setMcpHandler(McpStatelessServerHandler handler) {
this.serverHandler = handler;
if (!serverConnected.compareAndSet(false, true)) {
throw new IllegalStateException("Server already connected");
}
// 订阅 client → server 消息
inbound.asFlux()
.concatMap(this::handleServerMessage)
.doFinally(sig -> serverConnected.set(false))
.subscribe();
}
/**
* Server 端处理 JSONRPCMessage
*/
private Mono<Void> handleServerMessage(McpSchema.JSONRPCMessage msg) {
// 创建 transport context简单实现即可
McpTransportContext ctx = key -> null;
if (msg instanceof McpSchema.JSONRPCRequest req) {
return serverHandler.handleRequest(ctx, req)
.flatMap(this::sendMessage);
}
if (msg instanceof McpSchema.JSONRPCNotification noti) {
return serverHandler.handleNotification(ctx, noti);
}
return Mono.empty();
}
@Override
public void close() {
McpClientTransport.super.close();
}
/* ======================================================
* other boilerplate
* ====================================================== */
@Override
public Mono<Void> closeGracefully() {
inbound.tryEmitComplete();
clientConnected.set(false);
serverConnected.set(false);
return Mono.empty();
}
@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return McpJsonMapper.getDefault().convertValue(data, typeRef);
}
@Override
public List<String> protocolVersions() {
return McpClientTransport.super.protocolVersions();
}
public record Pair(InProcessMcpTransport clientSide, InProcessMcpTransport serverSide) {
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>Partner</artifactId>
<groupId>work.slhaf</groupId>
<version>0.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Partner-Main</artifactId>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>work.slhaf.partner.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

140
Partner-Core/pom.xml Normal file
View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>work.slhaf</groupId>
<artifactId>Partner</artifactId>
<version>0.5.0</version>
</parent>
<artifactId>Partner-Core</artifactId>
<dependencies>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>work.slhaf</groupId>
<artifactId>Partner-Framework</artifactId>
<version>0.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api -->
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-api</artifactId>
<version>1.0.0-M2.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.onnxruntime</groupId>
<artifactId>onnxruntime</artifactId>
<version>1.23.1</version>
</dependency>
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>0.34.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.modelcontextprotocol.sdk/mcp -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.17.0</version>
</dependency>
<dependency>
<groupId>work.slhaf</groupId>
<artifactId>Partner-Common</artifactId>
<version>0.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.20.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.2.1</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>work.slhaf.partner.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>${maven.compiler.target}</jvmTarget>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package work.slhaf.partner;
import work.slhaf.partner.common.vector.VectorClientRegistry;
import work.slhaf.partner.framework.agent.Agent;
import work.slhaf.partner.runtime.gateway.WebSocketGatewayRegistration;
public class Main {
public static void main(String[] args) {
Agent.newAgent(Main.class)
.addGatewayRegistration(WebSocketGatewayRegistration.INSTANCE)
.addConfigurable(new VectorClientRegistry())
.launch();
}
}

View File

@@ -0,0 +1,123 @@
package work.slhaf.partner.common.base
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
abstract class Block(
val blockName: String
) {
fun encodeToXml(): Element {
val document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument()
val root = document.createElement(blockName)
document.appendChild(root)
appendRootAttributes().forEach { attribute, value -> root.setAttribute(attribute, value) }
fillXml(document, root)
return root
}
protected open fun appendRootAttributes(): Map<String, String> {
return emptyMap()
}
fun encodeToXmlString(): String {
val transformer = TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
}
return StringWriter().use { writer ->
transformer.transform(DOMSource(encodeToXml()), StreamResult(writer))
writer.toString()
}
}
protected abstract fun fillXml(document: Document, root: Element)
protected fun appendTextElement(
document: Document,
parent: Element,
tagName: String,
value: Any?
): Element {
val element = document.createElement(tagName)
element.textContent = value?.toString() ?: ""
parent.appendChild(element)
return element
}
protected fun appendChildElement(
document: Document,
parent: Element,
tagName: String,
block: Element.() -> Unit = {}
): Element {
val element = document.createElement(tagName)
parent.appendChild(element)
element.block()
return element
}
protected fun appendCDataElement(
document: Document,
parent: Element,
tagName: String,
value: String?
): Element {
val element = document.createElement(tagName)
element.appendChild(document.createCDATASection(value ?: ""))
parent.appendChild(element)
return element
}
@JvmOverloads
protected fun <T> appendListElement(
document: Document,
parent: Element,
wrapperTagName: String,
itemTagName: String,
values: Iterable<T>,
block: Element.(T) -> Unit = { value ->
textContent = value?.toString() ?: ""
}
): Element {
val wrapper = document.createElement(wrapperTagName)
parent.appendChild(wrapper)
for (value in values) {
val item = document.createElement(itemTagName)
wrapper.appendChild(item)
item.block(value)
}
return wrapper
}
@JvmOverloads
protected fun <T> appendRepeatedElements(
document: Document,
parent: Element,
itemTagName: String,
values: Iterable<T>,
block: Element.(T) -> Unit = { value ->
textContent = value?.toString() ?: ""
}
) {
for (value in values) {
val item = document.createElement(itemTagName)
parent.appendChild(item)
item.block(value)
}
}
}

View File

@@ -0,0 +1,59 @@
package work.slhaf.partner.common.vector;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
import java.util.Map;
@Slf4j
public class OllamaVectorClient extends VectorClient {
private final String ollamaEmbeddingUrl;
private final String ollamaEmbeddingModel;
protected OllamaVectorClient(String url, String model) {
this.ollamaEmbeddingUrl = url;
this.ollamaEmbeddingModel = model;
compute("test");
}
@Override
protected float[] doCompute(String input) {
Map<String, String> param = Map.of("model", ollamaEmbeddingModel, "input", input);
HttpRequest request = HttpRequest.get(ollamaEmbeddingUrl).body(JSONObject.toJSONString(param));
try (HttpResponse response = request.execute()) {
if (!response.isOk())
throw new VectorClientExecutionException(
"Failed to execute embedding model",
"ollama",
"COMPUTE",
ollamaEmbeddingUrl
);
String resStr = response.body();
EmbeddingModelResponse embeddingResponse = JSONObject.parseObject(resStr, EmbeddingModelResponse.class);
return embeddingResponse.getEmbeddings()[0];
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to execute embedding model",
"ollama",
"COMPUTE",
ollamaEmbeddingUrl,
e
);
}
}
@Data
private static class EmbeddingModelResponse {
private String model;
private float[][] embeddings;
private long total_duration;
private long load_duration;
private int prompt_eval_count;
}
}

View File

@@ -0,0 +1,100 @@
package work.slhaf.partner.common.vector;
import ai.djl.huggingface.tokenizers.Encoding;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.onnxruntime.OnnxTensor;
import ai.onnxruntime.OrtEnvironment;
import ai.onnxruntime.OrtSession;
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("FieldMayBeFinal")
public class OnnxVectorClient extends VectorClient {
private String tokenizerPath;
private String modelPath;
private HuggingFaceTokenizer tokenizer;
private OrtSession session;
private OrtEnvironment env;
protected OnnxVectorClient(String tokenizer, String model) {
this.tokenizerPath = tokenizer;
this.modelPath = model;
loadTokenizer();
loadModel();
compute("test");
}
private void loadModel() {
try {
env = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions ops = new OrtSession.SessionOptions();
session = env.createSession(modelPath, ops);
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to load ONNX model",
"onnx",
"MODEL_LOAD",
modelPath,
e
);
}
}
private void loadTokenizer() {
try {
tokenizer = HuggingFaceTokenizer.newInstance(Path.of(tokenizerPath));
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to load tokenizer",
"onnx",
"TOKENIZER_LOAD",
tokenizerPath,
e
);
}
}
@Override
protected float[] doCompute(String input) {
try {
Encoding encode = tokenizer.encode(input);
long[] ids = encode.getIds();
long[] attentionMask = encode.getAttentionMask();
long[][] inputIdsBatch = {ids};
long[][] attentionMaskBatch = {attentionMask};
long[][] tokenTypeIdsBatch = {new long[ids.length]}; // 初始化全 0
for (int i = 0; i < ids.length; i++)
tokenTypeIdsBatch[0][i] = 0;
OnnxTensor inputTensor = OnnxTensor.createTensor(env, inputIdsBatch);
OnnxTensor maskTensor = OnnxTensor.createTensor(env, attentionMaskBatch);
OnnxTensor tokenTypeTensor = OnnxTensor.createTensor(env, tokenTypeIdsBatch);
Map<String, OnnxTensor> inputs = new HashMap<>();
inputs.put("input_ids", inputTensor);
inputs.put("attention_mask", maskTensor);
inputs.put("token_type_ids", tokenTypeTensor);
OrtSession.Result result = session.run(inputs);
OnnxTensor embeddingTensor = (OnnxTensor) result.get(0);
return embeddingTensor.getFloatBuffer().array();
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to execute embedding model",
"onnx",
"COMPUTE",
modelPath,
e
);
}
}
}

View File

@@ -0,0 +1,105 @@
package work.slhaf.partner.common.vector;
import lombok.extern.slf4j.Slf4j;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.ops.transforms.Transforms;
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
import work.slhaf.partner.common.vector.exception.VectorClientStartupException;
@Slf4j
public abstract class VectorClient {
public static boolean status = false;
public static VectorClient INSTANCE;
public static void startClient(VectorConfig config) {
try {
if (config instanceof VectorConfig.Ollama ollama) {
INSTANCE = new OllamaVectorClient(ollama.ollamaEmbeddingUrl, ollama.ollamaEmbeddingModel);
} else if (config instanceof VectorConfig.Onnx onnx) {
INSTANCE = new OnnxVectorClient(onnx.tokenizerPath, onnx.embeddingModelPath);
} else {
return;
}
status = true;
} catch (VectorClientStartupException e) {
throw e;
} catch (VectorClientExecutionException e) {
throw new VectorClientStartupException(
"Vector client startup failed",
e.getClientType(),
"COMPUTE".equals(e.getPhase()) ? "STARTUP_SELF_TEST" : e.getPhase(),
e.getTarget(),
e
);
} catch (Exception e) {
throw new VectorClientStartupException(
"Vector client startup failed",
resolveClientType(config),
"STARTUP",
resolveTarget(config),
e
);
}
}
public float[] compute(String input) {
if (!status) {
return null;
}
return doCompute(input);
}
protected abstract float[] doCompute(String input);
private static String resolveClientType(VectorConfig config) {
if (config instanceof VectorConfig.Onnx) {
return "onnx";
}
if (config instanceof VectorConfig.Ollama) {
return "ollama";
}
return "unknown";
}
private static String resolveTarget(VectorConfig config) {
if (config instanceof VectorConfig.Onnx onnx) {
return onnx.embeddingModelPath;
}
if (config instanceof VectorConfig.Ollama ollama) {
return ollama.ollamaEmbeddingUrl;
}
return null;
}
public double compare(float[] v1, float[] v2) {
if (!status) {
return 0;
}
try (INDArray a1 = Nd4j.create(v1); INDArray a2 = Nd4j.create(v2)) {
return Transforms.cosineSim(a1, a2);
}
}
public float[] weightedAverage(float[] newVector, float[] primaryVector) {
try (INDArray primary = Nd4j.create(primaryVector);
INDArray latest = Nd4j.create(newVector)) {
// 1⃣ 计算余弦相似度
double similarity = Transforms.cosineSim(primary, latest);
// 2⃣ 根据相似度决定更新比例 α(差异越大,新输入影响越强)
double alpha = (1.0 - similarity) * 0.5;
alpha = Math.clamp(alpha, 0.05, 0.5);
// 3⃣ 按比例混合旧向量与新向量
INDArray updated = primary.mul(1 - alpha).add(latest.mul(alpha));
// 4⃣ 归一化结果(保持方向空间一致)
updated = updated.div(updated.norm2Number());
return updated.toFloatVector();
}
}
}

View File

@@ -0,0 +1,50 @@
package work.slhaf.partner.common.vector;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.config.Config;
import work.slhaf.partner.framework.agent.config.ConfigRegistration;
import work.slhaf.partner.framework.agent.config.Configurable;
import java.nio.file.Path;
import java.util.Map;
public class VectorClientRegistry implements Configurable, ConfigRegistration<VectorConfig> {
@Override
public void init(@NotNull VectorConfig config, @Nullable JSONObject json) {
if (!config.enabled) {
return;
}
if (config.type == null) {
return;
}
if (json == null) {
return;
}
config = switch (config.type) {
case ONNX -> json.toJavaObject(VectorConfig.Onnx.class);
case OLLAMA -> json.toJavaObject(VectorConfig.Ollama.class);
};
VectorClient.startClient(config);
}
@Override
@NotNull
public Class<VectorConfig> type() {
return VectorConfig.class;
}
@Nullable
@Override
public VectorConfig defaultConfig() {
return new VectorConfig(false, null);
}
@Override
public @NotNull Map<Path, ConfigRegistration<? extends Config>> declare() {
return Map.of(Path.of("vector.json"), this);
}
}

View File

@@ -0,0 +1,44 @@
package work.slhaf.partner.common.vector;
import work.slhaf.partner.framework.agent.config.Config;
public sealed class VectorConfig extends Config permits VectorConfig.Ollama, VectorConfig.Onnx {
final boolean enabled;
final Type type;
public VectorConfig(boolean enabled, Type type) {
this.enabled = enabled;
this.type = type;
}
public enum Type {
ONNX,
OLLAMA
}
static final class Onnx extends VectorConfig {
final String tokenizerPath;
final String embeddingModelPath;
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath) {
super(enabled, type);
this.tokenizerPath = tokenizerPath;
this.embeddingModelPath = embeddingModelPath;
}
}
static final class Ollama extends VectorConfig {
final String ollamaEmbeddingUrl;
final String ollamaEmbeddingModel;
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel) {
super(enabled, type);
this.ollamaEmbeddingUrl = ollamaEmbeddingUrl;
this.ollamaEmbeddingModel = ollamaEmbeddingModel;
}
}
}

View File

@@ -0,0 +1,49 @@
package work.slhaf.partner.common.vector.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class VectorClientExecutionException extends AgentRuntimeException {
private final String clientType;
private final String phase;
private final String target;
public VectorClientExecutionException(String message, String clientType, String phase, @Nullable String target) {
super(message);
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
public VectorClientExecutionException(String message, String clientType, String phase, @Nullable String target, Throwable cause) {
super(message, cause);
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
public String getClientType() {
return clientType;
}
public String getPhase() {
return phase;
}
public String getTarget() {
return target;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("clientType", clientType);
report.getExtra().put("phase", phase);
if (target != null) {
report.getExtra().put("target", target);
}
return report;
}
}

View File

@@ -0,0 +1,37 @@
package work.slhaf.partner.common.vector.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class VectorClientStartupException extends AgentStartupException {
private final String clientType;
private final String phase;
private final String target;
public VectorClientStartupException(String message, String clientType, String phase, @Nullable String target) {
super(message, "vector-client-registry");
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
public VectorClientStartupException(String message, String clientType, String phase, @Nullable String target, Throwable cause) {
super(message, "vector-client-registry", cause);
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("clientType", clientType);
report.getExtra().put("phase", phase);
if (target != null) {
report.getExtra().put("target", target);
}
return report;
}
}

View File

@@ -0,0 +1,41 @@
package work.slhaf.partner.core.action;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.ExecutableAction;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@Capability(value = "action")
public interface ActionCapability {
void putAction(@NonNull ExecutableAction executableAction);
Set<ExecutableAction> listActions(@Nullable ExecutableAction.Status status, @Nullable String source);
ExecutorService getExecutor(ActionCore.ExecutorType type);
Result<MetaAction> loadMetaAction(@NonNull String actionKey);
Result<MetaActionInfo> loadMetaActionInfo(@NonNull String actionKey);
void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions);
Map<String, MetaActionInfo> listAvailableMetaActions();
boolean checkExists(String... actionKeys);
RunnerClient runnerClient();
void handleInterventions(List<MetaIntervention> interventions, ExecutableAction data);
}

View File

@@ -0,0 +1,306 @@
package work.slhaf.partner.core.action;
import com.alibaba.fastjson2.JSONObject;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.ExecutableAction;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.action.exception.ActionLookupException;
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.framework.agent.config.ConfigCenter;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.factory.context.Shutdown;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.framework.agent.support.Result;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@SuppressWarnings("FieldMayBeFinal")
@CapabilityCore(value = "action")
@Slf4j
public class ActionCore implements StateSerializable {
public static final String BUILTIN_LOCATION = "builtin";
public static final String ORIGIN_LOCATION = "origin";
// 由于当前的执行器逻辑实现,平台线程池大小不得小于 2这里规定为最小为 4
private final ExecutorService platformExecutor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4));
private final ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
/**
* 已存在的行动程序,键格式为‘<MCP-ServerName>::<Tool-Name>’,值为 MCP Server 通过 Resources 相关渠道传递的行动程序元信息
*/
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
/**
* 持久行动池
*/
private CopyOnWriteArraySet<ExecutableAction> actionPool = new CopyOnWriteArraySet<>();
private RunnerClient runnerClient;
public ActionCore() throws IOException, ClassNotFoundException {
String baseActionPath = ConfigCenter.INSTANCE.getPaths().getResourcesDir().resolve("action").normalize().toAbsolutePath().toString();
// TODO 通过 Config 指定采用何种 runnerClient当前只提供 LocalRunnerClient
runnerClient = new LocalRunnerClient(existedMetaActions, virtualExecutor, baseActionPath);
register();
}
@Shutdown
public void shutdown() {
try {
runnerClient.close();
} catch (Exception e) {
log.warn("runner client close error", e);
}
try {
platformExecutor.shutdown();
virtualExecutor.shutdown();
int count = 0;
if (!platformExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
count += platformExecutor.shutdownNow().size();
}
if (!virtualExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
count += virtualExecutor.shutdownNow().size();
}
if (count != 0) {
log.warn("{} tasks still running", count);
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
@CapabilityMethod
public void putAction(@NonNull ExecutableAction executableAction) {
actionPool.removeIf(data -> data.getUuid().equals(executableAction.getUuid())); // 用来应对 ScheduledActionData 的重新排列
actionPool.add(executableAction);
}
@CapabilityMethod
public Set<ExecutableAction> listActions(@Nullable ExecutableAction.Status status, @Nullable String source) {
return actionPool.stream()
.filter(actionData -> status == null || actionData.getStatus().equals(status))
.filter(actionData -> source == null || actionData.getSource().equals(source))
.collect(Collectors.toSet());
}
@CapabilityMethod
public ExecutorService getExecutor(ExecutorType type) {
return switch (type) {
case VIRTUAL -> virtualExecutor;
case PLATFORM -> platformExecutor;
};
}
@CapabilityMethod
public void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions) {
existedMetaActions.putAll(metaActions);
}
@CapabilityMethod
public Map<String, MetaActionInfo> listAvailableMetaActions() {
return existedMetaActions;
}
@CapabilityMethod
public Result<MetaAction> loadMetaAction(@NonNull String actionKey) {
MetaActionInfo metaActionInfo = existedMetaActions.get(actionKey);
if (metaActionInfo == null) {
return Result.failure(new ActionLookupException(
"Meta action info not found for action key: " + actionKey,
actionKey,
"META_ACTION"
));
}
String[] split = actionKey.split("::", 2);
if (split.length < 2) {
return Result.failure(new ActionLookupException(
"Invalid action key format: " + actionKey,
actionKey,
"META_ACTION"
));
}
MetaAction.Type type = switch (split[0]) {
case BUILTIN_LOCATION -> MetaAction.Type.BUILTIN;
case ORIGIN_LOCATION -> MetaAction.Type.ORIGIN;
default -> MetaAction.Type.MCP;
};
return Result.success(new MetaAction(
split[1],
metaActionInfo.getIo(),
metaActionInfo.getLauncher(),
type,
split[0]
));
}
@CapabilityMethod
public Result<MetaActionInfo> loadMetaActionInfo(@NonNull String actionKey) {
MetaActionInfo info = existedMetaActions.get(actionKey);
if (info == null) {
return Result.failure(new ActionLookupException(
"Meta action description not found for action key: " + actionKey,
actionKey,
"META_ACTION_INFO"
));
}
return Result.success(info);
}
@CapabilityMethod
public boolean checkExists(String... actionKeys) {
return existedMetaActions.keySet().containsAll(Arrays.asList(actionKeys));
}
@CapabilityMethod
public RunnerClient runnerClient() {
return runnerClient;
}
@CapabilityMethod
public void handleInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
// 加载数据
if (executableAction == null) {
return;
}
// 加锁确保同步
synchronized (executableAction.getExecutionLock()) {
applyInterventions(interventions, executableAction);
}
}
private void applyInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
boolean[] rebuildCleanTag = {false};
interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder));
for (MetaIntervention intervention : interventions) {
Result<List<MetaAction>> actionsResult = resolveInterventionActions(intervention);
actionsResult
.onFailure(ExceptionReporterHandler.INSTANCE::report)
.onSuccess(actions -> {
switch (intervention.getType()) {
case InterventionType.APPEND ->
handleAppend(executableAction, intervention.getOrder(), actions);
case InterventionType.INSERT ->
handleInsert(executableAction, intervention.getOrder(), actions);
case InterventionType.DELETE ->
handleDelete(executableAction, intervention.getOrder(), actions);
case InterventionType.CANCEL -> handleCancel(executableAction);
case InterventionType.REBUILD -> {
if (!rebuildCleanTag[0]) {
cleanActionData(executableAction);
rebuildCleanTag[0] = true;
}
handleRebuild(executableAction, intervention.getOrder(), actions);
}
}
});
}
}
private Result<List<MetaAction>> resolveInterventionActions(MetaIntervention intervention) {
List<MetaAction> actions = new ArrayList<>();
for (String actionKey : intervention.getActions()) {
Result<MetaAction> metaActionResult = loadMetaAction(actionKey);
AgentRuntimeException failure = metaActionResult.onSuccess(actions::add).exceptionOrNull();
if (failure != null) {
return Result.failure(failure);
}
}
return Result.success(actions);
}
/**
* 在未进入执行阶段的行动单元组新增新的行动
*/
private void handleAppend(ExecutableAction executableAction, int order, List<MetaAction> actions) {
if (order <= executableAction.getExecutingStage())
return;
executableAction.getActionChain().put(order, actions);
}
/**
* 在未进入执行阶段和正处于行动阶段的行动单元组插入新的行动
*/
private void handleInsert(ExecutableAction executableAction, int order, List<MetaAction> actions) {
if (order < executableAction.getExecutingStage())
return;
List<MetaAction> stageActions = executableAction.getActionChain().computeIfAbsent(order, k -> new ArrayList<>());
synchronized (stageActions) {
stageActions.addAll(actions);
}
}
private void handleDelete(ExecutableAction executableAction, int order, List<MetaAction> actions) {
if (order <= executableAction.getExecutingStage())
return;
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
if (actionChain.containsKey(order)) {
actionChain.get(order).removeAll(actions);
if (actionChain.get(order).isEmpty()) {
actionChain.remove(order);
}
}
}
private void handleCancel(ExecutableAction executableAction) {
executableAction.setStatus(ExecutableAction.Status.FAILED);
executableAction.setResult("行动取消");
}
private void handleRebuild(ExecutableAction executableAction, int order, List<MetaAction> actions) {
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
actionChain.put(order, actions);
}
private void cleanActionData(ExecutableAction executableAction) {
executableAction.getActionChain().clear();
executableAction.setExecutingStage(0);
executableAction.setStatus(ExecutableAction.Status.PREPARE);
executableAction.getHistory().clear();
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "action.json");
}
@Override
public void load(@NotNull JSONObject state) {
actionPool = ActionPoolStateCodec.decode(state.getJSONArray("action_pool"));
}
@Override
public @NotNull State convert() {
State state = new State();
List<StateValue.Obj> actionPoolState = ActionPoolStateCodec.encode(actionPool);
state.append("action_pool", StateValue.arr(actionPoolState));
return state;
}
public enum ExecutorType {
VIRTUAL, PLATFORM
}
}

View File

@@ -0,0 +1,341 @@
package work.slhaf.partner.core.action;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
final class ActionPoolStateCodec {
private ActionPoolStateCodec() {
}
static List<StateValue.Obj> encode(CopyOnWriteArraySet<ExecutableAction> actionPool) {
return actionPool.stream()
.map(ActionPoolStateCodec::encodeExecutableAction)
.toList();
}
static CopyOnWriteArraySet<ExecutableAction> decode(@Nullable JSONArray actionPoolArray) {
CopyOnWriteArraySet<ExecutableAction> restored = new CopyOnWriteArraySet<>();
if (actionPoolArray == null) {
return restored;
}
for (int i = 0; i < actionPoolArray.size(); i++) {
JSONObject actionObject = actionPoolArray.getJSONObject(i);
if (actionObject == null) {
continue;
}
try {
ExecutableAction executableAction = decodeExecutableAction(actionObject);
if (executableAction != null) {
restored.add(executableAction);
}
} catch (Exception e) {
log.warn("Skip invalid action_pool item at index {}", i, e);
}
}
return restored;
}
private static StateValue.Obj encodeExecutableAction(ExecutableAction action) {
Map<String, StateValue> actionMap = new LinkedHashMap<>();
actionMap.put("kind", StateValue.str(action instanceof SchedulableExecutableAction ? "schedulable" : "immediate"));
actionMap.put("uuid", StateValue.str(action.getUuid()));
actionMap.put("source", StateValue.str(action.getSource()));
actionMap.put("reason", StateValue.str(action.getReason()));
actionMap.put("description", StateValue.str(action.getDescription()));
actionMap.put("status", StateValue.str(action.getStatus().name()));
actionMap.put("tendency", StateValue.str(action.getTendency()));
actionMap.put("executing_stage", StateValue.num(action.getExecutingStage()));
String result = resolveExecutableResult(action);
if (result != null) {
actionMap.put("result", StateValue.str(result));
}
if (action instanceof SchedulableExecutableAction schedulableAction) {
actionMap.put("schedule_type", StateValue.str(schedulableAction.getScheduleType().name()));
actionMap.put("schedule_content", StateValue.str(schedulableAction.getScheduleContent()));
actionMap.put("enabled", StateValue.bool(schedulableAction.getEnabled()));
actionMap.put("schedule_histories", StateValue.arr(encodeScheduleHistories(schedulableAction)));
}
List<StateValue> chainStates = action.getActionChain().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.<StateValue>map(entry -> {
Map<String, StateValue> stageMap = new LinkedHashMap<>();
stageMap.put("stage", StateValue.num(entry.getKey()));
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
.map(metaAction -> (StateValue) encodeMetaAction(metaAction))
.toList()));
return StateValue.obj(stageMap);
}).toList();
actionMap.put("action_chain", StateValue.arr(chainStates));
actionMap.put("history", StateValue.arr(encodeHistoryStages(action.getHistory())));
return StateValue.obj(actionMap);
}
private static StateValue.Obj encodeMetaAction(MetaAction metaAction) {
Map<String, StateValue> metaMap = new LinkedHashMap<>();
metaMap.put("name", StateValue.str(metaAction.getName()));
metaMap.put("io", StateValue.bool(metaAction.getIo()));
if (metaAction.getLauncher() != null) {
metaMap.put("launcher", StateValue.str(metaAction.getLauncher()));
}
metaMap.put("type", StateValue.str(metaAction.getType().name()));
metaMap.put("location", StateValue.str(metaAction.getLocation()));
metaMap.put("params_json", StateValue.str(JSONObject.toJSONString(metaAction.getParams())));
metaMap.put("result_status", StateValue.str(metaAction.getResult().getStatus().name()));
if (metaAction.getResult().getData() != null) {
metaMap.put("result_data", StateValue.str(metaAction.getResult().getData()));
}
return StateValue.obj(metaMap);
}
private static StateValue.Obj encodeHistoryAction(HistoryAction historyAction) {
Map<String, StateValue> historyMap = new LinkedHashMap<>();
historyMap.put("action_key", StateValue.str(historyAction.actionKey()));
historyMap.put("description", StateValue.str(historyAction.description()));
historyMap.put("result", StateValue.str(historyAction.result()));
return StateValue.obj(historyMap);
}
private static ExecutableAction decodeExecutableAction(JSONObject actionObject) {
String kind = actionObject.getString("kind");
String uuid = actionObject.getString("uuid");
String source = actionObject.getString("source");
String reason = actionObject.getString("reason");
String description = actionObject.getString("description");
String tendency = actionObject.getString("tendency");
String status = actionObject.getString("status");
Integer executingStage = actionObject.getInteger("executing_stage");
if (kind == null || uuid == null || source == null || reason == null || description == null || tendency == null) {
return null;
}
Map<Integer, List<MetaAction>> restoredChain = decodeActionChain(actionObject.getJSONArray("action_chain"));
ExecutableAction executableAction;
if ("schedulable".equals(kind)) {
String scheduleType = actionObject.getString("schedule_type");
String scheduleContent = actionObject.getString("schedule_content");
if (scheduleType == null || scheduleContent == null) {
return null;
}
SchedulableExecutableAction schedulableAction = new SchedulableExecutableAction(
tendency,
restoredChain,
reason,
description,
source,
Schedulable.ScheduleType.valueOf(scheduleType),
scheduleContent,
uuid
);
Boolean enabled = actionObject.getBoolean("enabled");
if (enabled != null) {
schedulableAction.setEnabled(enabled);
}
schedulableAction.getScheduleHistories().addAll(decodeScheduleHistories(actionObject.getJSONArray("schedule_histories")));
executableAction = schedulableAction;
} else if ("immediate".equals(kind)) {
executableAction = new ImmediateExecutableAction(
tendency,
restoredChain,
reason,
description,
source,
uuid
);
} else {
return null;
}
if (status != null) {
executableAction.setStatus(Action.Status.valueOf(status));
}
if (executingStage != null) {
executableAction.setExecutingStage(executingStage);
}
String result = actionObject.getString("result");
if (result != null) {
executableAction.setResult(result);
}
executableAction.getHistory().putAll(decodeHistory(actionObject.getJSONArray("history")));
return executableAction;
}
private static Map<Integer, List<MetaAction>> decodeActionChain(@Nullable JSONArray actionChainArray) {
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
if (actionChainArray == null) {
return toMutableActionChain(restored);
}
for (int i = 0; i < actionChainArray.size(); i++) {
JSONObject stageObject = actionChainArray.getJSONObject(i);
if (stageObject == null) {
continue;
}
Integer stage = stageObject.getInteger("stage");
JSONArray actions = stageObject.getJSONArray("actions");
if (stage == null || actions == null) {
continue;
}
List<MetaAction> metaActions = new ArrayList<>();
for (int j = 0; j < actions.size(); j++) {
JSONObject actionObject = actions.getJSONObject(j);
MetaAction metaAction = decodeMetaAction(actionObject);
if (metaAction != null) {
metaActions.add(metaAction);
}
}
restored.put(stage, metaActions);
}
return toMutableActionChain(restored);
}
private static MetaAction decodeMetaAction(@Nullable JSONObject actionObject) {
if (actionObject == null) {
return null;
}
String name = actionObject.getString("name");
Boolean io = actionObject.getBoolean("io");
String type = actionObject.getString("type");
String location = actionObject.getString("location");
if (name == null || io == null || type == null || location == null) {
return null;
}
MetaAction metaAction = new MetaAction(
name,
io,
actionObject.getString("launcher"),
MetaAction.Type.valueOf(type),
location
);
String paramsJson = actionObject.getString("params_json");
if (paramsJson != null && !paramsJson.isBlank()) {
JSONObject paramsObject = JSONObject.parseObject(paramsJson);
if (paramsObject != null) {
metaAction.getParams().putAll(paramsObject);
}
}
String resultStatus = actionObject.getString("result_status");
if (resultStatus != null) {
metaAction.getResult().setStatus(MetaAction.Result.Status.valueOf(resultStatus));
}
metaAction.getResult().setData(actionObject.getString("result_data"));
return metaAction;
}
private static Map<Integer, List<HistoryAction>> decodeHistory(@Nullable JSONArray historyArray) {
Map<Integer, List<HistoryAction>> restored = new LinkedHashMap<>();
if (historyArray == null) {
return restored;
}
for (int i = 0; i < historyArray.size(); i++) {
JSONObject stageObject = historyArray.getJSONObject(i);
if (stageObject == null) {
continue;
}
Integer stage = stageObject.getInteger("stage");
JSONArray actions = stageObject.getJSONArray("actions");
if (stage == null || actions == null) {
continue;
}
List<HistoryAction> historyActions = new ArrayList<>();
for (int j = 0; j < actions.size(); j++) {
JSONObject historyObject = actions.getJSONObject(j);
if (historyObject == null) {
continue;
}
String actionKey = historyObject.getString("action_key");
String description = historyObject.getString("description");
String result = historyObject.getString("result");
if (actionKey == null || description == null || result == null) {
continue;
}
historyActions.add(new HistoryAction(actionKey, description, result));
}
restored.put(stage, historyActions);
}
return restored;
}
private static List<StateValue> encodeHistoryStages(Map<Integer, ? extends List<HistoryAction>> historyMap) {
return historyMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.<StateValue>map(entry -> {
Map<String, StateValue> stageMap = new LinkedHashMap<>();
stageMap.put("stage", StateValue.num(entry.getKey()));
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
.map(historyAction -> (StateValue) encodeHistoryAction(historyAction))
.toList()));
return StateValue.obj(stageMap);
}).toList();
}
private static List<StateValue> encodeScheduleHistories(SchedulableExecutableAction schedulableAction) {
return schedulableAction.getScheduleHistories().stream()
.<StateValue>map(scheduleHistory -> {
Map<String, StateValue> historyMap = new LinkedHashMap<>();
historyMap.put("end_time", StateValue.str(scheduleHistory.getEndTime().toString()));
historyMap.put("result", StateValue.str(scheduleHistory.getResult()));
historyMap.put("history", StateValue.arr(encodeHistoryStages(scheduleHistory.getHistory())));
return StateValue.obj(historyMap);
})
.toList();
}
private static List<SchedulableExecutableAction.ScheduleHistory> decodeScheduleHistories(@Nullable JSONArray scheduleHistoriesArray) {
List<SchedulableExecutableAction.ScheduleHistory> restored = new ArrayList<>();
if (scheduleHistoriesArray == null) {
return restored;
}
for (int i = 0; i < scheduleHistoriesArray.size(); i++) {
JSONObject historyObject = scheduleHistoriesArray.getJSONObject(i);
if (historyObject == null) {
continue;
}
try {
String endTime = historyObject.getString("end_time");
String result = historyObject.getString("result");
if (endTime == null || result == null) {
continue;
}
restored.add(new SchedulableExecutableAction.ScheduleHistory(
ZonedDateTime.parse(endTime),
result,
decodeHistory(historyObject.getJSONArray("history"))
));
} catch (Exception e) {
log.warn("Skip invalid schedule_history item at index {}", i, e);
}
}
return restored;
}
private static Map<Integer, List<MetaAction>> toMutableActionChain(Map<Integer, List<MetaAction>> actionChain) {
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
actionChain.forEach((stage, actions) -> restored.put(stage, new ArrayList<>(actions)));
return restored;
}
private static String resolveExecutableResult(ExecutableAction action) {
try {
return action.getResult();
} catch (RuntimeException ignored) {
return null;
}
}
}

View File

@@ -0,0 +1,290 @@
package work.slhaf.partner.core.action.entity
import work.slhaf.partner.module.action.executor.entity.HistoryAction
import java.time.Instant
import java.time.ZonedDateTime
import java.util.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
sealed class Action(
open val uuid: String = UUID.randomUUID().toString()
) {
/**
* 行动ID
*/
abstract val source: String
/**
* 行动原因
*/
abstract val reason: String
/**
* 行动描述
*/
abstract val description: String
abstract val timeout: Duration
val timeoutMills: Long
get() = timeout.inWholeMilliseconds
/**
* 行动状态
*/
var status: Status = Status.PREPARE
enum class Status {
/**
* 执行成功
*/
SUCCESS,
/**
* 执行失败
*/
FAILED,
/**
* 执行中
*/
EXECUTING,
/**
* 暂时中断
*/
INTERRUPTED,
/**
* 预备执行
*/
PREPARE
}
}
sealed interface Schedulable {
val scheduleType: ScheduleType
val scheduleContent: String
val uuid: String
val timeout: Duration
val timeoutMills: Long
var enabled: Boolean
enum class ScheduleType {
CYCLE,
ONCE
}
}
/**
* 行动模块传递的行动数据包含行动uuid、倾向、状态、行动链、结果、发起原因、行动描述等信息。
*/
sealed class ExecutableAction(
override val uuid: String = UUID.randomUUID().toString()
) : Action(uuid) {
val executionLock: Any = Any()
/**
* 行动倾向
*/
abstract val tendency: String
/**
* 行动链
*/
abstract val actionChain: MutableMap<Int, MutableList<MetaAction>>
/**
* 行动阶段(当前阶段)
*/
var executingStage: Int = 0
/**
* 行动结果
*/
var result: String? = null
val history: MutableMap<Int, MutableList<HistoryAction>> = mutableMapOf()
override val timeout: Duration = 10.minutes
/**
* @param timeout 最长打断时间
* @return 是否超时结束
*/
fun interrupt(timeout: Int): Boolean {
status = Status.INTERRUPTED
val interruptAt = Instant.now().epochSecond
while (status == Status.INTERRUPTED) {
Thread.sleep(500)
if (Instant.now().epochSecond - interruptAt > timeout) {
return false
}
}
return true
}
fun resume() {
status = Status.EXECUTING
}
fun snapshot(): ExecutableActionSnapshot {
val schedulable = this as? Schedulable
return ExecutableActionSnapshot(
uuid = uuid,
source = source,
reason = reason,
description = description,
timeoutMills = timeoutMills,
status = status,
tendency = tendency,
actionChainSize = actionChain.size,
executingStage = executingStage,
result = result,
history = history.mapValues { (_, value) -> value.toList() },
scheduleType = schedulable?.scheduleType,
scheduleContent = schedulable?.scheduleContent,
enabled = schedulable?.enabled
)
}
}
/**
* 计划行动数据类,继承自[Action],扩展了[Schedulable]相关调度属性,用于标识计划类型(单次还是周期性任务)和计划内容
*/
data class SchedulableExecutableAction @JvmOverloads constructor(
override val tendency: String,
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
override val reason: String,
override val description: String,
override val source: String,
override val scheduleType: Schedulable.ScheduleType,
override val scheduleContent: String,
override val uuid: String = UUID.randomUUID().toString(),
) : ExecutableAction(uuid), Schedulable {
override var enabled = true
val scheduleHistories = ArrayList<ScheduleHistory>()
fun recordAndReset() {
val newHistory = ScheduleHistory(ZonedDateTime.now(), result ?: "null", history.toMap())
scheduleHistories.add(newHistory)
executingStage = 0
for (entry in actionChain) {
for (action in entry.value) {
action.params.clear()
action.result.reset()
}
}
}
data class ScheduleHistory(
val endTime: ZonedDateTime,
val result: String,
val history: Map<Int, List<HistoryAction>>
)
}
/**
* 即时行动数据类
*/
data class ImmediateExecutableAction @JvmOverloads constructor(
override val tendency: String,
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
override val reason: String,
override val description: String,
override val source: String,
override val uuid: String = UUID.randomUUID().toString(),
) : ExecutableAction(uuid)
/**
* 用于计时的一次性或周期性触发或者针对某一数据源进行内容更新的行动
*/
data class StateAction @JvmOverloads constructor(
override val source: String,
override val reason: String,
override val description: String,
override val scheduleType: Schedulable.ScheduleType,
override val scheduleContent: String,
val trigger: Trigger,
override var enabled: Boolean = true,
override val timeout: Duration = 5.minutes,
) : Action(), Schedulable {
fun snapshot(): StateActionSnapshot {
return StateActionSnapshot(
uuid = uuid,
source = source,
reason = reason,
description = description,
timeoutMills = timeoutMills,
status = status,
scheduleType = scheduleType,
scheduleContent = scheduleContent,
enabled = enabled,
)
}
sealed interface Trigger {
fun onTrigger()
/**
* State 更新触发
*/
class Update<T>(val stateSource: T, val update: (stateSource: T) -> Unit) : Trigger {
override fun onTrigger() {
update(stateSource)
}
}
/**
* 常规逻辑触发
*/
class Call(val call: () -> Unit) : Trigger {
override fun onTrigger() {
call()
}
}
}
}
data class ExecutableActionSnapshot(
val uuid: String,
val source: String,
val reason: String,
val description: String,
val timeoutMills: Long,
val status: Action.Status,
val tendency: String,
val actionChainSize: Int,
val executingStage: Int,
val result: String?,
val history: Map<Int, List<HistoryAction>>,
val scheduleType: Schedulable.ScheduleType? = null,
val scheduleContent: String? = null,
val enabled: Boolean? = null
)
data class StateActionSnapshot(
val uuid: String,
val source: String,
val reason: String,
val description: String,
val timeoutMills: Long,
val status: Action.Status,
val scheduleType: Schedulable.ScheduleType,
val scheduleContent: String,
val enabled: Boolean
)

View File

@@ -0,0 +1,10 @@
package work.slhaf.partner.core.action.entity;
import lombok.Data;
@Data
public class ActionFileMetaData {
private String content;
private String name;
private String ext;
}

View File

@@ -0,0 +1,84 @@
package work.slhaf.partner.core.action.entity
/**
* 行动链中的单一元素,封装了调用外部行动程序的必要信息与结果容器,可被[work.slhaf.partner.core.action.ActionCapability]执行
*/
data class MetaAction(
/**
* 行动name用于标识行动程序
*/
val name: String,
/**
* 是否IO密集用于决定使用何种线程池
*/
val io: Boolean = false,
/**
* 启动器/解释器,对于原生 MCP Tool 、Dynamic Action 来说可忽略,目前仅用于 ORIGIN 类型
*/
val launcher: String? = null,
/**
* 行动程序类型,可分为 MCP、ORIGIN、BUILTIN 三种,
* 分别对应读取到的 MCP Tool、生成的临时行动程序、内置行动
*/
val type: Type,
/**
* 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id;
* 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串;
* 当类型为 BUILTIN 时,该字段固定为 builtin
*/
val location: String,
) {
/**
* 行动程序可接受的参数,由调用处设置
*/
val params: MutableMap<String, Any> = mutableMapOf()
/**
* 行动结果,包括执行状态和相应内容(执行结果或者错误信息)
*/
val result = Result()
val key: String
/**
* actionKey 将由 location+name 共同定位
*
* @return actionKey
*/
get() = "$location::$name"
class Result {
var status = Status.WAITING
var data: String? = null
fun reset() {
status = Status.WAITING
data = null
}
enum class Status {
SUCCESS,
FAILED,
WAITING
}
}
enum class Type {
/**
* 将调用的 MCP 工具,可包括远程、本地任意服务
*/
MCP,
/**
* 适用于‘临时生成’的行动程序,在生成后根据序列化选项及执行情况,进行持久化
*/
ORIGIN,
/**
* 由本地内置注册表直接执行的行动
*/
BUILTIN
}
}

View File

@@ -0,0 +1,44 @@
package work.slhaf.partner.core.action.entity
import com.alibaba.fastjson2.JSONObject
data class MetaActionInfo(
/**
* 是否 IO 密集
*/
val io: Boolean,
/**
* 所需的启动器/解释器
*/
val launcher: String?,
/**
* 参数描述
*/
val params: Map<String, String>,
/**
* 行动功能描述
*/
val description: String,
/**
* 行动标签
*/
val tags: Set<String>,
/**
* 前置行动依赖
*/
val preActions: Set<String>,
/**
* 后置行动依赖
*/
val postActions: Set<String>,
/**
* 是否严格依赖前置行动的成功执行若为true且前置行动失败则不执行该行动后置任务多为触发式。默认即执行。
*/
val strictDependencies: Boolean,
/**
* 响应格式说明
*/
val responseSchema: JSONObject,
)

View File

@@ -0,0 +1,28 @@
package work.slhaf.partner.core.action.entity.intervention;
public enum InterventionType {
/**
* 追加行动: 追加至指定行动链序列之后才执行
*/
APPEND,
/**
* 插入行动: 指定行动链序列执行过程中即时新增并执行
*/
INSERT,
/**
* 重建行动: 重建指定行动链序列之后的所有行动内容
*/
REBUILD,
/**
* 删除行动: 删除指定行动链序列上的指定行动单元
*/
DELETE,
/**
* 取消行动链: 中断并取消指定行动链的执行
*/
CANCEL
}

View File

@@ -0,0 +1,21 @@
package work.slhaf.partner.core.action.entity.intervention;
import lombok.Data;
import java.util.List;
@Data
public class MetaIntervention {
/**
* 干预数据类型
*/
private InterventionType type;
/**
* 干预数据对应的行动链序列
*/
private int order;
/**
* 干预数据所需的行动key列表
*/
private List<String> actions;
}

View File

@@ -0,0 +1,39 @@
package work.slhaf.partner.core.action.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class ActionInfrastructureStartupException extends AgentStartupException {
private final String component;
private final String path;
private final String command;
public ActionInfrastructureStartupException(String message, String component, @Nullable String path, @Nullable String command) {
super(message, "action-core");
this.component = component;
this.path = path;
this.command = command;
}
public ActionInfrastructureStartupException(String message, String component, @Nullable String path, @Nullable String command, Throwable cause) {
super(message, "action-core", cause);
this.component = component;
this.path = path;
this.command = command;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("component", component);
if (path != null) {
report.getExtra().put("path", path);
}
if (command != null) {
report.getExtra().put("command", command);
}
return report;
}
}

View File

@@ -0,0 +1,30 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class ActionLookupException extends AgentRuntimeException {
private final String actionKey;
private final String lookupTarget;
public ActionLookupException(String message, String actionKey, String lookupTarget) {
super(message);
this.actionKey = actionKey;
this.lookupTarget = lookupTarget;
}
public ActionLookupException(String message, String actionKey, String lookupTarget, Throwable cause) {
super(message, cause);
this.actionKey = actionKey;
this.lookupTarget = lookupTarget;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("actionKey", actionKey);
report.getExtra().put("lookupTarget", lookupTarget);
return report;
}
}

View File

@@ -0,0 +1,58 @@
package work.slhaf.partner.core.action.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class ActionSerializationException extends AgentRuntimeException {
private final String actionName;
private final String baseDir;
private final String fileExt;
private final String stage;
public ActionSerializationException(
String message,
@Nullable String actionName,
@Nullable String baseDir,
@Nullable String fileExt,
String stage
) {
super(message);
this.actionName = actionName;
this.baseDir = baseDir;
this.fileExt = fileExt;
this.stage = stage;
}
public ActionSerializationException(
String message,
@Nullable String actionName,
@Nullable String baseDir,
@Nullable String fileExt,
String stage,
Throwable cause
) {
super(message, cause);
this.actionName = actionName;
this.baseDir = baseDir;
this.fileExt = fileExt;
this.stage = stage;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("stage", stage);
if (actionName != null) {
report.getExtra().put("actionName", actionName);
}
if (baseDir != null) {
report.getExtra().put("baseDir", baseDir);
}
if (fileExt != null) {
report.getExtra().put("fileExt", fileExt);
}
return report;
}
}

View File

@@ -0,0 +1,207 @@
package work.slhaf.partner.core.action.runner;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
import work.slhaf.partner.core.action.runner.execution.McpActionExecutor;
import work.slhaf.partner.core.action.runner.execution.OriginExecutionService;
import work.slhaf.partner.core.action.runner.mcp.*;
import work.slhaf.partner.core.action.runner.policy.BwrapPolicyProvider;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.support.ActionSerializer;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class LocalRunnerClient extends RunnerClient {
public static final String MCP_NAME_DESC = "mcp-desc";
public static final String MCP_NAME_DYNAMIC = "mcp-dynamic";
private final String tmpActionPath;
private final String dynamicActionPath;
private final String mcpServerPath;
private final String mcpDescPath;
private final McpClientRegistry mcpClientRegistry;
private final McpTransportFactory mcpTransportFactory;
private final ActionSerializer actionSerializer;
private final OriginExecutionService originExecutionService;
private final McpActionExecutor mcpActionExecutor;
private final McpMetaRegistry mcpMetaRegistry;
private final McpDescWatcher mcpDescWatcher;
private final DynamicActionMcpManager dynamicActionMcpManager;
private final McpConfigWatcher mcpConfigWatcher;
private final AtomicBoolean closed = new AtomicBoolean(false);
public LocalRunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @Nullable String baseActionPath) {
super(existedMetaActions, executor, baseActionPath);
this.tmpActionPath = buildPathStr(ACTION_PATH, "tmp");
this.dynamicActionPath = buildPathStr(ACTION_PATH, "dynamic");
this.mcpServerPath = buildPathStr(ACTION_PATH, "mcp");
this.mcpDescPath = buildPathStr(mcpServerPath, "desc");
createPath(tmpActionPath);
createPath(dynamicActionPath);
createPath(mcpServerPath);
createPath(mcpDescPath);
McpClientRegistry clientRegistry = new McpClientRegistry();
McpTransportFactory transportFactory = new McpTransportFactory();
ActionSerializer serializer = new ActionSerializer(tmpActionPath, dynamicActionPath);
OriginExecutionService originService = new OriginExecutionService();
McpActionExecutor actionExecutor = new McpActionExecutor(clientRegistry);
McpMetaRegistry metaRegistry = null;
McpDescWatcher descWatcher = null;
DynamicActionMcpManager dynamicManager = null;
McpConfigWatcher configWatcher = null;
try {
registerPolicyProviders();
metaRegistry = new McpMetaRegistry(existedMetaActions);
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DESC, metaRegistry.clientConfig(MCP_NAME_DESC, 10));
log.info("DescMcp 注册完毕");
descWatcher = new McpDescWatcher(Path.of(mcpDescPath), metaRegistry, executor);
descWatcher.start();
dynamicManager = new DynamicActionMcpManager(
Path.of(dynamicActionPath),
existedMetaActions,
executor
);
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DYNAMIC, dynamicManager.clientConfig(10));
log.info("DynamicActionMcp 注册完毕");
dynamicManager.start();
configWatcher = new McpConfigWatcher(
Path.of(mcpServerPath),
existedMetaActions,
clientRegistry,
transportFactory,
metaRegistry,
executor
);
configWatcher.start();
configWatcher.registerPolicyListener();
} catch (ActionInfrastructureStartupException e) {
closeQuietly(configWatcher);
closeQuietly(dynamicManager);
closeQuietly(descWatcher);
closeQuietly(metaRegistry);
closeQuietly(clientRegistry);
throw e;
} catch (Exception e) {
closeQuietly(configWatcher);
closeQuietly(dynamicManager);
closeQuietly(descWatcher);
closeQuietly(metaRegistry);
closeQuietly(clientRegistry);
throw new ActionInfrastructureStartupException(
"LocalRunnerClient initialization failed",
"local-runner-client",
ACTION_PATH,
null,
e
);
}
this.mcpClientRegistry = clientRegistry;
this.mcpTransportFactory = transportFactory;
this.actionSerializer = serializer;
this.originExecutionService = originService;
this.mcpActionExecutor = actionExecutor;
this.mcpMetaRegistry = metaRegistry;
this.mcpDescWatcher = descWatcher;
this.dynamicActionMcpManager = dynamicManager;
this.mcpConfigWatcher = configWatcher;
}
private void registerPolicyProviders() {
OsInfo os = SystemUtil.getOsInfo();
if (os.isLinux()) {
ExecutionPolicyRegistry.INSTANCE.registerPolicyProvider(BwrapPolicyProvider.INSTANCE);
}
}
@Override
protected RunnerResponse doRun(MetaAction metaAction) {
RunnerResponse response;
response = switch (metaAction.getType()) {
case MCP -> mcpActionExecutor.run(metaAction);
case ORIGIN -> originExecutionService.run(metaAction);
case BUILTIN -> doRunWithBuiltin(metaAction);
};
return response;
}
@Override
public String buildTmpPath(String actionKey, String codeType) {
return actionSerializer.buildTmpPath(actionKey, codeType);
}
@Override
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
actionSerializer.tmpSerialize(tempAction, code, codeType);
}
@Override
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
actionSerializer.persistSerialize(metaActionInfo, fileMetaData);
}
private void registerMcpClient(McpClientRegistry clientRegistry, McpTransportFactory transportFactory, String id, McpTransportConfig transportConfig) {
val client = io.modelcontextprotocol.client.McpClient.sync(transportFactory.create(transportConfig))
.requestTimeout(java.time.Duration.ofSeconds(transportConfig.timeout()))
.clientInfo(new io.modelcontextprotocol.spec.McpSchema.Implementation(id, "PARTNER"))
.build();
clientRegistry.register(id, client);
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
mcpConfigWatcher.unregisterPolicyListener();
closeQuietly(mcpConfigWatcher);
closeQuietly(dynamicActionMcpManager);
closeQuietly(mcpDescWatcher);
closeQuietly(mcpMetaRegistry);
closeQuietly(mcpClientRegistry);
}
private void closeQuietly(AutoCloseable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (Exception ignored) {
}
}
public String buildPathStr(String... path) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < path.length; i++) {
str.append(path[i]);
if (i < path.length - 1) {
str.append("/");
}
}
return str.toString();
}
}

View File

@@ -0,0 +1,126 @@
package work.slhaf.partner.core.action.runner;
import lombok.Data;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaAction.Result;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
import work.slhaf.partner.module.action.builtin.BuiltinActionRegistry;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
/**
* 执行客户端抽象类
* <br/>
* 只负责暴露序列化、执行等相应接口,具体逻辑交给下游实现
* <br/>
* 默认存在两类实现,{@link LocalRunnerClient} 和 {@link SandboxRunnerClient}
* <ol>
* LocalRunnerClient:
* <li>
* 对应本地运行环境,可在本地启动 MCP 客户端将 RunnerClient 暴露的能力接口转发至本地 MCP Client 并执行
* </li>
* SandboxRunnerClient:
* <li>
* 对应沙盒运行环境,该 Client 仅作为沙盒环境的客户端,不持有额外能力,仅保持远端连接已存在行动的内容更新
* </li>
* </ol>
*/
@Slf4j
public abstract class RunnerClient implements AutoCloseable {
protected final String ACTION_PATH;
protected final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
protected final ExecutorService executor;
@Setter
protected BuiltinActionRegistry builtinActionRegistry;
/**
* ActionCore 将注入虚拟线程池
*/
public RunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @NotNull String baseActionPath) {
this.existedMetaActions = existedMetaActions;
this.executor = executor;
this.ACTION_PATH = baseActionPath;
createPath(ACTION_PATH);
}
/**
* 执行行动程序
*/
public void submit(MetaAction metaAction) {
log.debug("执行行动: {}", metaAction);
// 获取已存在行动列表
Result result = metaAction.getResult();
if (!result.getStatus().equals(Result.Status.WAITING)) {
return;
}
RunnerResponse response = work.slhaf.partner.framework.agent.support.Result.runCatching(() -> doRun(metaAction)).fold(
runnerResponse -> runnerResponse,
ex -> {
RunnerResponse r = new RunnerResponse();
r.setOk(false);
r.setData(ex.getLocalizedMessage());
return r;
}
);
result.setData(response.getData());
result.setStatus(response.isOk() ? Result.Status.SUCCESS : Result.Status.FAILED);
log.debug("行动执行结果: {}", response);
}
protected abstract RunnerResponse doRun(MetaAction metaAction);
public abstract String buildTmpPath(String actionKey, String codeType);
public abstract void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException;
public abstract void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData);
protected RunnerResponse doRunWithBuiltin(MetaAction metaAction) {
RunnerResponse response = new RunnerResponse();
if (builtinActionRegistry == null) {
response.setOk(false);
response.setData("BuiltinActionRegistry 未初始化");
return response;
}
response.setData(builtinActionRegistry.call(metaAction.getKey(), metaAction.getParams()));
response.setOk(true);
return response;
}
protected void createPath(String pathStr) {
val path = Path.of(pathStr);
try {
Files.createDirectory(path);
} catch (IOException e) {
if (!Files.exists(path)) {
throw new ActionInfrastructureStartupException(
"Failed to create action directory: " + pathStr,
"runner-client",
pathStr,
null,
e
);
}
}
}
@Data
public static class RunnerResponse {
private boolean ok;
private String data;
}
}

View File

@@ -0,0 +1,60 @@
package work.slhaf.partner.core.action.runner;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
/**
* 基于 Http 与 WebSocket 的沙盒执行器客户端,负责:
* <ul>
* <li>
* 发送行动单元数据
* </li>
* <li>
* 实时更新获取已存在行动列表
* </li>
* <li>
* 向传入的 MetaAction 回写执行结果
* </li>
* </ul>
*/
public class SandboxRunnerClient extends RunnerClient {
public SandboxRunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor) { // 连接沙盒执行器(websocket)
super(existedMetaActions, executor, null);
}
protected RunnerResponse doRun(MetaAction metaAction) {
return switch (metaAction.getType()) {
case BUILTIN -> doRunWithBuiltin(metaAction);
case MCP, ORIGIN -> {
// 调用沙盒执行器
yield null;
}
};
}
@Override
public String buildTmpPath(String actionKey, String codeType) {
throw new UnsupportedOperationException("Unimplemented method 'buildTmpPath'");
}
@Override
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
throw new UnsupportedOperationException("Unimplemented method 'tmpSerialize'");
}
@Override
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
throw new UnsupportedOperationException("Unimplemented method 'persistSerialize'");
}
@Override
public void close() throws Exception {
}
}

View File

@@ -0,0 +1,185 @@
package work.slhaf.partner.core.action.runner.execution;
import lombok.Data;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class CommandExecutionService {
public static final CommandExecutionService INSTANCE = new CommandExecutionService();
private CommandExecutionService() {
}
public String[] buildFileExecutionCommands(String launcher, Map<String, Object> params, String absolutePath) {
int paramSize = params == null ? 0 : params.size();
String[] commands = new String[paramSize + 2];
commands[0] = launcher;
commands[1] = absolutePath;
AtomicInteger paramCount = new AtomicInteger(2);
if (params != null) {
params.forEach((param, value) -> commands[paramCount.getAndIncrement()] = "--" + param + "=" + value);
}
return commands;
}
public Result exec(List<String> commands) {
return exec(commands.toArray(new String[0]));
}
public Result exec(WrappedLaunchSpec launchSpec) {
Result result = new Result();
List<String> output = Collections.synchronizedList(new ArrayList<>());
List<String> error = Collections.synchronizedList(new ArrayList<>());
try {
Process process = startProcess(launchSpec);
Thread stdoutThread = Thread.startVirtualThread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.add(line);
}
} catch (Exception ignored) {
}
});
Thread stderrThread = Thread.startVirtualThread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
error.add(line);
}
} catch (Exception ignored) {
}
});
int exitCode = process.waitFor();
stdoutThread.join();
stderrThread.join();
result.setOk(exitCode == 0);
List<String> stdoutLines = List.copyOf(output);
List<String> stderrLines = List.copyOf(error);
result.setStdoutLines(stdoutLines);
result.setStderrLines(stderrLines);
result.setResultList(stdoutLines.isEmpty() ? stderrLines : stdoutLines);
result.setTotal(buildDisplayText(stdoutLines, stderrLines));
} catch (Exception e) {
result.setOk(false);
result.setTotal(e.getMessage());
result.setStdoutLines(List.of());
result.setStderrLines(List.of(e.getMessage()));
result.setResultList(result.getStderrLines());
}
return result;
}
public Result exec(String... commands) {
return exec(defaultLaunchSpec(commands));
}
public CommandSession createSessionTask(List<String> commands) {
return createSessionTask(commands.toArray(new String[0]));
}
public CommandSession createSessionTask(WrappedLaunchSpec launchSpec) {
try {
Process process = startProcess(launchSpec);
CommandSession session = new CommandSession();
StringBuilder stdoutBuffer = new StringBuilder();
StringBuilder stderrBuffer = new StringBuilder();
session.setProcess(process);
session.setStdoutBuffer(stdoutBuffer);
session.setStderrBuffer(stderrBuffer);
Thread.startVirtualThread(() -> readToBuffer(process.getInputStream(), stdoutBuffer));
Thread.startVirtualThread(() -> readToBuffer(process.getErrorStream(), stderrBuffer));
return session;
} catch (Exception e) {
throw new IllegalStateException("创建命令会话失败", e);
}
}
public CommandSession createSessionTask(String... commands) {
return createSessionTask(defaultLaunchSpec(commands));
}
private void readToBuffer(java.io.InputStream inputStream, StringBuilder buffer) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
synchronized (buffer) {
if (!buffer.isEmpty()) {
buffer.append('\n');
}
buffer.append(line);
}
}
} catch (Exception ignored) {
}
}
private Process startProcess(WrappedLaunchSpec launchSpec) throws Exception {
ProcessBuilder processBuilder = new ProcessBuilder();
List<String> command = new ArrayList<>();
command.add(launchSpec.getCommand());
command.addAll(launchSpec.getArgs());
processBuilder.command(command);
processBuilder.redirectErrorStream(false);
if (launchSpec.getWorkingDirectory() != null && !launchSpec.getWorkingDirectory().isBlank()) {
processBuilder.directory(new File(launchSpec.getWorkingDirectory()));
}
Map<String, String> environment = processBuilder.environment();
environment.clear();
environment.putAll(launchSpec.getEnvironment());
return processBuilder.start();
}
private WrappedLaunchSpec defaultLaunchSpec(String... commands) {
return new WrappedLaunchSpec(
commands[0],
List.of(commands).subList(1, commands.length),
null,
System.getenv()
);
}
private String buildDisplayText(List<String> stdoutLines, List<String> stderrLines) {
if (stdoutLines.isEmpty()) {
return String.join("\n", stderrLines);
}
if (stderrLines.isEmpty()) {
return String.join("\n", stdoutLines);
}
return String.join("\n", stdoutLines) + "\n" + String.join("\n", stderrLines);
}
@Data
public static class Result {
private boolean ok;
private String total;
private List<String> resultList;
private List<String> stdoutLines;
private List<String> stderrLines;
}
@Data
public static class CommandSession {
private Process process;
private StringBuilder stdoutBuffer;
private StringBuilder stderrBuffer;
}
}

View File

@@ -0,0 +1,72 @@
package work.slhaf.partner.core.action.runner.execution;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
import java.util.List;
import java.util.stream.Collectors;
public class McpActionExecutor {
private final McpClientRegistry mcpClientRegistry;
public McpActionExecutor(McpClientRegistry mcpClientRegistry) {
this.mcpClientRegistry = mcpClientRegistry;
}
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation());
if (mcpClient == null) {
response.setOk(false);
response.setData("MCP client not found: " + metaAction.getLocation());
return response;
}
McpSchema.CallToolRequest callToolRequest = McpSchema.CallToolRequest.builder()
.name(metaAction.getName())
.arguments(metaAction.getParams())
.build();
McpSchema.CallToolResult callToolResult;
try {
callToolResult = mcpClient.callTool(callToolRequest);
} catch (Exception e) {
response.setOk(false);
response.setData("MCP tool call failed: " + e.getMessage());
return response;
}
Boolean error = callToolResult.isError();
response.setOk(error == null || !error);
response.setData(extractResponseData(callToolResult));
return response;
}
private String extractResponseData(McpSchema.CallToolResult callToolResult) {
Object structuredContent = callToolResult.structuredContent();
if (structuredContent != null) {
return String.valueOf(structuredContent);
}
List<McpSchema.Content> contents = callToolResult.content();
if (contents != null && !contents.isEmpty()) {
String contentSummary = contents.stream()
.map(this::renderContent)
.filter(text -> text != null && !text.isBlank())
.collect(Collectors.joining("\n"));
if (!contentSummary.isBlank()) {
return contentSummary;
}
}
return callToolResult.toString();
}
private String renderContent(McpSchema.Content content) {
if (content instanceof McpSchema.TextContent textContent) {
return textContent.text();
}
return String.valueOf(content);
}
}

View File

@@ -0,0 +1,35 @@
package work.slhaf.partner.core.action.runner.execution;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.io.File;
import java.util.Arrays;
import static work.slhaf.partner.core.action.ActionCore.ORIGIN_LOCATION;
public class OriginExecutionService {
public OriginExecutionService() {
}
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
File file = new File(resolveOriginPath(metaAction));
String[] commands = CommandExecutionService.INSTANCE.buildFileExecutionCommands(metaAction.getLauncher(), metaAction.getParams(), file.getAbsolutePath());
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(Arrays.stream(commands).toList());
CommandExecutionService.Result execResult = CommandExecutionService.INSTANCE.exec(wrapped);
response.setOk(execResult.isOk());
response.setData(execResult.getTotal());
return response;
}
private String resolveOriginPath(MetaAction metaAction) {
if (ORIGIN_LOCATION.equals(metaAction.getLocation())) {
return metaAction.getName();
}
return metaAction.getLocation();
}
}

View File

@@ -0,0 +1,361 @@
package work.slhaf.partner.core.action.runner.mcp;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class DynamicActionMcpManager implements AutoCloseable {
private final Path root;
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final CommandExecutionService commandExecutionService;
private final McpStatelessAsyncServer server;
private final InProcessMcpTransport clientTransport;
private final DirectoryWatchSupport watchSupport;
public DynamicActionMcpManager(Path root,
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
ExecutorService executor) throws IOException {
this.root = root;
this.existedMetaActions = existedMetaActions;
this.commandExecutionService = CommandExecutionService.INSTANCE;
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
this.clientTransport = pair.clientSide();
McpSchema.ServerCapabilities serverCapabilities = McpSchema.ServerCapabilities.builder()
.tools(true)
.build();
this.server = McpServer.async(pair.serverSide())
.capabilities(serverCapabilities)
.jsonMapper(McpJsonMapper.getDefault())
.build();
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 1, this::loadExisting)
.onCreate(this::handleCreate)
.onModify(this::handleModify)
.onDelete(this::handleDelete)
.onOverflow((thisDir, context) -> reconcile());
}
public McpTransportConfig.InProcess clientConfig(int timeout) {
return new McpTransportConfig.InProcess(timeout, clientTransport);
}
public void start() {
watchSupport.start();
log.info("DynamicActionMcp 文件监听注册完毕");
}
private void loadExisting() {
File file = root.toFile();
if (file.isFile()) {
throw new ActionInfrastructureStartupException(
"Expected a directory but found a file: " + root,
"dynamic-action-mcp-manager",
root.toString(),
null
);
}
File[] files = file.listFiles();
if (files == null) {
throw new ActionInfrastructureStartupException(
"Failed to read action directory: " + root,
"dynamic-action-mcp-manager",
root.toString(),
null
);
}
for (File dir : files) {
if (!normalPath(dir.toPath())) {
continue;
}
addAction(dir.getName(), dir.toPath());
}
}
private boolean isTmp(Path context) {
return context.getFileName().endsWith(".tmp");
}
private void handleModify(Path thisDir, Path context) {
if (context == null || isTmp(context) || !normalPath(thisDir)) {
return;
}
modify(thisDir, context);
}
private void handleCreate(Path thisDir, Path context) {
if (context == null || isTmp(context)) {
return;
}
if (thisDir.equals(root) && Files.isDirectory(context)) {
try {
watchSupport.registerDirectory(context);
} catch (IOException e) {
log.error("监听目录注册失败: {}", context);
}
}
if (normalPath(thisDir)) {
modify(thisDir, context);
}
if (Files.isDirectory(context) && normalPath(context)) {
File[] files = context.toFile().listFiles();
if (files == null) {
log.warn("目录无法访问: {}", context);
return;
}
for (File file : files) {
modify(context, file.toPath());
}
}
}
private void handleDelete(Path thisDir, Path context) {
if (context == null || isTmp(context)) {
return;
}
if (thisDir.equals(root)) {
String name = context.getFileName().toString();
Path candidate = root.resolve(name);
if (Files.isDirectory(candidate)) {
return;
}
removeAction(name);
AtomicReference<java.nio.file.WatchKey> toRemove = new AtomicReference<>();
watchSupport.context().watchKeys().forEach((key, path) -> {
if (path.getFileName().toString().equals(name)) {
key.cancel();
toRemove.set(key);
}
});
if (toRemove.get() != null) {
watchSupport.context().watchKeys().remove(toRemove.get());
}
return;
}
if (!thisDir.equals(root) && !normalPath(thisDir)) {
removeAction(thisDir.getFileName().toString());
}
}
private void reconcile() {
Set<String> existed = existedMetaActions.keySet().stream()
.filter(actionKey -> actionKey.startsWith("local::"))
.map(actionKey -> actionKey.split("::")[1])
.collect(Collectors.toSet());
Set<String> currentDirs = new HashSet<>();
try (Stream<Path> stream = Files.list(root).filter(Files::isDirectory)) {
stream.forEach(path -> {
String name = path.getFileName().toString();
currentDirs.add(name);
boolean contains = existed.contains(name);
boolean normal = normalPath(path);
if (contains && !normal) {
removeAction(name);
}
if (!contains) {
boolean alreadyWatching = watchSupport.isWatching(path);
if (!alreadyWatching) {
try {
watchSupport.registerDirectory(path);
} catch (IOException e) {
log.error("监听目录注册失败: {}", path);
}
}
if (normal) {
addAction(name, path);
}
}
});
} catch (IOException e) {
log.error("目录无法读取: {}", root);
return;
}
for (String existedName : existed) {
if (!currentDirs.contains(existedName)) {
removeAction(existedName);
}
}
}
private void modify(Path thisDir, Path context) {
String fileName = context.getFileName().toString();
if (fileName.equals("desc.json")) {
handleMetaModify(thisDir);
}
if (fileName.startsWith("run.")) {
handleProgramModify(thisDir);
}
}
private void handleProgramModify(Path thisDir) {
String name = thisDir.getFileName().toString();
String actionKey = "local::" + name;
if (existedMetaActions.containsKey(actionKey)) {
return;
}
if (!addAction(name, thisDir)) {
removeAction(name);
}
}
private void handleMetaModify(Path thisDir) {
String name = thisDir.getFileName().toString();
if (!addAction(name, thisDir)) {
removeAction(name);
}
}
private boolean addAction(String name, Path dir) {
File program = null;
try (Stream<Path> stream = Files.list(dir)) {
for (Path path : stream.toList()) {
if (isTmp(path)) {
continue;
}
if (path.getFileName().toString().startsWith("run.")) {
program = path.toFile();
}
}
} catch (Exception e) {
log.error("添加 action 失败", e);
return false;
}
MetaActionInfo info;
try {
info = JSON.parseObject(Files.readString(dir.resolve("desc.json"), StandardCharsets.UTF_8), MetaActionInfo.class);
} catch (Exception e) {
log.error("desc.json 加载失败: {}", dir);
return false;
}
String actionKey = "local::" + name;
existedMetaActions.put(actionKey, info);
server.addTool(buildAsyncToolSpecification(info, program, actionKey, name)).subscribe();
return true;
}
private void removeAction(String name) {
existedMetaActions.remove("local::" + name);
server.removeTool(name).subscribe();
}
private boolean normalPath(Path path) {
File[] files = loadFiles(path);
if (files == null || files.length < 2) {
return false;
}
boolean desc = false;
int run = 0;
for (File file : files) {
String fileName = file.getName();
if (fileName.endsWith(".tmp")) {
continue;
}
if (fileName.equals("desc.json")) {
desc = true;
}
if (fileName.startsWith("run.")) {
run++;
}
}
return run == 1 && desc;
}
private File[] loadFiles(Path path) {
if (!Files.isDirectory(path)) {
return null;
}
return path.toFile().listFiles();
}
private McpStatelessServerFeatures.AsyncToolSpecification buildAsyncToolSpecification(MetaActionInfo info, File program, String actionKey, String name) {
Map<String, Object> additional = Map.of(
"pre", info.getPreActions(),
"post", info.getPostActions(),
"strict_pre", info.getStrictDependencies(),
"io", info.getIo()
);
McpSchema.Tool tool = McpSchema.Tool.builder()
.name(name)
.description(info.getDescription())
.inputSchema(McpJsonMapper.getDefault(), JSONObject.toJSONString(info.getParams()))
.outputSchema(info.getResponseSchema())
.title(actionKey)
.meta(additional)
.build();
return McpStatelessServerFeatures.AsyncToolSpecification.builder()
.tool(tool)
.callHandler(buildToolHandler(program, info.getLauncher()))
.build();
}
private BiFunction<McpTransportContext, McpSchema.CallToolRequest, Mono<McpSchema.CallToolResult>> buildToolHandler(File program, String launcher) {
return (mcpTransportContext, callToolRequest) -> {
Map<String, Object> arguments = callToolRequest.arguments();
if (arguments == null) {
arguments = Map.of();
}
String[] commands = commandExecutionService.buildFileExecutionCommands(launcher, arguments, program.getAbsolutePath());
if (commands == null) {
return Mono.just(McpSchema.CallToolResult.builder()
.addTextContent("未知文件类型: " + program.getName())
.isError(true)
.build());
}
return Mono.fromCallable(() -> {
CommandExecutionService.Result execResult = commandExecutionService.exec(
ExecutionPolicyRegistry.INSTANCE.prepare(List.of(commands))
);
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder()
.isError(!execResult.isOk());
List<String> resultList = execResult.getResultList();
if (resultList != null && !resultList.isEmpty()) {
builder.textContent(resultList);
builder.structuredContent(resultList);
} else {
builder.addTextContent(execResult.getTotal());
builder.structuredContent(execResult.getTotal());
}
return builder.build();
}).subscribeOn(Schedulers.boundedElastic());
};
}
@Override
public void close() {
try {
watchSupport.close();
} catch (IOException ignored) {
}
server.close();
}
}

View File

@@ -0,0 +1,49 @@
package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.client.McpSyncClient;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class McpClientRegistry implements AutoCloseable {
private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>();
public McpSyncClient get(String serverName) {
return clients.get(serverName);
}
public void register(String serverName, McpSyncClient client) {
McpSyncClient old = clients.put(serverName, client);
if (old != null && old != client) {
old.close();
}
}
public McpSyncClient remove(String serverName) {
McpSyncClient client = detach(serverName);
if (client != null) {
client.close();
}
return client;
}
public McpSyncClient detach(String serverName) {
return clients.remove(serverName);
}
public boolean contains(String serverName) {
return clients.containsKey(serverName);
}
public Set<String> listIds() {
return new HashSet<>(clients.keySet());
}
@Override
public void close() {
clients.forEach((id, client) -> client.close());
clients.clear();
}
}

View File

@@ -0,0 +1,335 @@
package work.slhaf.partner.core.action.runner.mcp;
import cn.hutool.json.JSONUtil;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
import work.slhaf.partner.core.action.runner.policy.RunnerExecutionPolicyListener;
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
@Slf4j
public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyListener {
private final Path root;
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final McpClientRegistry mcpClientRegistry;
private final McpTransportFactory mcpTransportFactory;
private final McpMetaRegistry mcpMetaRegistry;
private final DirectoryWatchSupport watchSupport;
private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>();
public McpConfigWatcher(Path root,
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
McpClientRegistry mcpClientRegistry,
McpTransportFactory mcpTransportFactory,
McpMetaRegistry mcpMetaRegistry,
ExecutorService executor) throws IOException {
this.root = root;
this.existedMetaActions = existedMetaActions;
this.mcpClientRegistry = mcpClientRegistry;
this.mcpTransportFactory = mcpTransportFactory;
this.mcpMetaRegistry = mcpMetaRegistry;
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, this::loadInitial)
.onCreate(this::handleCreate)
.onModify((thisDir, context) -> checkAndReload(true))
.onDelete(this::handleDelete)
.onOverflow((thisDir, context) -> checkAndReload(false));
}
public void start() {
watchSupport.start();
log.info("CommonMcp 文件监听注册完毕");
}
private void loadInitial() {
File[] files = loadFiles(root);
if (files == null) {
return;
}
for (File file : files) {
if (!normalFile(file)) {
continue;
}
loadAndRegisterMcpClientsFromFile(file);
}
}
private void handleCreate(Path thisDir, Path context) {
if (context == null) {
return;
}
File file = context.toFile();
if (!normalFile(file)) {
return;
}
loadAndRegisterMcpClientsFromFile(file);
}
private void handleDelete(Path thisDir, Path context) {
if (context == null) {
return;
}
File file = context.toFile();
if (!file.getName().endsWith(".json")) {
return;
}
McpConfigFileRecord fileRecord = mcpConfigFileCache.remove(file);
if (fileRecord == null) {
return;
}
for (String clientId : fileRecord.paramsCacheMap().keySet()) {
unregisterClient(clientId);
}
}
private boolean normalFile(File file) {
return file.exists() && file.isFile() && file.getName().endsWith(".json");
}
private void registerMcpClient(String id, McpTransportConfig transportConfig) {
McpSyncClient client = McpClient.sync(mcpTransportFactory.create(transportConfig))
.requestTimeout(Duration.ofSeconds(transportConfig.timeout()))
.clientInfo(new McpSchema.Implementation(id, "PARTNER"))
.build();
try {
for (McpSchema.Tool tool : client.listTools().tools()) {
existedMetaActions.put(id + "::" + tool.name(), mcpMetaRegistry.buildMetaActionInfo(id, tool));
}
mcpClientRegistry.register(id, client);
} catch (Exception e) {
log.warn("[{}] MCP client init failed, skipped (probably non-stdio-safe)", id, e);
client.close();
}
}
private void unregisterClient(String clientId) {
McpSyncClient client = mcpClientRegistry.detach(clientId);
removeClientActions(clientId, client);
if (client != null) {
try {
client.close();
} catch (Exception e) {
log.warn("[{}] MCP client close failed", clientId, e);
}
}
}
private void removeClientActions(String clientId, McpSyncClient client) {
boolean removedByListing = false;
if (client != null) {
try {
List<McpSchema.Tool> tools = client.listTools().tools();
if (tools != null) {
for (McpSchema.Tool tool : tools) {
existedMetaActions.remove(clientId + "::" + tool.name());
}
removedByListing = true;
}
} catch (Exception e) {
log.warn("[{}] MCP client listTools failed during unregister, fallback to key scan", clientId, e);
}
}
if (!removedByListing) {
String prefix = clientId + "::";
existedMetaActions.keySet().removeIf(actionKey -> actionKey.startsWith(prefix));
}
}
private cn.hutool.json.JSONObject readJson(File file) {
try {
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
} catch (Exception ignored) {
return null;
}
}
private cn.hutool.json.JSONObject readMcp(cn.hutool.json.JSONObject json, String id) {
try {
return json.getJSONObject(id);
} catch (Exception ignored) {
return null;
}
}
@SuppressWarnings("unchecked")
private McpTransportConfig readParams(cn.hutool.json.JSONObject mcp) {
Set<String> keys = mcp.keySet();
int timeout = mcp.getInt("timeout", 30);
if (matchesKeys(keys, Set.of("command", "args", "env"), Set.of("timeout"))) {
String command = mcp.getStr("command");
Map<String, String> env = mcp.getBean("env", Map.class);
java.util.List<String> args = mcp.getBeanList("args", String.class);
if (command == null || env == null || args == null) {
return null;
}
return new McpTransportConfig.Stdio(timeout, command, env, args);
}
if (matchesKeys(keys, Set.of("uri", "endpoint", "headers"), Set.of("timeout"))) {
String uri = mcp.getStr("uri");
String endpoint = mcp.getStr("endpoint");
Map<String, String> headers = mcp.getBean("headers", Map.class);
if (uri == null || endpoint == null || headers == null) {
return null;
}
return new McpTransportConfig.Http(timeout, uri, endpoint, headers);
}
if (matchesKeys(keys, Set.of("url"), Set.of("timeout"))) {
String url = mcp.getStr("url");
if (url == null) {
return null;
}
return new McpTransportConfig.Http(timeout, url, "", Map.of());
}
return null;
}
private boolean matchesKeys(Set<String> actualKeys, Set<String> requiredKeys, Set<String> optionalKeys) {
if (!actualKeys.containsAll(requiredKeys)) {
return false;
}
Set<String> allowedKeys = new HashSet<>(requiredKeys);
allowedKeys.addAll(optionalKeys);
return allowedKeys.containsAll(actualKeys);
}
private void checkAndReload(boolean trustCache) {
HashMap<String, McpTransportConfig> changedMap = new HashMap<>();
HashSet<String> existingMcpIdSet = new HashSet<>();
File[] files = loadFiles(root);
if (files == null) {
return;
}
for (File file : files) {
if (!normalFile(file)) {
continue;
}
McpConfigFileRecord fileRecord = mcpConfigFileCache.get(file);
boolean fileRecordExists = fileRecord != null;
if (fileRecordExists && !fileChanged(file, fileRecord) && trustCache) {
existingMcpIdSet.addAll(fileRecord.paramsCacheMap().keySet());
continue;
}
cn.hutool.json.JSONObject mcpConfigJson = readJson(file);
if (mcpConfigJson == null) {
if (fileRecordExists) {
existingMcpIdSet.addAll(fileRecord.paramsCacheMap().keySet());
}
continue;
}
McpConfigFileRecord newFileRecord = new McpConfigFileRecord(file.lastModified(), file.length(), new HashMap<>());
for (String id : mcpConfigJson.keySet()) {
cn.hutool.json.JSONObject mcp = readMcp(mcpConfigJson, id);
if (mcp == null) {
continue;
}
McpTransportConfig params = readParams(mcp);
if (params == null) {
continue;
}
existingMcpIdSet.add(id);
newFileRecord.paramsCacheMap().put(id, params);
if (fileRecordExists) {
McpTransportConfig paramsCache = fileRecord.paramsCacheMap().get(id);
if (paramsCache != null && paramsCache.equals(params)) {
continue;
}
}
changedMap.put(id, params);
}
mcpConfigFileCache.put(file, newFileRecord);
}
updateMcpClients(changedMap, existingMcpIdSet);
}
private void updateMcpClients(HashMap<String, McpTransportConfig> changedMap, HashSet<String> existingMcpIdSet) {
changedMap.forEach((clientId, config) -> {
unregisterClient(clientId);
registerMcpClient(clientId, config);
});
for (String clientId : mcpClientRegistry.listIds()) {
if (clientId.equals(LocalRunnerClient.MCP_NAME_DESC) || clientId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)) {
continue;
}
if (!existingMcpIdSet.contains(clientId)) {
unregisterClient(clientId);
}
}
existedMetaActions.keySet().removeIf(actionKey -> {
String serverId = actionKey.split("::")[0];
return !serverId.equals("local")
&& !serverId.equals(LocalRunnerClient.MCP_NAME_DESC)
&& !serverId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)
&& !existingMcpIdSet.contains(serverId);
});
}
private boolean fileChanged(File file, McpConfigFileRecord fileRecord) {
return fileRecord.lastModified() != file.lastModified() || fileRecord.length() != file.length();
}
private void loadAndRegisterMcpClientsFromFile(File file) {
cn.hutool.json.JSONObject mcpConfigJson = readJson(file);
if (mcpConfigJson == null) {
return;
}
McpConfigFileRecord newFileRecord = new McpConfigFileRecord(file.lastModified(), file.length());
for (String id : mcpConfigJson.keySet()) {
cn.hutool.json.JSONObject mcp = readMcp(mcpConfigJson, id);
if (mcp == null) {
continue;
}
McpTransportConfig params = readParams(mcp);
if (params == null) {
continue;
}
registerMcpClient(id, params);
newFileRecord.paramsCacheMap().put(id, params);
}
mcpConfigFileCache.put(file, newFileRecord);
}
private File[] loadFiles(Path path) {
if (!path.toFile().isDirectory()) {
return null;
}
return path.toFile().listFiles();
}
@Override
public void close() throws Exception {
watchSupport.close();
}
@Override
public void onPolicyChanged(@NotNull ExecutionPolicy policy) {
checkAndReload(false);
}
private record McpConfigFileRecord(long lastModified, long length, Map<String, McpTransportConfig> paramsCacheMap) {
private McpConfigFileRecord(long lastModified, long length) {
this(lastModified, length, new HashMap<>());
}
}
}

View File

@@ -0,0 +1,53 @@
package work.slhaf.partner.core.action.runner.mcp;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
@Slf4j
public class McpDescWatcher implements AutoCloseable {
private final Path root;
private final McpMetaRegistry mcpMetaRegistry;
private final DirectoryWatchSupport watchSupport;
public McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
this.root = root;
this.mcpMetaRegistry = mcpMetaRegistry;
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, () -> mcpMetaRegistry.loadDirectory(root))
.onCreate(this::handleUpsert)
.onModify(this::handleUpsert)
.onDelete(this::handleDelete)
.onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root));
}
public void start() {
watchSupport.start();
log.info("DescMcp 文件监听注册完毕");
}
private void handleUpsert(Path thisDir, Path context) {
if (context == null || Files.isDirectory(context) || !mcpMetaRegistry.isValidDescFile(context.getFileName().toString())) {
return;
}
if (!mcpMetaRegistry.addOrUpdate(context)) {
mcpMetaRegistry.remove(context);
}
}
private void handleDelete(Path thisDir, Path context) {
if (context == null || !mcpMetaRegistry.isValidDescFile(context.getFileName().toString())) {
return;
}
mcpMetaRegistry.remove(context);
}
@Override
public void close() throws Exception {
watchSupport.close();
}
}

View File

@@ -0,0 +1,261 @@
package work.slhaf.partner.core.action.runner.mcp;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import javassist.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
@Slf4j
public class McpMetaRegistry implements AutoCloseable {
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, MetaActionInfo> descInfoCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, MetaActionInfo> originalInfoCache = new ConcurrentHashMap<>();
private final McpStatelessAsyncServer descServer;
private final InProcessMcpTransport clientTransport;
public McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) {
this.existedMetaActions = existedMetaActions;
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
this.clientTransport = pair.clientSide();
McpSchema.ServerCapabilities serverCapabilities = McpSchema.ServerCapabilities.builder()
.resources(true, true)
.build();
this.descServer = McpServer.async(pair.serverSide())
.capabilities(serverCapabilities)
.jsonMapper(McpJsonMapper.getDefault())
.build();
}
public McpTransportConfig.InProcess clientConfig(String serverName, int timeout) {
return new McpTransportConfig.InProcess(timeout, clientTransport);
}
public void loadDirectory(Path root) {
File[] files = loadFiles(root);
if (files == null) {
return;
}
for (File file : files) {
addOrUpdate(file);
}
}
public boolean addOrUpdate(Path path) {
return addOrUpdate(path.toFile());
}
public boolean addOrUpdate(File file) {
String name = file.getName();
if (!isValidDescFile(name)) {
return false;
}
try {
MetaActionInfo info = JSON.parseObject(Files.readString(file.toPath(), StandardCharsets.UTF_8), MetaActionInfo.class);
String uri = file.toPath().toUri().toString();
descCache.put(uri, JSONObject.toJSONString(info));
String actionKey = name.replace(".desc.json", "");
descInfoCache.put(actionKey, copyMetaActionInfo(info));
descServer.addResource(buildAsyncResourceSpecification(name, uri)).block();
if (existedMetaActions.containsKey(actionKey)) {
existedMetaActions.put(actionKey, mergeWithOriginal(actionKey, info));
}
return true;
} catch (Exception e) {
log.error("desc.json 解析失败: {}", file.getAbsolutePath());
return false;
}
}
public void remove(Path path) {
String uri = path.toUri().toString();
String actionKey = path.getFileName().toString().replace(".desc.json", "");
descCache.remove(uri);
descInfoCache.remove(actionKey);
descServer.removeResource(uri).block();
MetaActionInfo originalInfo = originalInfoCache.get(actionKey);
if (originalInfo != null) {
existedMetaActions.put(actionKey, copyMetaActionInfo(originalInfo));
return;
}
MetaActionInfo info = existedMetaActions.get(actionKey);
if (info != null) {
existedMetaActions.put(actionKey, resetMetaActionInfo(info));
}
}
public void reconcile(Path root) {
File[] files = loadFiles(root);
if (files == null) {
return;
}
Set<String> currentUris = ConcurrentHashMap.newKeySet();
for (File file : files) {
if (!isValidDescFile(file.getName())) {
continue;
}
currentUris.add(file.toURI().toString());
if (!addOrUpdate(file)) {
remove(file.toPath());
}
}
List<String> serverUris = descServer.listResources()
.map(McpSchema.Resource::uri)
.collectList()
.block();
if (serverUris == null) {
log.error("无法获取 DescMcpServer 持有的资源列表");
return;
}
for (String uri : serverUris) {
if (!currentUris.contains(uri)) {
remove(Path.of(java.net.URI.create(uri)));
}
}
}
public MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) {
String actionKey = serverId + "::" + tool.name();
MetaActionInfo baseInfo = buildToolMetaActionInfo(tool);
originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo));
MetaActionInfo override = descInfoCache.get(actionKey);
return override == null ? baseInfo : mergeWithOriginal(actionKey, override);
}
private McpStatelessServerFeatures.AsyncResourceSpecification buildAsyncResourceSpecification(String name, String uri) {
McpSchema.Resource resource = McpSchema.Resource.builder()
.name(name)
.title(name)
.description("Action descriptor for " + name)
.mimeType("application/json")
.uri(uri)
.build();
BiFunction<McpTransportContext, McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler = (context, request) -> {
String result = descCache.get(request.uri());
if (result == null) {
return Mono.error(new NotFoundException("未找到 Resource: " + request.uri()));
}
return Mono.just(new McpSchema.ReadResourceResult(List.of(
new McpSchema.TextResourceContents(request.uri(), "application/json", result)
)));
};
return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler);
}
public boolean isValidDescFile(String fileName) {
return fileName.endsWith(".desc.json") && fileName.contains("::");
}
private File[] loadFiles(Path root) {
if (!Files.isDirectory(root)) {
return null;
}
return root.toFile().listFiles();
}
private MetaActionInfo resetMetaActionInfo(@NotNull MetaActionInfo info) {
return new MetaActionInfo(
false,
info.getLauncher(),
copyParams(info.getParams()),
info.getDescription(),
new LinkedHashSet<>(),
new LinkedHashSet<>(),
new LinkedHashSet<>(),
false,
copyResponseSchema(info.getResponseSchema())
);
}
@Override
public void close() {
descServer.close();
}
private MetaActionInfo buildToolMetaActionInfo(McpSchema.Tool tool) {
Map<String, Object> outputSchema = tool.outputSchema();
boolean io = false;
Set<String> preActions = new LinkedHashSet<>();
Set<String> postActions = new LinkedHashSet<>();
boolean strictDependencies = false;
Set<String> tags = new LinkedHashSet<>();
Map<String, Object> meta = tool.meta();
if (meta != null) {
JSONObject metaJson = JSONObject.from(meta);
io = Boolean.TRUE.equals(metaJson.getBoolean("io"));
preActions = toOrderedSet(metaJson.getList("pre", String.class));
postActions = toOrderedSet(metaJson.getList("post", String.class));
strictDependencies = Boolean.TRUE.equals(metaJson.getBoolean("strict"));
tags = toOrderedSet(metaJson.getList("tag", String.class));
}
return new MetaActionInfo(
io,
null,
copyParams(tool.inputSchema().properties()),
tool.description(),
tags,
preActions,
postActions,
strictDependencies,
outputSchema == null ? JSONObject.of() : JSONObject.from(outputSchema)
);
}
private MetaActionInfo mergeWithOriginal(String actionKey, MetaActionInfo override) {
MetaActionInfo original = originalInfoCache.get(actionKey);
return override == null ? copyMetaActionInfo(original) : copyMetaActionInfo(override);
}
private MetaActionInfo copyMetaActionInfo(MetaActionInfo source) {
if (source == null) {
return null;
}
return new MetaActionInfo(
source.getIo(),
source.getLauncher(),
copyParams(source.getParams()),
source.getDescription(),
toOrderedSet(source.getTags()),
toOrderedSet(source.getPreActions()),
toOrderedSet(source.getPostActions()),
source.getStrictDependencies(),
copyResponseSchema(source.getResponseSchema())
);
}
private <T> LinkedHashSet<T> toOrderedSet(Collection<T> source) {
return source == null ? new LinkedHashSet<>() : new LinkedHashSet<>(source);
}
private Map<String, String> copyParams(Map<String, ?> params) {
if (params == null) {
return null;
}
Map<String, String> copied = new LinkedHashMap<>();
params.forEach((key, value) -> copied.put(key, value == null ? null : String.valueOf(value)));
return copied;
}
private JSONObject copyResponseSchema(JSONObject responseSchema) {
return responseSchema == null ? JSONObject.of() : JSONObject.from(responseSchema);
}
}

View File

@@ -0,0 +1,22 @@
package work.slhaf.partner.core.action.runner.mcp;
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
import java.util.List;
import java.util.Map;
public sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess {
int timeout();
record Http(int timeout, String baseUri, String endpoint,
Map<String, String> headers) implements McpTransportConfig {
}
record Stdio(int timeout, String command, Map<String, String> env,
List<String> args) implements McpTransportConfig {
}
record InProcess(int timeout, InProcessMcpTransport clientTransport) implements McpTransportConfig {
}
}

View File

@@ -0,0 +1,44 @@
package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.spec.McpClientTransport;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class McpTransportFactory {
public McpClientTransport create(McpTransportConfig config) {
return switch (config) {
case McpTransportConfig.Stdio stdio -> {
List<String> commands = new ArrayList<>();
commands.add(stdio.command());
commands.addAll(stdio.args());
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(commands);
Map<String, String> env = new HashMap<>(stdio.env());
env.putAll(wrapped.getEnvironment());
ServerParameters serverParameters = ServerParameters.builder(wrapped.getCommand())
.args(wrapped.getArgs())
.env(env)
.build();
yield new StdioClientTransport(serverParameters, McpJsonMapper.getDefault());
}
case McpTransportConfig.Http http -> {
McpSyncHttpClientRequestCustomizer customizer = (builder, method, endpoint, body, context) -> http.headers().forEach(builder::setHeader);
yield HttpClientSseClientTransport.builder(http.baseUri())
.httpRequestCustomizer(customizer)
.sseEndpoint(http.endpoint())
.build();
}
case McpTransportConfig.InProcess inProcess -> inProcess.clientTransport();
};
}
}

View File

@@ -0,0 +1,82 @@
package work.slhaf.partner.core.action.runner.policy
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException
private const val BWRAP_COMMAND = "bwrap"
object BwrapPolicyProvider : PolicyProvider(
policyName = "bwrap"
) {
init {
requireBwrapAvailable()
}
override fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec {
val (command, args) = splitCommands(commands)
val wrappedArgs = buildList {
add("--ro-bind")
add("/")
add("/")
add("--proc")
add("/proc")
add("--dev")
add("/dev")
if (policy.net == ExecutionPolicy.Network.DISABLE) {
add("--unshare-net")
}
if (!policy.workingDirectory.isNullOrBlank()) {
add("--chdir")
add(policy.workingDirectory)
}
policy.readOnlyPaths.forEach { path ->
add("--ro-bind")
add(path)
add(path)
}
policy.writablePaths.forEach { path ->
add("--bind")
add(path)
add(path)
}
add("--")
add(command)
addAll(args)
}
return WrappedLaunchSpec(
command = BWRAP_COMMAND,
args = wrappedArgs,
workingDirectory = policy.workingDirectory,
environment = resolveEnvironment(policy)
)
}
private fun requireBwrapAvailable() {
val available = try {
val process = ProcessBuilder(BWRAP_COMMAND, "--version")
.redirectErrorStream(true)
.start()
val exitCode = process.waitFor()
exitCode == 0
} catch (e: Exception) {
throw ActionInfrastructureStartupException(
"Failed to detect executable command '$BWRAP_COMMAND'",
"bwrap-policy-provider",
null,
BWRAP_COMMAND,
e
)
}
if (!available) {
throw ActionInfrastructureStartupException(
"Executable command '$BWRAP_COMMAND' is not available",
"bwrap-policy-provider",
null,
BWRAP_COMMAND
)
}
}
}

View File

@@ -0,0 +1,20 @@
package work.slhaf.partner.core.action.runner.policy
object DirectPolicyProvider : PolicyProvider(
policyName = "direct"
) {
override fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec {
val (command, args) = splitCommands(commands)
return WrappedLaunchSpec(
command = command,
args = args,
workingDirectory = policy.workingDirectory,
environment = resolveEnvironment(policy)
)
}
}

View File

@@ -0,0 +1,157 @@
package work.slhaf.partner.core.action.runner.policy
import com.alibaba.fastjson2.JSONObject
import work.slhaf.partner.framework.agent.config.Config
import work.slhaf.partner.framework.agent.config.ConfigRegistration
import work.slhaf.partner.framework.agent.config.Configurable
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
object ExecutionPolicyRegistry : Configurable, ConfigRegistration<ExecutionPolicy> {
private const val DEFAULT_PROVIDER = "direct"
private val policyProviders = ConcurrentHashMap<String, PolicyProvider>().apply {
put(DEFAULT_PROVIDER, DirectPolicyProvider)
}
private val listeners = CopyOnWriteArraySet<RunnerExecutionPolicyListener>()
init {
register()
}
@Volatile
private lateinit var currentPolicy: ExecutionPolicy
fun prepare(commands: List<String>): WrappedLaunchSpec {
val policy = currentPolicy
val provider = policyProviders[policy.provider]
?: policyProviders[DEFAULT_PROVIDER]
?: error("Default provider '${DEFAULT_PROVIDER}' is not registered")
return provider.prepare(policy, commands)
}
fun updatePolicy(policy: ExecutionPolicy) {
currentPolicy = policy
listeners.forEach { it.onPolicyChanged(policy) }
}
fun addListener(listener: RunnerExecutionPolicyListener) {
listeners += listener
}
fun removeListener(listener: RunnerExecutionPolicyListener) {
listeners -= listener
}
fun registerPolicyProvider(policyProvider: PolicyProvider) {
val name = policyProvider.policyName
if (policyProviders.containsKey(name)) {
return
}
policyProviders[name] = policyProvider
}
override fun declare(): Map<Path, ConfigRegistration<out Config>> {
return mapOf(Path.of("action", "runner_policy.json") to this)
}
override fun type(): Class<ExecutionPolicy> {
return ExecutionPolicy::class.java
}
override fun init(
config: ExecutionPolicy,
json: JSONObject?
) {
this.currentPolicy = config
}
override fun defaultConfig(): ExecutionPolicy {
return ExecutionPolicy(
provider = "direct",
mode = ExecutionPolicy.Mode.DIRECT,
net = ExecutionPolicy.Network.ENABLE,
inheritEnv = true,
env = emptyMap(),
workingDirectory = null,
readOnlyPaths = emptySet(),
writablePaths = emptySet(),
)
}
override fun onReload(
config: ExecutionPolicy,
json: JSONObject?
) {
this.currentPolicy = config
}
}
data class ExecutionPolicy(
val mode: Mode,
val provider: String,
val net: Network,
val inheritEnv: Boolean,
val env: Map<String, String>,
val workingDirectory: String?,
val readOnlyPaths: Set<String>,
val writablePaths: Set<String>,
) : Config() {
enum class Mode {
DIRECT,
SANDBOX
}
enum class Network {
DISABLE,
ENABLE
}
}
data class WrappedLaunchSpec(
val command: String,
val args: List<String>,
val workingDirectory: String? = null,
val environment: Map<String, String> = emptyMap()
)
abstract class PolicyProvider(
val policyName: String
) {
abstract fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec
protected fun resolveEnvironment(policy: ExecutionPolicy): Map<String, String> {
val result = LinkedHashMap<String, String>()
if (policy.inheritEnv) {
result.putAll(System.getenv())
}
result.putAll(policy.env)
return result
}
protected fun splitCommands(commands: List<String>): Pair<String, List<String>> {
require(commands.isNotEmpty()) { "commands must not be empty" }
return commands.first() to commands.drop(1)
}
}
interface RunnerExecutionPolicyListener {
fun onPolicyChanged(policy: ExecutionPolicy)
fun registerPolicyListener() {
ExecutionPolicyRegistry.addListener(this)
}
fun unregisterPolicyListener() {
ExecutionPolicyRegistry.removeListener(this)
}
}

View File

@@ -0,0 +1,131 @@
package work.slhaf.partner.core.action.runner.support;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionSerializationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@Slf4j
public class ActionSerializer {
private final String tmpActionPath;
private final String dynamicActionPath;
public ActionSerializer(String tmpActionPath, String dynamicActionPath) {
this.tmpActionPath = tmpActionPath;
this.dynamicActionPath = dynamicActionPath;
}
public static String normalizeCodeType(String codeType) {
if (codeType == null || codeType.isBlank()) {
throw new IllegalArgumentException("codeType 不能为空");
}
return codeType.startsWith(".") ? codeType : "." + codeType;
}
private static @NotNull Path createActionDir(String baseName, Path baseDir, String fileExt) {
for (int i = 0; ; i++) {
String dirName = i == 0 ? baseName : baseName + "(" + i + ")";
Path candidate = baseDir.resolve(dirName);
try {
Files.createDirectory(candidate);
return candidate;
} catch (FileAlreadyExistsException ignored) {
} catch (IOException e) {
throw new ActionSerializationException(
"Failed to create action directory: " + candidate.toAbsolutePath(),
baseName,
baseDir.toAbsolutePath().toString(),
fileExt,
"CREATE_DIRECTORY",
e
);
}
}
}
public String buildTmpPath(String actionKey, String codeType) {
return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString();
}
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
log.debug("行动程序临时序列化: {}", tempAction);
Path path = Path.of(tempAction.getLocation());
validateTmpLocation(path, codeType);
File file = path.toFile();
file.createNewFile();
Files.writeString(path, code);
log.debug("临时序列化完毕");
}
private void validateTmpLocation(Path path, String codeType) throws IOException {
String normalizedCodeType = normalizeCodeType(codeType);
String fileName = path.getFileName().toString();
if (!fileName.endsWith(normalizedCodeType)) {
throw new IOException("临时文件路径与 codeType 不匹配: " + path);
}
}
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
log.debug("行动程序持久序列化: {}", metaActionInfo);
val baseDir = Path.of(dynamicActionPath);
if (!Files.isDirectory(baseDir)) {
throw new ActionSerializationException(
"Action base directory is not available: " + baseDir.toAbsolutePath(),
fileMetaData.getName(),
baseDir.toAbsolutePath().toString(),
fileMetaData.getExt(),
"VALIDATE_BASE_DIR"
);
}
val actionDir = createActionDir(fileMetaData.getName(), baseDir, fileMetaData.getExt());
val runTmp = actionDir.resolve("run." + fileMetaData.getExt() + ".tmp");
val descTmp = actionDir.resolve("desc.json.tmp");
val runFinal = actionDir.resolve("run." + fileMetaData.getExt());
val descFinal = actionDir.resolve("desc.json");
try {
Files.writeString(runTmp, fileMetaData.getContent());
Files.writeString(descTmp, JSONObject.toJSONString(metaActionInfo));
Files.move(runTmp, runFinal, StandardCopyOption.ATOMIC_MOVE);
Files.move(descTmp, descFinal, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
safeDelete(runTmp);
safeDelete(descTmp);
safeDelete(runFinal);
safeDelete(descFinal);
safeDelete(actionDir);
throw new ActionSerializationException(
"Failed to persist action files",
fileMetaData.getName(),
baseDir.toAbsolutePath().toString(),
fileMetaData.getExt(),
"WRITE_FILES",
e
);
}
log.debug("持久序列化结束");
}
private void safeDelete(Path path) {
try {
if (Files.exists(path)) {
Files.delete(path);
}
} catch (IOException ignored) {
}
}
}

View File

@@ -0,0 +1,29 @@
package work.slhaf.partner.core.cognition;
import org.w3c.dom.Element;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
import java.util.concurrent.locks.Lock;
@Capability("cognition")
public interface CognitionCapability {
void initiateTurn(String input, String target, String... skippedModules);
ContextWorkspace contextWorkspace();
List<Message> getChatMessages();
List<Message> snapshotChatMessages();
void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor);
void refreshRecentChatMessagesContext();
Element messageNotesElement();
Lock getMessageLock();
}

View File

@@ -0,0 +1,214 @@
package work.slhaf.partner.core.cognition;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import kotlin.Unit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.common.base.Block;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@CapabilityCore(value = "cognition")
public class CognitionCore implements StateSerializable {
private static final String RECENT_CHAT_MESSAGE_NOTES = """
消息格式:
- 所有消息统一写为“标记行 + 空行 + 正文”,比如:
[[AGENT]: self]: [NOT_REPLIED][COMPRESSED]:
正文内容
- 标记行一定包含身份标签,通常格式为 [[USER]: <userName>] 或 [[AGENT]: self]
- 若身份标签提取失败,可能回退为 [[Unknown]: Unknown]
- 若存在其他标签,则写为“身份标签: 状态标签串:”
- 正文永远从空行后开始
标记含义:
- [USER]: 外部用户来源
- [AGENT]: 系统内部来源
- [NOT_REPLIED]: 仅保留在历史中的未直接回复结果
- [COMPRESSED]: 该消息正文经过压缩
""";
private final ReentrantLock messageLock = new ReentrantLock();
/**
* 主模型的聊天记录
*/
private List<Message> chatMessages = new ArrayList<>();
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
public CognitionCore() {
register();
}
@CapabilityMethod
public ContextWorkspace contextWorkspace() {
return contextWorkspace;
}
@CapabilityMethod
public void initiateTurn(String input, String target, String... skippedModules) {
PartnerRunningFlowContext primaryContext = PartnerRunningFlowContext.fromSelf(input);
primaryContext.setTarget(target);
if (skippedModules != null) {
for (String skippedModule : skippedModules) {
primaryContext.addSkippedModule(skippedModule);
}
}
AgentRuntime.INSTANCE.submit(primaryContext);
}
@CapabilityMethod
public List<Message> getChatMessages() {
return chatMessages;
}
@CapabilityMethod
public List<Message> snapshotChatMessages() {
messageLock.lock();
try {
return List.copyOf(chatMessages);
} finally {
messageLock.unlock();
}
}
@CapabilityMethod
public void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor) {
messageLock.lock();
try {
int safeSnapshotSize = Math.clamp(snapshotSize, 0, chatMessages.size());
if (safeSnapshotSize == 0) {
return;
}
int safeDivisor = Math.max(retainDivisor, 1);
int retainCount = safeSnapshotSize / safeDivisor;
int retainStart = Math.max(0, safeSnapshotSize - retainCount);
List<Message> rolled = new ArrayList<>(chatMessages.subList(retainStart, safeSnapshotSize));
if (chatMessages.size() > safeSnapshotSize) {
rolled.addAll(chatMessages.subList(safeSnapshotSize, chatMessages.size()));
}
chatMessages = rolled;
} finally {
messageLock.unlock();
}
}
@CapabilityMethod
public Lock getMessageLock() {
return messageLock;
}
@CapabilityMethod
public void refreshRecentChatMessagesContext() {
ContextBlock block = new ContextBlock(
new BlockContent("recent_chat_messages", "communication_producer") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
document.appendChild(document.importNode(messageNotesElement(), true));
Element chatMessagesElement = document.createElement("chat_messages");
root.appendChild(chatMessagesElement);
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
messageElement.setAttribute("role", message.roleValue());
messageElement.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
},
Set.of(ContextBlock.FocusedDomain.COMMUNICATION),
100,
10,
4
);
contextWorkspace.register(block);
}
@CapabilityMethod
public Element messageNotesElement() {
return new Block("message_tag_notes") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setTextContent(RECENT_CHAT_MESSAGE_NOTES);
}
}.encodeToXml();
}
private List<Message> resolveRecentChatMessages() {
int exclusiveEnd = Math.max(chatMessages.size() - 1, 0);
if (exclusiveEnd == 0) {
return List.of();
}
int start = Math.max(exclusiveEnd - 6, 0);
return chatMessages.subList(start, exclusiveEnd);
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "cognition.json");
}
@Override
public void load(@NotNull JSONObject state) {
JSONArray messageArray = state.getJSONArray("chat_messages");
if (messageArray == null) {
log.warn("chat_messages is missing");
return;
}
for (int i = 0; i < messageArray.size(); i++) {
JSONObject messageObject = messageArray.getJSONObject(i);
if (messageObject == null) {
continue;
}
String role = messageObject.getString("role");
String content = messageObject.getString("content");
if (role == null || content == null) {
continue;
}
this.chatMessages.add(new Message(Message.Character.fromValue(role), content));
}
refreshRecentChatMessagesContext();
}
@Override
public @NotNull State convert() {
State state = new State();
List<StateValue.Obj> convertedMessageList = chatMessages.stream().map(message -> {
Map<String, StateValue> convertedMap = Map.of(
"role", StateValue.str(message.roleValue()),
"content", StateValue.str(message.getContent())
);
return StateValue.obj(convertedMap);
}).toList();
state.append("chat_messages", StateValue.arr(convertedMessageList));
return state;
}
}

View File

@@ -0,0 +1,401 @@
package work.slhaf.partner.core.cognition
import com.alibaba.fastjson2.JSONObject
import org.w3c.dom.Document
import org.w3c.dom.Element
import work.slhaf.partner.common.base.Block
import work.slhaf.partner.framework.agent.config.ConfigCenter
import work.slhaf.partner.framework.agent.log.TraceEvent
import work.slhaf.partner.framework.agent.log.TraceRecorder
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
import kotlin.math.max
import kotlin.math.min
class ContextWorkspace {
private val tracePath: Path = ConfigCenter.paths.stateDir
.resolve("trace")
.resolve("context-workspace")
.normalize()
.toAbsolutePath()
private val stateSet = mutableSetOf<ContextBlock>()
private val lock = ReentrantReadWriteLock()
/**
* 根据传入的 [ContextBlock.FocusedDomain] 列表,获取上下文块
* @param domains 需要获取上下文的域列表,顺序将决定权重优先级,按照列表排序将具备线性权重分层,最终反映到 blockContent 列表的排序上
*/
fun resolve(domains: List<ContextBlock.FocusedDomain>): ResolvedContext = lock.write {
if (domains.isEmpty()) {
return@write ResolvedContext(emptyList())
}
val primaryDomain = domains.first()
val domainWeights = domains
.distinct()
.withIndex()
.associate { (index, domain) -> domain to (domains.size - index) }
val activeBlocks = mutableListOf<ResolvedContextBlock>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val block = iterator.next()
val fadedScore = block.applyTimeFade()
if (fadedScore <= 0.0) {
iterator.remove()
continue
}
val matchedDomains = block.focusedOn.intersect(domainWeights.keys)
if (matchedDomains.isEmpty()) {
continue
}
val exposure = if (primaryDomain in matchedDomains) {
ContextBlock.Exposure.PRIMARY
} else {
ContextBlock.Exposure.SECONDARY
}
val activationScore = block.activate(exposure)
if (activationScore <= 0.0) {
iterator.remove()
continue
}
activeBlocks += ResolvedContextBlock(
block = block,
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
activationScore = activationScore,
renderedBlock = block.render(exposure)
)
}
val blocks = activeBlocks
.sortedWith(
compareByDescending<ResolvedContextBlock> { it.domainWeight }
.thenByDescending { it.activationScore }
.thenBy { it.block.sourceKey.blockName }
.thenBy { it.block.sourceKey.source }
)
.groupBy { it.block.sourceKey }
.values
.map { groupedBlocks ->
if (groupedBlocks.size == 1) {
renderResolvedBlock(groupedBlocks.first())
} else {
AggregatedBlockContent(groupedBlocks)
}
}
ResolvedContext(blocks)
}
private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent {
return resolved.renderedBlock
}
/**
* @param contextBlock 注册的新上下文块
*/
fun register(contextBlock: ContextBlock) = lock.write {
val removedBlocks = mutableListOf<ContextBlock>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val currentBlock = iterator.next()
if (!currentBlock.sameWith(contextBlock)) {
continue
}
if (currentBlock.applyReplaceFade() <= 0.0) {
iterator.remove()
removedBlocks.add(currentBlock)
}
}
stateSet += contextBlock
recordRegister(contextBlock, removedBlocks)
}
fun expire(blockName: String, source: String) = lock.write {
val sourceKey = ContextBlock.SourceKey(blockName, source)
val removedBlocks = mutableListOf<ContextBlock>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val block = iterator.next()
if (block.sourceKey == sourceKey) {
iterator.remove()
removedBlocks.add(block)
}
}
if (removedBlocks.isNotEmpty()) {
recordExpire(sourceKey, removedBlocks)
}
}
private fun recordRegister(addedBlock: ContextBlock, removedBlocks: List<ContextBlock>) {
val payload = JSONObject()
payload["action"] = "register"
payload["added"] = blockSnapshot(addedBlock)
payload["removed"] = removedBlocks.map(::blockSnapshot)
TraceRecorder.record(TraceEvent(tracePath, payload))
}
private fun recordExpire(sourceKey: ContextBlock.SourceKey, removedBlocks: List<ContextBlock>) {
val payload = JSONObject()
payload["action"] = "expire"
payload["blockName"] = sourceKey.blockName
payload["source"] = sourceKey.source
payload["removed"] = removedBlocks.map(::blockSnapshot)
TraceRecorder.record(TraceEvent(tracePath, payload))
}
private fun blockSnapshot(block: ContextBlock): JSONObject {
val payload = JSONObject()
payload["blockName"] = block.sourceKey.blockName
payload["source"] = block.sourceKey.source
payload["focusedOn"] = block.focusedOn.map { it.name }
payload["fullRendered"] = block.blockContent.encodeToXmlString()
payload["compactRendered"] = block.compactBlock.encodeToXmlString()
payload["abstractRendered"] = block.abstractBlock.encodeToXmlString()
return payload
}
}
private data class ResolvedContextBlock(
val block: ContextBlock,
val domainWeight: Int,
val activationScore: Double,
val renderedBlock: BlockContent
)
data class ContextBlock @JvmOverloads constructor(
val blockContent: BlockContent,
val compactBlock: BlockContent = blockContent,
val abstractBlock: BlockContent = blockContent,
/**
* 该 block 集中在哪些域
*/
val focusedOn: Set<FocusedDomain>,
/**
* 新的 [blockContent] 属性与其相同时,发生的衰退步长
*/
private val replaceFadeFactor: Double,
/**
* 随时间发生的衰退步长,按照分钟定义
*/
private val timeFadeFactor: Double,
/**
* 触发一次激活时,发生的强化步长
*/
private val activateFactor: Double
) {
internal enum class Exposure {
PRIMARY,
SECONDARY
}
private enum class ProjectionLevel {
ABSTRACT,
COMPACT,
FULL
}
/**
* 默认活跃分数降低至0时将在 [ContextWorkspace] 中移除该 block
* 此外还参与到当同源 block 存在时的排序,按该分数升序排列,只影响同源 block 间的顺序
*/
private var activationScore = 100.0
private var lastTouchedAt = Instant.now()
enum class FocusedDomain {
ACTION,
MEMORY,
PERCEIVE,
COGNITION,
COMMUNICATION,
}
internal val sourceKey: SourceKey
get() = SourceKey(blockContent.blockName, blockContent.source)
fun applyTimeFade(): Double {
refreshByElapsedTime()
return activationScore
}
fun applyReplaceFade(): Double {
refreshByElapsedTime()
activationScore = max(0.0, activationScore - replaceFadeFactor)
return activationScore
}
internal fun activate(exposure: Exposure): Double {
refreshByElapsedTime()
val currentLevel = currentProjectionLevel()
val increasedScore = when (exposure) {
Exposure.PRIMARY -> activationScore + when (currentLevel) {
ProjectionLevel.FULL -> activateFactor
ProjectionLevel.COMPACT -> activateFactor * 0.6
ProjectionLevel.ABSTRACT -> activateFactor * 0.6
}
Exposure.SECONDARY -> activationScore + when (currentLevel) {
ProjectionLevel.COMPACT -> activateFactor * 0.2
ProjectionLevel.ABSTRACT -> activateFactor * 0.1
ProjectionLevel.FULL -> 0.0
}
}
activationScore = min(activationCeiling(exposure, currentLevel), increasedScore)
return activationScore
}
private fun refreshByElapsedTime() {
val now = Instant.now()
val elapsedSeconds = Duration.between(lastTouchedAt, now).toMillis() / 1000.0
activationScore = max(0.0, activationScore - elapsedSeconds * (timeFadeFactor / 60.0))
lastTouchedAt = now
}
fun sameWith(contextBlock: ContextBlock): Boolean {
return this.sourceKey == contextBlock.sourceKey
}
internal fun render(exposure: Exposure): BlockContent {
return when (exposure) {
Exposure.PRIMARY -> when (currentProjectionLevel()) {
ProjectionLevel.FULL -> blockContent
ProjectionLevel.COMPACT, ProjectionLevel.ABSTRACT -> compactBlock
}
Exposure.SECONDARY -> when (currentProjectionLevel()) {
ProjectionLevel.ABSTRACT -> abstractBlock
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> compactBlock
}
}
}
fun render(): BlockContent {
return when (currentProjectionLevel()) {
ProjectionLevel.ABSTRACT -> abstractBlock
ProjectionLevel.COMPACT -> compactBlock
ProjectionLevel.FULL -> blockContent
}
}
private fun currentProjectionLevel(): ProjectionLevel {
return when {
activationScore < ABSTRACT_TO_COMPACT_THRESHOLD -> ProjectionLevel.ABSTRACT
activationScore < COMPACT_TO_FULL_THRESHOLD -> ProjectionLevel.COMPACT
else -> ProjectionLevel.FULL
}
}
private fun activationCeiling(exposure: Exposure, currentLevel: ProjectionLevel): Double {
return when (exposure) {
Exposure.PRIMARY -> when (currentLevel) {
ProjectionLevel.ABSTRACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
}
Exposure.SECONDARY -> when (currentLevel) {
ProjectionLevel.ABSTRACT -> ABSTRACT_TO_COMPACT_THRESHOLD - PROJECTION_EPSILON
ProjectionLevel.COMPACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
}
}
}
data class SourceKey(
val blockName: String,
val source: String
)
companion object {
private const val MAX_ACTIVATION_SCORE = 100.0
private const val ABSTRACT_TO_COMPACT_THRESHOLD = 30.0
private const val COMPACT_TO_FULL_THRESHOLD = 70.0
private const val PROJECTION_EPSILON = 0.000001
}
}
private class AggregatedBlockContent(
private val groupedBlocks: List<ResolvedContextBlock>
) : BlockContent(
groupedBlocks.first().block.sourceKey.blockName,
groupedBlocks.first().block.sourceKey.source,
groupedBlocks.maxByOrNull { it.renderedBlock.urgency.ordinal }?.renderedBlock?.urgency ?: Urgency.NORMAL
) {
override fun fillXml(document: Document, root: Element) {
val snapshotIndex = groupedBlocks.withIndex()
.maxWithOrNull(
compareBy<IndexedValue<ResolvedContextBlock>> { it.value.activationScore }
.thenBy { it.index }
)?.index ?: 0
groupedBlocks.forEachIndexed { index, groupedBlock ->
val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot"
val wrapper = document.createElement(tagName)
val renderedBlock = groupedBlock.renderedBlock
val encoded = renderedBlock.encodeToXml()
val attributes = encoded.attributes
for (attributeIndex in 0 until attributes.length) {
val attribute = attributes.item(attributeIndex)
wrapper.setAttribute(attribute.nodeName, attribute.nodeValue)
}
root.appendChild(wrapper)
val childNodes = encoded.childNodes
for (childIndex in 0 until childNodes.length) {
wrapper.appendChild(document.importNode(childNodes.item(childIndex), true))
}
}
}
}
abstract class BlockContent @JvmOverloads protected constructor(
blockName: String,
val source: String,
val urgency: Urgency = Urgency.NORMAL
) : Block(blockName) {
enum class Urgency {
LOW,
NORMAL,
HIGH,
CRITICAL
}
override fun appendRootAttributes(): Map<String, String> {
return mapOf(
"source" to source,
"urgency" to urgency.name.lowercase(Locale.ROOT)
)
}
}
abstract class CommunicationBlockContent @JvmOverloads constructor(
blockName: String,
source: String,
urgency: Urgency = Urgency.NORMAL,
val type: Projection = Projection.CONTEXT,
) : BlockContent(
blockName,
source,
urgency
) {
enum class Projection {
SUPPLY,
CONTEXT
}
}

View File

@@ -0,0 +1,61 @@
package work.slhaf.partner.core.cognition
import org.w3c.dom.Document
import work.slhaf.partner.framework.agent.model.pojo.Message
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
data class ResolvedContext(
val blocks: List<BlockContent>
) {
fun encodeToMessage(): Message {
val content = if (blocks.isEmpty()) {
"<no_context></no_context>"
} else {
buildContextXml(blocks)
}
return Message(Message.Character.USER, content)
}
private fun buildContextXml(blocks: List<BlockContent>): String {
return try {
val document = newDocument()
val root = document.createElement("context")
document.appendChild(root)
blocks.stream()
.map(BlockContent::encodeToXml)
.forEach { blockElement ->
root.appendChild(document.importNode(blockElement, true))
}
toXmlString(document)
} catch (e: Exception) {
throw IllegalStateException("构建 context 区段失败", e)
}
}
private fun newDocument(): Document {
return DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument()
}
private fun toXmlString(document: Document): String {
val transformer = TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
}
return StringWriter().use { writer ->
transformer.transform(DOMSource(document), StreamResult(writer))
writer.toString()
}
}
}

View File

@@ -0,0 +1,27 @@
package work.slhaf.partner.core.memory;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.Collection;
import java.util.List;
@Capability(value = "memory")
public interface MemoryCapability {
MemoryUnit getMemoryUnit(String unitId);
Result<MemorySlice> getMemorySlice(String unitId, String sliceId);
MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary);
Collection<MemoryUnit> listMemoryUnits();
void refreshMemorySession();
String getMemorySessionId();
}

View File

@@ -0,0 +1,184 @@
package work.slhaf.partner.core.memory;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@CapabilityCore(value = "memory")
@Slf4j
public class MemoryCore implements StateSerializable {
private final Lock memoryLock = new ReentrantLock();
private final ConcurrentHashMap<String, MemoryUnit> memoryUnits = new ConcurrentHashMap<>();
// 默认值一般只存在于智能体初次启动时
private String memorySessionId = UUID.randomUUID().toString();
public MemoryCore() {
register();
}
@CapabilityMethod
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
memoryLock.lock();
try {
MemoryUnit unit = getMemoryUnit(memorySessionId);
unit.updateTimestamp();
List<Message> conversationMessages = unit.getConversationMessages();
int startIndex = conversationMessages.size();
MemorySlice memorySlice = new MemorySlice(
startIndex,
startIndex + chatMessages.size(),
summary
);
conversationMessages.addAll(chatMessages);
unit.getSlices().add(memorySlice);
normalizeMemoryUnit(unit);
return unit;
} finally {
memoryLock.unlock();
}
}
@CapabilityMethod
public MemoryUnit getMemoryUnit(String unitId) {
MemoryUnit unit = memoryUnits.computeIfAbsent(unitId, id -> {
MemoryUnit newUnit = new MemoryUnit(id);
newUnit.register();
return newUnit;
});
unit.load();
return unit;
}
@CapabilityMethod
public Result<MemorySlice> getMemorySlice(String unitId, String sliceId) {
MemoryUnit memoryUnit = memoryUnits.get(unitId);
if (memoryUnit == null) {
return Result.failure(new MemoryLookupException(
"Memory slice not found: " + unitId + ":" + sliceId,
unitId + ":" + sliceId,
"MEMORY_SLICE"
));
}
for (MemorySlice slice : memoryUnit.getSlices()) {
if (sliceId.equals(slice.getId())) {
return Result.success(slice);
}
}
return Result.failure(new MemoryLookupException(
"Memory slice not found: " + unitId + ":" + sliceId,
unitId + ":" + sliceId,
"MEMORY_SLICE"
));
}
@CapabilityMethod
public Collection<MemoryUnit> listMemoryUnits() {
return new ArrayList<>(memoryUnits.values());
}
@CapabilityMethod
public void refreshMemorySession() {
memorySessionId = UUID.randomUUID().toString();
}
@CapabilityMethod
public String getMemorySessionId() {
return memorySessionId;
}
private void normalizeMemoryUnit(MemoryUnit memoryUnit) {
if (memoryUnit.getTimestamp() == null || memoryUnit.getTimestamp() <= 0) {
memoryUnit.updateTimestamp();
}
int maxEndExclusive = memoryUnit.getConversationMessages().size();
List<MemorySlice> normalizedSlices = new ArrayList<>(memoryUnit.getSlices().size());
for (MemorySlice slice : memoryUnit.getSlices()) {
if (slice == null) {
continue;
}
String sliceId = slice.getId();
if (sliceId == null || sliceId.isBlank()) {
sliceId = UUID.randomUUID().toString();
}
long sliceTimestamp = slice.getTimestamp() == null || slice.getTimestamp() <= 0
? memoryUnit.getTimestamp()
: slice.getTimestamp();
int startIndex = slice.getStartIndex() == null || slice.getStartIndex() < 0
? 0
: Math.min(slice.getStartIndex(), maxEndExclusive);
int endIndex = slice.getEndIndex() == null || slice.getEndIndex() < startIndex
? maxEndExclusive
: Math.min(slice.getEndIndex(), maxEndExclusive);
normalizedSlices.add(MemorySlice.restore(
sliceId,
startIndex,
endIndex,
slice.getSummary(),
sliceTimestamp
));
}
memoryUnit.getSlices().clear();
memoryUnit.getSlices().addAll(normalizedSlices);
memoryUnit.getSlices().sort(Comparator.naturalOrder());
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "memory.json");
}
@Override
public void load(@NotNull JSONObject state) {
String memorySessionId = state.getString("memory_session_id");
if (memorySessionId == null) {
throw new IllegalStateException("Memory session id is missing");
}
JSONArray array = state.getJSONArray("memory_unit_uuid_set");
if (array == null) {
throw new IllegalStateException("Memory unit uuid set is missing");
}
for (int i = 0; i < array.size(); i++) {
String unitUuid = array.getString(i);
if (unitUuid == null) {
throw new IllegalStateException("memory_unit_uuid_set is not a uuid array, index: " + i);
}
MemoryUnit memoryUnit = new MemoryUnit(unitUuid);
memoryUnits.put(unitUuid, memoryUnit);
}
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("memory_session_id", StateValue.str(memorySessionId));
List<StateValue.Str> unitOverview = memoryUnits.keySet().stream()
.map(StateValue::str)
.toList();
state.append("memory_unit_uuid_set", StateValue.arr(unitOverview));
return state;
}
}

View File

@@ -0,0 +1,45 @@
package work.slhaf.partner.core.memory.pojo;
import lombok.Getter;
import java.util.UUID;
@Getter
public class MemorySlice implements Comparable<MemorySlice> {
private final String id;
private final Integer startIndex;
private final Integer endIndex;
private final String summary;
private final Long timestamp;
public MemorySlice(Integer startIndex, Integer endIndex, String summary) {
this.id = UUID.randomUUID().toString();
this.timestamp = System.currentTimeMillis();
this.startIndex = startIndex;
this.endIndex = endIndex;
this.summary = summary;
}
private MemorySlice(String id, Integer startIndex, Integer endIndex, String summary, Long timestamp) {
this.id = id;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.summary = summary;
this.timestamp = timestamp;
}
public static MemorySlice restore(String id, Integer startIndex, Integer endIndex, String summary, Long timestamp) {
return new MemorySlice(id, startIndex, endIndex, summary, timestamp);
}
@Override
public int compareTo(MemorySlice memorySlice) {
if (memorySlice.getTimestamp() > this.getTimestamp()) {
return -1;
} else if (memorySlice.getTimestamp() < this.timestamp) {
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,125 @@
package work.slhaf.partner.core.memory.pojo;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Getter
public class MemoryUnit implements StateSerializable {
private final String id;
private final List<Message> conversationMessages = new ArrayList<>();
private Long timestamp = 0L;
private final List<MemorySlice> slices = new ArrayList<>();
public MemoryUnit(String id) {
this.id = id;
this.register();
}
public void updateTimestamp() {
timestamp = System.currentTimeMillis();
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "memory", "memory-unit" + id + ".json");
}
@Override
public void load(@NotNull JSONObject state) {
Long loadedTimestamp = state.getLong("update_timestamp");
this.timestamp = loadedTimestamp != null ? loadedTimestamp : 0L;
this.conversationMessages.clear();
this.slices.clear();
JSONArray messageArray = state.getJSONArray("conversation_messages");
if (messageArray != null) {
for (int i = 0; i < messageArray.size(); i++) {
JSONObject messageObject = messageArray.getJSONObject(i);
if (messageObject == null) {
continue;
}
String role = messageObject.getString("role");
String content = messageObject.getString("content");
if (role == null || content == null) {
continue;
}
Message message = new Message(Message.Character.fromValue(role), content);
this.conversationMessages.add(message);
}
}
var sliceArray = state.getJSONArray("memory_slices");
if (sliceArray != null) {
for (int i = 0; i < sliceArray.size(); i++) {
JSONObject sliceObject = sliceArray.getJSONObject(i);
if (sliceObject == null) {
continue;
}
String sliceId = sliceObject.getString("id");
Integer startIndex = sliceObject.getInteger("start_index");
Integer endIndex = sliceObject.getInteger("end_index");
String summary = sliceObject.getString("summary");
Long createdTimestamp = sliceObject.getLong("created_timestamp");
if (sliceId == null || startIndex == null || endIndex == null || summary == null || createdTimestamp == null) {
continue;
}
MemorySlice slice = MemorySlice.restore(sliceId, startIndex, endIndex, summary, createdTimestamp);
this.slices.add(slice);
}
}
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("id", StateValue.str(id));
state.append("update_timestamp", StateValue.num(timestamp));
List<StateValue.Obj> convertedMessageList = conversationMessages.stream().map(message -> {
Map<String, StateValue> convertedMap = Map.of(
"role", StateValue.str(message.roleValue()),
"content", StateValue.str(message.getContent())
);
return StateValue.obj(convertedMap);
}).toList();
state.append("conversation_messages", StateValue.arr(convertedMessageList));
List<StateValue.Obj> convertedSliceList = slices.stream().map(slice -> {
Map<String, StateValue> convertedMap = Map.of(
"id", StateValue.str(slice.getId()),
"start_index", StateValue.num(slice.getStartIndex()),
"end_index", StateValue.num(slice.getEndIndex()),
"summary", StateValue.str(slice.getSummary()),
"created_timestamp", StateValue.num(slice.getTimestamp())
);
return StateValue.obj(convertedMap);
}).toList();
state.append("memory_slices", StateValue.arr(convertedSliceList));
return state;
}
@Override
public boolean autoLoadOnRegister() {
return false;
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.partner.core.memory.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SliceRef {
private String unitId;
private String sliceId;
}

View File

@@ -0,0 +1,12 @@
package work.slhaf.partner.core.perceive;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import java.time.Instant;
@Capability(value = "perceive")
public interface PerceiveCapability {
String refreshInteract();
Instant showLastInteract();
}

View File

@@ -0,0 +1,53 @@
package work.slhaf.partner.core.perceive;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@CapabilityCore(value = "perceive")
public class PerceiveCore implements StateSerializable {
private Instant lastInteractTime = Instant.ofEpochMilli(0);
public PerceiveCore() {
register();
}
@CapabilityMethod
public String refreshInteract() {
String last = lastInteractTime.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
lastInteractTime = Instant.now();
return last;
}
@CapabilityMethod
public Instant showLastInteract() {
return lastInteractTime;
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "perceive.json");
}
@Override
public void load(@NotNull JSONObject state) {
this.lastInteractTime = Instant.ofEpochMilli(state.getLong("last_interact_time"));
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("last_interact_time", StateValue.num(lastInteractTime.toEpochMilli()));
return state;
}
}

View File

@@ -0,0 +1,14 @@
package work.slhaf.partner.module
import work.slhaf.partner.common.base.Block
import work.slhaf.partner.framework.agent.model.pojo.Message
abstract class TaskBlock @JvmOverloads constructor(
blockName: String = "task_input"
) : Block(blockName) {
fun encodeToMessage(): Message {
return Message(Message.Character.USER, encodeToXmlString())
}
}

View File

@@ -0,0 +1,9 @@
package work.slhaf.partner.module.action.builtin;
import java.util.List;
interface BuiltinActionProvider {
List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions();
String createActionKey(String actionName);
}

View File

@@ -0,0 +1,142 @@
package work.slhaf.partner.module.action.builtin;
import lombok.Getter;
import lombok.NonNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionLookupException;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
@Getter
private final Map<String, BuiltinActionDefinition> definitions = new LinkedHashMap<>();
@InjectCapability
private ActionCapability actionCapability;
@Init
public void init() {
definitions.clear();
for (BuiltinActionDefinition definition : buildDefaultActionDefinitions()) {
definitions.put(definition.actionKey(), definition);
}
actionCapability.registerMetaActions(exportMetaActionInfos());
actionCapability.runnerClient().setBuiltinActionRegistry(this);
}
protected List<BuiltinActionDefinition> buildDefaultActionDefinitions() {
List<BuiltinActionDefinition> builtinActionDefinitions = new ArrayList<>();
BuiltinActionProvider commandActionProvider = new BuiltinCommandActionProvider();
BuiltinActionProvider capabilityActionProvider = new BuiltinCapabilityActionProvider();
BuiltinActionProvider interventionActionProvider = new BuiltinInterventionActionProvider();
BuiltinActionProvider dynamicActionProvider = new BuiltinDynamicActionProvider();
builtinActionDefinitions.addAll(commandActionProvider.provideBuiltinActions());
builtinActionDefinitions.addAll(capabilityActionProvider.provideBuiltinActions());
builtinActionDefinitions.addAll(interventionActionProvider.provideBuiltinActions());
builtinActionDefinitions.addAll(dynamicActionProvider.provideBuiltinActions());
return builtinActionDefinitions;
}
public void defineBuiltinAction(String name, MetaActionInfo metaActionInfo, Function<Map<String, Object>, String> invoker) {
BuiltinActionDefinition definition = new BuiltinActionDefinition(BUILTIN_LOCATION + "::" + name, metaActionInfo, invoker);
definitions.put(definition.actionKey(), definition);
}
public String call(@NonNull String actionKey, @NonNull Map<String, Object> params) {
BuiltinActionDefinition definition = definitions.get(actionKey);
if (definition == null) {
throw new ActionLookupException(
"Builtin action definition not found: " + actionKey,
actionKey,
"BUILTIN_DEFINITION"
);
}
String result = definition.invoker().apply(params);
if (result == null) {
return "null";
}
return result;
}
private Map<String, MetaActionInfo> exportMetaActionInfos() {
Map<String, MetaActionInfo> metaActions = new LinkedHashMap<>();
definitions.forEach((key, value) -> metaActions.put(key, value.metaActionInfo()));
return metaActions;
}
public record BuiltinActionDefinition(
String actionKey,
MetaActionInfo metaActionInfo,
Function<Map<String, Object>, String> invoker
) {
static String requireString(Map<String, Object> params, String key) {
Object value = params.get(key);
if (value == null) {
throw new IllegalArgumentException("缺少参数: " + key);
}
if (!(value instanceof String s)) {
throw new IllegalArgumentException("参数 " + key + " 必须为字符串");
}
return s;
}
static String optionalString(Map<String, Object> params, String key, String defaultValue) {
Object value = params.get(key);
if (value == null) {
return defaultValue;
}
if (!(value instanceof String s)) {
throw new IllegalArgumentException("参数 " + key + " 必须为字符串");
}
return s;
}
static Integer requireInt(Map<String, Object> params, String key) {
Object value = params.get(key);
if (value == null) {
throw new IllegalArgumentException("缺少参数: " + key);
}
if (value instanceof Number number) {
return number.intValue();
}
try {
if (value instanceof String string) {
return Integer.parseInt(string);
}
} catch (NumberFormatException ignored) {
}
throw new IllegalArgumentException("参数 " + key + " 必须为整数");
}
static Integer optionalInt(Map<String, Object> params, String key, Integer defaultValue) {
Object value = params.get(key);
if (value == null) {
return defaultValue;
}
if (value instanceof Number n) {
return n.intValue();
}
try {
if (value instanceof String string) {
return Integer.parseInt(string);
}
} catch (NumberFormatException ignored) {
}
throw new IllegalArgumentException("参数 " + key + " 必须为整数");
}
}
}

View File

@@ -0,0 +1,159 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSONObject;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.memory.MemoryCapability;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.*;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
private static final String CAPABILITY_LOCATION = BUILTIN_LOCATION + "::" + "capability";
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Agent Capability");
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private MemoryCapability memoryCapability;
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildInitiateTurnDefinition(),
buildMemoryRecallDefinition()
);
}
private BuiltinActionRegistry.BuiltinActionDefinition buildMemoryRecallDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Memory");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"unit_id", "The id of the memory unit that contains the target memory slice.",
"slice_id", "The id of the memory slice to recall into context."
),
"Recall the target memory slice into context using its unit_id and slice_id. " +
"This action loads the slice's original conversation messages as a short-lived recalled memory context block.",
tags,
Set.of(),
Set.of(),
false,
JSONObject.of(
"ok", "boolean, whether the target memory slice is found and recalled successfully",
"message", "string, short execution result description"
)
);
Function<Map<String, Object>, String> invoker = params -> {
String unitId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "unit_id");
String sliceId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "slice_id");
Result<MemorySlice> sliceResult = memoryCapability.getMemorySlice(unitId, sliceId);
if (sliceResult.exceptionOrNull() != null) {
return JSONObject.of(
"ok", false,
"message", sliceResult.exceptionOrNull().getLocalizedMessage()
).toJSONString();
}
MemorySlice slice = sliceResult.getOrThrow();
MemoryUnit unit = memoryCapability.getMemoryUnit(unitId);
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildMemoryRecallFullBlock(unit, slice),
Set.of(ContextBlock.FocusedDomain.MEMORY),
60,
16,
28
));
return JSONObject.of(
"ok", true,
"message", "Memory slice found and recalled into context"
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("memory_recall"),
info,
invoker
);
}
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnit unit, MemorySlice slice) {
return new BlockContent("memory_recall", "memory_capability") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setAttribute("unit_id", unit.getId());
root.setAttribute("slice_id", slice.getId());
appendRepeatedElements(document, root, "message", unit.getConversationMessages().subList(slice.getStartIndex(), slice.getEndIndex()), (messageElement, message) -> {
messageElement.setAttribute("role", message.getRole().name().toLowerCase(Locale.ROOT));
messageElement.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
};
}
/**
* 用于发起自对话的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildInitiateTurnDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Agent Turn");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"input", "Input required to initiate an internal Agent Turn.",
"target", "The people expected to reply to this internal Agent Turn."
),
"Create an internal Agent Turn to resolve a task.",
tags,
Set.of(),
Set.of(),
false,
JSONObject.of(
"result", "turn initiate result"
)
);
Function<Map<String, Object>, String> invoker = params -> {
String input = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "input");
String target = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "target");
cognitionCapability.initiateTurn(input, target);
return "agent turn initiated";
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("initiate_turn"),
info,
invoker
);
}
@Override
public String createActionKey(String actionName) {
return CAPABILITY_LOCATION + "::" + actionName;
}
}

View File

@@ -0,0 +1,594 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSONObject;
import lombok.AllArgsConstructor;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.time.Instant;
import java.time.Duration;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
class BuiltinCommandActionProvider implements BuiltinActionProvider {
private static final String COMMAND_LOCATION = BUILTIN_LOCATION + "::" + "command";
private static final String COMMAND_ARG_PREFIX = "arg";
private static final int DEFAULT_READ_LIMIT = 4096;
private static final int SUMMARY_MAX_LINES = 5;
private static final int SUMMARY_MAX_LENGTH = 2048;
private static final Duration COMMAND_SESSION_TTL = Duration.ofMinutes(10);
private static final int COMMAND_SESSION_TAIL_LIMIT = 64 * 1024;
private static final Duration OUTPUT_FLUSH_INTERVAL = Duration.ofMillis(100);
private static final Path COMMAND_SESSION_LOG_DIR = Path.of(System.getProperty("java.io.tmpdir"), "partner-command-sessions");
private final Set<String> basicTags = Set.of("Builtin MetaAction", "System Command Tool");
private final ConcurrentHashMap<String, CommandHandle> commandHandles = new ConcurrentHashMap<>();
private final CommandExecutionService commandExecutionService = CommandExecutionService.INSTANCE;
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildCommandExecuteDefinition(),
buildCommandStartDefinition(),
buildCommandInspectDefinition(),
buildCommandReadDefinition(),
buildCommandCancelDefinition(),
buildCommandOverviewDefinition()
);
}
/**
* 用于直接执行的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据,参数为常规命令列表,返回值为该命令的响应内容
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandExecuteDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Execution");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("arg / argN", "Command arguments. Use arg for first argument, arg1/arg2... for remaining arguments."),
"Execute any allowed system commands and get result instantly, the number of arguments is not limited.",
tags,
Set.of(),
Set.of(createActionKey("inspect")),
false,
JSONObject.of("result", "Command execution result text.")
);
Function<Map<String, Object>, String> invoker = params -> {
List<String> commands = requireCommandArguments(params);
CommandExecutionService.Result result = commandExecutionService.exec(wrapCommands(commands));
return JSONObject.of("result", result.getTotal()).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("execute"),
info,
invoker
);
}
/**
* 用于启动后台命令的 Builtin MetaAction后台将持续接收 stdout/stderr
*
* @return 内建 MetaAction 定义数据,参数为命令列表及进程描述,返回值为进程句柄 id
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandStartDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"desc", "Command session description.",
"arg / argN", "Command arguments. Use arg for first argument, arg1/arg2... for remaining arguments."
),
"Start a background command session and return execution id.",
tags,
Set.of(),
Set.of(createActionKey("inspect")),
false,
JSONObject.of("executionId", "Command execution session id.")
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc");
List<String> commands = requireCommandArguments(params);
CommandExecutionService.CommandSession session = commandExecutionService.createSessionTask(wrapCommands(commands));
String executionId = UUID.randomUUID().toString();
Path stdoutLogPath = createSessionLogPath(executionId, "stdout.log");
Path stderrLogPath = createSessionLogPath(executionId, "stderr.log");
CommandHandle handle = new CommandHandle(
executionId,
desc,
commands,
Instant.now(),
session.getProcess(),
session.getStdoutBuffer(),
session.getStderrBuffer(),
new StringBuilder(),
new StringBuilder(),
stdoutLogPath,
stderrLogPath,
null,
null
);
commandHandles.put(executionId, handle);
startOutputFlusher(handle);
monitorProcess(handle);
return JSONObject.of("executionId", executionId).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("start"), info, invoker);
}
/**
* 用于返回指定后台 Builtin MetaAction 的摘要内容
*
* @return 内建 MetaAction 定义数据,参数为进程 id返回值为摘要内容 JSON
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandInspectDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("id", "Command session id."),
"Inspect a background command session.",
tags,
Set.of(createActionKey("overview")),
Set.of(),
false,
JSONObject.of(
"executionId", "Command execution session id.",
"desc", "Command session description.",
"exitCode", "Process exit code. Null when the process is still running.",
"stdoutSize", "Current stdout buffer size in characters.",
"stderrSize", "Current stderr buffer size in characters.",
"stdoutSummary", "Summary text for stdout output.",
"stderrSummary", "Summary text for stderr output.",
"startAt", "Command session start time.",
"endAt", "Command session end time. Null when the process is still running."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
flushHandleBuffers(handle);
return JSONObject.of(
"executionId", handle.executionId,
"desc", handle.desc,
"exitCode", handle.exitCode,
"stdoutSize", streamLength(handle.stdoutLogPath, handle.stdoutSourceBuffer),
"stderrSize", streamLength(handle.stderrLogPath, handle.stderrSourceBuffer),
"stdoutSummary", summarizeBuffer(handle.stdoutBuffer),
"stderrSummary", summarizeBuffer(handle.stderrBuffer),
"startAt", handle.startAt,
"endAt", handle.exitAt
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("inspect"), info, invoker);
}
/**
* 用于读取指定后台 Builtin MetaAction 的输出内容
*
* @return 内建 MetaAction 定义数据,参数为进程 id 与读取流(stdout/stderr),返回值为读取内容 JSON
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandReadDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
tags.add("Command Read");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"id", "Command execution session id.",
"stream", "Target stream, stdout or stderr. Default stdout.",
"offset", "Read start offset. Default 0.",
"limit", "Read max length. Default 4096."
),
"Read output from a background command session.",
tags,
Set.of(createActionKey("overview")),
Set.of(),
false,
JSONObject.of(
"executionId", "Command execution session id.",
"desc", "Command session description.",
"stream", "The stream that was read, stdout or stderr.",
"content", "Content read from the selected stream in this call.",
"contentTruncated", "Whether the content was truncated because of the read limit.",
"offset", "Read start offset used in this call.",
"nextOffset", "Suggested next offset for the following read.",
"eof", "Whether the stream has reached end-of-file and the process has already exited."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
flushHandleBuffers(handle);
String stream = BuiltinActionRegistry.BuiltinActionDefinition.optionalString(params, "stream", "stdout");
if (!"stdout".equals(stream) && !"stderr".equals(stream)) {
throw new IllegalArgumentException("参数 stream 只能为 stdout 或 stderr");
}
int offset = BuiltinActionRegistry.BuiltinActionDefinition.optionalInt(params, "offset", 0);
int limit = BuiltinActionRegistry.BuiltinActionDefinition.optionalInt(params, "limit", DEFAULT_READ_LIMIT);
if (offset < 0) {
throw new IllegalArgumentException("参数 offset 必须大于等于 0");
}
if (limit <= 0) {
throw new IllegalArgumentException("参数 limit 必须大于 0");
}
Path logPath = "stderr".equals(stream) ? handle.stderrLogPath : handle.stdoutLogPath;
StreamChunk chunk = readChunk(logPath, offset, limit);
boolean eof = !handle.isRunning() && chunk.nextOffset >= chunk.totalLength;
return JSONObject.of(
"executionId", handle.executionId,
"desc", handle.desc,
"stream", stream,
"content", chunk.content,
"contentTruncated", chunk.truncated,
"offset", chunk.offset,
"nextOffset", chunk.nextOffset,
"eof", eof
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("read"), info, invoker);
}
/**
* 用于取消指定后台命令的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据,参数为进程 id返回值为是否成功取消
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandCancelDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
tags.add("Command Cancel");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("id", "Command session id."),
"Cancel a background command session.",
tags,
Set.of(),
Set.of(createActionKey("overview")),
false,
JSONObject.of(
"ok", "Whether the command has been cancelled.",
"executionId", "Command execution session id."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
flushHandleBuffers(handle);
if (handle.process.isAlive()) {
handle.process.destroy();
waitProcessExit(handle.process, 200);
if (handle.process.isAlive()) {
handle.process.destroyForcibly();
waitProcessExit(handle.process, 200);
}
}
if (!handle.process.isAlive()) {
try {
handle.exitCode = handle.process.exitValue();
} catch (IllegalThreadStateException ignored) {
}
if (handle.exitAt == null) {
handle.exitAt = Instant.now();
}
}
return JSONObject.of(
"ok", !handle.process.isAlive(),
"executionId", handle.executionId
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("cancel"), info, invoker);
}
/**
* 用于列出全量后台进程的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据,无参数,返回值为后台进程集合 JSON
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandOverviewDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
tags.add("Command Overview");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(),
"List all background command sessions.",
tags,
Set.of(createActionKey("start")),
Set.of(createActionKey("inspect"), createActionKey("read"), createActionKey("cancel")),
false,
JSONObject.of(
"commands", "Array of command session overview items.",
"command.executionId", "Command execution session id for each overview item.",
"command.desc", "Command session description for each overview item.",
"command.exitCode", "Process exit code for each overview item. Null when still running."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
commandHandles.values().forEach(this::flushHandleBuffers);
List<JSONObject> items = commandHandles.values().stream()
.sorted(Comparator.comparing(handle -> handle.startAt))
.map(handle -> JSONObject.of(
"executionId", handle.executionId,
"desc", handle.desc,
"exitCode", handle.exitCode
))
.toList();
return JSONObject.of("result", items).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("overview"), info, invoker);
}
@Override
public String createActionKey(String actionName) {
return COMMAND_LOCATION + "::" + actionName;
}
private void monitorProcess(CommandHandle handle) {
Thread.startVirtualThread(() -> {
try {
handle.exitCode = handle.process.waitFor();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} finally {
flushHandleBuffers(handle);
handle.exitAt = Instant.now();
}
});
}
private void startOutputFlusher(CommandHandle handle) {
Thread.startVirtualThread(() -> {
try {
while (handle.process.isAlive()) {
flushHandleBuffers(handle);
Thread.sleep(OUTPUT_FLUSH_INTERVAL.toMillis());
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} finally {
flushHandleBuffers(handle);
}
});
}
private void flushHandleBuffers(CommandHandle handle) {
flushStreamBuffer(handle.stdoutSourceBuffer, handle.stdoutBuffer, handle.stdoutLogPath);
flushStreamBuffer(handle.stderrSourceBuffer, handle.stderrBuffer, handle.stderrLogPath);
}
private void flushStreamBuffer(StringBuilder sourceBuffer, StringBuilder tailBuffer, Path logPath) {
String chunk;
synchronized (sourceBuffer) {
if (sourceBuffer.isEmpty()) {
return;
}
chunk = sourceBuffer.toString();
sourceBuffer.setLength(0);
}
try {
Files.writeString(logPath, chunk, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new IllegalStateException("写入命令输出日志失败: " + logPath, e);
}
synchronized (tailBuffer) {
tailBuffer.append(chunk);
trimTailBuffer(tailBuffer);
}
}
private void trimTailBuffer(StringBuilder buffer) {
if (buffer.length() <= COMMAND_SESSION_TAIL_LIMIT) {
return;
}
buffer.delete(0, buffer.length() - COMMAND_SESSION_TAIL_LIMIT);
}
private void cleanupExpiredHandles() {
Instant now = Instant.now();
Iterator<Map.Entry<String, CommandHandle>> iterator = commandHandles.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CommandHandle> entry = iterator.next();
CommandHandle handle = entry.getValue();
if (!isExpired(handle, now)) {
continue;
}
flushHandleBuffers(handle);
cleanupLogFiles(handle);
iterator.remove();
}
}
private boolean isExpired(CommandHandle handle, Instant now) {
Instant exitTime = handle.exitAt;
return exitTime != null && !exitTime.plus(COMMAND_SESSION_TTL).isAfter(now);
}
private Path createSessionLogPath(String executionId, String fileName) {
try {
Files.createDirectories(COMMAND_SESSION_LOG_DIR);
Path file = COMMAND_SESSION_LOG_DIR.resolve(executionId + "-" + fileName);
Files.deleteIfExists(file);
Files.createFile(file);
return file;
} catch (IOException e) {
throw new IllegalStateException("创建命令会话日志文件失败", e);
}
}
private long streamLength(Path logPath, StringBuilder sourceBuffer) {
long fileLength;
try {
fileLength = Files.exists(logPath) ? Files.size(logPath) : 0L;
} catch (IOException e) {
throw new IllegalStateException("读取命令会话日志长度失败", e);
}
synchronized (sourceBuffer) {
return fileLength + sourceBuffer.length();
}
}
private StreamChunk readChunk(Path logPath, int offset, int limit) {
if (!Files.exists(logPath)) {
return new StreamChunk("", 0, 0, false, 0);
}
try (RandomAccessFile raf = new RandomAccessFile(logPath.toFile(), "r")) {
int totalLength = (int) raf.length();
int safeOffset = Math.min(offset, totalLength);
int nextOffset = Math.min(safeOffset + limit, totalLength);
byte[] bytes = new byte[nextOffset - safeOffset];
raf.seek(safeOffset);
raf.readFully(bytes);
String content = new String(bytes, StandardCharsets.UTF_8);
return new StreamChunk(content, safeOffset, nextOffset, nextOffset < totalLength, totalLength);
} catch (IOException e) {
throw new IllegalStateException("读取命令会话日志失败", e);
}
}
private void cleanupLogFiles(CommandHandle handle) {
try {
Files.deleteIfExists(handle.stdoutLogPath);
Files.deleteIfExists(handle.stderrLogPath);
} catch (IOException ignored) {
}
}
private WrappedLaunchSpec wrapCommands(List<String> commands) {
return ExecutionPolicyRegistry.INSTANCE.prepare(commands);
}
private CommandHandle requireHandle(String id) {
CommandHandle handle = commandHandles.get(id);
if (handle == null) {
throw new IllegalArgumentException("未找到对应命令会话: " + id);
}
return handle;
}
private List<String> requireCommandArguments(Map<String, Object> params) {
List<Map.Entry<String, Object>> entries = params.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(COMMAND_ARG_PREFIX))
.sorted(Comparator.comparingInt(entry -> commandArgIndex(entry.getKey())))
.toList();
if (entries.isEmpty()) {
throw new IllegalArgumentException("缺少命令参数");
}
List<String> commands = new ArrayList<>();
for (Map.Entry<String, Object> entry : entries) {
commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, entry.getKey()));
}
return commands;
}
private int commandArgIndex(String key) {
String suffix = key.substring(COMMAND_ARG_PREFIX.length()).trim();
if (suffix.isEmpty()) {
return 0;
}
try {
return Integer.parseInt(suffix);
} catch (NumberFormatException ignored) {
return Integer.MAX_VALUE;
}
}
private String bufferSnapshot(StringBuilder buffer) {
synchronized (buffer) {
return buffer.toString();
}
}
private String summarizeBuffer(StringBuilder buffer) {
String snapshot = bufferSnapshot(buffer);
if (snapshot.isBlank()) {
return "";
}
List<String> lines = snapshot.lines().toList();
if (lines.size() <= SUMMARY_MAX_LINES * 2) {
return trimSummary(snapshot);
}
List<String> head = lines.subList(0, SUMMARY_MAX_LINES);
List<String> tail = lines.subList(Math.max(lines.size() - SUMMARY_MAX_LINES, SUMMARY_MAX_LINES), lines.size());
String summary = String.join("\n", head)
+ "\n...\n"
+ String.join("\n", tail);
return trimSummary(summary);
}
private String trimSummary(String content) {
if (content.length() <= SUMMARY_MAX_LENGTH) {
return content;
}
return content.substring(0, SUMMARY_MAX_LENGTH);
}
private void waitProcessExit(Process process, long millis) {
try {
process.waitFor(millis, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
private record StreamChunk(String content, int offset, int nextOffset, boolean truncated, int totalLength) {
}
@AllArgsConstructor
private static class CommandHandle {
private String executionId;
private String desc;
private List<String> commands;
private Instant startAt;
private Process process;
private StringBuilder stdoutSourceBuffer;
private StringBuilder stderrSourceBuffer;
/**
* stdout/stderr 摘要 tail
*/
private StringBuilder stdoutBuffer;
private StringBuilder stderrBuffer;
private Path stdoutLogPath;
private Path stderrLogPath;
/**
* 退出码:进程未结束时可为 null
*/
private volatile Integer exitCode;
private volatile Instant exitAt;
boolean isRunning() {
return exitCode == null && process.isAlive();
}
}
}

View File

@@ -0,0 +1,253 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import kotlin.Unit;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinDynamicActionProvider implements BuiltinActionProvider {
private static final String ORIGIN_LOCATION = "origin";
private static final long TEMP_ACTION_TTL_MILLIS = 30 * 60 * 1000L;
private static final String DYNAMIC_LOCATION = BUILTIN_LOCATION + "::" + "dynamic";
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Dynamic Generation");
private final ConcurrentHashMap<String, TempDynamicActionRecord> tempDynamicActions = new ConcurrentHashMap<>();
@InjectCapability
private ActionCapability actionCapability;
@InjectModule
private ActionScheduler actionScheduler;
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildGenerateDynamicActionDefinition(),
buildPersistDynamicActionDefinition()
);
}
private BuiltinActionRegistry.BuiltinActionDefinition buildGenerateDynamicActionDefinition() {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"desc", "Human-readable description for the temporary action. Required because the generated action name is only a short id.",
"code", "Dynamic action source code content.",
"codeType", "Code extension, for example py/sh/js.",
"launcher", "Interpreter or launcher used for ORIGIN execution.",
"meta", "MetaActionInfo extra fields as JSON string. Available fields example: {\"io\":true,\"params\":{\"input\":\"user input\"},\"tags\":[\"dynamic\"],\"preActions\":[\"builtin::command::execute\"],\"postActions\":[\"builtin::dynamic::persist\"],\"strictDependencies\":false,\"responseSchema\":{\"result\":\"dynamic result\"}}"
),
"Generate a temporary ORIGIN action from source code and return a temporary actionKey.",
basicTags,
Set.of(),
Set.of(createActionKey("persist")),
false,
JSONObject.of(
"ok", "Whether the dynamic action was generated successfully.",
"actionKey", "Temporary ORIGIN actionKey."
)
);
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("generate"), info, params -> {
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc").trim();
if (desc.isEmpty()) {
throw new IllegalArgumentException("参数 desc 不能为空");
}
String code = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "code");
String codeType = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "codeType");
String launcher = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "launcher");
String metaJson = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "meta");
JSONObject meta = parseMeta(metaJson);
MetaActionInfo metaActionInfo = buildMetaActionInfo(meta, launcher, desc);
String tempName = "dyn-" + shortUuid();
String location = actionCapability.runnerClient().buildTmpPath(tempName, codeType);
MetaAction tempAction = new MetaAction(
tempName,
metaActionInfo.getIo(),
launcher,
MetaAction.Type.ORIGIN,
location
);
String actionKey = ORIGIN_LOCATION + "::" + location;
try {
actionCapability.runnerClient().tmpSerialize(tempAction, code, codeType);
} catch (java.io.IOException e) {
throw new IllegalStateException("临时动态行动序列化失败", e);
}
actionCapability.registerMetaActions(Map.of(actionKey, metaActionInfo));
ActionFileMetaData fileMetaData = buildActionFileMetaData(location, code, codeType);
StateAction cleanupAction = buildCleanupAction(actionKey);
tempDynamicActions.put(actionKey, new TempDynamicActionRecord(
actionKey,
location,
cleanupAction.getUuid(),
fileMetaData,
metaActionInfo
));
actionScheduler.schedule(cleanupAction);
return JSONObject.of(
"ok", true,
"actionKey", actionKey
).toJSONString();
});
}
private BuiltinActionRegistry.BuiltinActionDefinition buildPersistDynamicActionDefinition() {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("actionKey", "Temporary ORIGIN actionKey returned by generate."),
"Persist a temporary ORIGIN action and cancel its cleanup task.",
basicTags,
Set.of(createActionKey("generate")),
Set.of(),
false,
JSONObject.of(
"ok", "Whether the dynamic action was persisted successfully.",
"actionKey", "Temporary ORIGIN actionKey."
)
);
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("persist"), info, params -> {
String actionKey = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionKey");
TempDynamicActionRecord record = tempDynamicActions.get(actionKey);
if (record == null) {
throw new IllegalArgumentException("未找到对应临时动态行动: " + actionKey);
}
actionCapability.runnerClient().persistSerialize(record.metaActionInfo(), record.fileMetaData());
actionScheduler.cancel(record.cleanupActionId());
removeTempDynamicAction(actionKey);
return JSONObject.of(
"ok", true,
"actionKey", actionKey
).toJSONString();
});
}
@Override
public String createActionKey(String actionName) {
return DYNAMIC_LOCATION + "::" + actionName;
}
private JSONObject parseMeta(String metaJson) {
try {
return JSON.parseObject(metaJson);
} catch (Exception e) {
throw new IllegalArgumentException("参数 meta 必须为合法 JSON 字符串", e);
}
}
private MetaActionInfo buildMetaActionInfo(JSONObject meta, String launcher, String description) {
return new MetaActionInfo(
Boolean.TRUE.equals(meta.getBoolean("io")),
launcher,
copyStringMap(meta.getJSONObject("params")),
description,
toOrderedSet(meta.getJSONArray("tags")),
toOrderedSet(meta.getJSONArray("preActions")),
toOrderedSet(meta.getJSONArray("postActions")),
Boolean.TRUE.equals(meta.getBoolean("strictDependencies")),
copyJsonObject(meta.getJSONObject("responseSchema"))
);
}
private ActionFileMetaData buildActionFileMetaData(String location, String code, String codeType) {
ActionFileMetaData fileMetaData = new ActionFileMetaData();
fileMetaData.setContent(code);
fileMetaData.setExt(normalizeCodeType(codeType));
fileMetaData.setName(extractFileBaseName(location, fileMetaData.getExt()));
return fileMetaData;
}
private StateAction buildCleanupAction(String actionKey) {
return new StateAction(
"system",
"dynamic-action-cleanup:" + actionKey,
"清理临时动态行动",
Schedulable.ScheduleType.ONCE,
ZonedDateTime.now().plusSeconds(TEMP_ACTION_TTL_MILLIS / 1000).toString(),
new StateAction.Trigger.Call(() -> {
removeTempDynamicAction(actionKey);
return Unit.INSTANCE;
})
);
}
private void removeTempDynamicAction(String actionKey) {
TempDynamicActionRecord record = tempDynamicActions.remove(actionKey);
if (record == null) {
return;
}
actionCapability.listAvailableMetaActions().remove(actionKey);
deleteTempFileQuietly(record.location());
}
private void deleteTempFileQuietly(String location) {
try {
Files.deleteIfExists(Path.of(location));
} catch (Exception ignored) {
}
}
private Map<String, String> copyStringMap(JSONObject jsonObject) {
if (jsonObject == null) {
return Map.of();
}
Map<String, String> params = new LinkedHashMap<>();
jsonObject.forEach((key, value) -> params.put(key, value == null ? "" : String.valueOf(value)));
return params;
}
private Set<String> toOrderedSet(com.alibaba.fastjson2.JSONArray jsonArray) {
if (jsonArray == null) {
return Set.of();
}
return jsonArray.toJavaList(String.class).stream()
.filter(item -> item != null && !item.isBlank())
.collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
}
private JSONObject copyJsonObject(JSONObject jsonObject) {
return jsonObject == null ? JSONObject.of() : JSONObject.from(jsonObject);
}
private String normalizeCodeType(String codeType) {
return codeType.startsWith(".") ? codeType.substring(1) : codeType;
}
private String extractFileBaseName(String location, String ext) {
String fileName = Path.of(location).getFileName().toString();
String suffix = "." + ext;
if (fileName.endsWith(suffix)) {
return fileName.substring(0, fileName.length() - suffix.length());
}
return fileName;
}
private String shortUuid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 12);
}
private record TempDynamicActionRecord(
String actionKey,
String location,
String cleanupActionId,
ActionFileMetaData fileMetaData,
MetaActionInfo metaActionInfo
) {
}
}

View File

@@ -0,0 +1,408 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.entity.Action;
import work.slhaf.partner.core.action.entity.ExecutableAction;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.cognition.*;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.*;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinInterventionActionProvider implements BuiltinActionProvider {
private static final String INTERVENTION_LOCATION = BUILTIN_LOCATION + "::" + "intervention";
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Action Intervention");
@InjectCapability
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildCreateInterventionDefinition(),
buildShowAvailableMetaActionsDefinition(),
buildShowIntervenableActionsDefinition(),
buildAcquireInterventionDefinition(),
buildResumeInterruptedActionDefinition()
);
}
private BuiltinActionRegistry.BuiltinActionDefinition buildResumeInterruptedActionDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Agent Turn");
tags.add("Action Management");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"actionId", "Uuid of the interrupted executable action to resume."
),
"Resume action that is interrupted.",
tags,
Set.of(),
Set.of(),
false,
JSONObject.of(
"result", "Plain text resume result, describes whether it is succeed or fail reason."
)
);
Function<Map<String, Object>, String> invoker = params -> {
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId");
try {
ExecutableAction executableAction = getExecutableAction(actionId);
executableAction.resume();
return "Resume succeed";
} catch (Exception e) {
return "Failed to resume action[" + actionId + "], reason: " + e.getLocalizedMessage();
}
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("resume_interrupted_action"),
info,
invoker
);
}
/**
* 尝试向指定用户请求干预,通过自对话通道
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildAcquireInterventionDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Agent Turn");
tags.add("Action Management");
MetaActionInfo info = new MetaActionInfo(
true,
null,
Map.of(
"actionId", "Uuid of the executing action that should enter intervention flow.",
"actionInfo", "Readable summary of the current action, used to explain the intervention context to the target.",
"demand", "What feedback, decision or operation is required from the target user.",
"target", "Target user or channel identifier that should receive the intervention request.",
"input", "Prompt content used to initiate the intervention turn toward the target.",
"timeout", "Maximum wait time for the interruption result, in the unit expected by ExecutableAction.interrupt(timeout)."
),
"Try to acquire the target user to intervene this Action.",
tags,
Set.of(),
Set.of(),
false,
JSONObject.of(
"result", "Plain text intervention status. It describes whether the target answered before timeout, or returns an error reason when the turn/interruption flow fails."
)
);
Function<Map<String, Object>, String> invoker = params -> {
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId").trim();
String actionInfo = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionInfo").trim();
String demand = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "demand").trim();
String target = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "target").trim();
String input = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "input").trim();
int timeout = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "timeout");
ContextWorkspace contextWorkspace = cognitionCapability.contextWorkspace();
String blockName = "acquire_intervention-" + actionId;
String source = "action_executor";
contextWorkspace.register(new ContextBlock(
new CommunicationBlockContent(blockName, source, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "Partner needs some help.");
appendTextElement(document, root, "action_id", actionId);
appendTextElement(document, root, "action_info", actionInfo);
appendTextElement(document, root, "demand", demand);
}
},
Set.of(ContextBlock.FocusedDomain.COMMUNICATION),
10,
10,
20
));
ExecutableAction executableAction = null;
try {
executableAction = getExecutableAction(actionId);
cognitionCapability.initiateTurn(input, target);
boolean normal = executableAction.interrupt(timeout);
return normal ? target + "not resumed execution in time" : target + "answered, looking for related answer in recent-chat-messages";
} catch (Exception e) {
return "Error happened while calling turn: " + e.getLocalizedMessage();
} finally {
contextWorkspace.expire(blockName, source);
if (executableAction != null) {
executableAction.resume();
}
}
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("acquire_intervention"),
info,
invoker
);
}
private ExecutableAction getExecutableAction(String actionId) {
return actionCapability.listActions(Action.Status.EXECUTING, null)
.stream()
.filter(action -> action.getUuid().equals(actionId))
.findFirst()
.orElseThrow();
}
/**
* 用于展示当前已存在的可被干预的行动
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildShowIntervenableActionsDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Available Resource");
tags.add("Agent State");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(),
"List existing actions that can be intervened",
tags,
Set.of(),
Set.of(),
false,
JSONObject.of(
"actions", "Info list of actions that could be intervened.",
"action.tendency", "Original tendency that the action is intended to resolve.",
"action.description", "Description of this action.",
"action.status", "Execution status of this action.",
"action.uuid", "Unique uuid of each action."
)
);
Function<Map<String, Object>, String> invoker = params -> {
Set<ExecutableAction> executableActions = actionCapability.listActions(null, null);
JSONArray interventions = new JSONArray();
for (ExecutableAction action : executableActions) {
JSONObject item = interventions.addObject();
item.put("tendency", action.getTendency());
item.put("description", action.getDescription());
item.put("status", action.getStatus().name().toLowerCase());
item.put("uuid", action.getUuid());
}
return interventions.toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("list_intervenable_actions"),
info,
invoker
);
}
/**
* 用于展示当前可用的 MetaAction
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildShowAvailableMetaActionsDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Available Resource");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(),
"List available MetaActions.",
tags,
Set.of(),
Set.of(),
false,
JSONObject.of(
"meta_actions", "Available MetaAction info list.",
"meta_action.actionKey", "MetaAction actionKey, describing action source and its unique calling id.",
"meta_action.description", "MetaAction description.",
"meta_action.tags", "MetaAction tag list as string.",
"meta_action.params", "MetaAction parameter definition JSON string."
)
);
Function<Map<String, Object>, String> invoker = params -> {
Map<String, MetaActionInfo> availableMetaActions = actionCapability.listAvailableMetaActions();
JSONArray actions = new JSONArray();
for (Map.Entry<String, MetaActionInfo> entry : availableMetaActions.entrySet()) {
JSONObject item = actions.addObject();
item.put("location", entry.getKey());
MetaActionInfo action = entry.getValue();
item.put("description", action.getDescription());
item.put("tags", Arrays.toString(action.getTags().toArray()));
item.put("params", JSONObject.toJSONString(action.getParams()));
}
return actions.toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("list_available_meta_actions"),
info,
invoker
);
}
/**
* 用于创建一个 Action Intervention并作用于指定的 Executable Action
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCreateInterventionDefinition() {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"id", "The uuid of the Action to be intervened on.",
"type", "Intervention type. Allowed values: APPEND, INSERT, DELETE, CANCEL.",
"order", "Action chain order/stage to apply the intervention on.",
"actions", "Comma-separated actionKey list to be inserted, appended, rebuilt or deleted. Example: \"builtin::command::execute, builtin::capability::show_memory_slices\""
),
"Used to create an Action Intervention and act on the specified Executable Action.",
basicTags,
Set.of(
createActionKey("list_available_meta_actions"),
createActionKey("list_intervenable_actions")
),
Set.of(),
true,
JSONObject.of(
"ok", "Intervene status",
"result", "Intervene result or failed message."
)
);
Function<Map<String, Object>, String> invoker = params -> {
String targetId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id").trim();
if (targetId.isEmpty()) {
throw new IllegalArgumentException("参数 id 不能为空");
}
InterventionType type = requireInterventionType(params);
Integer order = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "order");
List<String> actions = requireActions(params, type);
ExecutableAction target = requireTargetAction(targetId);
Result<Void> validationResult = validateActionKeys(actions);
AgentRuntimeException validationFailure = validationResult.onFailure(ExceptionReporterHandler.INSTANCE::report).exceptionOrNull();
if (validationFailure != null) {
return JSONObject.of("ok", false, "result", validationFailure.getLocalizedMessage()).toJSONString();
}
MetaIntervention intervention = new MetaIntervention();
intervention.setType(type);
intervention.setOrder(order);
intervention.setActions(actions);
actionCapability.handleInterventions(List.of(intervention), target);
ExecutableAction executableAction = getExecutableAction(targetId);
executableAction.resume();
return JSONObject.of("ok", true).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("create_intervention"),
info,
invoker
);
}
@Override
public String createActionKey(String actionName) {
return INTERVENTION_LOCATION + "::" + actionName;
}
private InterventionType requireInterventionType(Map<String, Object> params) {
String typeValue = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "type").trim();
if (typeValue.isEmpty()) {
throw new IllegalArgumentException("参数 type 不能为空");
}
try {
return InterventionType.valueOf(typeValue.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("参数 type 非法: " + typeValue, e);
}
}
private List<String> requireActions(Map<String, Object> params, InterventionType type) {
Object value = params.get("actions");
if (value == null) {
if (type == InterventionType.CANCEL) {
return List.of();
}
throw new IllegalArgumentException("缺少参数: actions");
}
if (!(value instanceof String actionsValue)) {
throw new IllegalArgumentException("参数 actions 必须为字符串");
}
String trimmedActionsValue = actionsValue.trim();
if (trimmedActionsValue.isEmpty()) {
if (type == InterventionType.CANCEL) {
return List.of();
}
throw new IllegalArgumentException("参数 actions 不能为空");
}
List<String> actions = Arrays.stream(trimmedActionsValue.split(","))
.map(String::trim)
.filter(item -> !item.isEmpty())
.toList();
if (type != InterventionType.CANCEL && actions.isEmpty()) {
throw new IllegalArgumentException("参数 actions 不能为空");
}
return actions;
}
private ExecutableAction requireTargetAction(String targetId) {
return actionCapability.listActions(null, null)
.stream()
.filter(action -> targetId.equals(action.getUuid()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未找到对应的 Action: " + targetId));
}
private Result<Void> validateActionKeys(List<String> actions) {
for (String actionKey : actions) {
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
AgentRuntimeException failure = metaActionResult.exceptionOrNull();
if (failure != null) {
return Result.failure(failure);
}
}
return Result.success(null);
}
}

View File

@@ -0,0 +1,129 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.action.executor.entity.CorrectorInput;
import work.slhaf.partner.module.action.executor.entity.RecognizerResult;
import java.util.List;
/**
* 负责在行动链执行过程中判断当前进度是否异常,是否需要引入 corrector 介入。
*/
public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<CorrectorInput, Result<RecognizerResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在行动链执行过程中识别当前执行是否出现异常,并判断是否需要引入 corrector 介入。
你会收到:
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
- 一条任务消息,其中包含:
- executable_action_info当前正在执行的行动信息包括 executing_action_id、original_tendency、evaluation_passed_reason、description 与 from_who
- current_action_chain_overview当前行动链概览按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description 与 status。
你的任务:
- 基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移,或与最新语境不再一致的情况;
- 若需要引入 corrector则返回 needCorrection=true并给出简洁明确的 reason
- 若当前仍可继续推进,则返回 needCorrection=false。
识别原则:
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的判断必须围绕这条行动本身,而不是被无关历史带偏。
- executing_action_id 用于标识当前正在执行的行动current_action_chain_overview 用于说明当前链路整体结构与已知阶段状态。
- original_tendency 表示这条行动链最初要解决的问题;若当前链路明显偏离这一倾向,应优先视为异常信号。
- evaluation_passed_reason 与 description 表示该行动最初为何被判定为可推进;若当前状态已经与这些前提不再一致,应考虑触发纠正。
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常或整体失衡的主要依据。
- communication 域用于判断最新交流语境是否已发生明显变化,导致当前行动继续推进不再合适。
- memory 域只在与当前行动明显相关时作为辅助参考使用。
应优先考虑需要纠正的情形:
- 当前行动长期没有形成有效推进,只在重复相近步骤、相近动作或相近结论;
- 当前执行显著偏离 original_tendency开始围绕无关目标展开
- 当前行动所依赖的前提已被新的交流内容或上下文推翻;
- 当前行动链反复遇到同类失败、阻塞或空转迹象,继续按原链推进意义不大;
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛。
不应轻易触发纠正的情形:
- 只是正常的多步推进;
- 存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
- 行动链整体仍与 original_tendency、evaluation_passed_reason 和当前语境保持一致;
- 仅凭旧对话、低相关记忆或轻微波动,不足以判定当前行动异常。
关于输出:
- needCorrection=true 表示当前应引入 corrector 介入。
- needCorrection=false 表示当前行动仍可继续按既有链路推进。
- reason 用于简洁说明判断依据;若 needCorrection=false也应给出简短理由说明为何当前仍可继续推进。
- 不要输出结构之外的解释、说明或额外文本。
输出要求:
- 严格按照 RecognizerResult 对应结构输出。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<RecognizerResult> doExecute(CorrectorInput input) {
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
return formattedChat(messages, RecognizerResult.class);
}
private Message resolveTaskMessage(CorrectorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "executable_action_info", block -> {
appendTextElement(document, block, "executing_action_id", input.getActionId());
appendTextElement(document, block, "original_tendency", input.getTendency());
appendTextElement(document, block, "evaluation_passed_reason", input.getReason());
appendTextElement(document, block, "description", input.getDescription());
appendTextElement(document, block, "from_who", input.getSource());
return Unit.INSTANCE;
});
appendListElement(document, root, "current_action_chain_overview", "action_chain_stage", input.getActionChainOverview().entrySet(), (stageElement, stageData) -> {
stageElement.setAttribute("stage_count", String.valueOf(stageData.getKey()));
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_correction_recognizer";
}
}

View File

@@ -0,0 +1,146 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.action.executor.entity.CorrectorInput;
import work.slhaf.partner.module.action.executor.entity.CorrectorResult;
import java.util.List;
/**
* 负责在单组行动执行后,根据行动意图与结果检查后续行动是否符合目的,必要时直接调整行动链,或发起自对话请求进行干预
*/
public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Result<CorrectorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在行动链执行过程中进行纠偏。你的任务不是重新评估是否要启动这条行动,而是在当前行动已经进入执行后,根据原始行动意图、当前链路结构与最新语境,判断是否需要调整后续行动链。
你会收到:
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
- 一条任务消息,其中包含:
- executable_action_info当前正在执行的行动信息包括 executing_action_id、original_tendency、evaluation_passed_reason、description 与 from_who
- current_action_chain_overview当前行动链概览按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description 与 status。
你的任务:
- 基于当前上下文、原始行动意图与当前行动链进展,判断后续行动是否仍然符合目的;
- 若当前链路仍可继续推进,则不要随意干预;
- 若当前链路已明显跑偏、缺少必要步骤、顺序不合理、存在冗余、已经不再适合继续,或需要引入新的动作单元,则输出干预方案;
- correctionReason 用于简洁说明为何需要这些纠偏。
纠偏原则:
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的纠偏必须围绕这条行动本身,而不是转向新的无关目标。
- executing_action_id 用于标识当前正在执行的行动current_action_chain_overview 用于说明当前链路整体结构与阶段状态。
- original_tendency 是这条行动链最初要解决的问题;后续行动的调整必须仍然围绕它。
- evaluation_passed_reason 与 description 表示该行动最初为何能够成立;若当前链路已经与这些前提不一致,可据此进行纠偏。
- current_action_chain_overview 是判断后续链路是否缺步骤、顺序失衡、重复冗余或整体方向错误的主要依据。
- communication 域用于判断最新交流语境是否已经变化,导致当前链路需要调整。
- memory 域只在与当前行动明显相关时作为辅助参考使用。
何时应考虑干预:
- 当前链路缺少继续推进所必需的动作;
- 当前链路顺序明显不合理,继续执行会降低成功率或偏离目的;
- 某些动作已经失效、重复、无意义,或不再适合当前状态;
- 当前链路在某一阶段之后整体方向需要重建;
- 当前行动已不应继续推进,应直接取消整条链路。
何时不应轻易干预:
- 当前只是正常的多步推进;
- 某一步刚完成,尚不足以说明后续链路错误;
- 没有足够依据证明当前行动链存在明显问题;
- 仅凭旧对话、低相关记忆或轻微波动,不足以支持修改行动链。
关于干预类型:
- APPEND: 在指定 order 之后追加新的动作。
- INSERT: 在指定 order 执行过程中即时插入并执行新的动作。
- DELETE: 删除指定 order 上的指定动作。
- CANCEL: 取消当前行动链后续执行。
- REBUILD: 清空当前行动链与既有执行进度,并用新的规划内容整体重建行动链。
关于 intervention 列表:
- 系统会按照 metaInterventionList 的顺序逐条应用干预;前面的干预可能改变后续可理解的 order 位置,因此你在生成后续 intervention 时,必须考虑前面 intervention 已经生效后的链路形态。
- 不要把一组本应整体表达的修改拆成互相冲突、顺序不自洽的 intervention。
- actions 中只能填写当前系统中真实存在、可用的 action_key不要编造不存在的动作。
- order 必须对应当前行动链中的合理阶段位置。
关于 REBUILD
- REBUILD 表示放弃当前既有链路,并重新给出新的整体规划。
- 一旦本次纠偏结果中使用了 REBUILD则 metaInterventionList 中所有 intervention 都必须是 REBUILD不要将 REBUILD 与 APPEND、INSERT、DELETE、CANCEL 混用。
- 使用 REBUILD 时,应把新的链路规划完整表达为一组按 order 分布的 REBUILD intervention而不是只局部补几步。
其他约束:
- 若无需干预,可返回空的 metaInterventionList并给出简短的 correctionReason 说明当前为何可继续推进。
- 不要输出结构之外的解释、说明或额外文本。
输出要求:
- 严格按照 CorrectorResult 对应结构输出。
- metaInterventionList 中每一项都必须是明确、可执行、且与其他 intervention 顺序一致的干预单元。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<CorrectorResult> doExecute(CorrectorInput input) {
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
return formattedChat(messages, CorrectorResult.class);
}
private Message resolveTaskMessage(CorrectorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "executable_action_info", block -> {
appendTextElement(document, block, "executing_action_id", input.getActionId());
appendTextElement(document, block, "original_tendency", input.getTendency());
appendTextElement(document, block, "evaluation_passed_reason", input.getReason());
appendTextElement(document, block, "description", input.getDescription());
appendTextElement(document, block, "from_who", input.getSource());
return Unit.INSTANCE;
});
appendListElement(document, root, "current_action_chain_overview", "action_chain_stage", input.getActionChainOverview().entrySet(), (stageElement, stageData) -> {
stageElement.setAttribute("stage_count", String.valueOf(stageData.getKey()));
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_corrector";
}
}

View File

@@ -0,0 +1,706 @@
package work.slhaf.partner.module.action.executor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.factory.context.Shutdown;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.executor.entity.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@Slf4j
public class ActionExecutor extends AbstractAgentModule.Standalone {
private static final int MAX_EXTRACTOR_ATTEMPTS = 3;
private final AssemblyHelper assemblyHelper = new AssemblyHelper();
@InjectCapability
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private ParamsExtractor paramsExtractor;
@InjectModule
private ActionCorrector actionCorrector;
@InjectModule
private ActionCorrectionRecognizer actionCorrectionRecognizer;
private ExecutingActionBlockManager blockManager;
private ExecutorService virtualExecutor;
private ExecutorService platformExecutor;
private RunnerClient runnerClient;
private final AtomicBoolean closed = new AtomicBoolean(false);
@Init
public void init() {
virtualExecutor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
platformExecutor = actionCapability.getExecutor(ActionCore.ExecutorType.PLATFORM);
runnerClient = actionCapability.runnerClient();
blockManager = new ExecutingActionBlockManager(cognitionCapability.contextWorkspace());
Set<ExecutableAction> recoveredActions = new HashSet<>();
recoveredActions.addAll(actionCapability.listActions(Action.Status.EXECUTING, null));
recoveredActions.addAll(actionCapability.listActions(Action.Status.INTERRUPTED, null).stream()
.peek(executableAction -> executableAction.setStatus(Action.Status.EXECUTING))
.collect(Collectors.toSet()));
if (recoveredActions.isEmpty()) {
return;
}
recoveredActions.forEach(this::execute);
blockManager.emitActionRecoveredBlock(recoveredActions);
}
@Shutdown
public void shutdown() {
closed.set(true);
}
public void execute(Action action) {
Future<?> future = virtualExecutor.submit(actionExecutionRouter(action));
virtualExecutor.execute(() -> {
try {
future.get(action.getTimeoutMills(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
action.setStatus(Action.Status.FAILED);
if (action instanceof ExecutableAction executableAction) {
ensureExecutableResult(executableAction, true, "行动执行超时");
}
log.warn("Action timeout, uuid: {}", action.getUuid());
} catch (Exception ignored) {
}
});
}
private Runnable actionExecutionRouter(Action action) {
return () -> {
try {
switch (action) {
case ExecutableAction executableAction -> handleExecutableAction(executableAction);
case StateAction stateAction -> handleStateAction(stateAction);
default -> handleUnknownAction(action);
}
if (action instanceof ExecutableAction executableAction) {
if (action.getStatus() == Action.Status.FAILED) {
ensureExecutableResult(executableAction, true, null);
blockManager.emitActionFinishedBlock(executableAction);
return;
}
action.setStatus(Action.Status.SUCCESS);
ensureExecutableResult(executableAction, false, null);
blockManager.emitActionFinishedBlock(executableAction);
return;
}
if (action.getStatus() == Action.Status.FAILED) {
return;
}
action.setStatus(Action.Status.SUCCESS);
} catch (Exception e) {
log.warn("Unexpected action execution failure, uuid: {}, description: {}, failure reason: {}", action.getUuid(), action.getDescription(), e.getLocalizedMessage());
action.setStatus(Action.Status.FAILED);
if (action instanceof ExecutableAction executableAction) {
ensureExecutableResult(executableAction, true, e.getLocalizedMessage());
blockManager.emitActionFinishedBlock(executableAction);
}
}
};
}
private void handleUnknownAction(Action action) {
log.warn("unknown Action type: {}", action.getClass().getSimpleName());
action.setStatus(Action.Status.FAILED);
}
private void handleStateAction(StateAction stateAction) {
if (closed.get()) {
return;
}
blockManager.emitStateActionTriggeredBlock(stateAction);
stateAction.getTrigger().onTrigger();
}
private void handleExecutableAction(ExecutableAction executableAction) {
actionCapability.putAction(executableAction);
val actionChain = executableAction.getActionChain();
val phaser = new Phaser();
if (!prepareExecutableAction(executableAction, actionChain)) {
return;
}
blockManager.emitActionLaunchedBlock(executableAction);
val stageCursor = initStageCursor(executableAction, actionChain);
while (true) {
val stageSelection = selectCurrentStage(executableAction, actionChain);
if (stageSelection.shouldReturn()) {
return;
}
if (stageSelection.shouldStop()) {
break;
}
val stageExecution = runCurrentStage(executableAction, phaser, stageCursor, stageSelection.metaActions());
if (stageExecution.closed()) {
return;
}
if (!applyStageCorrectionAndAdvance(executableAction, stageCursor, stageExecution)) {
break;
}
}
finishExecutableAction(executableAction);
}
private boolean prepareExecutableAction(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
synchronized (executableAction.getExecutionLock()) {
val status = executableAction.getStatus();
if (status != Action.Status.PREPARE && status != Action.Status.EXECUTING) {
return false;
}
if (actionChain.isEmpty()) {
executableAction.setStatus(Action.Status.FAILED);
executableAction.setResult("行动链为空");
return false;
}
normalizeExecutingStage(executableAction, actionChain);
executableAction.setStatus(Action.Status.EXECUTING);
return true;
}
}
private StageCursor initStageCursor(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
StageCursor stageCursor = new StageCursor(executableAction, actionChain);
synchronized (executableAction.getExecutionLock()) {
stageCursor.init();
}
return stageCursor;
}
private StageSelection selectCurrentStage(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
synchronized (executableAction.getExecutionLock()) {
if (closed.get()) {
return StageSelection.returnNow();
}
if (executableAction.getStatus() == Action.Status.FAILED) {
return StageSelection.stop();
}
return StageSelection.continueWith(actionChain.get(executableAction.getExecutingStage()));
}
}
private StageExecution runCurrentStage(
ExecutableAction executableAction,
Phaser phaser,
StageCursor stageCursor,
List<MetaAction> metaActions
) {
val recognizerRecord = startRecognizerIfNeeded(executableAction, phaser);
val listeningRecord = executeAndListening(metaActions, phaser, executableAction);
phaser.awaitAdvance(listeningRecord.phase());
// synchronized 同步防止 accepting 循环间、phase guard 判定后发生 stage 推进
// 导致新行动的 phaser 投放阶段错乱无法阻塞的场景
// 该 synchronized 将阶段推进与 accepting 监听 loop 捆绑为互斥的原子事件,避免了细粒度的 phaser 阶段竞态问题
if (closed.get()) {
return StageExecution.closed(recognizerRecord, metaActions);
}
synchronized (executableAction.getExecutionLock()) {
synchronized (listeningRecord.accepting()) {
listeningRecord.accepting().set(false);
// 立即尝试推进,本次推进中,如果前方仍有未执行 stage将执行一次阶段推进
stageCursor.requestAdvance();
}
}
blockManager.emitActionStageSettledBlock(executableAction);
return StageExecution.completed(recognizerRecord, metaActions);
}
private boolean applyStageCorrectionAndAdvance(
ExecutableAction executableAction,
StageCursor stageCursor,
StageExecution stageExecution
) {
boolean hasFailedMetaAction = hasFailedMetaAction(stageExecution.metaActions());
boolean shouldRunCorrector = hasFailedMetaAction;
if (!shouldRunCorrector) {
val recognizerResult = resolveRecognizerResult(stageExecution.recognizerRecord());
shouldRunCorrector = recognizerResult != null && recognizerResult.isNeedCorrection();
}
if (shouldRunCorrector) {
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction);
actionCorrector.execute(correctorInput)
.onSuccess(correctorResult -> {
actionCapability.handleInterventions(correctorResult.getMetaInterventionList(), executableAction);
blockManager.emitActionCorrectionBlock(
executableAction,
hasFailedMetaAction ? "has_failed_meta_action" : correctorResult.getCorrectionReason(),
correctorResult.getMetaInterventionList()
);
});
}
// 第二次尝试进行阶段推进,本次负责补充上一次在不存在 stage时但 corrector 执行期间发生了 actionChain 的插入事件
// 如果第一次已经推进完毕,本次将会跳过
synchronized (executableAction.getExecutionLock()) {
stageCursor.requestAdvance();
return stageCursor.next();
}
}
private void finishExecutableAction(ExecutableAction executableAction) {
// 如果是 ScheduledActionData, 则重置 ActionData 内容,记录执行历史与最终结果
if (executableAction instanceof SchedulableExecutableAction scheduledActionData) {
scheduledActionData.recordAndReset();
}
}
private MetaActionsListeningRecord executeAndListening(List<MetaAction> metaActions, Phaser phaser, ExecutableAction executableAction) {
AtomicBoolean accepting = new AtomicBoolean(true);
AtomicInteger cursor = new AtomicInteger();
CountDownLatch latch = new CountDownLatch(1);
val phase = phaser.register();
platformExecutor.execute(() -> {
boolean first = true;
while (accepting.get()) {
synchronized (accepting) {
MetaAction next = null;
synchronized (metaActions) {
if (cursor.get() < metaActions.size()) {
next = metaActions.get(cursor.getAndIncrement());
}
}
if (next == null) {
Thread.onSpinWait();
continue;
}
if (phaser.getPhase() != phase) {
metaActions.remove(next);
log.warn("行动阶段已推进,丢弃该行动: {}", next);
continue;
}
ExecutorService executor = next.getIo() ? virtualExecutor : platformExecutor;
executor.execute(buildMataActionTask(next, phaser, executableAction));
if (first) {
phaser.arriveAndDeregister();
latch.countDown();
first = false;
}
}
}
});
try {
// 确保执行一次,防止没来得及注册任务就已经结束
latch.await();
} catch (InterruptedException ignored) {
}
return new MetaActionsListeningRecord(accepting, phase);
}
private Runnable buildMataActionTask(MetaAction metaAction, Phaser phaser, ExecutableAction executableAction) {
phaser.register();
return () -> {
val actionKey = metaAction.getKey();
try {
executeMetaActionWithRetry(metaAction, executableAction);
} catch (Exception e) {
log.error("Action executing failed: {}", actionKey, e);
} finally {
phaser.arriveAndDeregister();
}
};
}
private void executeMetaActionWithRetry(MetaAction metaAction, ExecutableAction actionData) {
AtomicReference<String> failureReason = new AtomicReference<>("参数提取失败");
int executingStage = actionData.getExecutingStage();
boolean succeeded = false;
for (int attempt = 1; attempt <= MAX_EXTRACTOR_ATTEMPTS; attempt++) {
val result = metaAction.getResult();
result.reset();
metaAction.getParams().clear();
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription());
AgentRuntimeException exception = extractorInputResult.exceptionOrNull();
if (exception != null) {
failureReason.set(exception.getMessage());
break;
}
ExtractorInput extractorInput = extractorInputResult.getOrThrow();
Result<ExtractorResult> extractorResultWrapped = paramsExtractor.execute(extractorInput).onFailure(exp -> failureReason.set(exp.getLocalizedMessage()));
if (extractorResultWrapped.exceptionOrNull() != null) {
continue;
}
ExtractorResult extractorResult = extractorResultWrapped.getOrThrow();
if (!extractorResult.isOk()) {
failureReason.set(buildAttemptFailureReason("参数提取失败", null));
continue;
}
metaAction.getParams().putAll(toMetaActionParams(extractorResult.getParams()));
try {
runnerClient.submit(metaAction);
} catch (Exception e) {
failureReason.set(buildAttemptFailureReason("行动执行异常", e.getLocalizedMessage()));
continue;
}
if (result.getStatus() == MetaAction.Result.Status.SUCCESS) {
succeeded = true;
break;
}
failureReason.set(buildAttemptFailureReason("行动执行失败", result.getData()));
}
if (!succeeded) {
metaAction.getResult().setStatus(MetaAction.Result.Status.FAILED);
metaAction.getResult().setData(failureReason.get());
}
appendHistoryAction(actionData, executingStage, metaAction);
}
private Map<String, Object> toMetaActionParams(List<ExtractorResult.ParamEntry> params) {
if (params == null || params.isEmpty()) {
return Map.of();
}
Map<String, Object> converted = new LinkedHashMap<>();
for (ExtractorResult.ParamEntry entry : params) {
if (entry == null) {
continue;
}
String name = entry.getName();
if (name == null || name.isBlank()) {
continue;
}
converted.put(name, entry.getValue());
}
return converted;
}
private void appendHistoryAction(ExecutableAction actionData, int executingStage, MetaAction metaAction) {
HistoryAction historyAction = new HistoryAction(
metaAction.getKey(),
resolveHistoryDescription(metaAction.getKey()),
metaAction.getResult().getData()
);
actionData.getHistory()
.computeIfAbsent(executingStage, integer -> new ArrayList<>())
.add(historyAction);
}
private RecognizerTaskRecord startRecognizerIfNeeded(ExecutableAction executableAction, Phaser phaser) {
if (!shouldRunCorrectionRecognizer(executableAction)) {
return RecognizerTaskRecord.disabled();
}
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction);
val task = buildRecognizerTask(recognizerInput, phaser);
Future<RecognizerResult> future = virtualExecutor.submit(task);
return new RecognizerTaskRecord(true, future);
}
private Callable<RecognizerResult> buildRecognizerTask(CorrectorInput input, Phaser phaser) {
phaser.register();
return () -> {
try {
return actionCorrectionRecognizer.execute(input)
.getOrDefault(new RecognizerResult());
} finally {
phaser.arriveAndDeregister();
}
};
}
private RecognizerResult resolveRecognizerResult(RecognizerTaskRecord record) {
if (record == null || !record.enabled() || record.future() == null) {
return null;
}
try {
if (!record.future().isDone()) {
return null;
}
return record.future().get();
} catch (Exception e) {
return null;
}
}
private String buildAttemptFailureReason(String prefix, String detail) {
if (detail == null || detail.isBlank()) {
return prefix;
}
return prefix + ": " + detail;
}
private boolean hasFailedMetaAction(List<MetaAction> metaActions) {
return metaActions.stream().anyMatch(metaAction -> metaAction.getResult().getStatus() == MetaAction.Result.Status.FAILED);
}
private boolean shouldRunCorrectionRecognizer(ExecutableAction executableAction) {
val orderedStages = new ArrayList<>(executableAction.getActionChain().keySet());
orderedStages.sort(Integer::compareTo);
int totalStages = orderedStages.size();
if (totalStages < 3) {
return false;
}
int stageIndex = orderedStages.indexOf(executableAction.getExecutingStage());
if (stageIndex < 0) {
return false;
}
if (stageIndex == totalStages - 1) {
return true;
}
return stageIndex >= 2 && (stageIndex - 2) % 2 == 0;
}
private void normalizeExecutingStage(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
Integer firstStage = actionChain.keySet().stream()
.min(Integer::compareTo)
.orElse(null);
if (firstStage == null) {
return;
}
if (actionChain.containsKey(executableAction.getExecutingStage())) {
return;
}
if (executableAction.getStatus() == Action.Status.EXECUTING) {
resetExecutableActionForReplay(executableAction);
}
executableAction.setExecutingStage(firstStage);
}
private void resetExecutableActionForReplay(ExecutableAction executableAction) {
executableAction.getHistory().clear();
executableAction.getActionChain().values().forEach(metaActions -> metaActions.forEach(metaAction -> {
metaAction.getParams().clear();
metaAction.getResult().reset();
}));
if (hasExecutableResult(executableAction)) {
executableAction.setResult("");
}
}
private void ensureExecutableResult(ExecutableAction executableAction, boolean failed, String failureReason) {
if (hasExecutableResult(executableAction)) {
return;
}
executableAction.setResult(resolveExecutableResult(executableAction, failed, failureReason));
}
private String resolveExecutableResult(ExecutableAction executableAction, boolean failed, String failureReason) {
String extracted = extractLastMetaActionResult(executableAction);
if (extracted != null && !extracted.isBlank()) {
return extracted;
}
if (!failed) {
return "行动执行成功";
}
if (failureReason != null && !failureReason.isBlank()) {
return "行动执行失败: " + failureReason;
}
return "行动执行失败";
}
private String extractLastMetaActionResult(ExecutableAction executableAction) {
if (!executableAction.getHistory().isEmpty()) {
Integer lastStage = executableAction.getHistory().keySet().stream()
.max(Integer::compareTo)
.orElse(null);
if (lastStage != null) {
List<HistoryAction> historyActions = executableAction.getHistory().get(lastStage);
if (historyActions != null && !historyActions.isEmpty()) {
String result = historyActions.getLast().result();
if (result != null && !result.isBlank()) {
return result;
}
}
}
}
if (!executableAction.getActionChain().isEmpty()) {
Integer lastStage = executableAction.getActionChain().keySet().stream()
.max(Integer::compareTo)
.orElse(null);
if (lastStage != null) {
List<MetaAction> metaActions = executableAction.getActionChain().get(lastStage);
if (metaActions != null && !metaActions.isEmpty()) {
String result = metaActions.getLast().getResult().getData();
if (result != null && !result.isBlank()) {
return result;
}
}
}
}
return null;
}
private boolean hasExecutableResult(ExecutableAction executableAction) {
try {
String result = executableAction.getResult();
return result != null && !result.isBlank();
} catch (Exception e) {
return false;
}
}
private String resolveHistoryDescription(String actionKey) {
return actionCapability.loadMetaActionInfo(actionKey)
.fold(
metaActionInfo -> metaActionInfo.getDescription().isBlank() ? actionKey : metaActionInfo.getDescription(),
exception -> actionKey
);
}
private enum StageSelectionType {
CONTINUE,
STOP,
RETURN
}
private record StageSelection(StageSelectionType type, List<MetaAction> metaActions) {
private static StageSelection continueWith(List<MetaAction> metaActions) {
return new StageSelection(StageSelectionType.CONTINUE, metaActions);
}
private static StageSelection stop() {
return new StageSelection(StageSelectionType.STOP, null);
}
private static StageSelection returnNow() {
return new StageSelection(StageSelectionType.RETURN, null);
}
private boolean shouldStop() {
return type == StageSelectionType.STOP;
}
private boolean shouldReturn() {
return type == StageSelectionType.RETURN;
}
}
private record StageExecution(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions, boolean closed) {
private static StageExecution completed(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions) {
return new StageExecution(recognizerRecord, metaActions, false);
}
private static StageExecution closed(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions) {
return new StageExecution(recognizerRecord, metaActions, true);
}
}
private record MetaActionsListeningRecord(AtomicBoolean accepting, int phase) {
}
private record RecognizerTaskRecord(boolean enabled, Future<RecognizerResult> future) {
private static RecognizerTaskRecord disabled() {
return new RecognizerTaskRecord(false, null);
}
}
private static final class StageCursor {
private final ExecutableAction executableAction;
private final Map<Integer, List<MetaAction>> actionChain;
private int stageCount;
private boolean executingStageUpdated;
private boolean stageCountUpdated;
private StageCursor(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
this.executableAction = executableAction;
this.actionChain = actionChain;
}
private void init() {
val orderList = new ArrayList<>(actionChain.keySet());
orderList.sort(Integer::compareTo);
stageCount = orderList.indexOf(executableAction.getExecutingStage());
update();
}
private void requestAdvance() {
if (!stageCountUpdated) {
stageCount++;
stageCountUpdated = true;
}
if (stageCount < actionChain.size() && !executingStageUpdated) {
update();
executingStageUpdated = true;
}
}
private boolean next() {
executingStageUpdated = false;
stageCountUpdated = false;
return stageCount < actionChain.size();
}
private void update() {
val orderList = new ArrayList<>(actionChain.keySet());
orderList.sort(Integer::compareTo);
executableAction.setExecutingStage(orderList.get(stageCount));
}
}
@SuppressWarnings("InnerClassMayBeStatic")
private class AssemblyHelper {
private AssemblyHelper() {
}
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) {
return actionCapability.loadMetaActionInfo(actionKey).fold(
metaActionInfo -> {
ExtractorInput input = new ExtractorInput();
input.setMetaActionInfo(metaActionInfo);
input.setTargetActionId(uuid);
input.setTargetActionDesc(description);
return Result.success(input);
},
Result::failure
);
}
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) {
Map<Integer, List<CorrectorInput.ActionChainItem>> overview = new LinkedHashMap<>();
executableAction.getActionChain().forEach((stage, list) -> {
List<CorrectorInput.ActionChainItem> overviewItems = list.stream()
.map(metaAction -> new CorrectorInput.ActionChainItem(
metaAction.getKey(),
resolveHistoryDescription(metaAction.getKey()),
metaAction.getResult().getStatus().name().toLowerCase(Locale.ROOT)
))
.toList();
overview.put(stage, overviewItems);
});
return CorrectorInput.builder()
.tendency(executableAction.getTendency())
.source(executableAction.getSource())
.reason(executableAction.getReason())
.description(executableAction.getDescription())
.actionId(executableAction.getUuid())
.actionChainOverview(overview)
.build();
}
}
}

View File

@@ -0,0 +1,432 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.ContextWorkspace;
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
class ExecutingActionBlockManager {
private static final String SOURCE = "action_executor";
private final ContextWorkspace contextWorkspace;
ExecutingActionBlockManager(ContextWorkspace contextWorkspace) {
this.contextWorkspace = contextWorkspace;
}
void emitActionRecoveredBlock(Set<ExecutableAction> recoveredActions) {
Set<ExecutableActionSnapshot> snapshots = recoveredActions.stream().map(ExecutableAction::snapshot).collect(Collectors.toSet());
String blockName = "actions_recovered";
String emittedAt = emittedAt();
String event = "actions_recovered";
contextWorkspace.register(new ContextBlock(
buildExecutingActionRecoveredFullBlock(snapshots, blockName, emittedAt, event),
buildExecutingActionRecoveredCompactBlock(snapshots, blockName, emittedAt, event),
buildExecutingActionRecoveredAbstractBlock(snapshots, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
100,
12,
1
));
}
private @NotNull BlockContent buildExecutingActionRecoveredAbstractBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "abstract", recoveredExecutingActions.size() + " executing actions recovered.");
}
};
}
private @NotNull BlockContent buildExecutingActionRecoveredCompactBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendListElement(document, root, "recovered_actions", "action", recoveredExecutingActions, (actionElement, action) -> {
appendTextElement(document, actionElement, "description", action.getDescription());
return Unit.INSTANCE;
});
}
};
}
private @NotNull BlockContent buildExecutingActionRecoveredFullBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendListElement(document, root, "recovered_actions", "action", recoveredExecutingActions, (actionElement, action) -> {
appendTextElement(document, actionElement, "description", action.getDescription());
appendTextElement(document, actionElement, "source", action.getSource());
appendTextElement(document, actionElement, "executing_stage", action.getExecutingStage());
return Unit.INSTANCE;
});
}
};
}
void emitStateActionTriggeredBlock(StateAction stateAction) {
StateActionSnapshot snapshot = stateAction.snapshot();
String blockName = buildBlockName(stateAction.getUuid());
String emittedAt = emittedAt();
String event = "state_action_triggered";
contextWorkspace.register(new ContextBlock(
buildStateActionFullBlock(snapshot, blockName, emittedAt, event),
buildStateActionCompactBlock(snapshot, blockName, emittedAt, event),
buildStateActionAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
70,
18,
10
));
}
private @NotNull BlockContent buildStateActionAbstractBlock(StateActionSnapshot snapshot, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "description", snapshot.getDescription());
}
};
}
private @NotNull BlockContent buildStateActionCompactBlock(StateActionSnapshot snapshot, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
}
};
}
private @NotNull BlockContent buildStateActionFullBlock(StateActionSnapshot snapshot, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
appendTextElement(document, root, "source", snapshot.getSource());
appendTextElement(document, root, "schedule_type", snapshot.getScheduleType().name().toLowerCase(Locale.ROOT));
}
};
}
void emitActionLaunchedBlock(ExecutableAction action) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_launched";
contextWorkspace.register(new ContextBlock(
buildActionLaunchedFullBlock(snapshot, blockName, event, emittedAt),
buildActionCompactBlock(snapshot, blockName, event, emittedAt),
buildActionAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
28,
6,
18
));
}
private @NotNull BlockContent buildActionAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "description", snapshot.getDescription());
}
};
}
private @NotNull BlockContent buildActionCompactBlock(ExecutableActionSnapshot snapshot, String blockName, String event, String emittedAt) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "primary_action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
Schedulable.ScheduleType scheduleType = snapshot.getScheduleType();
String scheduleContent = snapshot.getScheduleContent();
if (scheduleType != null && scheduleContent != null) {
appendChildElement(document, root, "schedule_info", (element) -> {
appendTextElement(document, element, "schedule_type", scheduleType.name().toLowerCase(Locale.ROOT));
appendTextElement(document, element, "schedule_content", scheduleContent);
return Unit.INSTANCE;
});
}
}
};
}
private @NotNull BlockContent buildActionLaunchedFullBlock(ExecutableActionSnapshot snapshot, String blockName, String event, String emittedAt) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "primary_action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
appendTextElement(document, root, "source", snapshot.getSource());
appendTextElement(document, root, "tendency", snapshot.getTendency());
Schedulable.ScheduleType scheduleType = snapshot.getScheduleType();
String scheduleContent = snapshot.getScheduleContent();
if (scheduleType != null && scheduleContent != null) {
appendChildElement(document, root, "schedule_info", (element) -> {
appendTextElement(document, element, "schedule_type", scheduleType.name().toLowerCase(Locale.ROOT));
appendTextElement(document, element, "schedule_content", scheduleContent);
return Unit.INSTANCE;
});
}
}
};
}
void emitActionStageSettledBlock(ExecutableAction action) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_stage_settled";
contextWorkspace.register(new ContextBlock(
buildActionStageFullBlock(snapshot, blockName, emittedAt, event),
buildActionStageCompactBlock(snapshot, blockName, emittedAt, event),
buildActionStageAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
55,
10,
12
));
}
private @NotNull BlockContent buildActionStageAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
int settledStage = snapshot.getExecutingStage();
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "abstract", history.size() + " meta actions are resolved in stage " + settledStage);
}
};
}
private @NotNull BlockContent buildActionStageCompactBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
int settledStage = snapshot.getExecutingStage();
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "abstract", history.size() + " meta actions are resolved in stage " + settledStage);
}
};
}
private @NotNull BlockContent buildActionStageFullBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
int settledStage = snapshot.getExecutingStage();
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "settled_action_chain_stage", settledStage);
appendListElement(document,
root,
"settled_meta_actions",
"meta_action",
history.subList(0, Math.min(3, history.size())),
(item, action) -> {
String primaryResult = action.result();
String result = primaryResult.length() > 160 ? primaryResult.substring(0, 160) : primaryResult;
appendTextElement(document, item, "action_key", action.actionKey());
appendTextElement(document, item, "result", result);
return Unit.INSTANCE;
}
);
}
};
}
void emitActionCorrectionBlock(ExecutableAction action, String reason, List<MetaIntervention> interventions) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_correction_triggered";
contextWorkspace.register(new ContextBlock(
buildActionCorrectionFullBlock(snapshot, reason, interventions, blockName, emittedAt, event),
buildActionCorrectionCompactBlock(snapshot, reason, interventions, blockName, emittedAt, event),
buildActionCorrectionAbstractBlock(snapshot, interventions, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
22,
5,
22
));
}
private List<HistoryAction> resolveStageHistory(ExecutableActionSnapshot snapshot, int settledStage) {
List<HistoryAction> history = snapshot.getHistory().get(settledStage);
return history == null ? List.of() : history;
}
private @NotNull BlockContent buildActionCorrectionAbstractBlock(ExecutableActionSnapshot snapshot, List<MetaIntervention> interventions, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "abstract", interventions.size() + " interventions occurred after stage " + snapshot.getExecutingStage());
}
};
}
private @NotNull BlockContent buildActionCorrectionCompactBlock(ExecutableActionSnapshot snapshot, String reason, List<MetaIntervention> interventions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
Set<Integer> affectedStage = interventions.stream().map(MetaIntervention::getOrder).collect(Collectors.toSet());
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "abstract", interventions.size() + " interventions occurred after stage " + snapshot.getExecutingStage() + ", stage: " + affectedStage + " are affected");
appendTextElement(document, root, "correction_reason", reason);
}
};
}
private @NotNull BlockContent buildActionCorrectionFullBlock(ExecutableActionSnapshot snapshot, String reason, List<MetaIntervention> interventions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "correction_reason", reason);
appendListElement(document,
root,
"applied_interventions",
"intervention",
interventions,
(item, intervention) -> {
appendTextElement(document, item, "type", intervention.getType().name().toLowerCase(Locale.ROOT));
appendTextElement(document, item, "affected_stage", intervention.getOrder());
appendTextElement(document, item, "applied_action_key_set", intervention.getActions());
return Unit.INSTANCE;
}
);
}
};
}
void emitActionFinishedBlock(ExecutableAction action) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_finished";
contextWorkspace.register(new ContextBlock(
buildActionFinishedFullBlock(snapshot, blockName, emittedAt, event),
buildActionFinishedFullBlock(snapshot, blockName, emittedAt, event),
buildActionFinishedAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
35,
14,
24
));
}
private @NotNull BlockContent buildActionFinishedAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
}
};
}
private @NotNull BlockContent buildActionFinishedFullBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
appendTextElement(document, root, "result", snapshot.getResult());
}
};
}
private String emittedAt() {
return ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
private String buildBlockName(String actionId) {
return "executing_action-" + actionId;
}
private static abstract class ActionBlockContent extends BlockContent {
private ActionBlockContent(@NotNull String blockName, @NotNull String source, @NotNull Urgency urgency) {
super(blockName, source, urgency);
}
private ActionBlockContent(@NotNull String blockName, @NotNull String source) {
super(blockName, source);
}
protected void appendEventElement(@NotNull Document document, @NotNull Element root, String event) {
appendTextElement(document, root, "event", event);
}
}
}

View File

@@ -0,0 +1,119 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.action.executor.entity.ExtractorInput;
import work.slhaf.partner.module.action.executor.entity.ExtractorResult;
import java.util.List;
/**
* 负责依据输入内容进行行动单元的参数信息提取
*/
public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Result<ExtractorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责为指定行动提取参数信息。
你会收到:
- 一条结构化上下文消息,其中可能包含当前行动相关状态、近期交流轨迹、以及活跃记忆切片;
- 一条任务消息,其中包含:
- target_action本次参数提取所面向的目标行动用于帮助你判断上下文中哪些内容与当前提取直接相关
- meta_action_info该行动对应的说明以及允许提取的参数列表。每个 <param name="..."> 节点的文本内容表示该参数的含义或期望内容。
你的任务:
- 根据当前上下文与目标行动信息,提取本次行动所需的参数;
- 只提取能够从当前输入、近期会话、相关行动历史或明确上下文中得到支持的参数;
- 若当前信息不足以支持可靠提取,则返回 ok=false而不是猜测或编造参数。
提取原则:
- target_action 用于帮助你在上下文中定位当前面对的是哪一个行动,不要把无关行动历史混入当前参数提取。
- action 域中的执行中行动块及其阶段历史,可作为理解当前行动推进位置、已知条件与已出现参数的参考。
- communication 域主要用于理解用户最近表达的条件、补充、修正、确认或否定信息。
- memory 域只在与当前目标行动明显相关时作为辅助参考使用。
- params 中只能填写 meta_action_info.params 中声明过的参数名,不要编造不存在的参数。
- 每个参数值都应尽量保持为简洁、明确、可直接使用的文本,不要输出冗长解释。
- 若某个参数无法从现有信息中可靠确定,就不要填写它。
关于输出:
- ok=true 表示你已经提取出了一组可用参数;不要求必须填满全部参数,但结果应足以支持后续执行继续推进。
- ok=false 表示当前信息不足以形成可靠参数结果。
- params 为参数项列表;每一项都必须包含:
- name: 参数名
- value: 参数值文本
- 不要把 params 输出为对象、Map 或其他结构,只能输出 ParamEntry 列表。
- 不要输出结构之外的解释、说明或额外文本。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<ExtractorResult> doExecute(ExtractorInput input) {
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
return formattedChat(messages, ExtractorResult.class);
}
private Message resolveTaskMessage(ExtractorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "target_action", block -> {
appendTextElement(document, block, "uuid", input.getTargetActionId());
appendTextElement(document, block, "description", input.getTargetActionDesc());
return Unit.INSTANCE;
});
appendChildElement(document, root, "meta_action_info", element -> {
MetaActionInfo info = input.getMetaActionInfo();
appendTextElement(document, element, "description", info.getDescription());
appendListElement(document, element, "params", "param", info.getParams().entrySet(), (item, param) -> {
item.setAttribute("name", param.getKey());
item.setTextContent(param.getValue());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace()
.resolve(List.of(
ContextBlock.FocusedDomain.ACTION,
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.MEMORY
))
.encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "params_extractor";
}
}

View File

@@ -0,0 +1,28 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class CorrectorInput {
private String tendency;
private String source;
private String reason;
private String description;
private String actionId;
private Map<Integer, List<ActionChainItem>> actionChainOverview;
@Data
@AllArgsConstructor
public static class ActionChainItem {
private String actionKey;
private String description;
private String status;
}
}

View File

@@ -0,0 +1,12 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.Data;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import java.util.List;
@Data
public class CorrectorResult {
private List<MetaIntervention> metaInterventionList;
private String correctionReason;
}

View File

@@ -0,0 +1,23 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.Data;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
@Data
public class ExtractorInput {
/**
* 目标行动数据的 uuid
*/
private String targetActionId;
/**
* 目标行动的 description
*/
private String targetActionDesc;
/**
* 目标 MetaActionInfo
*/
private MetaActionInfo metaActionInfo;
}

View File

@@ -0,0 +1,19 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
public class ExtractorResult {
private boolean ok;
private List<ParamEntry> params;
@Data
@AllArgsConstructor
public static class ParamEntry {
private String name;
private String value;
}
}

View File

@@ -0,0 +1,4 @@
package work.slhaf.partner.module.action.executor.entity;
public record HistoryAction(String actionKey, String description, String result) {
}

View File

@@ -0,0 +1,9 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.Data;
@Data
public class RecognizerResult {
private boolean needCorrection = false;
private String reason;
}

View File

@@ -0,0 +1,471 @@
package work.slhaf.partner.module.action.planner;
import kotlin.Unit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.CommunicationBlockContent;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.executor.ActionExecutor;
import work.slhaf.partner.module.action.planner.evaluator.ActionEvaluator;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorResult;
import work.slhaf.partner.module.action.planner.extractor.ActionExtractor;
import work.slhaf.partner.module.action.planner.extractor.entity.ExtractorResult;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import work.slhaf.partner.runtime.exception.ContextExceptionReporter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 负责针对本次输入生成基础的行动计划
*/
@Slf4j
public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
private static final String IMMEDIATE_WATCHER_CRON = "0/5 * * * * ?";
private static final String BLOCK_SOURCE = "action_planner_pending";
private static final String TENDENCIES_EVALUATING_BLOCK_NAME = "tendencies_in_evaluating";
private final ActionAssemblyHelper assemblyHelper = new ActionAssemblyHelper();
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private ActionCapability actionCapability;
@InjectModule
private ActionEvaluator actionEvaluator;
@InjectModule
private ActionExtractor actionExtractor;
@InjectModule
private ActionScheduler actionScheduler;
@InjectModule
private ActionExecutor actionExecutor;
private ExecutorService executor;
@Init
public void init() {
this.setModuleName("action_planner");
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
String input = context.encodeInputsBlock().encodeToXmlString();
Result<ExtractorResult> result = actionExtractor.execute(input)
.onFailure(exp -> ExceptionReporterHandler.INSTANCE.report(exp, ContextExceptionReporter.REPORTER_NAME));
if (result.exceptionOrNull() != null) {
return;
}
ExtractorResult extractorResult = result.getOrThrow();
List<String> tendencies = extractorResult.getTendencies();
if (tendencies.isEmpty()) {
return;
}
appendTendencyBlock(tendencies);
evaluateTendency(context.getSource(), input, extractorResult);
}
private void appendTendencyBlock(List<String> tendencies) {
String datetime = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildTendenciesEvaluatingFullBlock(tendencies),
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
buildTendenciesEvaluatingAbstractBlock(tendencies, datetime),
Set.of(ContextBlock.FocusedDomain.ACTION),
60,
18,
4
));
}
private @NotNull BlockContent buildTendenciesEvaluatingAbstractBlock(List<String> tendencies, String datetime) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "datetime", datetime);
appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " related tendencies in evaluating.");
}
};
}
private @NotNull BlockContent buildTendenciesEvaluatingCompactBlock(List<String> tendencies, String datetime) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
int size = tendencies.size();
boolean num = size > 3;
appendTextElement(document, root, "datetime", datetime);
appendTextElement(document, root, "state", "Partner is considering whether to do these");
appendTextElement(document, root, "tendencies_count", size);
appendListElement(document, root, num ? "tendencies_truncated" : "tendencies", "tendency", num ? tendencies.subList(0, 3) : tendencies);
}
};
}
private @NotNull BlockContent buildTendenciesEvaluatingFullBlock(List<String> tendencies) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "Partner is considering whether to do these");
appendListElement(document, root, "tendencies", "tendency", tendencies);
}
};
}
private void evaluateTendency(String source, String input, ExtractorResult extractorResult) {
executor.execute(() -> {
EvaluatorInput evaluatorInput = assemblyHelper.buildEvaluatorInput(extractorResult);
List<EvaluatorResult> evaluatorResults = actionEvaluator.execute(evaluatorInput); // 并发操作均为访问
handleEvaluatorResults(evaluatorResults, source, input);
cognitionCapability.contextWorkspace().expire(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName());
});
}
private void handleEvaluatorResults(List<EvaluatorResult> evaluatorResults, String source, String input) {
List<ExecutableAction> passedActions = new ArrayList<>();
int approvedExecutableCount = 0;
int pendingConfirmCount = 0;
for (EvaluatorResult evaluatorResult : evaluatorResults) {
expireResolvedPending(evaluatorResult);
if (!evaluatorResult.isOk()) {
continue;
}
ExecutableAction executableAction = assemblyHelper.buildActionData(evaluatorResult, source);
if (executableAction == null) {
continue;
}
passedActions.add(executableAction);
if (evaluatorResult.isNeedConfirm()) {
registerPendingContextBlock(executableAction, evaluatorResult, input);
pendingConfirmCount++;
continue;
}
executeOrSchedule(executableAction);
approvedExecutableCount++;
}
if (passedActions.isEmpty()) {
return;
}
createTurn(approvedExecutableCount, pendingConfirmCount, source, input);
}
private void createTurn(int approvedExecutableCount, int pendingConfirmCount, String source, String input) {
String turnInput = approvedExecutableCount + " actions are approved for execution, " +
pendingConfirmCount + " actions are waiting for confirmation, " +
"according to input: " + trimInput(input) + ". For more information, please refer to the context content or other tags in this input block.";
cognitionCapability.initiateTurn(turnInput, source, getModuleName());
}
private void expireResolvedPending(EvaluatorResult evaluatorResult) {
EvaluatorResult.ResolvedPending resolvedPending = evaluatorResult.getResolvedPending();
if (resolvedPending == null) {
return;
}
if (resolvedPending.getBlockName() == null || resolvedPending.getSource() == null) {
return;
}
cognitionCapability.contextWorkspace().expire(
resolvedPending.getBlockName(),
resolvedPending.getSource()
);
}
private void registerPendingContextBlock(ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
String blockName = buildPendingBlockName(executableAction);
input = trimInput(input);
ContextBlock block = new ContextBlock(
buildPendingBlock(blockName, executableAction, evaluatorResult),
buildPendingCompactBlock(blockName, executableAction, evaluatorResult, input),
buildPendingAbstractBlock(blockName, executableAction, evaluatorResult, input),
Set.of(ContextBlock.FocusedDomain.ACTION),
30,
10,
5
);
cognitionCapability.contextWorkspace().register(block);
}
private String buildPendingBlockName(ExecutableAction executableAction) {
return "pending_action-" + executableAction.getUuid();
}
private BlockContent buildPendingBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult) {
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "need_user_confirm");
appendTextElement(document, root, "action_uuid", executableAction.getUuid());
appendTextElement(document, root, "action_type", evaluatorResult.getType());
appendTextElement(document, root, "tendency", executableAction.getTendency());
appendTextElement(document, root, "reason", executableAction.getReason());
appendTextElement(document, root, "description", executableAction.getDescription());
appendTextElement(document, root, "source_user", executableAction.getSource());
EvaluatorResult.ScheduleData scheduleData = evaluatorResult.getScheduleData();
if (scheduleData != null) {
appendTextElement(document, root, "schedule_type", scheduleData.getType());
appendTextElement(document, root, "schedule_content", scheduleData.getContent());
}
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return;
}
Element chainRoot = document.createElement("primary_action_chain");
root.appendChild(chainRoot);
List<Integer> orders = new ArrayList<>(primaryActionChain.keySet());
orders.sort(Integer::compareTo);
for (Integer order : orders) {
Element orderElement = document.createElement("step");
orderElement.setAttribute("order", String.valueOf(order));
chainRoot.appendChild(orderElement);
appendRepeatedElements(
document,
orderElement,
"action_key",
primaryActionChain.getOrDefault(order, List.of())
);
}
}
};
}
private BlockContent buildPendingCompactBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "need_user_confirm");
appendTextElement(document, root, "related_input", input);
appendTextElement(document, root, "tendency", executableAction.getTendency());
appendTextElement(document, root, "description", executableAction.getDescription());
appendTextElement(document, root, "action_type", evaluatorResult.getType());
}
};
}
private BlockContent buildPendingAbstractBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
return new BlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "need_user_confirm");
appendTextElement(document, root, "related_input", input);
appendTextElement(document, root, "pending_tendency", executableAction.getTendency());
appendTextElement(document, root, "summary", "exists pending action waiting for confirmation");
appendTextElement(document, root, "action_type", evaluatorResult.getType());
}
};
}
private void executeOrSchedule(ExecutableAction executableAction) {
switch (executableAction) {
case ImmediateExecutableAction action -> executeImmediateWithWatcher(action);
case SchedulableExecutableAction action -> actionScheduler.schedule(action);
default -> log.error("unknown executable action type: {}", executableAction.getClass().getSimpleName());
}
}
private void executeImmediateWithWatcher(ImmediateExecutableAction action) {
actionExecutor.execute(action);
AtomicBoolean notified = new AtomicBoolean(false);
final StateAction[] watcherRef = new StateAction[1];
StateAction watcher = new StateAction(
action.getSource(),
"immediate-action-watcher:" + action.getUuid(),
"轮询即时行动执行结果",
Schedulable.ScheduleType.CYCLE,
IMMEDIATE_WATCHER_CRON,
new StateAction.Trigger.Call(() -> {
Action.Status status = action.getStatus();
if (status != Action.Status.SUCCESS && status != Action.Status.FAILED) {
return Unit.INSTANCE;
}
if (watcherRef[0] != null) {
watcherRef[0].setEnabled(false);
}
if (!notified.compareAndSet(false, true)) {
return Unit.INSTANCE;
}
watcherSelfTalk(action);
return Unit.INSTANCE;
})
);
watcherRef[0] = watcher;
actionScheduler.schedule(watcher);
}
private void watcherSelfTalk(ImmediateExecutableAction action) {
String result = action.getResult();
String structuredSignal = String.format(
"{event=immediate_action_finished,actionUuid=%s,tendency=%s,status=%s,source=%s,result=%s}",
action.getUuid(),
action.getTendency(),
action.getStatus(),
action.getSource(),
result == null ? "" : result //将会在 ActionExecutor
);
try {
cognitionCapability.initiateTurn(structuredSignal, action.getSource());
} catch (Exception e) {
log.warn("触发 immediate 行动完成自对话失败, actionUuid: {}", action.getUuid(), e);
}
}
@Override
public int order() {
return 2;
}
private String trimInput(@NotNull String input) {
input = input.trim();
input = input.length() <= 100 ? input : input.substring(0, 100);
return input;
}
private final class ActionAssemblyHelper {
private ActionAssemblyHelper() {
}
private EvaluatorInput buildEvaluatorInput(ExtractorResult extractorResult) {
EvaluatorInput input = new EvaluatorInput();
input.setTendencies(extractorResult.getTendencies());
return input;
}
private ExecutableAction buildActionData(EvaluatorResult evaluatorResult, String userId) {
Map<Integer, List<MetaAction>> actionChain = getActionChain(evaluatorResult);
if (actionChain == null) {
return null;
}
return switch (evaluatorResult.getType()) {
case PLANNING -> new SchedulableExecutableAction(
evaluatorResult.getTendency(),
actionChain,
evaluatorResult.getReason(),
evaluatorResult.getDescription(),
userId,
evaluatorResult.getScheduleData().getType(),
evaluatorResult.getScheduleData().getContent()
);
case IMMEDIATE -> new ImmediateExecutableAction(
evaluatorResult.getTendency(),
actionChain,
evaluatorResult.getReason(),
evaluatorResult.getDescription(),
userId
);
};
}
private Map<Integer, List<MetaAction>> getActionChain(EvaluatorResult evaluatorResult) {
Map<Integer, List<MetaAction>> actionChain = new HashMap<>();
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
if (!fixDependencies(primaryActionChain)) {
return null;
}
for (Map.Entry<Integer, List<String>> entry : primaryActionChain.entrySet()) {
List<MetaAction> metaActions = new ArrayList<>();
for (String actionKey : entry.getValue()) {
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
AgentRuntimeException failure = metaActionResult.onSuccess(metaActions::add)
.exceptionOrNull();
if (failure != null) {
return null;
}
}
actionChain.put(entry.getKey(), metaActions);
}
return actionChain;
}
private boolean fixDependencies(Map<Integer, List<String>> primaryActionChain) {
// 先将 primaryActionChain 的节点序号修正为从1开始依次增大
fixOrder(primaryActionChain);
List<Integer> fixedOrders = new ArrayList<>(primaryActionChain.keySet().stream().toList());
AtomicBoolean fixed = new AtomicBoolean(false);
do {
Set<Integer> tempOrders = new HashSet<>();
fixedOrders.sort(Integer::compareTo);
for (Integer fixedOrder : fixedOrders) {
int lastOrder = fixedOrder - 1;
List<String> actionKeys = primaryActionChain.get(fixedOrder);
for (String actionKey : actionKeys) {
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
Result<MetaActionInfo> infoResult = actionCapability.loadMetaActionInfo(actionKey);
if (infoResult.exceptionOrNull() != null) {
return false;
}
MetaActionInfo metaActionInfo = infoResult.getOrThrow();
Set<String> preActions = metaActionInfo.getPreActions();
boolean preActionsExist = preActions.isEmpty();
if (!preActionsExist) {
continue;
}
if (!metaActionInfo.getStrictDependencies()) {
continue;
}
if (checkDependenciesExist(lastOrder, preActions, primaryActionChain)) {
continue;
}
// 如果存在前置依赖,则将其放置在当前order之前的位置,
// 放置位置优先选择已存在的上一节点,如果不存在(行动链的头节点时)则需要向行动链新增order
// 不需要检查行动链的当前节点的已存在 Action 是否为新 Action 的依赖项,因为这些 Action 实际来自 LLM
// 的评估结果,并非作为依赖项存在
fixed.set(true);
List<String> actionsInChain = primaryActionChain.computeIfAbsent(lastOrder,
list -> new ArrayList<>());
preActions = new HashSet<>(preActions);
actionsInChain.forEach(preActions::remove);
actionsInChain.addAll(preActions);
tempOrders.add(lastOrder);
}
}
fixedOrders.clear();
fixedOrders.addAll(tempOrders);
} while (fixed.getAndSet(false));
return true;
}
private void fixOrder(Map<Integer, List<String>> primaryActionChain) {
Map<Integer, List<String>> tempChain = new HashMap<>(primaryActionChain);
primaryActionChain.clear();
int chainSize = tempChain.size();
for (int i = 0; i < chainSize; i++) {
primaryActionChain.put(i, tempChain.get(i));
}
}
private boolean checkDependenciesExist(int lastOrder, Set<String> preActions,
Map<Integer, List<String>> primaryActionChain) {
if (!primaryActionChain.containsKey(lastOrder)) {
return false;
}
List<String> existActions = primaryActionChain.get(lastOrder);
//noinspection SlowListContainsAll
return existActions.containsAll(preActions);
}
}
}

View File

@@ -0,0 +1,168 @@
package work.slhaf.partner.module.action.planner.evaluator;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.ResolvedContext;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorResult;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责评估单条行动倾向,并判断它是否值得进入后续行动链。
你会收到:
- 一条结构化上下文消息,其中可能包含当前活跃的行动相关状态、近期交流轨迹、认知相关上下文、以及当前活跃记忆切片;
- 一组 <available_meta_actions>,表示当前可用的 MetaAction 候选;
- 一条最新的 user message其内容就是当前需要评估的单条 tendency。
你的任务:
- 判断该 tendency 是否成立、是否值得推进;
- 判断它更适合立即执行,还是先进入规划;
- 判断是否需要先向用户确认;
- 若可推进,则基于 available_meta_actions 生成 primaryActionChain
- 若当前 tendency 与某个待处理 pending 明确对应,且本轮已完成承接或推进,应正确填写 resolvedPending
- 若当前 tendency 明确包含可调度语义,再填写 scheduleData。
评估原则:
- 结合上下文理解当前 tendency 与近期交流、当前行动状态、活跃记忆之间的关系。
- 若 tendency 与已有正在执行、等待确认、或已明确覆盖的行动完全等价,通常不应重复建立新的行动链。
- 若 tendency 是对已有待确认行动的确认、拒绝、补充条件、修改要求或继续推进,则应优先视为对原有行动状态的承接,而不是无关新任务。
- 若 action 相关上下文中存在等待确认的 block且当前 tendency 明显与其相关,则必须显式承接这一点,不要因为其已存在于上下文中而省略。
- 只有在 tendency 明确具有可执行意义时,才返回 ok=true。
- 若 tendency 更适合直接交流回应,或尚不足以形成行动推进,则返回 ok=false。
- primaryActionChain 中只能使用 available_meta_actions 内出现的 action_key不要编造不存在的动作。
- 不要输出完整执行细节、自然语言计划正文或额外解释,只输出 EvaluatorResult 对应结构。
关于字段:
- ok 表示该 tendency 是否值得进入后续行动推进。
- needConfirm 表示在真正推进前是否必须先得到用户确认。
- type:
- IMMEDIATE: 可直接进入即时执行链;
- PLANNING: 需要先形成或进入规划链,再决定后续执行。
- primaryActionChain 表示按 order 分组的候选动作链,每个元素包含:
- order: 执行顺序
- actionKeys: 该顺序下的 action_key 列表
- reason 用于说明你为何做出该判断,应简洁明确。
- description 用于概括本次行动评估结果,应能帮助后续模块快速理解该 tendency 的推进方向。
- scheduleData 仅在该 tendency 明确包含可调度语义时填写;否则留空。
- scheduleData.type: 一次性计划或周期性计划
- scheduleData.content: 符合 Quartz 标准的 Cron 表达式
- resolvedPending 仅在你能明确判断当前 tendency 已承接某个 pending block 时填写;否则留空。
- 当 ok=false 时type、primaryActionChain、scheduleData、resolvedPending 通常应留空或保持无效默认值,不要强行填充。
输出要求:
- 严格按照 EvaluatorResult 对应结构输出。
- 不要输出结构之外的解释、注释或额外文本。
""";
@InjectCapability
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
/**
* 对输入的行为倾向进行评估,并根据评估结果,对缓存做出调整
*
* @param data 评估输入内容,包含提取/命中缓存的行动倾向、近几条聊天记录,正在生效的记忆切片内容
* @return 评估结果集合
*/
@Override
protected List<EvaluatorResult> doExecute(EvaluatorInput data) {
List<String> tendencies = data.getTendencies();
CountDownLatch latch = new CountDownLatch(tendencies.size());
List<EvaluatorResult> evaluatorResults = new ArrayList<>();
for (String tendency : tendencies) {
executor.execute(() -> {
try {
List<Message> messages = List.of(
cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.ACTION,
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.COGNITION,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage(),
availableMetaActionContext(),
new Message(Message.Character.USER, tendency)
);
Result<EvaluatorResult> result = formattedChat(
messages,
EvaluatorResult.class
);
result.onSuccess(evaluatorResult -> {
evaluatorResult.setTendency(tendency);
synchronized (evaluatorResults) {
evaluatorResults.add(evaluatorResult);
}
});
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException ignored) {
}
return evaluatorResults;
}
private Message availableMetaActionContext() {
// TODO select and filter available MetaActions by tags and embedding
BlockContent content = new BlockContent("available_meta_actions", "action_planner") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendRepeatedElements(
document,
root,
"available_meta_action",
actionCapability.listAvailableMetaActions().entrySet(),
(block, value) -> {
appendTextElement(document, root, "action_key", value.getKey());
appendTextElement(document, root, "action_value", value.getValue().getDescription());
return Unit.INSTANCE;
}
);
}
};
return new ResolvedContext(List.of(content)).encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_evaluator";
}
}

View File

@@ -0,0 +1,16 @@
package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
import java.util.List;
import java.util.Map;
@Data
public class EvaluatorBatchInput {
private List<Message> recentMessages;
private List<ActivatedMemorySlice> activatedSlices;
private Map<String, String> availableActions;
private String tendency;
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data;
import java.util.List;
@Data
public class EvaluatorInput {
private List<String> tendencies;
}

View File

@@ -0,0 +1,53 @@
package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data;
import work.slhaf.partner.core.action.entity.Schedulable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
public class EvaluatorResult {
private boolean ok;
private boolean needConfirm;
private ResolvedPending resolvedPending;
private ActionType type;
private ScheduleData scheduleData;
private List<ChainElement> primaryActionChain;
private String tendency;
private String reason;
private String description;
public enum ActionType {
IMMEDIATE, PLANNING
}
@Data
public static class ResolvedPending {
private String blockName;
private String source;
}
public Map<Integer, List<String>> getPrimaryActionChainAsMap() {
return primaryActionChain.stream().collect(Collectors.toMap(
ChainElement::getOrder,
ChainElement::getActionKeys,
(oldValue, newValue) -> newValue,
LinkedHashMap::new
));
}
@Data
public static class ScheduleData {
private String content;
private Schedulable.ScheduleType type;
}
@Data
public static class ChainElement {
private Integer order;
private List<String> actionKeys;
}
}

View File

@@ -0,0 +1,73 @@
package work.slhaf.partner.module.action.planner.extractor;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ModuleExecutionException;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.planner.extractor.entity.ExtractorResult;
import java.util.List;
public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责从当前输入中提取可能的行动倾向,供后续模块继续评估。
你会收到:
- 一条结构化上下文消息,主要包含 communication 域与 action 域内容;
- 一条最新输入,作为本轮重点分析对象。
规则:
- communication 域主要用于理解当前会话语境、主题延续与用户意图。
- action 域主要用于判断相关行动是否已经在执行、等待确认,或已被覆盖。
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤或工具细节。
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息或条件变化,应避免重复提出。
- 若 action 域中存在等待确认的 block且当前输入与其相关则必须提取出对应的行动倾向不要因为其已存在于上下文中而省略。
- 若当前输入没有明显行动倾向,可返回空结果。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
- 不要输出结构之外的解释或额外文本。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<ExtractorResult> doExecute(String input) {
List<Message> messages = List.of(
cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.ACTION
)).encodeToMessage(),
new Message(Message.Character.USER, input)
);
try {
return Result.success(formattedChat(messages, ExtractorResult.class).getOrThrow());
} catch (AgentRuntimeException e) {
return Result.failure(new ModuleExecutionException(
"collecting action tendencies failed",
this.getClass(),
getModuleName()
));
}
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_extractor";
}
}

View File

@@ -0,0 +1,12 @@
package work.slhaf.partner.module.action.planner.extractor.entity;
import lombok.Data;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
@Data
public class ExtractorInput {
private String input;
private List<Message> recentMessages;
}

View File

@@ -0,0 +1,11 @@
package work.slhaf.partner.module.action.planner.extractor.entity;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ExtractorResult {
private List<String> tendencies = new ArrayList<>();
}

View File

@@ -0,0 +1,460 @@
package work.slhaf.partner.module.action.scheduler
import com.cronutils.model.CronType
import com.cronutils.model.definition.CronDefinition
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.slf4j.LoggerFactory
import work.slhaf.partner.core.action.ActionCapability
import work.slhaf.partner.core.action.entity.Action
import work.slhaf.partner.core.action.entity.Schedulable
import work.slhaf.partner.core.action.entity.SchedulableExecutableAction
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule
import work.slhaf.partner.framework.agent.factory.component.annotation.Init
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule
import work.slhaf.partner.module.action.executor.ActionExecutor
import java.io.Closeable
import java.time.Duration
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.stream.Collectors
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
class ActionScheduler : AbstractAgentModule.Standalone() {
@InjectCapability
private lateinit var actionCapability: ActionCapability
@InjectModule
private lateinit var actionExecutor: ActionExecutor
private lateinit var timeWheel: TimeWheel
private val runtimeSchedulables: MutableSet<Schedulable> =
Collections.synchronizedSet(mutableSetOf())
private val schedulerScope =
CoroutineScope(Dispatchers.Default + SupervisorJob() + CoroutineName("ActionScheduler"))
companion object {
private val log = LoggerFactory.getLogger(ActionScheduler::class.java)
}
@Init
fun init() {
fun loadScheduledActions() {
val listScheduledActions: () -> Set<Schedulable> = {
val persistedExecutable = actionCapability.listActions(null, null)
.stream()
.filter { it is SchedulableExecutableAction }
.map { it as SchedulableExecutableAction }
.collect(Collectors.toSet())
val persisted: MutableSet<Schedulable> = mutableSetOf()
persisted.addAll(persistedExecutable)
synchronized(runtimeSchedulables) {
persisted.addAll(runtimeSchedulables.filter { it.enabled })
}
persisted
}
val onTrigger: (Set<Schedulable>) -> Unit = { schedulableSet ->
schedulableSet.filterIsInstance<Action>()
.forEach { actionExecutor.execute(it) }
}
val doneCondition: (Schedulable) -> Boolean = { schedulable ->
if (schedulable is Action) {
schedulable.status == Action.Status.FAILED || schedulable.status == Action.Status.SUCCESS
}
true
}
timeWheel = TimeWheel(listScheduledActions, onTrigger, doneCondition)
}
loadScheduledActions()
setupShutdownHook()
}
private fun setupShutdownHook() {
Runtime.getRuntime().addShutdownHook(Thread {
timeWheel.close()
schedulerScope.cancel()
})
}
fun <T> schedule(schedulableAction: T) where T : Action, T : Schedulable = schedulerScope.launch {
if (!schedulableAction.enabled) {
return@launch
}
runtimeSchedulables.add(schedulableAction)
log.debug("New data to schedule: {}", schedulableAction)
timeWheel.schedule(schedulableAction)
if (schedulableAction is SchedulableExecutableAction) {
actionCapability.putAction(schedulableAction)
}
}
fun cancel(actionId: String): Boolean {
val runtimeMatches = synchronized(runtimeSchedulables) {
runtimeSchedulables.filter { it.uuid == actionId }.toSet()
}
val persistedMatches = actionCapability.listActions(null, null)
.asSequence()
.filterIsInstance<SchedulableExecutableAction>()
.filter { it.uuid == actionId }
.toSet()
val matches = LinkedHashSet<Schedulable>()
matches.addAll(runtimeMatches)
matches.addAll(persistedMatches)
matches.forEach { it.enabled = false }
val removedFromWheel = if (::timeWheel.isInitialized) {
runBlocking { timeWheel.cancel(actionId) }
} else {
false
}
return matches.isNotEmpty() || removedFromWheel
}
private class TimeWheel(
val listSource: () -> Set<Schedulable>,
val onTrigger: (toTrigger: Set<Schedulable>) -> Unit,
val doneCondition: (schedulable: Schedulable) -> Boolean
) : Closeable {
private val schedulableGroupByHour = Array<MutableSet<Schedulable>>(24) { mutableSetOf() }
private val wheel = Array<MutableSet<Schedulable>>(60 * 60) { mutableSetOf() }
private var recordHour: Int = -1
private var recordDay: Int = -1
private val state = MutableStateFlow(WheelState.SLEEPING)
private val wheelActionsLock = Mutex()
private val timeWheelScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("TimeWheel"))
private val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
private val cronParser: CronParser = CronParser(cronDefinition)
init {
// 启动时间轮
wheel()
}
suspend fun schedule(schedulableData: Schedulable) {
checkThenExecute {
val parseToZonedDateTime = parseToZonedDateTime(
schedulableData.scheduleType,
schedulableData.scheduleContent,
it
) ?: run {
logFailedStatus(schedulableData)
return@checkThenExecute
}
log.debug("Action next execution time: {}", parseToZonedDateTime)
val hour = parseToZonedDateTime.hour
schedulableGroupByHour[hour].add(schedulableData)
log.debug("Action scheduled at {}", hour)
if (it.hour == hour) {
val wheelOffset = parseToZonedDateTime.minute * 60 + parseToZonedDateTime.second
wheel[wheelOffset].add(schedulableData)
state.value = WheelState.ACTIVE
log.debug("Action scheduled at wheel offset {}", wheelOffset)
}
}
}
suspend fun cancel(actionId: String): Boolean = wheelActionsLock.withLock {
var found = false
for (bucket in schedulableGroupByHour) {
var bucketFound = false
bucket.removeIf {
if (it.uuid == actionId) {
it.enabled = false
bucketFound = true
true
} else {
false
}
}
found = found || bucketFound
}
for (bucket in wheel) {
var bucketFound = false
bucket.removeIf {
if (it.uuid == actionId) {
it.enabled = false
bucketFound = true
true
} else {
false
}
}
found = found || bucketFound
}
found
}
private fun wheel() {
data class WheelStepResult(
val toTrigger: Set<Schedulable>?,
val shouldBreak: Boolean
)
fun collectToTrigger(tick: Int, previousTick: Int, triggerHour: Int): Set<Schedulable>? {
if (tick > previousTick) {
val toTrigger = mutableSetOf<Schedulable>()
for (i in previousTick..tick) {
val bucket = wheel[i]
if (bucket.isNotEmpty()) {
toTrigger.addAll(bucket.filter { it.enabled })
val bucketUuids = bucket.asSequence().map { it.uuid }.toHashSet()
schedulableGroupByHour[triggerHour].removeIf { it.uuid in bucketUuids }
bucket.clear() // 避免重复触发
}
}
return toTrigger
}
return null
}
fun handleToTrigger(toTrigger: Set<Schedulable>) {
timeWheelScope.launch {
onTrigger(toTrigger)
}
for (schedulable in toTrigger) timeWheelScope.launch {
if (schedulable.scheduleType == Schedulable.ScheduleType.ONCE) {
return@launch
}
withTimeoutOrNull(schedulable.timeout) {
while (!doneCondition(schedulable)) {
delay(50.milliseconds)
}
}
if (!schedulable.enabled) {
return@launch
}
this@TimeWheel.schedule(schedulable)
}
}
suspend fun CoroutineScope.wheel(launchingTime: ZonedDateTime, primaryTickAdvanceTime: Long) {
val launchingHour = launchingTime.hour
var tick = launchingTime.minute * 60 + launchingTime.second
// 让节拍器从“启动时刻的下一秒”开始(避免立即 step=0
var nextTickNanos = primaryTickAdvanceTime + 1_000_000_000L
while (isActive) {
// 1) 计算落后多少秒:至少 1正常推进也可能 >1追赶
val now0 = System.nanoTime()
val lagNanos = now0 - nextTickNanos
val step = if (lagNanos < 0) 1 else (lagNanos / 1_000_000_000L).toInt() + 1
val previousTick = tick
tick = (tick + step).coerceAtMost(wheel.lastIndex)
// 2) 推进节拍器:按“理论秒”前进 step 次
nextTickNanos += step.toLong() * 1_000_000_000L
val stepResult = run {
var shouldBreak = false
var toTrigger: Set<Schedulable>? = null
checkThenExecute(false) {
if (it.hour != launchingHour) {
shouldBreak = true
toTrigger = collectToTrigger(wheel.lastIndex, previousTick, launchingHour)
log.debug(
"Hour changed, previousTick: {}, tick: {}, toTriggerSize: {}",
previousTick,
tick,
toTrigger?.size
)
return@checkThenExecute
}
toTrigger = collectToTrigger(tick, previousTick, launchingHour)
if (tick >= wheel.lastIndex || schedulableGroupByHour[launchingHour].isEmpty()) {
state.value = WheelState.SLEEPING
shouldBreak = true
}
}
WheelStepResult(toTrigger, shouldBreak)
}
stepResult.toTrigger?.let { handleToTrigger(it) }
if (stepResult.shouldBreak) {
log.debug("Wheel stopped at tick {}", tick)
break
}
// 3) 精确睡到下一次理论 tick用最新 nanoTime
val now1 = System.nanoTime()
val sleepNanos = nextTickNanos - now1
if (sleepNanos > 0) {
delay((sleepNanos / 1_000_000L).milliseconds) // 毫秒级 delay 足够;剩余 nanos 不必忙等
}
}
}
suspend fun wait(currentTime: ZonedDateTime) {
val nextHour = currentTime.truncatedTo(ChronoUnit.HOURS).plusHours(1)
val seconds = Duration.between(
currentTime, nextHour
).toMillis()
// withTimeoutOrNull 内部已处理 seconds 小于 0 的情况
log.debug("Start waiting {} ms at {}, target time: {}", seconds, currentTime, nextHour)
withTimeoutOrNull(seconds.milliseconds) {
state.first { it == WheelState.ACTIVE }
}
log.debug("Waiting ended at {}", ZonedDateTime.now())
}
timeWheelScope.launch {
while (isActive) {
// 判断是否该步入下一小时
var shouldWait: Boolean? = null
var currentTime: ZonedDateTime? = null
var primaryTickAdvanceTime: Long? = null
checkThenExecute {
currentTime = it
shouldWait = schedulableGroupByHour[it.hour].isEmpty()
// 由于 wheel 的启动时间可能存在延迟,而时内推进由 nanoTime 保证不会漏发,
// 正常的时序结束又由 tick 是否触顶、当前时是否存在额外任务触发,
// 而启动时无触发保障,此时一并初始化 tick 推进时间,足以应对 check 与 wheel 间的这段时间间隔
primaryTickAdvanceTime = System.nanoTime()
}
// 如果该时无任务则等待,插入事件可提前唤醒
if (shouldWait!!) {
// 计算距离下一小时的时间,等待
currentTime?.let { wait(it) }
continue
}
// 唤醒进行时间轮循环
wheel(currentTime!!, primaryTickAdvanceTime!!)
}
}
}
suspend fun checkThenExecute(finallyToExecute: Boolean = true, then: (currentTime: ZonedDateTime) -> Unit) =
wheelActionsLock.withLock {
fun loadActions(
source: Set<Schedulable>,
now: ZonedDateTime,
load: (latestExecutingTime: ZonedDateTime, schedulableData: Schedulable) -> Unit,
repair: () -> Unit
) {
val runLoading = {
for (schedulableData in source) {
val nextExecutingTime =
parseToZonedDateTime(
schedulableData.scheduleType,
schedulableData.scheduleContent,
now
) ?: run {
logFailedStatus(schedulableData)
continue
}
load(nextExecutingTime, schedulableData)
}
}
repair()
runLoading()
}
fun loadHourActions(currentTime: ZonedDateTime) {
val load: (ZonedDateTime, Schedulable) -> Unit =
{ latestExecutionTime, schedulableData ->
val secondsTime = latestExecutionTime.minute * 60 + latestExecutionTime.second
wheel[secondsTime].add(schedulableData)
log.debug("Action loaded to hour: {}", schedulableData)
}
val repair: () -> Unit = {
for (set in wheel) {
set.clear()
}
}
loadActions(schedulableGroupByHour[currentTime.hour], currentTime, load, repair)
}
fun loadDayActions(currentTime: ZonedDateTime) {
val load: (ZonedDateTime, Schedulable) -> Unit =
{ latestExecutingTime, schedulableData ->
schedulableGroupByHour[latestExecutingTime.hour].add(schedulableData)
log.debug("Action loaded to day: {}", schedulableData)
}
val repair: () -> Unit = {
for (set in schedulableGroupByHour) {
set.clear()
}
}
loadActions(listSource(), currentTime, load, repair)
}
fun refreshIfNeeded(now: ZonedDateTime) {
val d = now.dayOfMonth
val h = now.hour
if (d != recordDay) {
recordDay = d
recordHour = h
loadDayActions(now)
loadHourActions(now)
} else if (h != recordHour) {
recordHour = h
loadHourActions(now)
}
}
val now = ZonedDateTime.now()
if (finallyToExecute) {
refreshIfNeeded(now)
then(now)
} else {
then(now)
refreshIfNeeded(now)
}
}
private fun parseToZonedDateTime(
scheduleType: Schedulable.ScheduleType,
scheduleContent: String,
now: ZonedDateTime
): ZonedDateTime? {
return when (scheduleType) {
Schedulable.ScheduleType.CYCLE
-> {
val cron = try {
cronParser.parse(scheduleContent).validate()
} catch (_: Exception) {
return null
}
val executionTime = ExecutionTime.forCron(cron)
executionTime.nextExecution(now).getOrNull()
}
Schedulable.ScheduleType.ONCE -> {
val executionTime = try {
ZonedDateTime.parse(scheduleContent)
} catch (_: Exception) {
return null
}
if (executionTime.plusSeconds(1).isBefore(now) || executionTime.dayOfMonth != now.dayOfMonth)
null
else
executionTime
}
}
}
private fun logFailedStatus(scheduleData: Schedulable) {
log.warn(
"行动未加载scheduleType: {}, scheduleContent: {}",
scheduleData.scheduleType,
scheduleData.scheduleContent,
)
}
override fun close() {
timeWheelScope.cancel()
}
private enum class WheelState {
ACTIVE,
SLEEPING,
}
}
}

View File

@@ -0,0 +1,5 @@
package work.slhaf.partner.module.communication;
public interface AfterRolling {
void consume(RollingResult result);
}

View File

@@ -0,0 +1,52 @@
package work.slhaf.partner.module.communication;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class AfterRollingRegistry extends AbstractAgentModule.Standalone {
private final CopyOnWriteArrayList<AfterRolling> consumers = new CopyOnWriteArrayList<>();
@InjectCapability
private ActionCapability actionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
public void register(AfterRolling consumer) {
if (consumers.contains(consumer)) {
return;
}
consumers.add(consumer);
}
public void trigger(RollingResult result) {
if (consumers.isEmpty()) {
return;
}
executor.execute(() -> {
for (AfterRolling consumer : List.copyOf(consumers)) {
try {
consumer.consume(result);
} catch (Exception e) {
ExceptionReporterHandler.INSTANCE.report(new AgentRuntimeException("after-rolling consumer occurred exception: " + consumer.getClass().getSimpleName(), e));
}
}
});
}
}

View File

@@ -0,0 +1,276 @@
package work.slhaf.partner.module.communication;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.*;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRunningFlowContext> implements ActivateModel {
private static final String INTERRUPTED_MARKER = " [response interrupted due to internal exception]";
private static final String NO_REPLY_MARKER = "NO_REPLY";
private static final String AGENT_MARKER = "[[AGENT]: self]";
private static final String NOT_REPLIED_PREFIX = "[NOT_REPLIED]";
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
private static final String MODULE_PROMPT = """
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
你接下来收到的消息,将按照出现顺序,固定分为三个区段:
1. system message 是 Head用于说明整个输入结构与输出要求即本条消息。
2. <context> 区段承载系统中所有模块产生的上下文块。它表示 Partner 在当前时刻的系统状态投影,不应被理解为普通聊天历史;其中每个子块都带有独立来源,可作为理解当前状态和辅助决策的依据。
3. <conversation> 区段是系统此刻保留的对话轨迹,用于帮助你理解当前交流延续、最近问答关系与最新输入所处的位置;最新的一条 user message 会使用 <input> 结构,其中 <inputs> 承载本轮按时间顺序排列的输入序列,每个 <input> 节点会带有相对首条输入的时间间隔属性;其他子标签是输入元信息与 type=SUPPLY 的补充块,补充块会按 blockName 分区。
你的任务:
- 最新输入是当前交流的直接触发点。
- <conversation> 主要用于理解对话延续关系。
- <context> 主要用于理解 Partner 此刻的系统状态;其中明显相关的状态信号不应被当作普通历史材料忽略。
- 若最新输入与已有上下文存在张力,应以最新输入为当前交流的直接依据,再结合 <conversation> 与 <context> 判断如何回应。
- 你当前负责的是对外交流,不负责直接规划行动、修改系统状态,或伪造并不存在的执行结果。
输出契约:
- 默认情况下,直接输出要发送给用户的最终回复正文,不要添加额外标签、解释或前后缀。
- 若当前情境下不应直接向用户发出回复,但仍需要留下本轮交流结果供系统后续保留在交流轨迹中,则输出以 NO_REPLY 开头。
- 使用 NO_REPLY 时,格式为:
NO_REPLY
这里写本轮交流结果正文
- 以 NO_REPLY 开头的输出不会直接展示给用户;系统在写入交流轨迹时,会以单独的历史标记形式保留该结果。
- 不要输出空字符串;若选择不直接回复用户,应使用 NO_REPLY 契约明确表达。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Init
public void init() {
log.info("CommunicationProducer 注册完毕...");
}
@Override
public @NotNull String modelKey() {
return "communication_producer";
}
@Override
public @NotNull List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@Override
protected void doExecute(PartnerRunningFlowContext runningFlowContext) {
log.debug("Communicating with: {}", runningFlowContext.getSource());
executeChat(runningFlowContext);
}
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
this.streamChat(buildChatMessages(runningFlowContext), consumer)
.onFailure(exception -> consumer.onDelta(INTERRUPTED_MARKER));
updateChatMessages(runningFlowContext, consumer.collectResponse());
cognitionCapability.refreshRecentChatMessagesContext();
}
private List<Message> buildChatMessages(PartnerRunningFlowContext runningFlowContext) {
ResolvedContext resolvedContext = cognitionCapability.contextWorkspace()
.resolve(List.of(ContextBlock.FocusedDomain.COGNITION, ContextBlock.FocusedDomain.ACTION, ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.PERCEIVE));
List<BlockContent> communicationBlocks = resolvedContext.getBlocks();
List<Message> historyMessages = snapshotConversationMessages();
List<Message> temp = new ArrayList<>(historyMessages.size() + 2);
temp.add(buildContextMessage(communicationBlocks));
temp.addAll(historyMessages);
temp.add(buildInputMessage(runningFlowContext, communicationBlocks));
return temp;
}
private void updateChatMessages(PartnerRunningFlowContext runningFlowContext, String response) {
cognitionCapability.getMessageLock().lock();
try {
List<Message> chatMessages = cognitionCapability.getChatMessages();
chatMessages.removeIf(this::isStructuredUserMessage);
Message primaryUserMessage = new Message(
Message.Character.USER,
formatConversationUserMessage(runningFlowContext)
);
chatMessages.add(primaryUserMessage);
Message assistantMessage = new Message(
Message.Character.ASSISTANT,
normalizeAssistantHistoryMessage(response)
);
chatMessages.add(assistantMessage);
} finally {
cognitionCapability.getMessageLock().unlock();
}
}
private String normalizeAssistantHistoryMessage(String response) {
String trimmed = response == null ? "" : response.trim();
if (trimmed.equals(NO_REPLY_MARKER)) {
return formatMarkedHistoryMessage(AGENT_MARKER, NOT_REPLIED_PREFIX, "");
}
if (trimmed.startsWith(NO_REPLY_MARKER + "\n")) {
return formatMarkedHistoryMessage(
AGENT_MARKER,
NOT_REPLIED_PREFIX,
trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim()
);
}
if (trimmed.startsWith(NO_REPLY_MARKER + "\r\n")) {
return formatMarkedHistoryMessage(
AGENT_MARKER,
NOT_REPLIED_PREFIX,
trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim()
);
}
return formatMarkedHistoryMessage(AGENT_MARKER, "", trimmed);
}
private List<Message> snapshotConversationMessages() {
List<Message> snapshot = new ArrayList<>(cognitionCapability.snapshotChatMessages());
snapshot.removeIf(this::isStructuredUserMessage);
return snapshot;
}
private Message buildContextMessage(List<BlockContent> communicationBlocks) {
List<BlockContent> contextBlocks = communicationBlocks.stream()
.filter(this::belongsToContextSection)
.toList();
return new ResolvedContext(contextBlocks).encodeToMessage();
}
private Message buildInputMessage(PartnerRunningFlowContext runningFlowContext, List<BlockContent> communicationBlocks) {
return new Message(Message.Character.USER, buildInputXml(runningFlowContext, communicationBlocks));
}
private String buildInputXml(PartnerRunningFlowContext runningFlowContext, List<BlockContent> communicationBlocks) {
try {
Document document = newDocument();
Element root = document.createElement("input");
document.appendChild(root);
document.appendChild(document.importNode(runningFlowContext.encodeInputsBlock().encodeToXml(), true));
appendTextElement(document, root, "source", runningFlowContext.getSource());
for (Map.Entry<String, String> entry : runningFlowContext.getAdditionalUserInfo().entrySet()) {
appendTextElement(document, root, sanitizeTagName(entry.getKey()), entry.getValue());
}
appendSupplyBlocks(document, root, communicationBlocks);
return toXmlString(document);
} catch (Exception e) {
throw new IllegalStateException("构建 input 区段失败", e);
}
}
private boolean isStructuredUserMessage(Message message) {
if (message.getRole() == Message.Character.ASSISTANT) {
return false;
}
String content = message.getContent();
String trimmed = content.trim();
return trimmed.startsWith("<input>") || trimmed.startsWith("<context>") || trimmed.startsWith("<?xml");
}
private boolean belongsToContextSection(BlockContent blockContent) {
if (!(blockContent instanceof CommunicationBlockContent communicationBlockContent)) {
return true;
}
return communicationBlockContent.getType() == CommunicationBlockContent.Projection.CONTEXT;
}
private String formatConversationUserMessage(PartnerRunningFlowContext runningFlowContext) {
return formatMarkedHistoryMessage("[" + runningFlowContext.getSource() + "]", "", runningFlowContext.formatInputsForHistory());
}
private String formatMarkedHistoryMessage(String identityMarker, String statusMarkers, String body) {
String markerLine = statusMarkers == null || statusMarkers.isBlank()
? identityMarker
: identityMarker + ": " + statusMarkers;
if (body == null || body.isBlank()) {
return markerLine + ":";
}
return markerLine + MARKER_BODY_SEPARATOR + body.trim();
}
private Document newDocument() throws Exception {
return DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument();
}
private void appendTextElement(Document document, Element parent, String tagName, String value) {
Element element = document.createElement(tagName);
element.setTextContent(value == null ? "" : value);
parent.appendChild(element);
}
private void appendSupplyBlocks(Document document, Element inputRoot, List<BlockContent> contextBlocks) {
Map<String, List<CommunicationBlockContent>> groupedBlocks = contextBlocks.stream()
.filter(CommunicationBlockContent.class::isInstance)
.map(CommunicationBlockContent.class::cast)
.filter(block -> block.getType() == CommunicationBlockContent.Projection.SUPPLY)
.collect(Collectors.groupingBy(
block -> sanitizeTagName(block.getBlockName()),
LinkedHashMap::new,
Collectors.toList()
));
for (Map.Entry<String, List<CommunicationBlockContent>> entry : groupedBlocks.entrySet()) {
Element groupElement = document.createElement(entry.getKey());
inputRoot.appendChild(groupElement);
for (CommunicationBlockContent block : entry.getValue()) {
Element blockElement = block.encodeToXml();
groupElement.appendChild(document.importNode(blockElement, true));
}
}
}
private String sanitizeTagName(String rawTagName) {
if (rawTagName == null || rawTagName.isBlank()) {
return "meta";
}
String sanitized = rawTagName.replaceAll("[^A-Za-z0-9_.-]", "_");
if (!Character.isLetter(sanitized.charAt(0)) && sanitized.charAt(0) != '_') {
sanitized = "_" + sanitized;
}
return sanitized;
}
private String toXmlString(Document document) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(document), new StreamResult(writer));
return writer.toString();
}
@Override
public int order() {
return 5;
}
}

View File

@@ -0,0 +1,200 @@
package work.slhaf.partner.module.communication;
import kotlin.Unit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.action.entity.Schedulable;
import work.slhaf.partner.core.action.entity.StateAction;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.memory.MemoryCapability;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.core.perceive.PerceiveCapability;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
private static final String AUTO_UPDATE_CRON = "0/10 * * * * ?";
private static final long UPDATE_TRIGGER_INTERVAL = 60 * 60 * 1000;
private static final int CONTEXT_RETAIN_DIVISOR = 6;
private static final int DIALOG_ROLLING_TRIGGER_LIMIT = 36;
private final AtomicBoolean rolling = new AtomicBoolean(false);
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private MemoryCapability memoryCapability;
@InjectCapability
private PerceiveCapability perceiveCapability;
@InjectCapability
private ActionCapability actionCapability;
@InjectModule
private MessageSummarizer messageSummarizer;
@InjectModule
private MessageCompressor messageCompressor;
@InjectModule
private ActionScheduler actionScheduler;
@InjectModule
private AfterRollingRegistry afterRollingRegistry;
@Init
public void init() {
registerScheduledUpdater();
}
private void registerScheduledUpdater() {
StateAction stateAction = new StateAction(
"system",
"dialog-rolling-auto-update",
"定时检查并触发对话滚动",
Schedulable.ScheduleType.CYCLE,
AUTO_UPDATE_CRON,
new StateAction.Trigger.Call(() -> {
tryAutoRolling();
return Unit.INSTANCE;
})
);
actionScheduler.schedule(stateAction);
log.info("Dialog rolling has been registered into ActionScheduler, cron={}", AUTO_UPDATE_CRON);
}
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
if (cognitionCapability.getChatMessages().size() < DIALOG_ROLLING_TRIGGER_LIMIT) {
return;
}
actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL).execute(() -> triggerRolling(false));
}
private void tryAutoRolling() {
long currentTime = System.currentTimeMillis();
int chatCount = cognitionCapability.snapshotChatMessages().size();
if (currentTime - perceiveCapability.showLastInteract().toEpochMilli() > UPDATE_TRIGGER_INTERVAL && chatCount > 1) {
triggerRolling(true);
log.debug("Dialog rolling: auto triggered");
}
}
private void triggerRolling(boolean refreshMemoryId) {
if (!rolling.compareAndSet(false, true)) {
log.debug("Dialog rolling: rolling is already executing");
return;
}
try {
List<Message> fullChatSnapshot = cognitionCapability.snapshotChatMessages();
if (fullChatSnapshot.size() <= 1) {
return;
}
List<Message> chatIncrement = resolveChatIncrement(fullChatSnapshot);
if (chatIncrement.isEmpty()) {
if (refreshMemoryId) {
memoryCapability.refreshMemorySession();
}
return;
}
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), CONTEXT_RETAIN_DIVISOR);
applyRolling(result);
afterRollingRegistry.trigger(result);
if (refreshMemoryId) {
memoryCapability.refreshMemorySession();
}
} catch (Exception e) {
ExceptionReporterHandler.INSTANCE.report(new AgentRuntimeException("Dialog rolling failed", e));
} finally {
rolling.set(false);
}
}
List<Message> resolveChatIncrement(List<Message> fullChatSnapshot) {
String memoryId = memoryCapability.getMemorySessionId();
if (memoryId.isBlank()) {
return fullChatSnapshot;
}
MemoryUnit existingUnit = memoryCapability.getMemoryUnit(memoryId);
if (existingUnit.getConversationMessages().isEmpty()) {
return fullChatSnapshot;
}
List<Message> existingMessages = existingUnit.getConversationMessages();
int maxOverlap = Math.min(existingMessages.size(), fullChatSnapshot.size());
for (int overlap = maxOverlap; overlap > 0; overlap--) {
List<Message> existingSuffix = existingMessages.subList(existingMessages.size() - overlap, existingMessages.size());
List<Message> snapshotPrefix = fullChatSnapshot.subList(0, overlap);
if (existingSuffix.equals(snapshotPrefix)) {
return fullChatSnapshot.subList(overlap, fullChatSnapshot.size());
}
}
return fullChatSnapshot;
}
@NotNull
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
messageCompressor.execute(chatSnapshot);
Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
String summary = summaryResult.fold(
value -> value,
exp -> "no summary, due to exception"
);
if (summary.isBlank()) {
summary = "no summary, due to empty summarize result";
}
MemoryUnit memoryUnit = memoryCapability.updateMemoryUnit(chatSnapshot, summary);
MemorySlice newSlice = memoryUnit.getSlices().getLast();
return new RollingResult(memoryUnit, newSlice, List.copyOf(chatSnapshot), newSlice.getSummary(), rollingSize, retainDivisor);
}
private void applyRolling(RollingResult result) {
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildDialogAbstractBlock(result.summary(), result.memoryUnit().getId(), result.memorySlice().getId()),
Set.of(ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.COMMUNICATION),
20,
5,
10
));
cognitionCapability.rollChatMessagesWithSnapshot(result.rollingSize(), result.retainDivisor());
}
private @NotNull BlockContent buildDialogAbstractBlock(String summary, String unitId, String sliceId) {
return new BlockContent("dialog_history", "dialog_rolling") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setAttribute("related_memory_unit_id", unitId);
root.setAttribute("related_memory_slice_id", sliceId);
root.setAttribute("datetime", ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
appendTextElement(document, root, "summary", summary);
}
};
}
@Override
public int order() {
return 7;
}
}

View File

@@ -0,0 +1,166 @@
package work.slhaf.partner.module.communication
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import work.slhaf.partner.framework.agent.interaction.AgentRuntime
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent.EventStatus
import work.slhaf.partner.framework.agent.interaction.data.Reply
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer
import kotlin.time.Duration.Companion.milliseconds
object ReplyDispatcher {
private const val AGGREGATE_WINDOW_MILLIS = 100L
private const val NO_REPLY_MARKER = "NO_REPLY"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val collectorChannel = Channel<ReplyChunk>(Channel.UNLIMITED)
init {
scope.launch {
var pendingChunk: ReplyChunk? = null
while (true) {
val firstChunk = pendingChunk ?: collectorChannel.receiveCatching().getOrNull() ?: break
pendingChunk = null
val builder = StringBuilder(firstChunk.delta)
while (true) {
val nextChunk = withTimeoutOrNull(AGGREGATE_WINDOW_MILLIS.milliseconds) {
collectorChannel.receiveCatching()
} ?: break
if (nextChunk.isClosed) {
flush(builder.toString(), firstChunk.target)
return@launch
}
val chunk = nextChunk.getOrNull() ?: break
if (chunk.target == firstChunk.target) {
builder.append(chunk.delta)
} else {
pendingChunk = chunk
break
}
}
flush(builder.toString(), firstChunk.target)
}
}
}
/**
* flush 将推送至 AgentRuntime 的默认通道。
*/
private fun flush(content: String, target: String) {
if (content.isEmpty()) {
return
}
val event = Reply(
status = EventStatus.RUNNING,
target = target,
content = content,
mode = Reply.ContentMode.APPEND,
done = false
)
AgentRuntime.response(event)
}
fun createConsumer(target: String): StreamChatMessageConsumer = ReplyConsumer(
collectorChannel = collectorChannel,
target = target,
)
private data class ReplyChunk(
val delta: String,
val target: String,
)
private class ReplyConsumer(
private val collectorChannel: Channel<ReplyChunk>,
private val target: String,
) : StreamChatMessageConsumer() {
private enum class VisibilityState {
UNDECIDED,
NO_REPLY,
VISIBLE
}
private val rawResponse = StringBuilder()
private val undecidedBuffer = StringBuilder()
private var visibilityState = VisibilityState.UNDECIDED
override fun onDelta(delta: String) {
rawResponse.append(delta)
routeDelta(delta)
}
override fun collectResponse(): String {
finalizeUndecidedBuffer()
return rawResponse.toString()
}
private fun routeDelta(delta: String) {
when (visibilityState) {
VisibilityState.NO_REPLY -> return
VisibilityState.VISIBLE -> flushVisible(delta)
VisibilityState.UNDECIDED -> {
undecidedBuffer.append(delta)
resolveVisibility()
}
}
}
private fun resolveVisibility() {
val content = undecidedBuffer.toString()
if (content.length <= NO_REPLY_MARKER.length) {
if (NO_REPLY_MARKER.startsWith(content)) {
return
}
revealBufferedContent()
return
}
if (!content.startsWith(NO_REPLY_MARKER)) {
revealBufferedContent()
return
}
val suffixFirstChar = content[NO_REPLY_MARKER.length]
if (suffixFirstChar == '\n' || suffixFirstChar == '\r') {
visibilityState = VisibilityState.NO_REPLY
undecidedBuffer.setLength(0)
return
}
revealBufferedContent()
}
private fun finalizeUndecidedBuffer() {
if (visibilityState != VisibilityState.UNDECIDED) {
return
}
if (undecidedBuffer.toString() == NO_REPLY_MARKER) {
visibilityState = VisibilityState.NO_REPLY
undecidedBuffer.setLength(0)
return
}
revealBufferedContent()
}
private fun revealBufferedContent() {
visibilityState = VisibilityState.VISIBLE
if (undecidedBuffer.isNotEmpty()) {
flushVisible(undecidedBuffer.toString())
undecidedBuffer.setLength(0)
}
}
private fun flushVisible(delta: String) {
collectorChannel.trySend(ReplyChunk(delta, target)).isSuccess
}
override fun consumeDelta(delta: String?) {
}
}
}

View File

@@ -0,0 +1,17 @@
package work.slhaf.partner.module.communication;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
public record RollingResult(
MemoryUnit memoryUnit,
MemorySlice memorySlice,
List<Message> incrementMessages,
String summary,
int rollingSize,
int retainDivisor
) {
}

View File

@@ -0,0 +1,238 @@
package work.slhaf.partner.module.communication.summarizer;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
public class MessageCompressor extends AbstractAgentModule.Sub<List<Message>, Void> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责对单条消息进行压缩改写。
目标:
- 在不改变原意的前提下,压缩冗余表达,减少长度;
- 保留消息中真正有价值的信息;
- 让压缩结果仍然像原消息,而不是另一种文体的摘要。
核心要求:
- 尽量保留原消息的视角、语气、态度、情绪与表达倾向,不要无故改写成中性、客观、旁白式总结。
- 不要把第一人称改成第三人称;不要把直接表达改成“用户表示……”“其意思是……”这类转述,除非原消息本身就是这种口吻。
- 若原消息包含明显的情绪、评价、犹豫、强调、否定、推进意图、反问、吐槽等内容,压缩后应尽量保留这些信息。
- 压缩的重点是删除冗余、合并重复、收紧表达,不是改写说话风格。
格式要求:
- 允许保留原消息中已有的 markdown、标题、项目符号、编号列表、引用、代码块、代码片段等结构。
- 不要为了压缩而强行去除这些结构;若这些结构本身承载了信息层级或语义边界,应尽量保留。
- 也不要为了“更整齐”主动新增原消息没有的标题、列表或代码块。
- 原消息有结构时,优先继承其组织方式;原消息没有结构时,保持自然文本即可。
压缩策略:
- 删除明显重复、空转、口头垫话、对主旨无帮助的展开。
- 合并语义接近、重复推进的句子。
- 保留真正影响理解的事实、判断、条件、限制、结论、态度和情绪。
- 若原消息包含技术内容、代码、配置、接口、规则、步骤等,优先保留这些实质信息,不要只保留泛泛结论。
- 若原消息本身已经很短或进一步压缩会损失重要语义,则可基本保持原样。
关于日志、代码及长文本片段:
- 若原消息中包含日志、代码、配置、报错堆栈、命令输出等长片段,且内容较长、重复性强或并非全部都对理解当前消息同等重要,则可以进行截断。
- 截断时应优先保留:
- 与当前问题、判断、结论直接相关的部分;
- 首尾中能体现上下文和结果的关键部分;
- 报错、异常、返回值、状态变化、关键参数、关键命令、关键代码段。
- 不要无说明地直接删去中间内容;若发生截断,必须显式标注。
- 截断标注统一使用以下格式之一,并与原文风格保持尽量一致:
- `...[中间内容已截断]...`
- 代码或日志块内可使用:`// ...[中间内容已截断]...` 或 `# ...[中间内容已截断]...`
- 截断后的内容仍应保持可读,且不能歪曲原始含义。
- 若长片段本身就是当前消息的核心,且截断会损失关键语义,则不要截断。
禁止事项:
- 不要补充原消息没有的新信息。
- 不要替原消息做解释、分析、总结或评价。
- 不要把技术表达改写得过于口语化,也不要把口语表达改写得过于书面化。
- 不要输出“压缩后:”之类前缀,只直接输出压缩结果。
输出要求:
- 只输出压缩后的消息正文。
""";
private static final int COMPRESS_TRIGGER_LENGTH = 1200;
private static final int FALLBACK_MAX_LENGTH = 900;
private static final String FALLBACK_OMITTED_MARKER = "\n...[中间内容已裁剪]...\n";
private static final String COMPRESSED_MARKER = "[COMPRESSED]";
private static final String UNKNOWN_ROLE_MARKER = "[[Unknown]: Unknown]";
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
private static final Pattern ROLE_PREFIX_PATTERN = Pattern.compile("(\\[\\[(?:USER|AGENT)]:\\s*[^]]+])");
@InjectCapability
private ActionCapability actionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
@Override
protected Void doExecute(List<Message> chatMessages) {
List<Integer> targetIndexes = IntStream.range(0, chatMessages.size())
.filter(index -> shouldCompress(chatMessages.get(index)))
.boxed()
.toList();
CountDownLatch latch = new CountDownLatch(targetIndexes.size());
for (Integer index : targetIndexes) {
Message chatMessage = chatMessages.get(index);
ParsedMessage parsedMessage = parseMessage(chatMessage.getContent());
executor.execute(() -> {
try {
String summarized = summarizeOrFallback(parsedMessage.body());
chatMessages.set(index, new Message(chatMessage.getRole(), rebuildMessage(parsedMessage, summarized)));
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
return null;
}
private boolean shouldCompress(Message chatMessage) {
return parseMessage(chatMessage.getContent()).body().length() > COMPRESS_TRIGGER_LENGTH;
}
private String summarizeOrFallback(String content) {
String summarized = chat(List.of(new Message(Message.Character.USER, content))).fold(
res -> res,
exp -> null
);
if (isAcceptableSummary(summarized, content)) {
return summarized.trim();
}
return truncateForFallback(content);
}
private boolean isAcceptableSummary(String summarized, String originalContent) {
if (summarized == null) {
return false;
}
String normalized = summarized.trim();
return !normalized.isEmpty() && normalized.length() < originalContent.length();
}
private String truncateForFallback(String content) {
if (content == null || content.length() <= FALLBACK_MAX_LENGTH) {
return content;
}
int available = FALLBACK_MAX_LENGTH - FALLBACK_OMITTED_MARKER.length();
int headBudget = available / 2;
int tailBudget = available - headBudget;
int headEnd = adjustHeadEnd(content, headBudget);
int tailStart = adjustTailStart(content, content.length() - tailBudget);
if (tailStart <= headEnd) {
return content.substring(0, FALLBACK_MAX_LENGTH).stripTrailing();
}
return content.substring(0, headEnd).stripTrailing()
+ FALLBACK_OMITTED_MARKER
+ content.substring(tailStart).stripLeading();
}
private int adjustHeadEnd(String content, int preferredEnd) {
int safePreferredEnd = Math.clamp(preferredEnd, 0, content.length());
int windowEnd = Math.min(content.length(), safePreferredEnd + 80);
for (int i = safePreferredEnd; i < windowEnd; i++) {
if (isBoundary(content.charAt(i))) {
return i + 1;
}
}
return safePreferredEnd;
}
private int adjustTailStart(String content, int preferredStart) {
int safePreferredStart = Math.clamp(preferredStart, 0, content.length());
int windowStart = Math.max(0, safePreferredStart - 80);
for (int i = safePreferredStart; i > windowStart; i--) {
if (isBoundary(content.charAt(i - 1))) {
return i;
}
}
return safePreferredStart;
}
private boolean isBoundary(char ch) {
return ch == '\n'
|| ch == '。'
|| ch == ''
|| ch == ''
|| ch == ';'
|| ch == ''
|| ch == '.';
}
private ParsedMessage parseMessage(String content) {
String source = content == null ? "" : content;
int separatorIndex = source.indexOf(MARKER_BODY_SEPARATOR);
String markerLine = separatorIndex >= 0 ? source.substring(0, separatorIndex).trim() : "";
String remaining = separatorIndex >= 0 ? source.substring(separatorIndex + MARKER_BODY_SEPARATOR.length()).trim() : source.trim();
String rolePrefix = null;
String statusMarkers = "";
Matcher roleMatcher = ROLE_PREFIX_PATTERN.matcher(markerLine);
if (roleMatcher.find()) {
rolePrefix = roleMatcher.group(1);
statusMarkers = markerLine.substring(roleMatcher.end()).trim();
if (statusMarkers.startsWith(":")) {
statusMarkers = statusMarkers.substring(1).trim();
}
if (statusMarkers.endsWith(":")) {
statusMarkers = statusMarkers.substring(0, statusMarkers.length() - 1).trim();
}
}
return new ParsedMessage(rolePrefix, statusMarkers, remaining);
}
private String rebuildMessage(ParsedMessage parsedMessage, String compressedBody) {
return buildMarkerHeader(parsedMessage.rolePrefix(), parsedMessage.statusMarkers())
+ MARKER_BODY_SEPARATOR
+ compressedBody;
}
private String buildMarkerHeader(String rolePrefix, String statusMarkers) {
String identityMarker = rolePrefix == null || rolePrefix.isBlank() ? UNKNOWN_ROLE_MARKER : rolePrefix;
String normalizedStatusMarkers = statusMarkers == null ? "" : statusMarkers.trim();
normalizedStatusMarkers = normalizedStatusMarkers.replace(COMPRESSED_MARKER, "").trim();
normalizedStatusMarkers += COMPRESSED_MARKER;
return identityMarker + ": " + normalizedStatusMarkers;
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "single_summarizer";
}
private record ParsedMessage(String rolePrefix, String statusMarkers, String body) {
}
}

View File

@@ -0,0 +1,89 @@
package work.slhaf.partner.module.communication.summarizer;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import java.util.List;
public class MessageSummarizer extends AbstractAgentModule.Sub<List<Message>, Result<String>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责对一组已经发生过的聊天消息进行总结整理,生成一段可供后续系统使用的摘要结果。
你会收到一条结构化任务消息,其中:
- <message_tag_notes> 说明聊天消息中可能出现的标签及其含义;
- <chat_messages> 承载本次需要总结的消息列表,每条消息都带有 role 与正文内容。
你的任务:
- 基于整组消息,提炼出一段紧凑、连贯、信息完整的摘要;
- 摘要应尽量覆盖这组消息中的主要事实、结论、约束、推进情况、未决点、明显态度与情绪变化;
- 若消息中包含技术讨论、配置、代码、报错、规则、方案比较、设计判断等内容,应优先保留这些对后续理解真正有帮助的信息。
摘要视角要求:
- 摘要默认采用 AGENT 视角书写,即以“我”的立场整理这组对话,而不是使用外部旁观者口吻。
- 对于来自 [AGENT] 或 assistant 的消息,可将其理解为我的表达、我的判断、我的推进、我的反思或我的内部反馈,并以“我”来概括。
- 对于来自 [USER] 的消息,应明确保留其“用户”身份,不要模糊为无来源的陈述,也不要误写成“我”的观点。
- 不要默认把整组消息改写成“用户近期……”“系统如何……”这类第三人称阶段报告,除非原消息本身就是这种汇报视角。
- 若消息中出现 [NOT_REPLIED],表示这是一条我未直接发给用户、但保留在交流轨迹中的内部交流结果;必要时可在摘要中说明这是我内部保留的判断或反馈。
总结原则:
- 重点提炼这组消息中真正影响后续理解和推进的信息,不要平均分配篇幅。
- 合并重复表达、重复确认和多轮来回拉扯后的同类结论。
- 若消息中形成了明确结论、决定、偏好、限制条件、行动推进或阶段性判断,应优先写出。
- 若消息中仍存在未解决问题、待确认事项、分歧点或风险点,也应明确保留。
- 若消息整体只是闲聊、感叹或状态表达,也应如实概括其主要情绪和交流走向,不要硬总结出不存在的任务结论。
关于技术内容:
- 若消息中包含代码、日志、命令输出、配置片段等长内容,不要原样大段复写;
- 应概括其中真正关键的信息,例如:关键报错、关键配置、关键判断、关键修改点、关键结果。
- 只有在少量原文片段对后续理解不可替代时,才可保留必要短句。
输出要求:
- 只输出一段摘要正文,不要添加标题、前缀、说明或额外标签。
- 不要输出项目符号列表,除非原始内容极度结构化且不用列表会明显损失可读性。
- 不要输出结构之外的解释、注释或额外文本。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<String> doExecute(List<Message> messages) {
return chat(List.of(buildChatMessagesBlock(messages).encodeToMessage()));
}
private @NotNull TaskBlock buildChatMessagesBlock(List<Message> messages) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
document.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
appendListElement(document, root, "chat_messages", "message", messages, (element, message) -> {
element.setAttribute("role", message.roleValue());
element.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
};
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "multi_summarizer";
}
}

View File

@@ -0,0 +1,14 @@
package work.slhaf.partner.module.memory.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ActivationProfile {
private Float activationWeight;
private Float diffusionWeight;
private Float contextIndependenceWeight;
}

View File

@@ -0,0 +1,36 @@
package work.slhaf.partner.module.memory.runtime;
import work.slhaf.partner.core.memory.pojo.SliceRef;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
final class DateMemoryIndex {
private final Map<LocalDate, CopyOnWriteArrayList<SliceRef>> dateIndex = new HashMap<>();
void record(SliceRef sliceRef, LocalDate date) {
dateIndex.computeIfAbsent(date, key -> new CopyOnWriteArrayList<>()).addIfAbsent(sliceRef);
}
List<SliceRef> find(LocalDate date) {
List<SliceRef> refs = dateIndex.get(date);
return refs == null ? null : new ArrayList<>(refs);
}
void reset() {
dateIndex.clear();
}
void restore(LocalDate date, CopyOnWriteArrayList<SliceRef> refs) {
dateIndex.put(date, refs);
}
Map<LocalDate, CopyOnWriteArrayList<SliceRef>> entries() {
return dateIndex;
}
}

View File

@@ -0,0 +1,221 @@
package work.slhaf.partner.module.memory.runtime;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.memory.MemoryCapability;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.core.memory.pojo.SliceRef;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class MemoryRuntime extends AbstractAgentModule.Standalone implements StateSerializable {
@InjectCapability
private MemoryCapability memoryCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
private final ReentrantLock runtimeLock = new ReentrantLock();
private final TopicMemoryIndex topicIndex = new TopicMemoryIndex();
private final DateMemoryIndex dateIndex = new DateMemoryIndex();
private final TopicRecallCollector topicRecallCollector = new TopicRecallCollector(new TopicRecallScorer());
private final MemoryRuntimeStateCodec stateCodec = new MemoryRuntimeStateCodec();
@Init
public void init() {
register();
checkAndSetMemoryId();
}
private void checkAndSetMemoryId() {
if (cognitionCapability.getChatMessages().isEmpty()) {
memoryCapability.refreshMemorySession();
}
}
public void recordMemory(MemoryUnit memoryUnit,
String topicPath,
List<String> relatedTopicPaths,
ActivationProfile activationProfile) {
MemorySlice memorySlice = memoryUnit.getSlices().getLast();
SliceRef sliceRef = new SliceRef(memoryUnit.getId(), memorySlice.getId());
LocalDate date = toLocalDate(memorySlice.getTimestamp());
runtimeLock.lock();
try {
List<String> normalizedRelatedTopicPaths = topicIndex.normalizeTopicPaths(relatedTopicPaths);
dateIndex.record(sliceRef, date);
if (topicPath != null && !topicPath.isBlank()) {
topicIndex.recordBinding(
topicPath,
sliceRef,
memorySlice.getTimestamp(),
normalizedRelatedTopicPaths,
activationProfile
);
}
topicIndex.ensureTopicPaths(normalizedRelatedTopicPaths);
} finally {
runtimeLock.unlock();
}
}
public List<ActivatedMemorySlice> queryActivatedMemoryByTopicPath(String topicPath) {
return buildActivatedMemorySlices(findByTopicPath(topicPath));
}
public List<ActivatedMemorySlice> queryActivatedMemoryByDate(LocalDate date) {
return buildActivatedMemorySlices(findByDate(date));
}
public String getTopicTree() {
runtimeLock.lock();
try {
return topicIndex.getTopicTree();
} finally {
runtimeLock.unlock();
}
}
public String fixTopicPath(String topicPath) {
String[] parts = topicPath.split("->");
List<String> cleanedParts = new ArrayList<>();
for (String part : parts) {
String cleaned = part.replaceAll("\\[[^]]*]", "").trim();
if (!cleaned.isEmpty()) {
cleanedParts.add(cleaned);
}
}
return String.join("->", cleanedParts);
}
private List<SliceRef> findByTopicPath(String topicPath) {
runtimeLock.lock();
try {
TopicMemoryIndex.TopicTreeNode topicNode = topicIndex.findTopicNode(topicPath);
if (topicNode == null) {
String normalizedPath = topicIndex.normalizeTopicPath(topicPath);
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
"Unexisted topic path: " + normalizedPath,
normalizedPath,
"TOPIC"
));
return List.of();
}
return topicRecallCollector.collect(topicIndex, topicNode);
} finally {
runtimeLock.unlock();
}
}
private List<SliceRef> findByDate(LocalDate date) {
runtimeLock.lock();
try {
List<SliceRef> refs = dateIndex.find(date);
if (refs == null) {
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
"Unexisted date index: " + date,
date.toString(),
"DATE_INDEX"
));
return List.of();
}
return refs;
} finally {
runtimeLock.unlock();
}
}
private List<ActivatedMemorySlice> buildActivatedMemorySlices(List<SliceRef> refs) {
List<ActivatedMemorySlice> slices = new ArrayList<>();
for (SliceRef ref : refs) {
ActivatedMemorySlice slice = buildActivatedMemorySlice(ref);
if (slice != null) {
slices.add(slice);
}
}
return slices;
}
private ActivatedMemorySlice buildActivatedMemorySlice(SliceRef ref) {
MemoryUnit memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
Result<MemorySlice> memorySliceResult = memoryCapability.getMemorySlice(ref.getUnitId(), ref.getSliceId());
if (memoryUnit == null || memorySliceResult.exceptionOrNull() != null) {
return null;
}
MemorySlice memorySlice = memorySliceResult.getOrThrow();
List<Message> messages = sliceMessages(memoryUnit, memorySlice);
LocalDate date = toLocalDate(memorySlice.getTimestamp());
return ActivatedMemorySlice.builder()
.unitId(ref.getUnitId())
.sliceId(ref.getSliceId())
.summary(memorySlice.getSummary())
.timestamp(memorySlice.getTimestamp())
.date(date)
.messages(messages)
.build();
}
private List<Message> sliceMessages(MemoryUnit memoryUnit, MemorySlice memorySlice) {
List<Message> conversationMessages = memoryUnit.getConversationMessages();
if (conversationMessages.isEmpty()) {
return List.of();
}
int size = conversationMessages.size();
int start = Math.clamp(memorySlice.getStartIndex(), 0, size);
int end = Math.clamp(memorySlice.getEndIndex(), start, size);
if (start >= end) {
return List.of();
}
return new ArrayList<>(conversationMessages.subList(start, end));
}
private LocalDate toLocalDate(Long timestamp) {
return Instant.ofEpochMilli(timestamp)
.atZone(ZoneId.systemDefault())
.toLocalDate();
}
@Override
public @NotNull Path statePath() {
return Path.of("module", "memory", "topic_based_memory.json");
}
@Override
public void load(@NotNull JSONObject state) {
runtimeLock.lock();
try {
stateCodec.load(state, topicIndex, dateIndex);
} finally {
runtimeLock.unlock();
}
}
@Override
public @NotNull State convert() {
runtimeLock.lock();
try {
return stateCodec.convert(topicIndex, dateIndex);
} finally {
runtimeLock.unlock();
}
}
}

Some files were not shown because too many files have changed in this diff Show More