Author SHA1 Message Date
slhaf bc2c993473 docs: update README document references 2026-06-27 23:15:30 +08:00
slhaf 0e693310f8 docs(impression): design observation update pipeline 2026-06-24 18:20:46 +08:00
slhaf 82a33c3909 feat(impression): add after-rolling updater pipeline 2026-06-19 22:51:33 +08:00
slhaf 6a64ff29c4 refactor(memory): snapshot rolling results 2026-06-14 18:27:41 +08:00
slhaf 03f0e1e11f feat(impression): model update plans for impression updater 2026-06-14 16:48:55 +08:00
slhaf 0211ba9ac8 refactor(impression): expose choice of keepOldSubjectAsAlias in ImpressionCore 2026-06-14 16:42:05 +08:00
slhaf 9f9f7247f0 docs: clarify license status 2026-06-12 22:08:00 +08:00
slhaf 15c24154f8 feat(impression): expose entity identity updates
Add core-owned APIs for renaming canonical subjects and adding aliases so updater logic can request identity changes without bypassing indexes.

Synchronize bound active entity subjects after renames and keep capability test stubs aligned.
2026-06-10 14:50:13 +08:00
slhaf a23657ec0c feat(impression): support entity aliases
Separate canonical entity subject from aliases and persist alias metadata for recall.

Index aliases as subject-like search documents and cover alias recall in SimpleTextSearch tests.
2026-06-10 14:44:50 +08:00
slhaf 371b4a01d7 feat(impression): add core mutation APIs
Expose core-owned entity creation, binding, and impression update methods so updater logic can request mutations without bypassing indexes.

Add ActiveEntity snapshots for safe inspection and keep test stubs aligned with CognitionCapability.
2026-06-10 14:02:31 +08:00
slhaf 0567837dfe feat(impression): add impression recaller for projected entities 2026-06-09 11:43:28 +08:00
slhaf 6dad6fdd6f fix(impression): compare active entities by runtimeId 2026-06-09 11:32:29 +08:00
slhaf e583276938 Remove empty impression capability methods 2026-06-07 22:42:37 +08:00
slhaf 42407567b1 docs(impression): document vector fusion plan 2026-06-06 23:02:48 +08:00
slhaf e5d19f31ca doc: adjust location of design documents 2026-06-06 22:59:43 +08:00
slhaf ddf7f8da98 feat(impression): project text search hits into active entities 2026-06-06 22:57:55 +08:00
slhaf 9269d4f678 chore: update gitignore 2026-06-06 22:57:55 +08:00
slhaf 03087fb259 docs: note first encounter module design 2026-06-05 22:45:57 +08:00
slhaf b73696cc24 feat(impression): Add impression text search 2026-05-31 21:06:02 +08:00
slhaf 96817d84fe feat(impression): introduce active entity recall model 2026-05-30 23:26:08 +08:00
slhaf dd64599154 feat(impression): add structured entity evidence metadata 2026-05-30 21:34:32 +08:00
slhaf 4b638b756e fix(vector): skip upsert when vector client is unavailable 2026-05-30 21:04:22 +08:00
slhaf 23a1b7093e feat(vector): implement Impression vector upsert and sync 2026-05-28 22:41:41 +08:00
slhaf 9de46f3589 refactor(vector): add model id assigning support 2026-05-28 21:57:11 +08:00
slhaf fd8a0642b3 chore: remove meaningless warn 2026-05-28 21:05:26 +08:00
slhaf cffb369aef feat(impression): persist entity vectors 2026-05-28 20:59:20 +08:00
slhaf a929b3e0e6 feat(impression): add vector index skeleton 2026-05-27 23:27:51 +08:00
slhaf fe6895d10b feat(impression): serialize entity state 2026-05-27 23:11:41 +08:00
slhaf 8323f8ed13 feat(impression): add entity overview state 2026-05-26 21:50:23 +08:00
slhaf 0e1201253d refactor(impression): Improve impression entity snapshots 2026-05-26 21:29:09 +08:00
slhaf f3213675ff feat(impression): Add ActiveEntity to support runtime entity-discovering and block production 2026-05-23 23:42:06 +08:00
slhaf 26ef5d875d fix(impression): correct impression update behavior 2026-05-23 23:22:15 +08:00
slhaf 047d1b56fe refactor(impression): add feature attribute as index, which will work with high-confidence impressions 2026-05-18 23:05:35 +08:00
slhaf 11aae1a353 feat(impression): introduce entity relation and impression model 2026-05-18 00:01:00 +08:00
slhaf e5dcb49028 chore: update gitignore 2026-05-17 21:07:21 +08:00
slhaf 70a94d9c30 refactor(cognition): move context classes into context package 2026-05-16 21:57:29 +08:00
slhaf ef096e76b3 refactor(state): optimize StateValue building methods 2026-05-15 23:07:40 +08:00
slhaf ed743521ec chore: rename cognition core into context core 2026-05-15 16:16:44 +08:00
slhaf cb8ddfe4e2 docs: update README startup guide for PartnerCtl 2026-05-14 22:05:37 +08:00
slhaf 756c0a12ad fix(partnerctl): include zh-CN locale in native image build 2026-05-14 20:41:53 +08:00
slhaf 8a5b844a4a feat(partnerctl-init): add release download install option 2026-05-14 19:47:18 +08:00
github-actions[bot] 8d29ea4c9e chore(registry): update latest core release to release-core/0.9.0-preview 2026-05-14 09:08:43 +00:00
github-actions[bot] 4770eaf42f chore(registry): update latest buildable to buildable/0.9.0-preview 2026-05-14 09:07:33 +00:00
slhaf 8bb266a1c3 chore(versioning): bump runtime, framework, and core to 0.9.0-preview 2026-05-14 17:06:06 +08:00
slhaf 9054a9b4ad fix(onebot): correct interaction api dependency version 2026-05-13 13:04:45 +08:00
github-actions[bot] c8d5f577a1 chore: update registry index 2026-05-13 03:35:47 +00:00
slhaf 7c82c4aea5 chore: update onebot registry index version 2026-05-13 11:35:24 +08:00
slhaf 5491ad1747 refactor(versioning): decouple module release versions
- bump parent and external module parent POMs to 1.0.0
- make runtime, interaction API, ctl, and external modules use explicit versions
- centralize internal dependency versions with dedicated properties
- keep framework and core on the shared runtime 0.5.0 line
- prepare partnerctl and module releases for independent versioning
2026-05-13 11:33:34 +08:00
slhaf 1be6ed0198 chore: update idea settings 2026-05-13 10:19:55 +08:00
slhaf 01bfc3ee18 feat(partnerctl-fetch): support fetching raw data via https proxy 2026-05-13 10:16:18 +08:00
slhaf 2d45adf8c3 feat(partnerctl-module): load external modules from remote registry index 2026-05-12 23:34:35 +08:00
slhaf 707fddda79 fix(release): checkout repository before creating ctl release 2026-05-11 18:16:40 +08:00
slhaf 68589a21fc chore(release): add partnerctl release workflow 2026-05-11 15:18:15 +08:00
slhaf e8228d0cc1 fix(ci): make partnerctl native test workflow dispatchable 2026-05-11 11:33:00 +08:00
slhaf cc99e565f0 chore(ci): add partnerctl native build test workflow 2026-05-11 11:29:33 +08:00
github-actions[bot] 718271417d chore(registry): update latest core release to release-core/0.5.0 2026-05-11 03:15:42 +00:00
slhaf 04ee38726b chore(release): use release-core tag workflow 2026-05-11 11:09:10 +08:00
github-actions[bot] 4a00e65868 chore(registry): update latest core release to rel-v0.5.0 2026-05-11 03:00:38 +00:00
slhaf 6c3c08b5d5 chore: unify workflows name style 2026-05-11 10:24:29 +08:00
slhaf ef5a332f17 chore(release): add partner core release workflow 2026-05-11 10:05:43 +08:00
github-actions[bot] 9acabca40e chore(registry): update latest buildable to buildable/0.5.0 2026-05-10 15:28:36 +00:00
slhaf b654090a6e feat(git): add latest buildable update workflow 2026-05-10 23:18:07 +08:00
github-actions[bot] c9a7343b30 chore: update registry index 2026-05-10 14:27:29 +00:00
slhaf 6250b480e0 chore: add onebot adapter registry manifest 2026-05-10 22:27:13 +08:00
github-actions[bot] 63e8cc314a chore: update registry index 2026-05-10 14:22:35 +00:00
slhaf 451f83e14d fix(module-registry): resolve manifest paths from registry in index update script 2026-05-10 22:22:12 +08:00
slhaf 2b7e6718d9 fix(git): stage registry/index.json in update workflow 2026-05-10 22:10:41 +08:00
slhaf ed806c668a feat(git): add module index updating workflow 2026-05-10 22:08:47 +08:00
slhaf da381c3adf refactor(partnerctl-module): add version attribute to manifest 2026-05-10 21:40:20 +08:00
slhaf 1cbff98b36 chore(git): remove legacy gitea sync workflow 2026-05-10 21:36:12 +08:00
slhaf 95a30f147c chore: add initial module registry structure 2026-05-10 21:32:06 +08:00
slhaf 0ea8dfc7d9 fix(core): correct logback log level fallback syntax (#1) 2026-05-09 14:51:28 +08:00
slhaf 8cb876b532 feat(partnerctl): add --log-level option for run command and wire runtime log level to Partner-Core 2026-05-09 14:12:32 +08:00
slhaf d72b34acfd fix(partnerctl): align Partner-Core jar path to resources/partner-core.jar across init/control 2026-05-09 13:38:51 +08:00
slhaf 79dcda082f refactor(partnerctl-chat): optimize color of chat prompt 2026-05-09 10:14:36 +08:00
slhaf fbd1d17fc4 refactor(partnerctl): extract TerminalText styling utility and reuse it in prompt/chat rendering 2026-05-08 23:44:08 +08:00
slhaf a8e3a84db8 feat(partnerctl): implement interactive WebSocket chat client with live event rendering 2026-05-08 23:16:49 +08:00
slhaf e6e0eef161 refactor(interaction-api): extract interaction data models into new Partner-Interaction-Api module and update imports/dependencies 2026-05-08 20:32:11 +08:00
slhaf 4c042769f2 refactor(partnerctl): disable Config and Module command temporally in root command 2026-05-06 15:18:20 +08:00
slhaf 7bee14452e feat(partnerctl): add shared help option mixin across commands 2026-05-06 15:06:20 +08:00
slhaf 442397c1f5 feat(partnerctl): i18n-ize command and option descriptions via message bundles 2026-05-06 14:55:16 +08:00
slhaf 0b329aa370 docs(readme): update build/run commands for Partner-Core module and jar naming 2026-05-06 13:55:14 +08:00
slhaf ffc96bbb64 refactor(partnerctl): split control commands into dedicated classes and extract shared runtime utilities 2026-05-06 13:26:46 +08:00
slhaf 8398c14794 feat(partnerctl): add background run mode and log command with pid/log file management 2026-05-05 23:57:32 +08:00
slhaf bd4d5164d3 feat(partnerctl): add run and shutdown commands with process lifecycle handling 2026-05-05 23:39:17 +08:00
slhaf 9073f88117 feat(partnerctl-init): add i18n message bundles for init flow and source build output 2026-05-05 22:37:41 +08:00
slhaf 0710f60ed6 fix(partnerctl-init): use numeric default for heartbeat interval prompt 2026-05-05 17:33:44 +08:00
slhaf 1bf02f543e feat(partnerctl-init): add safe Partner Home selection with overwrite/cancel flow 2026-05-05 17:21:02 +08:00
slhaf 19f87c93e3 feat(partnerctl): add ANSI-styled interactive prompt output and labels 2026-05-05 16:36:59 +08:00
slhaf 828ddff2c2 chore(log): remove legacy log output 2026-05-05 01:29:12 +08:00
slhaf 57b23e0dcb feat(partnerctl-init): add finalize step 2026-05-05 00:41:25 +08:00
slhaf 11f7200e23 feat(partnerctl-init): add interactive model provider configuration 2026-05-04 23:52:13 +08:00
slhaf ee8f568006 refactor(partnerctl): move GatewayConfig into commands.data model file 2026-05-04 20:35:36 +08:00
slhaf 393dcff6df feat(partnerctl-init): add interactive gateway configuration and gateway.json generation 2026-05-04 17:48:58 +08:00
slhaf 0fdc0038a5 docs(partnerctl-support): add KDoc for source build spec and external module manifest models 2026-05-04 17:47:20 +08:00
slhaf 97bf0618f4 feat(partnerctl): add details output helper for key-value prompt sections 2026-05-04 17:08:20 +08:00
slhaf 4200df6525 feat(partnerctl-support): add external module manifest models and gateway/module loaders 2026-05-04 15:20:34 +08:00
slhaf 686dc76b55 chore(partnerctl-pom): add kotlinx serialization 2026-05-03 23:30:26 +08:00
slhaf adab925e7d refactor(partnerctl-init): extract source build/install flow into reusable support helper 2026-05-03 21:50:25 +08:00
slhaf 6c74de2126 feat(partnerctl-init): add source build install choice 2026-05-03 21:06:46 +08:00
slhaf 7fcdb1c671 refactor(partnerctl-exception): add command interrupted exception, and unify exception handling 2026-05-03 20:08:18 +08:00
slhaf 78877f14d5 feat(partnerctl-init): support initialize Partner Home 2026-05-03 19:35:43 +08:00
slhaf 729700ceb7 feat(partnerctl): add path prompt with completion and validation 2026-05-03 19:27:07 +08:00
slhaf 6f48c36f67 feat(partnerctl): add interactive single select 2026-05-03 18:45:03 +08:00
slhaf 551a2a16d9 feat(partnerctl): add command execution helpers with captured and inherited IO 2026-05-03 18:34:01 +08:00
slhaf 40af7a4de6 feat(partnerctl): add Prompt UI helper with ask/confirm/select and multi-select support 2026-05-03 17:38:56 +08:00
slhaf cbdb33fefe chore(partnerctl): add GraalVM native-image setup and arm64 build tooling 2026-05-03 15:46:26 +08:00
slhaf d2c3416f48 feat(partnerctl): add main command with init/run/module and chat demo subcommands 2026-05-03 15:44:53 +08:00
slhaf c3dc8180f1 feat(partnerctl): initialize PartnerCtl maven module with CLI dependencies 2026-05-02 23:35:39 +08:00
slhaf 1564c7d5e1 refactor(gateway): provide websocket as default agent gateway 2026-05-02 22:46:14 +08:00
slhaf 9e33f16aae fix(onebot): flush buffered replies on terminal events 2026-05-02 21:55:28 +08:00
slhaf c84d88eab7 refactor(communication): simplify reply consumer buffering 2026-05-02 21:55:11 +08:00
slhaf 692c601f17 refactor(model): expose stream chat lifecycle callbacks 2026-05-02 21:54:48 +08:00
slhaf 2a8b2aff7b test(onebot): add test class for OneBot adapter 2026-05-02 16:32:49 +08:00
slhaf afb896e6db refactor(context): support assign response channel while running flow context creating 2026-05-02 16:32:25 +08:00
slhaf 2b575df3f9 fix(agent): add url class loader to support load external modules 2026-05-02 00:36:34 +08:00
slhaf db18c818ac chore(pom): add kotlin-maven-plugin setup 2026-05-02 00:16:19 +08:00
slhaf 973875b2e7 feat(onebot): add OneBot v11 reverse websocket adapter 2026-05-02 00:02:22 +08:00
slhaf 018b524be3 refactor(gateway): remove unused abstract methods 2026-05-01 22:09:16 +08:00
slhaf 843549dadf chore(gateway): update log output in websocket gateway 2026-05-01 21:43:40 +08:00
slhaf 32f0c5c6a8 feat(pom): add Partner-Onebot-Adapter module and Maven config 2026-05-01 21:11:40 +08:00
slhaf dd31f6a20d chore(idea): update project dictionary and encodings 2026-05-01 21:11:13 +08:00
slhaf 178c5138f7 refactor(gateway): remove duplicate response channel register 2026-05-01 21:10:13 +08:00
slhaf f9cdf164f3 feat(pom): create Partner-External-Modules 2026-04-30 22:55:20 +08:00
slhaf 0c2843a889 chore(gitignore): update gitignore 2026-04-30 22:50:43 +08:00
slhaf 572bedcd18 refactor(pom): align Maven coordinates to work.slhaf.partner and lowercase artifactIds 2026-04-30 22:45:08 +08:00
slhaf 32ee6b5ed6 docs(architecture): update startup/register-chain flow for AgentBootstrap and registerContext 2026-04-30 22:24:19 +08:00
slhaf 428f133ac3 refactor(agent): introduce AgentBootstrap for startup wiring and simplify app launch 2026-04-30 22:12:04 +08:00
slhaf 7aab236221 docs(readme): update readme 2026-04-30 16:15:55 +08:00
slhaf 0d456596a8 docs(readme): update readme 2026-04-30 16:12:10 +08:00
slhaf cbd926bd41 docs(memory): add document for memory 2026-04-30 16:08:18 +08:00
slhaf 4a00e5f320 docs(action): add document for action 2026-04-29 22:27:27 +08:00
slhaf 690f258dc9 docs(context): add document for context workspace 2026-04-29 11:03:03 +08:00
slhaf d7b271bf71 docs(model): add model provider runtime/config docs and link from README 2026-04-28 23:00:23 +08:00
slhaf ff80d6bbc4 docs(config): add config center lifecycle docs and split config reference into dedicated doc 2026-04-28 22:13:12 +08:00
slhaf 3e73d574df docs(readme): update readme 2026-04-28 21:12:08 +08:00
slhaf 9ea475432f docs(architecture): add startup/register/runtime/shutdown flow docs and architecture overview 2026-04-28 21:09:33 +08:00
slhaf e66ed9e9c0 refactor(trace): move sink registration to TraceSink default methods and restrict registry APIs 2026-04-28 14:58:09 +08:00
slhaf 73f6ff2745 refactor(trace): decouple recorder from file persistence
Turn TraceRecorder into a lightweight trace event entry point and move
file persistence responsibilities into the default FileTraceSink. Trace
events are now published through TraceSinkRegistry, allowing additional
runtime observers to subscribe without parsing trace files.

Add TraceSink and TraceSinkRegistry, keep FileTraceSink registered as the
default sink, and preserve the existing active/historical/archived trace
file rotation behavior inside the file sink.

Also change TraceEvent to carry a logical key instead of a caller-provided
path, so trace storage locations are resolved internally under the traceroot. Update existing trace producers to emit logical keys such ascontext-workspace, exception, and advice targets.
2026-04-27 23:53:20 +08:00
slhaf 3eac52f4e2 refactor(action): switch builtin actions to provider-driven self-registration 2026-04-27 20:53:45 +08:00
slhaf cf61c171a5 fix(action): stabilize staged action execution metadata
Use a stage-local Phaser in ActionExecutor so each action-chain stage
waits on its own execution phase. This avoids reusing a terminated
Phaser after the first stage completes, which could cause later stages
to advance without actually waiting for their MetaActions and trigger
FINAL_CHECK prematurely.

Add stage-level descriptions to evaluated action chains and pass them
through execution/correction contexts. Stage descriptions are now aligned
with normalized action-chain order, exposed to ParamsExtractor, persisted
in action state, and included in CorrectorInput so repeated MetaActions
can be parameterized and corrected according to their concrete stage
objective.

Also fix APPEND intervention semantics to append after the requested
stage instead of overwriting or dropping stages, and include stage
descriptions in correction prompts to distinguish stage intent from
generic MetaAction capability descriptions.
2026-04-27 18:41:32 +08:00
slhaf 822ea82593 feat(action): add stage descriptions to action chains 2026-04-27 17:54:38 +08:00
slhaf f957d7caa4 fix(action): continue execution after final correction 2026-04-27 17:01:50 +08:00
slhaf 9be499eb64 refactor(action): optimize prompt of ActionExtractor 2026-04-27 17:01:38 +08:00
slhaf 184c396752 fix(action): stabilize scheduler wheel lifecycle 2026-04-27 16:25:42 +08:00
slhaf 04ceccc46f fix(action): clarify pending action intention context 2026-04-27 15:51:42 +08:00
slhaf b0185c65c8 chore(action): remove unused variable 2026-04-27 15:34:48 +08:00
slhaf d43476864c docs: update README 2026-04-27 15:07:55 +08:00
slhaf e022f134ac docs: update README and add documentation skeleton 2026-04-27 15:03:03 +08:00
slhaf 9144cd90ce refactor(project): remove Partner-Common module and move InProcessMcpTransport into Partner-Core 2026-04-26 19:45:38 +08:00
slhaf c23bda430f refactor(project): remove Partner-SandboxRunner module and clean related files 2026-04-26 18:20:07 +08:00
slhaf aa98c29744 refactor(communication): optimize prompt to avoid unexpected output in communication producer 2026-04-26 16:38:13 +08:00
slhaf eade39328a refactor(memory-selector): refactor memory recalling into async worker 2026-04-25 23:22:43 +08:00
slhaf 94adf9a368 fix(openai-provider): add cached structured-output downgrade to prompt-only JSON fallback 2026-04-25 22:12:07 +08:00
slhaf dd5ab3aaf3 fix(openai-provider): generate and inject response-type JSON shape instruction for structured chat 2026-04-25 20:23:28 +08:00
slhaf 075a8ece3e refactor(action-planner): optimize prompts and initiate-turn input 2026-04-25 19:31:51 +08:00
slhaf 4bc2e4adbf refactor(action-executor): add FINAL_CHECK fallback correction at chain end and pass check mode/result in corrector input 2026-04-25 16:56:47 +08:00
slhaf 9240ed44c4 refactor(action-evaluator): optimize prompt 2026-04-25 14:41:56 +08:00
slhaf 878fe4dc10 refactor(action-planner): simplify tendencies state hint naming and always follow up on refused candidates 2026-04-25 13:17:23 +08:00
slhaf 03bae6424e fix(openai-provider): ensure structured chat adds JSON instruction when missing 2026-04-25 00:11:57 +08:00
slhaf a14e123ff6 fix(communication): rolling out all chat messages on auto triggered and raise dialog rolling trigger length limit 2026-04-24 23:20:56 +08:00
slhaf d4277aed33 chore(config-center): downgrade skipped reload deletion log from info to
debug
2026-04-24 23:08:58 +08:00
slhaf e37dc7bc9b refactor(action-executor): emit action-finished state hint and trigger cognition turn on executable completion 2026-04-24 22:28:25 +08:00
slhaf 51706eb55e refactor(action-planner): replace pending confirm blocks with StateHintContent and streamline tendency turns 2026-04-24 22:04:02 +08:00
slhaf eb3e5f3f34 refactor(context): add StateHintContent to fit information exchange between different modules with communication producer 2026-04-23 22:01:10 +08:00
slhaf 164078bf42 refactor(action-evaluator): optimize prompt 2026-04-22 15:41:53 +08:00
slhaf 13d04f85e2 fix(action): correct behavior of resume intervention 2026-04-21 21:38:04 +08:00
slhaf 3a5d0f1cb4 refactor(action): optimize builtin actions descriptions and param explanations 2026-04-21 21:31:23 +08:00
slhaf aa05d3817b fix(action-evaluator): fix content of available-meta-actions block 2026-04-21 17:23:08 +08:00
slhaf 45f7d1c51e refactor(action): optimize resolved context domains 2026-04-21 17:21:59 +08:00
slhaf 3d56966381 fix(action): fix unexpected scheduled action losing 2026-04-21 17:12:44 +08:00
slhaf de340029c6 refactor(action): optimize prompts 2026-04-21 16:56:24 +08:00
slhaf 96101de227 refactor(trace): trace full snapshot of context workspace on changes 2026-04-21 16:48:21 +08:00
slhaf 2927cb2b6f fix(config): fix config reloading errors that caused by using wrong relative paths to load config file 2026-04-21 15:07:35 +08:00
slhaf 4af8feb178 fix(memory): enhance null-safe in memory-related modules 2026-04-21 09:18:49 +08:00
slhaf 43c2c648ac refactor(framework): support inject Standalone module into Sub module 2026-04-21 09:05:51 +08:00
slhaf a64c8f6c97 feat(exception): trace exception log in LogExceptionReporter 2026-04-21 08:49:05 +08:00
slhaf 406b7ba831 refactor(action): update watcher self-turn message for immediate action 2026-04-21 08:48:25 +08:00
slhaf 9c902e68d0 fix(dto): add NoArgsConstructor to support formatted chat 2026-04-20 23:10:39 +08:00
slhaf 4e894384bf refactor(block): optimize state action triggered block fading factor 2026-04-20 23:08:23 +08:00
slhaf a5cee79a8f fix(state): enable first-time state file save and block overwrites until explicit load 2026-04-20 22:40:39 +08:00
slhaf d1da99c44d fix(action): correct action chain fixing error 2026-04-20 22:26:31 +08:00
slhaf d90c514159 fix(communication): fix errors while import external xml node into input xml 2026-04-20 16:40:36 +08:00
slhaf ac715602a6 refactor(gateway): print warn log while receiving invalid input 2026-04-20 15:36:23 +08:00
slhaf e1a5a07e2a Merge branch 'codex-review-localrunnerclient-v2' 2026-04-20 14:48:51 +08:00
slhaf 9108d1db46 feat(gateway): support assign hostname on websocket gateway 2026-04-20 14:38:58 +08:00
slhaf 95a3f782e6 Merge branch 'codex-review-localrunnerclient-v2' 2026-04-20 14:06:03 +08:00
slhaf 6ec3bfcc9b fix(model): correct deserialization behavior of model provider config 2026-04-19 18:52:57 +08:00
slhaf b4d6be849b fix(agent): run system exit after agent launch failed 2026-04-19 18:52:10 +08:00
slhaf a9e41eb4c7 fix(config): repair unexpected exiting in method initAll 2026-04-19 18:51:23 +08:00
slhaf 7ce7461f79 refactor(exception): correct exception report print message 2026-04-19 17:26:42 +08:00
233 changed files with 14729 additions and 2138 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+137
View File
@@ -0,0 +1,137 @@
name: Release Partner Core
on:
workflow_dispatch:
inputs:
tag:
description: "Core release tag, for example release-core/0.5.0"
required: true
type: string
push:
tags:
- "release-core/*"
permissions:
contents: write
jobs:
release-core:
runs-on: ubuntu-latest
steps:
- name: Resolve release metadata
id: release
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
VERSION="${TAG#release-core/}"
VERSION="${VERSION#v}"
ENCODED_TAG="${TAG//\//%2F}"
ASSET_NAME="partner-core-${VERSION}.jar"
ASSET_URL="https://github.com/slhaf/Partner/releases/download/${ENCODED_TAG}/${ASSET_NAME}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "asset_name=${ASSET_NAME}" >> "$GITHUB_OUTPUT"
echo "asset_url=${ASSET_URL}" >> "$GITHUB_OUTPUT"
echo "Core release tag: ${TAG}"
echo "Core release version: ${VERSION}"
echo "Core release asset: ${ASSET_NAME}"
- name: Checkout release source
uses: actions/checkout@v4
with:
ref: ${{ steps.release.outputs.tag }}
- name: Set up Java 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Build Partner Core
run: |
mvn -B -DskipTests=true -pl Partner-Core -am package
- name: Prepare release artifact
shell: bash
run: |
mkdir -p dist
SOURCE_JAR="Partner-Core/target/partner-core-${{ steps.release.outputs.version }}.jar"
if [ ! -f "$SOURCE_JAR" ]; then
echo "Expected artifact not found: $SOURCE_JAR"
echo "Available Partner-Core target files:"
find Partner-Core/target -maxdepth 1 -type f -name "*.jar" -print
exit 1
fi
cp "$SOURCE_JAR" "dist/${{ steps.release.outputs.asset_name }}"
ls -lh dist
- name: Create GitHub Release
shell: bash
run: |
if gh release view "${{ steps.release.outputs.tag }}" >/dev/null 2>&1; then
echo "Release ${{ steps.release.outputs.tag }} already exists."
exit 1
fi
gh release create "${{ steps.release.outputs.tag }}" \
"dist/${{ steps.release.outputs.asset_name }}" \
--title "Partner Core ${{ steps.release.outputs.version }}" \
--notes "Partner Core ${{ steps.release.outputs.version }}"
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout master for registry update
uses: actions/checkout@v4
with:
ref: master
path: registry-worktree
- name: Update latestRelease
working-directory: registry-worktree
run: |
python3 - <<'PY'
import json
from pathlib import Path
version = "${{ steps.release.outputs.version }}"
url = "${{ steps.release.outputs.asset_url }}"
index_path = Path("registry/index.json")
index = json.loads(index_path.read_text(encoding="utf-8"))
index["partner"]["latestRelease"] = {
"url": url,
"version": version
}
index_path.write_text(
json.dumps(index, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8"
)
PY
- name: Commit registry update
working-directory: registry-worktree
run: |
if git diff --quiet registry/index.json; then
echo "No latestRelease changes."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add registry/index.json
git commit -m "chore(registry): update latest core release to ${{ steps.release.outputs.tag }}"
git push origin HEAD:master
+161
View File
@@ -0,0 +1,161 @@
name: Release PartnerCtl
on:
workflow_dispatch:
inputs:
tag:
description: "Ctl release tag, for example release-ctl/0.5.0"
required: true
type: string
push:
tags:
- "release-ctl/*"
permissions:
contents: write
jobs:
build:
name: Build ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x64
runner: ubuntu-latest
binaryPath: PartnerCtl/target/partnerctl
assetSuffix: linux-x64
- platform: linux-arm64
runner: ubuntu-24.04-arm
binaryPath: PartnerCtl/target/partnerctl
assetSuffix: linux-arm64
- platform: windows-x64
runner: windows-latest
binaryPath: PartnerCtl/target/partnerctl.exe
assetSuffix: windows-x64.exe
runs-on: ${{ matrix.runner }}
steps:
- name: Resolve release metadata
id: release
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
VERSION="${TAG#release-ctl/}"
VERSION="${VERSION#v}"
ASSET_NAME="partnerctl-${VERSION}-${{ matrix.assetSuffix }}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "asset_name=${ASSET_NAME}" >> "$GITHUB_OUTPUT"
echo "Ctl release tag: ${TAG}"
echo "Ctl release version: ${VERSION}"
echo "Ctl release asset: ${ASSET_NAME}"
- name: Checkout release source
uses: actions/checkout@v4
with:
ref: ${{ steps.release.outputs.tag }}
- name: Set up GraalVM 21
uses: graalvm/setup-graalvm@v1
with:
distribution: graalvm-community
java-version: "21"
github-token: ${{ github.token }}
- name: Build partnerctl native image
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
- name: Prepare release artifact
shell: bash
run: |
mkdir -p dist
BINARY_PATH="${{ matrix.binaryPath }}"
ASSET_PATH="dist/${{ steps.release.outputs.asset_name }}"
if [ ! -f "$BINARY_PATH" ]; then
echo "Expected native binary not found: $BINARY_PATH"
echo "Available PartnerCtl target files:"
find PartnerCtl/target -maxdepth 2 -type f -print
exit 1
fi
cp "$BINARY_PATH" "$ASSET_PATH"
if [[ "${{ matrix.platform }}" == linux-* ]]; then
chmod +x "$ASSET_PATH"
fi
ls -lh dist
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.release.outputs.asset_name }}
path: dist/${{ steps.release.outputs.asset_name }}
if-no-files-found: error
retention-days: 1
release:
name: Create GitHub Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Resolve release metadata
id: release
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
VERSION="${TAG#release-ctl/}"
VERSION="${VERSION#v}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Ctl release tag: ${TAG}"
echo "Ctl release version: ${VERSION}"
- name: Checkout release source
uses: actions/checkout@v4
with:
ref: ${{ steps.release.outputs.tag }}
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Show release artifacts
run: |
ls -lh dist
- name: Create GitHub Release
shell: bash
run: |
if gh release view "${{ steps.release.outputs.tag }}" >/dev/null 2>&1; then
echo "Release ${{ steps.release.outputs.tag }} already exists."
exit 1
fi
gh release create "${{ steps.release.outputs.tag }}" \
dist/* \
--title "PartnerCtl ${{ steps.release.outputs.version }}" \
--notes "PartnerCtl ${{ steps.release.outputs.version }}"
env:
GH_TOKEN: ${{ github.token }}
-36
View File
@@ -1,36 +0,0 @@
name: Sync from Gitea
# 1. 给 GITHUB_TOKEN 开写权限
permissions:
contents: write
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: 配置 Git 用户
run: |
git config --global user.name "Gitea Sync Bot"
git config --global user.email "slhafzjw@slhaf.work"
- name: 关闭全局 SSL 校验
run: git config --global http.sslVerify false
- name: Clone from Gitea (mirror)
run: |
git clone --mirror \
https://${{ secrets.GITEA_USER }}:${{ secrets.GITEA_TOKEN }}@${{ secrets.GITEA_URL }} \
gitea-mirror
- name: Push to GitHub
run: |
cd gitea-mirror
# 明确推到名为 "github" 的 remote
git remote add github \
https://${{ github.repository_owner }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git push --mirror github
@@ -0,0 +1,137 @@
name: Test PartnerCtl Native Build
on:
workflow_dispatch:
inputs:
platform:
description: "Target platform to test"
required: true
default: "windows-x64"
type: choice
options:
- windows-x64
- linux-x64
- linux-arm64
permissions:
contents: read
jobs:
test-linux-x64:
name: Test linux-x64
if: ${{ inputs.platform == 'linux-x64' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up GraalVM 21
uses: graalvm/setup-graalvm@v1
with:
distribution: graalvm-community
java-version: "21"
github-token: ${{ github.token }}
- name: Build partnerctl native image
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
- name: Check Linux output
shell: bash
run: |
if [ ! -f "PartnerCtl/target/partnerctl" ]; then
echo "Expected native binary not found: PartnerCtl/target/partnerctl"
echo "Available PartnerCtl target files:"
find PartnerCtl/target -maxdepth 2 -type f -print
exit 1
fi
chmod +x PartnerCtl/target/partnerctl
./PartnerCtl/target/partnerctl --help
- name: Upload test artifact
uses: actions/upload-artifact@v4
with:
name: partnerctl-linux-x64-test
path: PartnerCtl/target/partnerctl
if-no-files-found: error
retention-days: 1
test-linux-arm64:
name: Test linux-arm64
if: ${{ inputs.platform == 'linux-arm64' }}
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up GraalVM 21
uses: graalvm/setup-graalvm@v1
with:
distribution: graalvm-community
java-version: "21"
github-token: ${{ github.token }}
- name: Build partnerctl native image
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
- name: Check Linux output
shell: bash
run: |
if [ ! -f "PartnerCtl/target/partnerctl" ]; then
echo "Expected native binary not found: PartnerCtl/target/partnerctl"
echo "Available PartnerCtl target files:"
find PartnerCtl/target -maxdepth 2 -type f -print
exit 1
fi
chmod +x PartnerCtl/target/partnerctl
./PartnerCtl/target/partnerctl --help
- name: Upload test artifact
uses: actions/upload-artifact@v4
with:
name: partnerctl-linux-arm64-test
path: PartnerCtl/target/partnerctl
if-no-files-found: error
retention-days: 1
test-windows-x64:
name: Test windows-x64
if: ${{ inputs.platform == 'windows-x64' }}
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up GraalVM 21
uses: graalvm/setup-graalvm@v1
with:
distribution: graalvm-community
java-version: "21"
github-token: ${{ github.token }}
- name: Build partnerctl native image
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
- name: Check Windows output
shell: pwsh
run: |
if (!(Test-Path "PartnerCtl/target/partnerctl.exe")) {
Write-Host "Expected native binary not found: PartnerCtl/target/partnerctl.exe"
Write-Host "Available PartnerCtl target files:"
Get-ChildItem -Recurse PartnerCtl/target | Select-Object FullName
throw "partnerctl.exe not found"
}
.\PartnerCtl\target\partnerctl.exe --help
- name: Upload test artifact
uses: actions/upload-artifact@v4
with:
name: partnerctl-windows-x64-test
path: PartnerCtl/target/partnerctl.exe
if-no-files-found: error
retention-days: 1
@@ -0,0 +1,70 @@
name: Update Latest Buildable
on:
workflow_dispatch:
inputs:
ref:
description: "Buildable ref, for example buildable/v0.5.0"
required: true
type: string
push:
tags:
- "buildable/*"
permissions:
contents: write
jobs:
update-latest-buildable:
runs-on: ubuntu-latest
steps:
- name: Resolve buildable ref
id: buildable
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "ref=${{ inputs.ref }}" >> "$GITHUB_OUTPUT"
else
echo "ref=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi
- name: Checkout master
uses: actions/checkout@v4
with:
ref: master
- name: Update latestBuildable
run: |
python3 - <<'PY'
import json
from pathlib import Path
ref = "${{ steps.buildable.outputs.ref }}"
index_path = Path("registry/index.json")
index = json.loads(index_path.read_text(encoding="utf-8"))
index["partner"]["latestBuildable"] = {
"url": "https://github.com/slhaf/Partner.git",
"ref": ref
}
index_path.write_text(
json.dumps(index, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8"
)
PY
- name: Commit registry update
run: |
if git diff --quiet registry/index.json; then
echo "No latestBuildable changes."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add registry/index.json
git commit -m "chore(registry): update latest buildable to ${{ steps.buildable.outputs.ref }}"
git push
+38
View File
@@ -0,0 +1,38 @@
name: Update Module Index
on:
push:
branches:
- master
paths:
- "registry/modules/*.json"
- "registry/index.json"
- "scripts/update_module_index.py"
- "update-module-index.yml"
permissions:
contents: write
jobs:
update-registry-index:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Update module index
run: python3 scripts/update_module_index.py
- name: Commit updated index
run: |
if git diff --quiet registry/index.json; then
echo "No module index changes"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add registry/index.json
git commit -m "chore: update registry index"
git push
+5 -6
View File
@@ -4,12 +4,7 @@ target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/db-forest-config.xml
.idea/markdown.xml
.idea/
*.iws
*.iml
*.ipr
@@ -63,3 +58,7 @@ build/
/Partner-Core/data/
/.ai/mcp/mcp.json
/.codex
# Maven / build outputs
dependency-reduced-pom.xml
/.backup/
+3
View File
@@ -1,7 +1,10 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>Onebot</w>
<w>onebot</w>
<w>openai</w>
<w>partnerctl</w>
<w>zuper</w>
</words>
</dictionary>
+8 -5
View File
@@ -1,17 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<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-External-Modules/Partner-Onebot-Adapter/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-External-Modules/Partner-Onebot-Adapter/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-External-Modules/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-External-Modules/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$/Partner-Interaction-Api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/Partner-Interaction-Api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/PartnerCtl/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/PartnerCtl/src/main/resources" 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" />
</component>
+23 -18
View File
@@ -1,26 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="19">
<list size="22">
<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" />
<item index="2" class="java.lang.String" itemvalue="picocli.CommandLine.Command" />
<item index="3" class="java.lang.String" itemvalue="picocli.CommandLine.Mixin" />
<item index="4" class="java.lang.String" itemvalue="picocli.CommandLine.Option" />
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
<item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
<item index="20" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
<item index="21" 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" />
@@ -40,6 +43,8 @@
</option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/Partner-Common/pom.xml" />
<option value="$PROJECT_DIR$/Partner-SandboxRunner/pom.xml" />
<option value="$PROJECT_DIR$/Partner-Test-Demo/pom.xml" />
</set>
</option>
-28
View File
@@ -1,28 +0,0 @@
<?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>
+77 -42
View File
@@ -1,44 +1,79 @@
<?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 xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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.partner</groupId>
<version>0.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>partner-core</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>
<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>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<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>
+13 -13
View File
@@ -4,12 +4,13 @@
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>
<groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>Partner-Core</artifactId>
<artifactId>partner-core</artifactId>
<version>0.9.0-preview</version>
<dependencies>
<dependency>
@@ -18,9 +19,9 @@
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>work.slhaf</groupId>
<artifactId>Partner-Framework</artifactId>
<version>0.5.0</version>
<groupId>work.slhaf.partner</groupId>
<artifactId>partner-framework</artifactId>
<version>${partner.runtime.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api -->
<dependency>
@@ -44,12 +45,6 @@
<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>
@@ -70,6 +65,11 @@
<artifactId>cron-utils</artifactId>
<version>9.2.1</version>
</dependency>
<dependency>
<groupId>com.huaban</groupId>
<artifactId>jieba-analysis</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>
<properties>
@@ -1,14 +1,12 @@
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();
boolean launched = Agent.newAgent(Main.class).launch();
if (!launched) {
System.exit(1);
}
}
}
@@ -12,6 +12,7 @@ public abstract class VectorClient {
public static boolean status = false;
public static VectorClient INSTANCE;
public static String VECTOR_MODEL_ID;
public static void startClient(VectorConfig config) {
try {
@@ -23,6 +24,7 @@ public abstract class VectorClient {
return;
}
status = true;
VECTOR_MODEL_ID = config.modelId;
} catch (VectorClientStartupException e) {
throw e;
} catch (VectorClientExecutionException e) {
@@ -39,7 +39,7 @@ public class VectorClientRegistry implements Configurable, ConfigRegistration<Ve
@Nullable
@Override
public VectorConfig defaultConfig() {
return new VectorConfig(false, null);
return new VectorConfig(false, null, null);
}
@Override
@@ -5,10 +5,16 @@ 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;
final String modelId;
public VectorConfig(boolean enabled, Type type) {
public VectorConfig(boolean enabled, Type type, String modelId) {
this.enabled = enabled;
this.type = type;
this.modelId = modelId;
}
protected static String fallbackModelId(String modelId, String fallback) {
return modelId == null || modelId.isBlank() ? fallback : modelId;
}
public enum Type {
@@ -21,8 +27,8 @@ public sealed class VectorConfig extends Config permits VectorConfig.Ollama, Vec
final String tokenizerPath;
final String embeddingModelPath;
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath) {
super(enabled, type);
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath, String modelId) {
super(enabled, type, fallbackModelId(modelId, embeddingModelPath));
this.tokenizerPath = tokenizerPath;
this.embeddingModelPath = embeddingModelPath;
}
@@ -33,12 +39,10 @@ public sealed class VectorConfig extends Config permits VectorConfig.Ollama, Vec
final String ollamaEmbeddingUrl;
final String ollamaEmbeddingModel;
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel) {
super(enabled, type);
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel, String modelId) {
super(enabled, type, fallbackModelId(modelId, ollamaEmbeddingModel));
this.ollamaEmbeddingUrl = ollamaEmbeddingUrl;
this.ollamaEmbeddingModel = ollamaEmbeddingModel;
}
}
}
}
@@ -229,13 +229,20 @@ public class ActionCore implements StateSerializable {
}
/**
* 在未进入执行阶段的行动单元组新增新的行动
* 在指定阶段之后追加新的行动单元组
*/
private void handleAppend(ExecutableAction executableAction, int order, List<MetaAction> actions) {
if (order <= executableAction.getExecutingStage())
return;
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
int targetOrder = nextAvailableOrder(actionChain, order + 1);
actionChain.put(targetOrder, actions);
}
executableAction.getActionChain().put(order, actions);
private int nextAvailableOrder(Map<Integer, List<MetaAction>> actionChain, int startOrder) {
int order = startOrder;
while (actionChain.containsKey(order)) {
order++;
}
return order;
}
/**
@@ -50,66 +50,70 @@ final class ActionPoolStateCodec {
}
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()));
Map<String, Object> actionMap = new LinkedHashMap<>();
actionMap.put("kind", action instanceof SchedulableExecutableAction ? "schedulable" : "immediate");
actionMap.put("uuid", action.getUuid());
actionMap.put("source", action.getSource());
actionMap.put("reason", action.getReason());
actionMap.put("description", action.getDescription());
actionMap.put("status", action.getStatus().name());
actionMap.put("tendency", action.getTendency());
actionMap.put("executing_stage", action.getExecutingStage());
String result = resolveExecutableResult(action);
if (result != null) {
actionMap.put("result", StateValue.str(result));
actionMap.put("result", 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)));
actionMap.put("schedule_type", schedulableAction.getScheduleType().name());
actionMap.put("schedule_content", schedulableAction.getScheduleContent());
actionMap.put("enabled", schedulableAction.getEnabled());
actionMap.put("schedule_histories", encodeScheduleHistories(schedulableAction));
}
List<StateValue> chainStates = action.getActionChain().entrySet().stream()
List<StateValue.Obj> 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()));
.map(entry -> {
Map<String, Object> stageMap = new LinkedHashMap<>();
stageMap.put("stage", entry.getKey());
String stageDescription = action.getStageDescriptions().get(entry.getKey());
if (stageDescription != null && !stageDescription.isBlank()) {
stageMap.put("description", stageDescription);
}
stageMap.put("actions", entry.getValue().stream()
.map(ActionPoolStateCodec::encodeMetaAction)
.toList());
return StateValue.obj(stageMap);
}).toList();
actionMap.put("action_chain", StateValue.arr(chainStates));
actionMap.put("action_chain", chainStates);
actionMap.put("history", StateValue.arr(encodeHistoryStages(action.getHistory())));
actionMap.put("history", 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()));
Map<String, Object> metaMap = new LinkedHashMap<>();
metaMap.put("name", metaAction.getName());
metaMap.put("io", metaAction.getIo());
if (metaAction.getLauncher() != null) {
metaMap.put("launcher", StateValue.str(metaAction.getLauncher()));
metaMap.put("launcher", 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()));
metaMap.put("type", metaAction.getType().name());
metaMap.put("location", metaAction.getLocation());
metaMap.put("params_json", JSONObject.toJSONString(metaAction.getParams()));
metaMap.put("result_status", metaAction.getResult().getStatus().name());
if (metaAction.getResult().getData() != null) {
metaMap.put("result_data", StateValue.str(metaAction.getResult().getData()));
metaMap.put("result_data", 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()));
Map<String, Object> historyMap = new LinkedHashMap<>();
historyMap.put("action_key", historyAction.actionKey());
historyMap.put("description", historyAction.description());
historyMap.put("result", historyAction.result());
return StateValue.obj(historyMap);
}
@@ -126,7 +130,11 @@ final class ActionPoolStateCodec {
return null;
}
Map<Integer, List<MetaAction>> restoredChain = decodeActionChain(actionObject.getJSONArray("action_chain"));
Map<Integer, String> restoredStageDescriptions = new LinkedHashMap<>();
Map<Integer, List<MetaAction>> restoredChain = decodeActionChain(
actionObject.getJSONArray("action_chain"),
restoredStageDescriptions
);
ExecutableAction executableAction;
if ("schedulable".equals(kind)) {
String scheduleType = actionObject.getString("schedule_type");
@@ -173,11 +181,15 @@ final class ActionPoolStateCodec {
if (result != null) {
executableAction.setResult(result);
}
executableAction.getStageDescriptions().putAll(restoredStageDescriptions);
executableAction.getHistory().putAll(decodeHistory(actionObject.getJSONArray("history")));
return executableAction;
}
private static Map<Integer, List<MetaAction>> decodeActionChain(@Nullable JSONArray actionChainArray) {
private static Map<Integer, List<MetaAction>> decodeActionChain(
@Nullable JSONArray actionChainArray,
Map<Integer, String> stageDescriptions
) {
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
if (actionChainArray == null) {
return toMutableActionChain(restored);
@@ -188,10 +200,14 @@ final class ActionPoolStateCodec {
continue;
}
Integer stage = stageObject.getInteger("stage");
String description = stageObject.getString("description");
JSONArray actions = stageObject.getJSONArray("actions");
if (stage == null || actions == null) {
continue;
}
if (description != null && !description.isBlank()) {
stageDescriptions.put(stage, description);
}
List<MetaAction> metaActions = new ArrayList<>();
for (int j = 0; j < actions.size(); j++) {
JSONObject actionObject = actions.getJSONObject(j);
@@ -272,26 +288,26 @@ final class ActionPoolStateCodec {
return restored;
}
private static List<StateValue> encodeHistoryStages(Map<Integer, ? extends List<HistoryAction>> historyMap) {
private static List<StateValue.Obj> 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()));
.map(entry -> {
Map<String, Object> stageMap = new LinkedHashMap<>();
stageMap.put("stage", entry.getKey());
stageMap.put("actions", entry.getValue().stream()
.map(ActionPoolStateCodec::encodeHistoryAction)
.toList());
return StateValue.obj(stageMap);
}).toList();
}
private static List<StateValue> encodeScheduleHistories(SchedulableExecutableAction schedulableAction) {
private static List<StateValue.Obj> 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())));
.map(scheduleHistory -> {
Map<String, Object> historyMap = new LinkedHashMap<>();
historyMap.put("end_time", scheduleHistory.getEndTime().toString());
historyMap.put("result", scheduleHistory.getResult());
historyMap.put("history", encodeHistoryStages(scheduleHistory.getHistory()));
return StateValue.obj(historyMap);
})
.toList();
@@ -96,6 +96,11 @@ sealed class ExecutableAction(
*/
abstract val actionChain: MutableMap<Int, MutableList<MetaAction>>
/**
* 行动阶段描述,用于为每个 stage 提供可落地的执行目标说明
*/
val stageDescriptions: MutableMap<Int, String> = mutableMapOf()
/**
* 行动阶段(当前阶段)
*/
@@ -143,6 +148,7 @@ sealed class ExecutableAction(
status = status,
tendency = tendency,
actionChainSize = actionChain.size,
stageDescriptions = stageDescriptions.toMap(),
executingStage = executingStage,
result = result,
history = history.mapValues { (_, value) -> value.toList() },
@@ -268,6 +274,7 @@ data class ExecutableActionSnapshot(
val tendency: String,
val actionChainSize: Int,
val stageDescriptions: Map<Int, String>,
val executingStage: Int,
val result: String?,
val history: Map<Int, List<HistoryAction>>,
@@ -11,7 +11,6 @@ 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;
@@ -1,4 +1,4 @@
package work.slhaf.partner.common.mcp;
package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
@@ -12,7 +12,6 @@ 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;
@@ -1,7 +1,5 @@
package work.slhaf.partner.core.action.runner.mcp;
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
import java.util.List;
import java.util.Map;
@@ -1,10 +1,15 @@
package work.slhaf.partner.core.cognition;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
import work.slhaf.partner.core.cognition.impression.ActiveEntity;
import work.slhaf.partner.core.cognition.impression.Entity;
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.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
@Capability("cognition")
@@ -26,4 +31,60 @@ public interface CognitionCapability {
Lock getMessageLock();
/**
* Project user input onto known or currently active entities and append the input as runtime evidence.
*/
Set<ActiveEntity> projectEntity(String input);
/**
* Return current active entities with their bound known entities when available.
* ActiveEntity values are snapshots; Entity values are live known-entity references and should be updated through this capability.
*/
Map<ActiveEntity, Entity> showEntities();
/**
* Create and register a new known entity by subject, then refresh search indexes for it.
*/
String createEntity(String subject);
/**
* Return a known entity by uuid, or null when it does not exist.
*/
Entity getEntity(String uuid);
/**
* Activate a known entity into the runtime context and return a detached active-entity snapshot.
*/
ActiveEntity activateKnownEntity(String entityUuid);
/**
* Bind an active runtime entity to a known entity and refresh the active-entity search document.
*/
boolean bindActiveEntity(String runtimeId, String entityUuid);
/**
* Rename the canonical subject of a known entity and refresh entity/active-entity indexes.
*/
boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias);
/**
* Add an alias or mention form for a known entity and refresh entity indexes.
*/
boolean addEntityAlias(String entityUuid, String alias, boolean deprecated);
/**
* Add or replace an impression on a known entity and refresh all entity indexes.
*/
boolean updateEntityImpression(String entityUuid, String impression, String newImpression, double confidence);
/**
* Add or replace a stable feature on a known entity and refresh all entity indexes.
*/
boolean updateEntityFeature(String entityUuid, String feature, String newFeature, double confidence);
/**
* Add or update a relation from one known entity to another target and refresh all entity indexes.
*/
boolean updateEntityRelation(String entityUuid, String target, String relation, double strength);
}
@@ -1,4 +1,4 @@
package work.slhaf.partner.core.cognition;
package work.slhaf.partner.core.cognition.context;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
@@ -27,7 +27,7 @@ import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@CapabilityCore(value = "cognition")
public class CognitionCore implements StateSerializable {
public class ContextCore implements StateSerializable {
private static final String RECENT_CHAT_MESSAGE_NOTES = """
消息格式:
@@ -58,7 +58,7 @@ public class CognitionCore implements StateSerializable {
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
public CognitionCore() {
public ContextCore() {
register();
}
@@ -127,7 +127,7 @@ public class CognitionCore implements StateSerializable {
new BlockContent("recent_chat_messages", "communication_producer") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
document.appendChild(document.importNode(messageNotesElement(), true));
root.appendChild(document.importNode(messageNotesElement(), true));
Element chatMessagesElement = document.createElement("chat_messages");
root.appendChild(chatMessagesElement);
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
@@ -200,13 +200,12 @@ public class CognitionCore implements StateSerializable {
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();
List<StateValue.Obj> convertedMessageList = chatMessages.stream()
.map(message -> StateValue.obj(Map.of(
"role", message.roleValue(),
"content", message.getContent()
)))
.toList();
state.append("chat_messages", StateValue.arr(convertedMessageList));
return state;
@@ -1,13 +1,11 @@
package work.slhaf.partner.core.cognition
package work.slhaf.partner.core.cognition.context
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.*
@@ -18,11 +16,6 @@ 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()
@@ -103,7 +96,7 @@ class ContextWorkspace {
* @param contextBlock 注册的新上下文块
*/
fun register(contextBlock: ContextBlock) = lock.write {
val removedBlocks = mutableListOf<ContextBlock>()
val changedSourceKeys = linkedSetOf<ContextBlock.SourceKey>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val currentBlock = iterator.next()
@@ -113,44 +106,51 @@ class ContextWorkspace {
if (currentBlock.applyReplaceFade() <= 0.0) {
iterator.remove()
removedBlocks.add(currentBlock)
changedSourceKeys.add(currentBlock.sourceKey)
}
}
stateSet += contextBlock
recordRegister(contextBlock, removedBlocks)
changedSourceKeys.add(contextBlock.sourceKey)
record("register", changedSourceKeys)
}
fun expire(blockName: String, source: String) = lock.write {
val sourceKey = ContextBlock.SourceKey(blockName, source)
val removedBlocks = mutableListOf<ContextBlock>()
val changedSourceKeys = linkedSetOf<ContextBlock.SourceKey>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val block = iterator.next()
if (block.sourceKey == sourceKey) {
iterator.remove()
removedBlocks.add(block)
changedSourceKeys.add(block.sourceKey)
}
}
if (removedBlocks.isNotEmpty()) {
recordExpire(sourceKey, removedBlocks)
if (changedSourceKeys.isNotEmpty()) {
record("expire", changedSourceKeys)
}
}
private fun recordRegister(addedBlock: ContextBlock, removedBlocks: List<ContextBlock>) {
private fun record(action: String, changedSourceKeys: Set<ContextBlock.SourceKey>) {
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))
payload["action"] = action
payload["changedSourceKeys"] = changedSourceKeys
.sortedWith(
compareBy<ContextBlock.SourceKey> { it.blockName }
.thenBy { it.source }
)
.map { sourceKey ->
JSONObject.of(
"blockName", sourceKey.blockName,
"source", sourceKey.source
)
}
payload["blocks"] = stateSet
.sortedWith(
compareBy<ContextBlock> { it.sourceKey.blockName }
.thenBy { it.sourceKey.source }
)
.map(::blockSnapshot)
TraceRecorder.record(TraceEvent("context-workspace", payload))
}
private fun blockSnapshot(block: ContextBlock): JSONObject {
@@ -176,7 +176,7 @@ private data class ResolvedContextBlock(
data class ContextBlock @JvmOverloads constructor(
val blockContent: BlockContent,
val compactBlock: BlockContent = blockContent,
val abstractBlock: BlockContent = blockContent,
val abstractBlock: BlockContent = compactBlock,
/**
* 该 block 集中在哪些域
*/
@@ -1,4 +1,4 @@
package work.slhaf.partner.core.cognition
package work.slhaf.partner.core.cognition.context
import org.w3c.dom.Document
import work.slhaf.partner.framework.agent.model.pojo.Message
@@ -0,0 +1,145 @@
package work.slhaf.partner.core.cognition.impression
import org.w3c.dom.Document
import org.w3c.dom.Element
import work.slhaf.partner.core.cognition.context.BlockContent
import java.time.Instant
import java.time.ZoneId
import java.util.*
import java.util.concurrent.atomic.AtomicReference
class ActiveEntity @JvmOverloads constructor(
val runtimeId: String = newActiveEntityRuntimeId(),
val createdAt: Instant = Instant.now(),
boundEntityUuid: String? = null,
private val _evidences: MutableList<EntityEvidence> = mutableListOf(),
) : BlockContent("active_entity_$runtimeId", "impression") {
val evidences: List<EntityEvidence>
get() = synchronized(_evidences) { _evidences.toList() }
@Volatile
var lastMentionedAt: Instant = createdAt
private set
private val _subject = AtomicReference("UNKNOWN")
val subject: String get() = _subject.get()
private val _boundEntityUuid = AtomicReference<String?>(boundEntityUuid)
val boundEntityUuid: String? get() = _boundEntityUuid.get()
private val _projectedFeatures: MutableMap<String, Double> = mutableMapOf()
val projectedFeatures: Map<String, Double>
get() = synchronized(_projectedFeatures) { _projectedFeatures.toMap() }
private val _projectedImpressions: MutableMap<String, Double> = mutableMapOf()
val projectedImpressions: Map<String, Double>
get() = synchronized(_projectedImpressions) { _projectedImpressions.toMap() }
@JvmOverloads
fun addEvidence(
content: String,
associationConfidence: Double = 1.0,
source: EntityEvidence.Source = EntityEvidence.Source.USER_INPUT,
timestamp: Long = System.currentTimeMillis(),
) = addEvidence(EntityEvidence(content, associationConfidence, source, timestamp))
fun addEvidence(evidence: EntityEvidence) = synchronized(_evidences) {
_evidences.add(evidence)
touch(Instant.ofEpochMilli(evidence.timestamp))
}
fun updateSubject(subject: String) = _subject.set(subject)
fun bindEntity(uuid: String?) = _boundEntityUuid.set(uuid)
fun touch(time: Instant = Instant.now()) {
lastMentionedAt = time
}
fun addProjectedFeatures(vararg features: Pair<String, Double>) = synchronized(_projectedFeatures) {
features.forEach { _projectedFeatures[it.first] = it.second }
}
fun addProjectedImpressions(vararg impressions: Pair<String, Double>) = synchronized(_projectedImpressions) {
impressions.forEach { _projectedImpressions[it.first] = it.second }
}
/**
* Creates a detached runtime snapshot for external inspection without exposing mutable internal collections.
*/
fun snapshot(): ActiveEntity {
val copied = ActiveEntity(
runtimeId = runtimeId,
createdAt = createdAt,
boundEntityUuid = boundEntityUuid,
_evidences = synchronized(_evidences) { _evidences.toMutableList() },
)
copied.updateSubject(subject)
copied.touch(lastMentionedAt)
copied.addProjectedFeatures(*projectedFeatures.entries.map { it.key to it.value }.toTypedArray())
copied.addProjectedImpressions(*projectedImpressions.entries.map { it.key to it.value }.toTypedArray())
return copied
}
override fun fillXml(document: Document, root: Element) {
root.setAttribute("runtime_id", runtimeId)
boundEntityUuid?.let { root.setAttribute("bound_entity_uuid", it) }
root.setAttribute("created_at", modelTime(createdAt))
root.setAttribute("last_mentioned_at", modelTime(lastMentionedAt))
appendTextElement(document, root, "subject", subject)
appendListElement(
document,
root,
"evidences",
"evidence",
synchronized(_evidences) { _evidences.toList() }
) { evidence ->
setAttribute("association_confidence", evidence.associationConfidence.toString())
setAttribute("source", evidence.source.name)
setAttribute("timestamp", evidence.timestamp.toString())
setAttribute("truncated", evidence.isContentTruncated().toString())
setAttribute("original_length", evidence.content.length.toString())
textContent = evidence.contentForContext()
}
appendListElement(
document,
root,
"projected_features",
"feature",
synchronized(_projectedFeatures) { _projectedFeatures.entries.toList() }
) { entry ->
setAttribute("confidence", entry.value.toString())
textContent = entry.key
}
appendListElement(
document,
root,
"projected_impressions",
"impression",
synchronized(_projectedImpressions) { _projectedImpressions.entries.toList() }
) { entry ->
setAttribute("confidence", entry.value.toString())
textContent = entry.key
}
}
private fun modelTime(time: Instant): String =
time.atZone(ZoneId.systemDefault()).toString()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ActiveEntity) return false
return runtimeId == other.runtimeId
}
override fun hashCode(): Int {
return runtimeId.hashCode()
}
}
private fun newActiveEntityRuntimeId(): String =
UUID.randomUUID().toString().replace("-", "").take(12)
@@ -0,0 +1,399 @@
package work.slhaf.partner.core.cognition.impression
import com.alibaba.fastjson2.JSONArray
import com.alibaba.fastjson2.JSONObject
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.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class Entity @JvmOverloads constructor(
val uuid: String = UUID.randomUUID().toString(),
subject: String,
private val relations: MutableMap<String, MutableMap<String, Double>> = mutableMapOf(),
private val impressions: MutableMap<String, IndexableData> = mutableMapOf(),
private val features: MutableMap<String, IndexableData> = mutableMapOf(),
private val aliases: MutableMap<String, AliasMetadata> = mutableMapOf()
) : StateSerializable {
private var _subject: String = normalizeIdentityText(subject)
private val impressionLock = ReentrantLock()
private val relationLock = ReentrantLock()
private val featureLock = ReentrantLock()
private val identityLock = ReentrantLock()
val subject: String
get() = identityLock.withLock { _subject }
@JvmOverloads
fun renameSubject(newSubject: String, keepOldSubjectAsAlias: Boolean = true): Boolean = identityLock.withLock {
val normalizedSubject = normalizeIdentityText(newSubject)
if (normalizedSubject.isBlank() || normalizedSubject == _subject) {
return@withLock false
}
val previousSubject = _subject
if (keepOldSubjectAsAlias && previousSubject.isNotBlank()) {
aliases[previousSubject] = aliases[previousSubject]?.copy(deprecated = true)
?: AliasMetadata(Instant.now(), deprecated = true)
}
aliases.remove(normalizedSubject)
_subject = normalizedSubject
true
}
@JvmOverloads
fun addAlias(alias: String, deprecated: Boolean = false): Boolean = identityLock.withLock {
val normalizedAlias = normalizeIdentityText(alias)
if (normalizedAlias.isBlank() || normalizedAlias == _subject) {
return@withLock false
}
aliases[normalizedAlias] = aliases[normalizedAlias]?.copy(deprecated = deprecated)
?: AliasMetadata(Instant.now(), deprecated)
true
}
@JvmOverloads
fun showAliases(includeDeprecated: Boolean = false): Set<AliasView> = identityLock.withLock {
aliases.asSequence()
.filter { (_, metadata) -> includeDeprecated || !metadata.deprecated }
.map { (alias, metadata) ->
AliasView(alias, metadata.instant, metadata.deprecated)
}
.sortedWith(compareBy<AliasView> { it.createdAt }.thenBy { it.alias })
.toCollection(LinkedHashSet())
}
fun snapshotAliases(): Map<String, AliasMetadata> = identityLock.withLock {
aliases.mapValues { (_, metadata) -> metadata.copy() }
}
@JvmOverloads
fun updateRelation(
target: String,
relation: String,
strength: Double = 1.0
) = relationLock.withLock {
relations.computeIfAbsent(target) { mutableMapOf() }[relation] = strength
}
@JvmOverloads
fun updateImpression(
impression: String,
newImpression: String? = null,
confidence: Double = 1.0
): IndexableData = impressionLock.withLock {
if (newImpression == null) {
impressions.computeIfAbsent(impression) { IndexableData(confidence) }
.also {
it.confidence = confidence
if (it.confidence >= 0.9) {
featureLock.withLock { features[impression] = it }
}
}
} else {
impressions.remove(impression)
IndexableData(confidence).also {
impressions[newImpression] = it
if (it.confidence >= 0.9) {
featureLock.withLock { features[newImpression] = it }
}
}
}
}
fun updateFeature(feature: String, newFeature: String? = null, confidence: Double = 1.0) = featureLock.withLock {
if (newFeature == null) {
features.computeIfAbsent(feature) { IndexableData(confidence) }
.also { it.confidence = confidence }
} else {
features.remove(feature)
IndexableData(confidence).also {
features[newFeature] = it
}
}
}
fun removeFeature(feature: String) = featureLock.withLock {
features.remove(feature)
}
fun removeImpression(impression: String) = impressionLock.withLock {
impressions.remove(impression)
}
@JvmOverloads
fun removeRelation(
target: String,
relation: String? = null
) = relationLock.withLock {
if (relation == null) {
relations.remove(target)
} else {
relations[target]?.remove(relation)
if (relations[target].isNullOrEmpty()) {
relations.remove(target)
}
}
}
fun showRelations(): Set<RelationView> = relationLock.withLock {
relations.map {
RelationView(
it.key,
it.value.toMap()
)
}.toSet()
}
fun showImpressions(embeddingModel: String): Set<ImpressionView> = impressionLock.withLock {
impressions.map {
ImpressionView(
it.key,
it.value.confidence,
it.value.getVector(embeddingModel)
)
}.toSet()
}
fun showFeatures(): Set<FeatureView> = featureLock.withLock {
features.map {
FeatureView(
it.key,
it.value.confidence
)
}.toSet()
}
fun snapshotImpressions(): Map<String, IndexableData> = impressionLock.withLock {
impressions.toMap()
}
fun snapshotFeatures(): Map<String, IndexableData> = featureLock.withLock {
features.toMap()
}
override fun statePath(): Path = Path.of("core", "impression", "entity-$uuid.json")
override fun load(state: JSONObject) {
state.getJSONObject("relations")?.let { loadedRelations ->
relationLock.withLock {
relations.clear()
loadedRelations.forEach { (target, relationValue) ->
val relationObject = relationValue as? JSONObject ?: return@forEach
val relationMap = mutableMapOf<String, Double>()
relationObject.forEach { (relation, strengthValue) ->
doubleValue(strengthValue)?.let { relationMap[relation] = it }
}
if (relationMap.isNotEmpty()) {
relations[target] = relationMap
}
}
}
}
state.getJSONObject("impressions")?.let { loadedImpressions ->
impressionLock.withLock {
impressions.clear()
impressions.putAll(loadIndexableDataMap(loadedImpressions))
}
}
identityLock.withLock {
state.getString("subject")
?.let(::normalizeIdentityText)
?.takeIf(String::isNotBlank)
?.let { _subject = it }
}
state.getJSONObject("features")?.let { loadedFeatures ->
featureLock.withLock {
features.clear()
features.putAll(loadIndexableDataMap(loadedFeatures))
}
}
state.getJSONObject("aliases")?.let { loadedAliases ->
identityLock.withLock {
aliases.clear()
loadedAliases.forEach { (alias, metadataValue) ->
val normalizedAlias = normalizeIdentityText(alias)
if (normalizedAlias.isBlank() || normalizedAlias == _subject) {
return@forEach
}
val metadata = when (metadataValue) {
is JSONObject -> loadAliasMetadata(metadataValue)
else -> AliasMetadata(Instant.now(), deprecated = false)
}
aliases[normalizedAlias] = metadata
}
}
}
}
override fun convert(): State {
val state = State()
state.append("uuid", StateValue.str(uuid))
val identityState = identityLock.withLock {
IdentityState(
subject = _subject,
aliases = aliases.mapValues { (_, metadata) ->
mapOf(
"timestamp" to metadata.instant.toEpochMilli(),
"deprecated" to metadata.deprecated
)
}
)
}
state.append("subject", StateValue.str(identityState.subject))
state.append("aliases", StateValue.obj(identityState.aliases))
val relationState = relationLock.withLock {
relations.mapValues { (_, relationMap) -> relationMap.toMap() }
}
state.append("relations", StateValue.obj(relationState))
val impressionState = impressionLock.withLock {
indexableDataState(impressions)
}
state.append("impressions", StateValue.obj(impressionState))
val featureState = featureLock.withLock {
indexableDataState(features)
}
state.append("features", StateValue.obj(featureState))
return state
}
override fun autoLoadOnRegister(): Boolean = false
private fun normalizeIdentityText(value: String): String =
value.replace(IDENTITY_WHITESPACE_REGEX, " ").trim()
private fun loadAliasMetadata(state: JSONObject): AliasMetadata {
val instant = state.getLong("timestamp")
?.let(Instant::ofEpochMilli)
?: state.getString("instant")
?.let { runCatching { Instant.parse(it) }.getOrNull() }
?: Instant.now()
return AliasMetadata(
instant = instant,
deprecated = state.getBoolean("deprecated") ?: false
)
}
private fun loadIndexableDataMap(state: JSONObject): Map<String, IndexableData> {
val loaded = mutableMapOf<String, IndexableData>()
state.forEach { (key, value) ->
val data = when (value) {
is JSONObject -> loadIndexableData(value)
else -> IndexableData(doubleValue(value) ?: return@forEach)
}
loaded[key] = data
}
return loaded
}
private fun loadIndexableData(state: JSONObject): IndexableData {
val data = IndexableData(state.getDouble("confidence") ?: 1.0)
state.getJSONObject("vectors")?.forEach { (embeddingModel, vectorValue) ->
val vectorArray = vectorValue as? JSONArray ?: return@forEach
val vector = FloatArray(vectorArray.size)
for (index in vectorArray.indices) {
vector[index] = floatValue(vectorArray[index]) ?: return@forEach
}
data.updateVector(embeddingModel, vector)
}
return data
}
private fun indexableDataState(source: Map<String, IndexableData>): Map<String, Map<String, Any>> =
source.mapValues { (_, data) ->
mapOf(
"confidence" to data.confidence,
"vectors" to data.snapshotVectors().mapValues { (_, vector) -> vector.toList() }
)
}
private fun doubleValue(value: Any?): Double? = when (value) {
is Number -> value.toDouble()
is String -> value.toDoubleOrNull()
else -> null
}
private fun floatValue(value: Any?): Float? = when (value) {
is Number -> value.toFloat()
is String -> value.toFloatOrNull()
else -> null
}
data class IndexableData(
var confidence: Double
) {
private val vectors: ConcurrentHashMap<String, FloatArray> = ConcurrentHashMap()
fun updateVector(
embeddingModel: String,
vector: FloatArray
) {
vectors[embeddingModel] = vector.copyOf()
}
fun getVector(embeddingModel: String): FloatArray? {
return vectors[embeddingModel]?.copyOf()
}
fun snapshotVectors(): Map<String, FloatArray> {
return vectors.mapValues { (_, vector) -> vector.copyOf() }
}
}
data class RelationView(
val target: String,
val relations: Map<String, Double>
)
data class FeatureView(
val feature: String,
val confidence: Double
)
@Suppress("ArrayInDataClass")
data class ImpressionView(
val impression: String,
val confidence: Double,
val vector: FloatArray?
)
private data class IdentityState(
val subject: String,
val aliases: Map<String, Map<String, Any>>
)
data class AliasView(
val alias: String,
val createdAt: Instant,
val deprecated: Boolean
)
data class AliasMetadata(
val instant: Instant,
val deprecated: Boolean
)
companion object {
private val IDENTITY_WHITESPACE_REGEX = Regex("\\s+")
}
}
@@ -0,0 +1,112 @@
package work.slhaf.partner.core.cognition.impression
/**
* Runtime evidence associated with an active entity.
*
* The confidence describes how strongly this evidence is associated with the
* current active entity, not whether the evidence content itself is true.
*/
data class EntityEvidence @JvmOverloads constructor(
val content: String,
val associationConfidence: Double = 1.0,
val source: Source = Source.USER_INPUT,
val timestamp: Long = System.currentTimeMillis(),
) {
enum class Source {
USER_INPUT,
ASSISTANT_REPLY
}
fun isContentTruncated(maxLength: Int = CONTEXT_CONTENT_MAX_LENGTH): Boolean =
content.length > maxLength
fun contentForContext(maxLength: Int = CONTEXT_CONTENT_MAX_LENGTH): String {
if (content.length <= maxLength) {
return content
}
val available = maxLength - OMITTED_MARKER.length
if (available <= 0) {
return content.take(maxLength)
}
val headBudget = available / 2
val tailBudget = available - headBudget
val headEnd = adjustHeadEnd(content, headBudget)
val tailStart = adjustTailStart(content, content.length - tailBudget)
if (tailStart <= headEnd) {
return content.take(maxLength).trimEnd()
}
return content.substring(0, headEnd).trimEnd() +
OMITTED_MARKER +
content.substring(tailStart).trimStart()
}
private fun adjustHeadEnd(source: String, preferredEnd: Int): Int {
val safePreferredEnd = preferredEnd.coerceIn(0, source.length)
findForwardBoundary(source, safePreferredEnd, STRONG_BOUNDARY_SEARCH_WINDOW, ::isStrongBoundary)?.let {
return it + 1
}
findForwardBoundary(source, safePreferredEnd, SOFT_BOUNDARY_SEARCH_WINDOW, ::isSoftBoundary)?.let {
return it + 1
}
return safePreferredEnd
}
private fun adjustTailStart(source: String, preferredStart: Int): Int {
val safePreferredStart = preferredStart.coerceIn(0, source.length)
findBackwardBoundary(source, safePreferredStart, STRONG_BOUNDARY_SEARCH_WINDOW, ::isStrongBoundary)?.let {
return it
}
findBackwardBoundary(source, safePreferredStart, SOFT_BOUNDARY_SEARCH_WINDOW, ::isSoftBoundary)?.let {
return it
}
return safePreferredStart
}
private fun findForwardBoundary(
source: String,
start: Int,
window: Int,
predicate: (Char) -> Boolean,
): Int? {
val end = (start + window).coerceAtMost(source.length)
for (index in start until end) {
if (predicate(source[index])) {
return index
}
}
return null
}
private fun findBackwardBoundary(
source: String,
start: Int,
window: Int,
predicate: (Char) -> Boolean,
): Int? {
val end = (start - window).coerceAtLeast(0)
for (index in start downTo end + 1) {
if (predicate(source[index - 1])) {
return index
}
}
return null
}
private fun isStrongBoundary(char: Char): Boolean = char == '\n'
private fun isSoftBoundary(char: Char): Boolean = when (char) {
'。', '!', '?', ';', ';', '.' -> true
else -> false
}
companion object {
const val CONTEXT_CONTENT_MAX_LENGTH = 480
private const val OMITTED_MARKER = "\n...[omitted]...\n"
private const val STRONG_BOUNDARY_SEARCH_WINDOW = 120
private const val SOFT_BOUNDARY_SEARCH_WINDOW = 80
}
}
@@ -0,0 +1,496 @@
package work.slhaf.partner.core.cognition.impression;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.cognition.impression.search.*;
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.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@CapabilityCore(value = "cognition")
public class ImpressionCore implements StateSerializable {
/**
* Keyed by entity uuid. Subject can be revised or merged later, so it should not be used as the stable key.
*/
private final ConcurrentHashMap<String, Entity> knownEntitiesByUuid = new ConcurrentHashMap<>();
private final ImpressionVectorIndex vectorIndex = new ImpressionVectorIndex();
private final Set<ActiveEntity> activeEntities = new HashSet<>();
private final ImpressionTextSearch textSearch = new SimpleTextSearch();
private static final int TEXT_SEARCH_LIMIT = 20;
private static final int ASSOCIATION_MATCH_LIMIT = 8;
private static final double SUPPORTING_HIT_FACTOR = 0.3;
private static final double ASSOCIATION_CONFIDENCE_DIVISOR = 5.0;
/**
* 根据新的 Input 召回相关的实体,如果实体已重复,则将输入追加到 ActiveEntity 的证据中。
*
* @param input 本次输入内容
* @return 本次被召回的活跃实体(包括重复的实体)
*/
@CapabilityMethod
public Set<ActiveEntity> projectEntity(String input) {
if (input == null || input.isBlank()) {
return Set.of();
}
List<ImpressionSearchHit> textSearchHits = textSearch.search(input, TEXT_SEARCH_LIMIT);
List<EntityAssociationMatch> associationMatches = aggregateMatches(textSearchHits, ASSOCIATION_MATCH_LIMIT);
if (associationMatches.isEmpty()) {
return Set.of();
}
Set<ActiveEntity> projected = new HashSet<>();
for (EntityAssociationMatch match : associationMatches) {
Optional<ActiveEntity> activeEntity = resolveActiveEntity(match.getTarget());
if (activeEntity.isEmpty()) {
continue;
}
ActiveEntity entity = activeEntity.get();
entity.addEvidence(
input,
associationConfidence(match),
EntityEvidence.Source.USER_INPUT
);
refreshActiveEntityTextSearch(entity);
projected.add(entity);
}
return projected;
}
/**
* 列出当前已存在的 ActiveEntity 以及对应的 Entity。ActiveEntity 返回快照,Entity 返回当前已知实体引用。
*
* 注意:外部模块不要直接修改返回的 Entity,否则文本索引 / 向量索引不会刷新。
* Impression 更新应走 updateEntity* 系列接口。
*
* @return ActiveEntity 快照与已绑定 Entity 的映射
*/
@CapabilityMethod
public Map<ActiveEntity, Entity> showEntities() {
Map<ActiveEntity, Entity> result = new LinkedHashMap<>();
List<ActiveEntity> entities;
synchronized (activeEntities) {
entities = activeEntities.stream()
.sorted(Comparator
.comparing(ActiveEntity::getLastMentionedAt)
.reversed()
.thenComparing(ActiveEntity::getRuntimeId))
.toList();
}
for (ActiveEntity activeEntity : entities) {
Entity boundEntity = Optional.ofNullable(activeEntity.getBoundEntityUuid())
.map(knownEntitiesByUuid::get)
.orElse(null);
result.put(activeEntity.snapshot(), boundEntity);
}
return Collections.unmodifiableMap(result);
}
/**
* Create a new known entity and make it visible to recall/update indexes immediately.
*/
@CapabilityMethod
public String createEntity(String subject) {
if (subject == null || subject.isBlank()) {
throw new IllegalArgumentException("subject must not be blank");
}
Entity entity = new Entity(UUID.randomUUID().toString(), subject.trim());
entity.register();
knownEntitiesByUuid.put(entity.getUuid(), entity);
refreshKnownEntityIndexes(entity);
return entity.getUuid();
}
/**
* Look up a known entity by stable uuid.
*/
@CapabilityMethod
public Entity getEntity(String uuid) {
if (uuid == null || uuid.isBlank()) {
return null;
}
return knownEntitiesByUuid.get(uuid);
}
/**
* Activate a known entity and return a detached snapshot for external consumers.
*/
@CapabilityMethod
public ActiveEntity activateKnownEntity(String entityUuid) {
return activateKnownEntityLive(entityUuid)
.map(ActiveEntity::snapshot)
.orElse(null);
}
/**
* Bind a runtime active entity to a known entity.
* This keeps the active entity in current context while giving later updates a stable storage target.
*/
@CapabilityMethod
public boolean bindActiveEntity(String runtimeId, String entityUuid) {
if (runtimeId == null || runtimeId.isBlank() || entityUuid == null || entityUuid.isBlank()) {
return false;
}
Entity entity = knownEntitiesByUuid.get(entityUuid);
if (entity == null) {
return false;
}
Optional<ActiveEntity> activeEntity = findActiveEntityByRuntimeId(runtimeId);
if (activeEntity.isEmpty()) {
return false;
}
ActiveEntity active = activeEntity.get();
active.bindEntity(entityUuid);
active.updateSubject(entity.getSubject());
refreshActiveEntityTextSearch(active);
return true;
}
/**
* Rename the canonical subject of a known entity and optionally keep its previous subject as a historical alias.
*/
@CapabilityMethod
public boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias) {
Entity entity = knownEntitiesByUuid.get(entityUuid);
if (entity == null || newSubject == null || newSubject.isBlank()) {
return false;
}
boolean renamed = entity.renameSubject(newSubject.trim(), keepOldSubjectAsAlias);
if (!renamed) {
return false;
}
refreshKnownEntityIndexes(entity);
syncBoundActiveEntitySubjects(entity);
return true;
}
/**
* Add an alias or mention form for a known entity and refresh search indexes.
*/
@CapabilityMethod
public boolean addEntityAlias(String entityUuid, String alias, boolean deprecated) {
Entity entity = knownEntitiesByUuid.get(entityUuid);
if (entity == null || alias == null || alias.isBlank()) {
return false;
}
boolean added = entity.addAlias(alias.trim(), deprecated);
if (!added) {
return false;
}
refreshKnownEntityIndexes(entity);
return true;
}
/**
* Update a known entity impression through the core so text/vector indexes stay consistent.
* newImpression can be null or blank to update the existing impression in place.
*/
@CapabilityMethod
public boolean updateEntityImpression(
String entityUuid,
String impression,
String newImpression,
double confidence
) {
Entity entity = knownEntitiesByUuid.get(entityUuid);
if (entity == null || impression == null || impression.isBlank()) {
return false;
}
entity.updateImpression(
impression.trim(),
normalizeNullableText(newImpression),
confidence
);
refreshKnownEntityIndexes(entity);
return true;
}
/**
* Update a known entity feature through the core so text/vector indexes stay consistent.
* newFeature can be null or blank to update the existing feature in place.
*/
@CapabilityMethod
public boolean updateEntityFeature(
String entityUuid,
String feature,
String newFeature,
double confidence
) {
Entity entity = knownEntitiesByUuid.get(entityUuid);
if (entity == null || feature == null || feature.isBlank()) {
return false;
}
entity.updateFeature(
feature.trim(),
normalizeNullableText(newFeature),
confidence
);
refreshKnownEntityIndexes(entity);
return true;
}
/**
* Update a known entity relation through the core so search documents reflect the changed relation.
*/
@CapabilityMethod
public boolean updateEntityRelation(
String entityUuid,
String target,
String relation,
double strength
) {
Entity entity = knownEntitiesByUuid.get(entityUuid);
if (entity == null || target == null || target.isBlank() || relation == null || relation.isBlank()) {
return false;
}
entity.updateRelation(target.trim(), relation.trim(), strength);
refreshKnownEntityIndexes(entity);
return true;
}
/**
* Normalize optional replacement text used by update methods.
*/
private String normalizeNullableText(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
private List<EntityAssociationMatch> aggregateMatches(
List<ImpressionSearchHit> hits,
int limit
) {
if (hits == null || hits.isEmpty() || limit <= 0) {
return List.of();
}
return hits.stream()
.collect(Collectors.groupingBy(
hit -> hit.getDocument().getTarget(),
LinkedHashMap::new,
Collectors.toList()
))
.entrySet()
.stream()
.map(entry -> {
List<ImpressionSearchHit> sortedHits = entry.getValue()
.stream()
.sorted(Comparator
.comparingDouble(ImpressionSearchHit::getScore)
.reversed()
.thenComparing(hit -> hit.getDocument().getId()))
.toList();
return new EntityAssociationMatch(
entry.getKey(),
aggregateScore(sortedHits),
sortedHits
);
})
.sorted(Comparator
.comparingDouble(EntityAssociationMatch::getScore)
.reversed()
.thenComparing(match -> match.getTarget().getType().name())
.thenComparing(match -> match.getTarget().getId()))
.limit(limit)
.toList();
}
private double aggregateScore(List<ImpressionSearchHit> sortedHits) {
if (sortedHits.isEmpty()) {
return 0.0;
}
double bestHitScore = sortedHits.getFirst().getScore();
double supportingScore = sortedHits.stream()
.skip(1)
.limit(2)
.mapToDouble(hit -> hit.getScore() * SUPPORTING_HIT_FACTOR)
.sum();
return bestHitScore + supportingScore;
}
private Optional<ActiveEntity> resolveActiveEntity(ImpressionSearchTarget target) {
return switch (target.getType()) {
case ACTIVE_ENTITY -> findActiveEntityByRuntimeId(target.getId());
case ENTITY -> activateKnownEntityLive(target.getId());
};
}
private Optional<ActiveEntity> findActiveEntityByRuntimeId(String runtimeId) {
synchronized (activeEntities) {
return activeEntities.stream()
.filter(activeEntity -> activeEntity.getRuntimeId().equals(runtimeId))
.findFirst();
}
}
private Optional<ActiveEntity> findActiveEntityByBoundEntityUuid(String uuid) {
synchronized (activeEntities) {
return activeEntities.stream()
.filter(activeEntity -> uuid.equals(activeEntity.getBoundEntityUuid()))
.findFirst();
}
}
private Optional<ActiveEntity> activateKnownEntityLive(String uuid) {
Entity knownEntity = knownEntitiesByUuid.get(uuid);
if (knownEntity == null) {
return Optional.empty();
}
Optional<ActiveEntity> existing = findActiveEntityByBoundEntityUuid(uuid);
if (existing.isPresent()) {
return existing;
}
ActiveEntity activeEntity = new ActiveEntity();
activeEntity.updateSubject(knownEntity.getSubject());
activeEntity.bindEntity(uuid);
synchronized (activeEntities) {
activeEntities.add(activeEntity);
}
refreshActiveEntityTextSearch(activeEntity);
return Optional.of(activeEntity);
}
private double associationConfidence(EntityAssociationMatch match) {
double normalized = match.getScore() / ASSOCIATION_CONFIDENCE_DIVISOR;
return Math.clamp(normalized, 0.05, 1.0);
}
private void refreshActiveEntityTextSearch(ActiveEntity activeEntity) {
ImpressionSearchTarget target = new ImpressionSearchTarget(
ImpressionSearchTarget.Type.ACTIVE_ENTITY,
activeEntity.getRuntimeId()
);
textSearch.removeByTarget(target);
for (ImpressionSearchDocument document : ImpressionSearchDocuments.INSTANCE.fromActiveEntity(activeEntity)) {
textSearch.upsert(document);
}
}
/**
* Refresh every index derived from a known entity after mutation.
*/
private void refreshKnownEntityIndexes(Entity entity) {
vectorIndex.sync(entity);
refreshKnownEntityTextSearch(entity);
}
private void syncBoundActiveEntitySubjects(Entity entity) {
List<ActiveEntity> boundEntities;
synchronized (activeEntities) {
boundEntities = activeEntities.stream()
.filter(activeEntity -> entity.getUuid().equals(activeEntity.getBoundEntityUuid()))
.toList();
}
boundEntities.forEach(activeEntity -> {
activeEntity.updateSubject(entity.getSubject());
refreshActiveEntityTextSearch(activeEntity);
});
}
/**
* Replace text-search documents for one known entity.
*/
private void refreshKnownEntityTextSearch(Entity entity) {
ImpressionSearchTarget target = new ImpressionSearchTarget(
ImpressionSearchTarget.Type.ENTITY,
entity.getUuid()
);
textSearch.removeByTarget(target);
for (ImpressionSearchDocument document : ImpressionSearchDocuments.INSTANCE.fromEntity(entity)) {
textSearch.upsert(document);
}
}
private void rebuildTextSearch() {
List<ImpressionSearchDocument> documents = new ArrayList<>();
knownEntitiesByUuid.values().forEach(entity ->
documents.addAll(ImpressionSearchDocuments.INSTANCE.fromEntity(entity))
);
synchronized (activeEntities) {
activeEntities.forEach(activeEntity ->
documents.addAll(ImpressionSearchDocuments.INSTANCE.fromActiveEntity(activeEntity))
);
}
textSearch.rebuild(documents);
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "impression.json");
}
@Override
public void load(@NotNull JSONObject state) {
JSONArray entityArray = state.getJSONArray("entities");
if (entityArray == null) {
return;
}
knownEntitiesByUuid.clear();
for (int i = 0; i < entityArray.size(); i++) {
JSONObject entityObject = entityArray.getJSONObject(i);
if (entityObject == null) {
continue;
}
String uuid = entityObject.getString("uuid");
String subject = entityObject.getString("subject");
if (uuid == null || uuid.isBlank() || subject == null || subject.isBlank()) {
continue;
}
Entity entity = new Entity(uuid, subject);
entity.register();
entity.load();
vectorIndex.sync(entity);
knownEntitiesByUuid.put(uuid, entity);
}
rebuildTextSearch();
}
@Override
public @NotNull State convert() {
State state = new State();
List<StateValue.Obj> entities = knownEntitiesByUuid.values().stream()
.map(entity -> StateValue.obj(Map.of(
"uuid", entity.getUuid(),
"subject", entity.getSubject()
)))
.toList();
state.append("entities", StateValue.arr(entities));
return state;
}
}
@@ -0,0 +1,38 @@
package work.slhaf.partner.core.cognition.impression;
import work.slhaf.partner.common.vector.VectorClient;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ImpressionVectorIndex {
private final Executor executor = Executors.newFixedThreadPool(2, r -> {
Thread thread = new Thread(r, "impression-vector-index");
thread.setDaemon(true);
return thread;
});
public void sync(Entity entity) {
if (!VectorClient.status){
return;
}
entity.snapshotFeatures().forEach(this::upsert);
entity.snapshotImpressions().forEach(this::upsert);
}
public void upsert(String text, Entity.IndexableData indexableData){
if (!VectorClient.status){
return;
}
String modelId = VectorClient.VECTOR_MODEL_ID;
if (indexableData.getVector(modelId) != null) {
return;
}
executor.execute(() -> {
float[] vector = VectorClient.INSTANCE.compute(text);
indexableData.updateVector(modelId,vector);
});
}
}
@@ -0,0 +1,7 @@
package work.slhaf.partner.core.cognition.impression.search
data class EntityAssociationMatch(
val target: ImpressionSearchTarget,
val score: Double,
val hits: List<ImpressionSearchHit> = emptyList(),
)
@@ -0,0 +1,10 @@
package work.slhaf.partner.core.cognition.impression.search
data class ImpressionSearchDocument(
val id: String,
val target: ImpressionSearchTarget,
val field: ImpressionSearchField,
val text: String,
val weight: Double = 1.0,
val metadata: Map<String, String> = emptyMap(),
)
@@ -0,0 +1,151 @@
package work.slhaf.partner.core.cognition.impression.search
import work.slhaf.partner.core.cognition.impression.ActiveEntity
import work.slhaf.partner.core.cognition.impression.Entity
object ImpressionSearchDocuments {
fun fromActiveEntity(activeEntity: ActiveEntity): List<ImpressionSearchDocument> {
val target = ImpressionSearchTarget(
ImpressionSearchTarget.Type.ACTIVE_ENTITY,
activeEntity.runtimeId
)
val metadata = activeEntity.boundEntityUuid
?.let { mapOf("boundEntityUuid" to it) }
.orEmpty()
return buildList {
add(
ImpressionSearchDocument(
id = "active:${activeEntity.runtimeId}:subject",
target = target,
field = ImpressionSearchField.SUBJECT,
text = activeEntity.subject,
weight = SUBJECT_WEIGHT,
metadata = metadata,
)
)
activeEntity.evidences.forEachIndexed { index, evidence ->
add(
ImpressionSearchDocument(
id = "active:${activeEntity.runtimeId}:evidence:$index",
target = target,
field = ImpressionSearchField.EVIDENCE,
text = evidence.contentForContext(),
weight = EVIDENCE_WEIGHT * evidence.associationConfidence,
metadata = metadata,
)
)
}
activeEntity.projectedFeatures.entries.forEachIndexed { index, entry ->
add(
ImpressionSearchDocument(
id = "active:${activeEntity.runtimeId}:feature:$index",
target = target,
field = ImpressionSearchField.FEATURE,
text = entry.key,
weight = FEATURE_WEIGHT * entry.value,
metadata = metadata,
)
)
}
activeEntity.projectedImpressions.entries.forEachIndexed { index, entry ->
add(
ImpressionSearchDocument(
id = "active:${activeEntity.runtimeId}:impression:$index",
target = target,
field = ImpressionSearchField.IMPRESSION,
text = entry.key,
weight = IMPRESSION_WEIGHT * entry.value,
metadata = metadata,
)
)
}
}
}
fun fromEntity(entity: Entity): List<ImpressionSearchDocument> {
val target = ImpressionSearchTarget(
ImpressionSearchTarget.Type.ENTITY,
entity.uuid
)
return buildList {
add(
ImpressionSearchDocument(
id = "entity:${entity.uuid}:subject",
target = target,
field = ImpressionSearchField.SUBJECT,
text = entity.subject,
weight = SUBJECT_WEIGHT,
)
)
entity.showAliases(includeDeprecated = true).forEachIndexed { index, alias ->
add(
ImpressionSearchDocument(
id = "entity:${entity.uuid}:alias:$index",
target = target,
field = ImpressionSearchField.SUBJECT,
text = alias.alias,
weight = SUBJECT_WEIGHT * ALIAS_WEIGHT_FACTOR,
)
)
}
entity.snapshotFeatures().keys.forEachIndexed { index, feature ->
add(
ImpressionSearchDocument(
id = "entity:${entity.uuid}:feature:$index",
target = target,
field = ImpressionSearchField.FEATURE,
text = feature,
weight = FEATURE_WEIGHT,
)
)
}
entity.snapshotImpressions().keys.forEachIndexed { index, impression ->
add(
ImpressionSearchDocument(
id = "entity:${entity.uuid}:impression:$index",
target = target,
field = ImpressionSearchField.IMPRESSION,
text = impression,
weight = IMPRESSION_WEIGHT,
)
)
}
entity.showRelations().forEachIndexed { index, relation ->
val relationText = buildString {
append(relation.target)
relation.relations.keys.forEach { name ->
append(' ')
append(name)
}
}
add(
ImpressionSearchDocument(
id = "entity:${entity.uuid}:relation:$index",
target = target,
field = ImpressionSearchField.RELATION,
text = relationText,
weight = RELATION_WEIGHT,
)
)
}
}
}
private const val SUBJECT_WEIGHT = 1.0
private const val ALIAS_WEIGHT_FACTOR = 0.9
private const val FEATURE_WEIGHT = 0.85
private const val IMPRESSION_WEIGHT = 0.75
private const val RELATION_WEIGHT = 0.65
private const val EVIDENCE_WEIGHT = 0.8
}
@@ -0,0 +1,9 @@
package work.slhaf.partner.core.cognition.impression.search
enum class ImpressionSearchField {
SUBJECT,
FEATURE,
IMPRESSION,
RELATION,
EVIDENCE
}
@@ -0,0 +1,7 @@
package work.slhaf.partner.core.cognition.impression.search
data class ImpressionSearchHit(
val document: ImpressionSearchDocument,
val score: Double,
val matchedTerms: Set<String> = emptySet(),
)
@@ -0,0 +1,11 @@
package work.slhaf.partner.core.cognition.impression.search
data class ImpressionSearchTarget(
val type: Type,
val id: String,
) {
enum class Type {
ACTIVE_ENTITY,
ENTITY
}
}
@@ -0,0 +1,15 @@
package work.slhaf.partner.core.cognition.impression.search
interface ImpressionTextSearch {
fun rebuild(documents: Collection<ImpressionSearchDocument>)
fun upsert(document: ImpressionSearchDocument)
fun removeByTarget(target: ImpressionSearchTarget)
fun search(
query: String,
limit: Int = 20,
): List<ImpressionSearchHit>
}
@@ -0,0 +1,5 @@
package work.slhaf.partner.core.cognition.impression.search
interface ImpressionTokenizer {
fun tokenize(text: String): Set<String>
}
@@ -0,0 +1,37 @@
package work.slhaf.partner.core.cognition.impression.search
import com.huaban.analysis.jieba.JiebaSegmenter
class JiebaImpressionTokenizer(
private val segmenter: JiebaSegmenter = JiebaSegmenter(),
private val mode: JiebaSegmenter.SegMode = JiebaSegmenter.SegMode.SEARCH,
) : ImpressionTokenizer {
override fun tokenize(text: String): Set<String> {
val normalized = normalize(text)
if (normalized.isBlank()) {
return emptySet()
}
val jiebaTerms = segmenter.process(normalized, mode)
.asSequence()
.map { it.word }
.map(::normalize)
.filter { it.isNotBlank() }
return (jiebaTerms + alphaNumericTerms(normalized)).toSet()
}
private fun alphaNumericTerms(text: String): Sequence<String> =
ALPHA_NUMERIC_REGEX.findAll(text).map { it.value }
private fun normalize(text: String): String =
text.lowercase()
.replace(WHITESPACE_REGEX, " ")
.trim()
companion object {
private val WHITESPACE_REGEX = Regex("\\s+")
private val ALPHA_NUMERIC_REGEX = Regex("[a-z0-9]+(?:[-_./][a-z0-9]+)*")
}
}
@@ -0,0 +1,136 @@
package work.slhaf.partner.core.cognition.impression.search
class SimpleTextSearch(
private val tokenizer: ImpressionTokenizer = JiebaImpressionTokenizer(),
) : ImpressionTextSearch {
private val documents = linkedMapOf<String, IndexedDocument>()
private val invertedIndex = linkedMapOf<String, MutableSet<String>>()
@Synchronized
override fun rebuild(documents: Collection<ImpressionSearchDocument>) {
this.documents.clear()
invertedIndex.clear()
documents.forEach(::upsertInternal)
}
@Synchronized
override fun upsert(document: ImpressionSearchDocument) {
removeByDocumentId(document.id)
upsertInternal(document)
}
@Synchronized
override fun removeByTarget(target: ImpressionSearchTarget) {
documents.values
.asSequence()
.filter { it.document.target == target }
.map { it.document.id }
.toList()
.forEach(::removeByDocumentId)
}
@Synchronized
override fun search(query: String, limit: Int): List<ImpressionSearchHit> {
if (limit <= 0) {
return emptyList()
}
val normalizedQuery = normalize(query)
if (normalizedQuery.isBlank()) {
return emptyList()
}
val queryTerms = tokenizer.tokenize(normalizedQuery)
val candidateIds = if (queryTerms.isEmpty()) {
documents.keys.toSet()
} else {
queryTerms
.asSequence()
.flatMap { invertedIndex[it].orEmpty().asSequence() }
.toSet()
}
return candidateIds
.asSequence()
.mapNotNull { documentId -> scoreDocument(documents[documentId] ?: return@mapNotNull null, normalizedQuery, queryTerms) }
.filter { it.score > 0.0 }
.sortedWith(compareByDescending<ImpressionSearchHit> { it.score }.thenBy { it.document.id })
.take(limit)
.toList()
}
private fun upsertInternal(document: ImpressionSearchDocument) {
val normalizedText = normalize(document.text)
val terms = tokenizer.tokenize(normalizedText)
val indexedDocument = IndexedDocument(document, normalizedText, terms)
documents[document.id] = indexedDocument
terms.forEach { term ->
invertedIndex.getOrPut(term) { linkedSetOf() }.add(document.id)
}
}
private fun removeByDocumentId(documentId: String) {
val indexedDocument = documents.remove(documentId) ?: return
indexedDocument.terms.forEach { term ->
val ids = invertedIndex[term] ?: return@forEach
ids.remove(documentId)
if (ids.isEmpty()) {
invertedIndex.remove(term)
}
}
}
private fun scoreDocument(
indexedDocument: IndexedDocument,
normalizedQuery: String,
queryTerms: Set<String>,
): ImpressionSearchHit? {
val matchedTerms = if (queryTerms.isEmpty()) {
emptySet()
} else {
queryTerms.intersect(indexedDocument.terms)
}
val exactPhraseMatched = indexedDocument.normalizedText.contains(normalizedQuery)
if (matchedTerms.isEmpty() && !exactPhraseMatched) {
return null
}
val coverage = if (queryTerms.isEmpty()) 0.0 else matchedTerms.size.toDouble() / queryTerms.size.toDouble()
val termScore = matchedTerms.size.toDouble()
val exactPhraseBonus = if (exactPhraseMatched) EXACT_PHRASE_BONUS else 0.0
val fieldBonus = fieldBonus(indexedDocument.document.field)
val score = (termScore + coverage + exactPhraseBonus + fieldBonus) * indexedDocument.document.weight
return ImpressionSearchHit(
document = indexedDocument.document,
score = score,
matchedTerms = matchedTerms,
)
}
private fun fieldBonus(field: ImpressionSearchField): Double = when (field) {
ImpressionSearchField.SUBJECT -> 0.8
ImpressionSearchField.FEATURE -> 0.35
ImpressionSearchField.IMPRESSION -> 0.25
ImpressionSearchField.RELATION -> 0.15
ImpressionSearchField.EVIDENCE -> 0.0
}
private fun normalize(text: String): String =
text.lowercase()
.replace(WHITESPACE_REGEX, " ")
.trim()
private data class IndexedDocument(
val document: ImpressionSearchDocument,
val normalizedText: String,
val terms: Set<String>,
)
companion object {
private const val EXACT_PHRASE_BONUS = 1.5
private val WHITESPACE_REGEX = Regex("\\s+")
}
}
@@ -1,7 +1,7 @@
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.core.memory.pojo.MemorySliceSnapshot;
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
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;
@@ -12,13 +12,13 @@ import java.util.List;
@Capability(value = "memory")
public interface MemoryCapability {
MemoryUnit getMemoryUnit(String unitId);
MemoryUnitSnapshot getMemoryUnit(String unitId);
Result<MemorySlice> getMemorySlice(String unitId, String sliceId);
Result<MemorySliceSnapshot> getMemorySlice(String unitId, String sliceId);
MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary);
MemoryUnitSnapshot updateMemoryUnit(List<Message> chatMessages, String summary);
Collection<MemoryUnit> listMemoryUnits();
Collection<MemoryUnitSnapshot> listMemoryUnits();
void refreshMemorySession();
@@ -5,7 +5,9 @@ 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.MemorySliceSnapshot;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
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;
@@ -36,10 +38,10 @@ public class MemoryCore implements StateSerializable {
}
@CapabilityMethod
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
public MemoryUnitSnapshot updateMemoryUnit(List<Message> chatMessages, String summary) {
memoryLock.lock();
try {
MemoryUnit unit = getMemoryUnit(memorySessionId);
MemoryUnit unit = getOrLoadMemoryUnit(memorySessionId);
unit.updateTimestamp();
List<Message> conversationMessages = unit.getConversationMessages();
@@ -55,14 +57,60 @@ public class MemoryCore implements StateSerializable {
unit.getSlices().add(memorySlice);
normalizeMemoryUnit(unit);
return unit;
return unit.snapshot();
} finally {
memoryLock.unlock();
}
}
@CapabilityMethod
public MemoryUnit getMemoryUnit(String unitId) {
public MemoryUnitSnapshot getMemoryUnit(String unitId) {
memoryLock.lock();
try {
MemoryUnit unit = getOrLoadMemoryUnit(unitId);
normalizeMemoryUnit(unit);
return unit.snapshot();
} finally {
memoryLock.unlock();
}
}
@CapabilityMethod
public Result<MemorySliceSnapshot> getMemorySlice(String unitId, String sliceId) {
memoryLock.lock();
try {
MemoryUnit memoryUnit = memoryUnits.get(unitId);
if (memoryUnit == null) {
return memorySliceNotFound(unitId, sliceId);
}
memoryUnit.load();
normalizeMemoryUnit(memoryUnit);
for (MemorySlice slice : memoryUnit.getSlices()) {
if (sliceId.equals(slice.getId())) {
return Result.success(slice.snapshot());
}
}
return memorySliceNotFound(unitId, sliceId);
} finally {
memoryLock.unlock();
}
}
@CapabilityMethod
public Collection<MemoryUnitSnapshot> listMemoryUnits() {
memoryLock.lock();
try {
return memoryUnits.values().stream()
.peek(MemoryUnit::load)
.peek(this::normalizeMemoryUnit)
.map(MemoryUnit::snapshot)
.toList();
} finally {
memoryLock.unlock();
}
}
private MemoryUnit getOrLoadMemoryUnit(String unitId) {
MemoryUnit unit = memoryUnits.computeIfAbsent(unitId, id -> {
MemoryUnit newUnit = new MemoryUnit(id);
newUnit.register();
@@ -72,21 +120,7 @@ public class MemoryCore implements StateSerializable {
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);
}
}
private Result<MemorySliceSnapshot> memorySliceNotFound(String unitId, String sliceId) {
return Result.failure(new MemoryLookupException(
"Memory slice not found: " + unitId + ":" + sliceId,
unitId + ":" + sliceId,
@@ -94,11 +128,6 @@ public class MemoryCore implements StateSerializable {
));
}
@CapabilityMethod
public Collection<MemoryUnit> listMemoryUnits() {
return new ArrayList<>(memoryUnits.values());
}
@CapabilityMethod
public void refreshMemorySession() {
memorySessionId = UUID.randomUUID().toString();
@@ -175,8 +204,7 @@ public class MemoryCore implements StateSerializable {
State state = new State();
state.append("memory_session_id", StateValue.str(memorySessionId));
List<StateValue.Str> unitOverview = memoryUnits.keySet().stream()
.map(StateValue::str)
List<String> unitOverview = memoryUnits.keySet().stream()
.toList();
state.append("memory_unit_uuid_set", StateValue.arr(unitOverview));
return state;
@@ -33,6 +33,16 @@ public class MemorySlice implements Comparable<MemorySlice> {
return new MemorySlice(id, startIndex, endIndex, summary, timestamp);
}
public MemorySliceSnapshot snapshot() {
return new MemorySliceSnapshot(
id,
startIndex == null ? 0 : startIndex,
endIndex == null ? 0 : endIndex,
summary,
timestamp == null ? 0L : timestamp
);
}
@Override
public int compareTo(MemorySlice memorySlice) {
if (memorySlice.getTimestamp() > this.getTimestamp()) {
@@ -0,0 +1,9 @@
package work.slhaf.partner.core.memory.pojo
data class MemorySliceSnapshot(
val id: String,
val startIndex: Int,
val endIndex: Int,
val summary: String?,
val timestamp: Long,
)
@@ -31,6 +31,15 @@ public class MemoryUnit implements StateSerializable {
timestamp = System.currentTimeMillis();
}
public MemoryUnitSnapshot snapshot() {
return new MemoryUnitSnapshot(
id,
List.copyOf(conversationMessages),
timestamp == null ? 0L : timestamp,
slices.stream().map(MemorySlice::snapshot).toList()
);
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "memory", "memory-unit" + id + ".json");
@@ -95,25 +104,23 @@ public class MemoryUnit implements StateSerializable {
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();
List<StateValue.Obj> convertedMessageList = conversationMessages.stream()
.map(message -> StateValue.obj(Map.of(
"role", message.roleValue(),
"content", message.getContent()
)))
.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();
List<StateValue.Obj> convertedSliceList = slices.stream()
.map(slice -> StateValue.obj(Map.of(
"id", slice.getId(),
"start_index", slice.getStartIndex(),
"end_index", slice.getEndIndex(),
"summary", slice.getSummary(),
"created_timestamp", slice.getTimestamp()
)))
.toList();
state.append("memory_slices", StateValue.arr(convertedSliceList));
return state;
}
@@ -0,0 +1,23 @@
package work.slhaf.partner.core.memory.pojo
import work.slhaf.partner.framework.agent.model.pojo.Message
data class MemoryUnitSnapshot(
val id: String,
val conversationMessages: List<Message>,
val timestamp: Long,
val slices: List<MemorySliceSnapshot>,
) {
fun messagesOf(slice: MemorySliceSnapshot): List<Message> {
if (conversationMessages.isEmpty()) {
return emptyList()
}
val start = slice.startIndex.coerceIn(0, conversationMessages.size)
val end = slice.endIndex.coerceIn(start, conversationMessages.size)
if (start >= end) {
return emptyList()
}
return conversationMessages.subList(start, end).toList()
}
}
@@ -1,14 +0,0 @@
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())
}
}
@@ -2,8 +2,13 @@ package work.slhaf.partner.module.action.builtin;
import java.util.List;
interface BuiltinActionProvider {
public interface BuiltinActionProvider {
List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions();
String createActionKey(String actionName);
default String param(String requirement, String type, String detail) {
return requirement + "|" + type + "|" + detail;
}
}
@@ -9,7 +9,6 @@ import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCa
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;
@@ -26,32 +25,26 @@ public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
@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 register(BuiltinActionProvider provider) {
List<BuiltinActionDefinition> builtinActionDefinitions = provider.provideBuiltinActions();
if (builtinActionDefinitions == null || builtinActionDefinitions.isEmpty()) {
return;
}
Map<String, MetaActionInfo> metaActionInfos = new LinkedHashMap<>();
for (BuiltinActionDefinition definition : builtinActionDefinitions) {
definitions.put(definition.actionKey(), definition);
metaActionInfos.put(definition.actionKey(), definition.metaActionInfo());
}
actionCapability.registerMetaActions(metaActionInfos);
}
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);
actionCapability.registerMetaActions(Map.of(definition.actionKey(), definition.metaActionInfo()));
}
public String call(@NonNull String actionKey, @NonNull Map<String, Object> params) {
@@ -70,11 +63,6 @@ public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
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,
@@ -6,14 +6,16 @@ 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.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.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.memory.pojo.MemorySliceSnapshot;
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
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.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.*;
@@ -32,6 +34,13 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
private CognitionCapability cognitionCapability;
@InjectCapability
private MemoryCapability memoryCapability;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
@@ -49,11 +58,10 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
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."
"unit_id", param("required", "string", "Memory unit id that contains the target memory slice."),
"slice_id", param("required", "string", "Memory slice id to load into short-lived 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.",
"Purpose: bring a known memory slice back into the agent's working context so later reasoning can use the original conversation messages. Use when: the exact memory unit and slice have already been identified. Input: memory slice id and memory unit id. Does not: search, rank, summarize, or infer memories. Returns: whether the slice was recalled and a short result message.",
tags,
Set.of(),
Set.of(),
@@ -67,16 +75,16 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
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);
Result<MemorySliceSnapshot> sliceResult = memoryCapability.getMemorySlice(unitId, sliceId);
if (sliceResult.exceptionOrNull() != null) {
return JSONObject.of(
"ok", false,
"message", sliceResult.exceptionOrNull().getLocalizedMessage()
).toJSONString();
}
MemorySlice slice = sliceResult.getOrThrow();
MemorySliceSnapshot slice = sliceResult.getOrThrow();
MemoryUnit unit = memoryCapability.getMemoryUnit(unitId);
MemoryUnitSnapshot unit = memoryCapability.getMemoryUnit(unitId);
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildMemoryRecallFullBlock(unit, slice),
Set.of(ContextBlock.FocusedDomain.MEMORY),
@@ -97,13 +105,13 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
);
}
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnit unit, MemorySlice slice) {
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnitSnapshot unit, MemorySliceSnapshot 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) -> {
appendRepeatedElements(document, root, "message", unit.messagesOf(slice), (messageElement, message) -> {
messageElement.setAttribute("role", message.getRole().name().toLowerCase(Locale.ROOT));
messageElement.setTextContent(message.getContent());
return Unit.INSTANCE;
@@ -125,10 +133,10 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
false,
null,
Map.of(
"input", "Input required to initiate an internal Agent Turn.",
"target", "The people expected to reply to this internal Agent Turn."
"input", param("required", "string", "Prompt content for the internal Agent Turn."),
"target", param("required", "string", "Target user expected to receive/respond to the turn.")
),
"Create an internal Agent Turn to resolve a task.",
"Purpose: initiate a self-dialogue or proactive dialogue by sending input to a target participant. Use when: the agent needs to start a new turn for reflection, clarification, coordination, or active communication. Input: input content and the user expected to respond. Returns: confirmation that the turn was initiated. Does not: wait for or include the target's answer.",
tags,
Set.of(),
Set.of(),
@@ -6,21 +6,25 @@ 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 work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
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.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinCommandActionProvider implements BuiltinActionProvider {
private static final String COMMAND_LOCATION = BUILTIN_LOCATION + "::" + "command";
@@ -38,6 +42,30 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
private final ConcurrentHashMap<String, CommandHandle> commandHandles = new ConcurrentHashMap<>();
private final CommandExecutionService commandExecutionService = CommandExecutionService.INSTANCE;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
private Map<String, String> commandParams() {
Map<String, String> params = new LinkedHashMap<>();
params.put("arg", param("required", "string", "Command executable name or path."));
params.put("arg1", param("optional", "string", "First command argument after the executable."));
params.put("arg2", param("optional", "string", "Second command argument after the executable."));
params.put("argN", param("optional", "string", "Additional command arguments after arg2. Use consecutive numeric keys such as arg3, arg4, arg5, ... when more arguments are needed."));
return params;
}
private Map<String, String> commandSessionParams() {
Map<String, String> params = new LinkedHashMap<>();
params.put("desc", param("required", "string", "Short human-readable description of why this background command session exists."));
params.putAll(commandParams());
return params;
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
@@ -61,8 +89,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
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.",
commandParams(),
"Purpose: execute a short, non-interactive system command synchronously and return its completed output. Inputs: argv entries through arg/argN. Returns: JSON with result containing stdout/stderr/exit summary text. Use when: the task can finish quickly in one process call. Notes: do not use for long-running services, interactive programs, streaming output, or processes that need later inspection.",
tags,
Set.of(),
Set.of(createActionKey("inspect")),
@@ -92,11 +120,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
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.",
commandSessionParams(),
"Purpose: start a long-running or streaming command as a background session. Inputs: session description and argument entries. Returns: JSON with executionId; pass it as id to inspect, read, or cancel. Use when: the command may run for a while, produce incremental output, or need later control. Notes: usually follow with builtin::command::inspect or builtin::command::read.",
tags,
Set.of(),
Set.of(createActionKey("inspect")),
@@ -145,8 +170,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("id", "Command session id."),
"Inspect a background command session.",
Map.of("id", param("required", "string", "Command executionId returned by builtin::command::start.")),
"Purpose: inspect status and short output summaries for an existing background command session. Inputs: id from command::start. Returns: JSON with executionId, desc, exitCode, stdoutSize, stderrSize, stdoutSummary, stderrSummary, startAt, and endAt. Use when: deciding whether a background command is still running, failed, or needs a full output read. Notes: use builtin::command::read for complete or paginated output.",
tags,
Set.of(createActionKey("overview")),
Set.of(),
@@ -195,12 +220,12 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
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."
"id", param("required", "string", "Command executionId returned by builtin::command::start."),
"stream", param("optional", "string", "Target stream to read. Allowed values: stdout, stderr. Default: stdout."),
"offset", param("optional", "int", "Start byte offset in the persisted stream log. Default: 0. Use nextOffset from a previous read to continue."),
"limit", param("optional", "int", "Maximum bytes to read from the persisted stream log. Default: 4096. Must be greater than 0.")
),
"Read output from a background command session.",
"Purpose: read stdout or stderr content from a background command session with offset pagination. Returns: JSON with content, contentTruncated, nextOffset, and eof. Use when: inspect summaries are insufficient or a command failed and stderr/stdout details are needed. Notes: repeat with nextOffset until eof or enough evidence is collected.",
tags,
Set.of(createActionKey("overview")),
Set.of(),
@@ -263,8 +288,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("id", "Command session id."),
"Cancel a background command session.",
Map.of("id", param("required", "string", "Command executionId returned by builtin::command::start.")),
"Purpose: terminate an existing background command session. Inputs: id from command::start. Returns: JSON with ok and executionId. Use when: the user explicitly asks to stop a command, or correction determines the running command is wrong, stuck, unsafe, or no longer needed. Notes: this has process side effects and should not be used as a status check.",
tags,
Set.of(),
Set.of(createActionKey("overview")),
@@ -316,16 +341,16 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
false,
null,
Map.of(),
"List all background command sessions.",
"Purpose: list known background command sessions. Inputs: none. Returns: JSON with result array of session overviews containing executionId, desc, and exitCode. Use when: an id is missing or correction needs to discover active command sessions before inspect/read/cancel. Notes: this does not start, stop, or read a command.",
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."
"result", "Array of command session overview items.",
"result[].executionId", "Command execution session id for each overview item.",
"result[].desc", "Command session description for each overview item.",
"result[].exitCode", "Process exit code for each overview item. Null when still running."
)
);
Function<Map<String, Object>, String> invoker = params -> {
@@ -492,30 +517,36 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
}
private List<String> requireCommandArguments(Map<String, Object> params) {
List<Map.Entry<String, Object>> entries = params.entrySet().stream()
List<Map.Entry<String, Object>> unexpectedArgs = params.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(COMMAND_ARG_PREFIX))
.sorted(Comparator.comparingInt(entry -> commandArgIndex(entry.getKey())))
.filter(entry -> !COMMAND_ARG_PREFIX.equals(entry.getKey()) && !isNumberedCommandArg(entry.getKey()))
.toList();
if (entries.isEmpty()) {
throw new IllegalArgumentException("缺少命令参数");
if (!unexpectedArgs.isEmpty()) {
throw new IllegalArgumentException("非法命令参数: " + unexpectedArgs.getFirst().getKey());
}
List<String> commands = new ArrayList<>();
for (Map.Entry<String, Object> entry : entries) {
commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, entry.getKey()));
}
commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, COMMAND_ARG_PREFIX));
params.entrySet().stream()
.filter(entry -> isNumberedCommandArg(entry.getKey()))
.sorted(Comparator.comparingInt(entry -> commandArgIndex(entry.getKey())))
.forEach(entry -> commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, entry.getKey())));
return commands;
}
private int commandArgIndex(String key) {
private boolean isNumberedCommandArg(String key) {
if (!key.startsWith(COMMAND_ARG_PREFIX) || COMMAND_ARG_PREFIX.equals(key)) {
return false;
}
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;
return false;
}
return suffix.chars().allMatch(Character::isDigit);
}
private int commandArgIndex(String key) {
return Integer.parseInt(key.substring(COMMAND_ARG_PREFIX.length()).trim());
}
private String bufferSnapshot(StringBuilder buffer) {
@@ -7,6 +7,7 @@ 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.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
@@ -29,6 +30,13 @@ class BuiltinDynamicActionProvider implements BuiltinActionProvider {
private ActionCapability actionCapability;
@InjectModule
private ActionScheduler actionScheduler;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
@@ -43,13 +51,13 @@ class BuiltinDynamicActionProvider implements BuiltinActionProvider {
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\"}}"
"desc", param("required", "string", "Human-readable description for the generated temporary action. This becomes the new action's planning description."),
"code", param("required", "string", "Dynamic action source code content."),
"codeType", param("required", "string", "Source file extension without or with leading dot, for example py, sh, js, or .py."),
"launcher", param("required", "string", "Interpreter or launcher command used to run the generated code, for example python3, bash, or node."),
"meta", param("required", "json-string", "MetaActionInfo fields for the generated action. Supported fields: io, params, tags, preActions, postActions, strictDependencies, responseSchema.")
),
"Generate a temporary ORIGIN action from source code and return a temporary actionKey.",
"Purpose: generate and register a temporary script as an ORIGIN MetaAction from supplied source code. Inputs: desc, code, codeType, launcher, and meta info. Returns: JSON with ok and temporary actionKey. Use when: existing MetaActions cannot perform the task and a temporary code-backed action is needed. Notes: this registers executable code with cleanup TTL; the generated action is temporary until builtin::dynamic::persist is called.",
basicTags,
Set.of(),
Set.of(createActionKey("persist")),
@@ -110,8 +118,8 @@ class BuiltinDynamicActionProvider implements BuiltinActionProvider {
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.",
Map.of("actionKey", param("required", "string", "Temporary ORIGIN actionKey returned by builtin::dynamic::generate.")),
"Purpose: persist a previously generated temporary ORIGIN MetaAction and cancel its cleanup task. Inputs: actionKey from dynamic::generate. Returns: JSON with ok and actionKey. Use when: a generated temporary action should become reusable beyond its cleanup TTL. Notes: this has persistence side effects and should only be used when the generated action is validated or explicitly requested.",
basicTags,
Set.of(createActionKey("generate")),
Set.of(),
@@ -12,11 +12,17 @@ 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.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
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.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.*;
@@ -35,6 +41,13 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
@@ -56,9 +69,9 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
false,
null,
Map.of(
"actionId", "Uuid of the interrupted executable action to resume."
"actionId", param("required", "string", "Uuid of an executable action currently in INTERRUPTED status.")
),
"Resume action that is interrupted.",
"Purpose: resume an executable action that is currently interrupted. Inputs: actionId. Returns: plain text success or failure reason. Use when: an intervention or user decision is complete and the original action should continue. Notes: this does not modify the action chain; use create_intervention before resuming when chain changes are required.",
tags,
Set.of(),
Set.of(),
@@ -71,7 +84,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
Function<Map<String, Object>, String> invoker = params -> {
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId");
try {
ExecutableAction executableAction = getExecutableAction(actionId);
ExecutableAction executableAction = getExecutableAction(actionId, Action.Status.INTERRUPTED);
executableAction.resume();
return "Resume succeed";
} catch (Exception e) {
@@ -101,14 +114,14 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
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)."
"actionId", param("required", "string", "Uuid of the executing action that should be interrupted for external intervention."),
"actionInfo", param("required", "string", "Readable summary of the current action and why intervention is needed."),
"demand", param("required", "string", "Specific feedback, decision, permission, or operation required from the target."),
"target", param("required", "string", "Target user or channel identifier that should receive the intervention request."),
"input", param("required", "string", "Prompt content used to initiate the intervention turn toward the target."),
"timeout", param("required", "int", "Maximum wait time passed to ExecutableAction.interrupt(timeout).")
),
"Try to acquire the target user to intervene this Action.",
"Purpose: interrupt an executing action and ask a target participant/channel for intervention. Inputs: actionId, actionInfo, demand, target, input, timeout. Returns: plain text intervention status. Use when: execution cannot safely continue without user decision, missing information, permission, or manual operation. Notes: this creates communication side effects and temporarily interrupts the action.",
tags,
Set.of(),
Set.of(),
@@ -147,7 +160,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
ExecutableAction executableAction = null;
try {
executableAction = getExecutableAction(actionId);
executableAction = getExecutableAction(actionId, Action.Status.EXECUTING);
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";
@@ -168,12 +181,12 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
);
}
private ExecutableAction getExecutableAction(String actionId) {
return actionCapability.listActions(Action.Status.EXECUTING, null)
private ExecutableAction getExecutableAction(String actionId, Action.Status status) {
return actionCapability.listActions(status, null)
.stream()
.filter(action -> action.getUuid().equals(actionId))
.findFirst()
.orElseThrow();
.orElseThrow(() -> new IllegalArgumentException("未找到对应状态的 Action: " + actionId + ", status=" + status));
}
/**
@@ -190,7 +203,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
false,
null,
Map.of(),
"List existing actions that can be intervened",
"Purpose: list executable actions that can be inspected for possible intervention. Inputs: none. Returns: JSON array with tendency, description, status, and uuid for each action. Use when: an action id is needed before acquire_intervention, create_intervention, or resume_interrupted_action. Notes: this only discovers actions; it does not modify them.",
tags,
Set.of(),
Set.of(),
@@ -237,7 +250,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
false,
null,
Map.of(),
"List available MetaActions.",
"Purpose: list currently available MetaActions that can be used in action chains or interventions. Inputs: none. Returns: JSON array with action key/location, description, tags, and params. Use when: correction needs to discover valid action_key values for APPEND, INSERT, DELETE, or REBUILD. Notes: this is a discovery action and does not execute those MetaActions.",
tags,
Set.of(),
Set.of(),
@@ -282,12 +295,12 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
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\""
"id", param("required", "string", "Uuid of the executable action to modify."),
"type", param("required", "string", "Intervention type. Allowed values: APPEND, INSERT, DELETE, CANCEL, REBUILD."),
"order", param("required", "int", "Action chain order/stage where the intervention applies."),
"actions", param("conditional", "string", "Comma-separated actionKey list to insert, append, rebuild, or delete. Required except for CANCEL. Example: builtin::command::execute,builtin::command::read.")
),
"Used to create an Action Intervention and act on the specified Executable Action.",
"Purpose: apply a structural intervention to an executable action chain. Inputs: id, type, order, and conditional actions. Returns: JSON with ok and optional result. Use when: correction must append, insert, delete, cancel, or rebuild future action-chain steps. Notes: actions must be valid action_key values; CANCEL may omit actions; this has direct action-chain side effects and resumes the target action after applying changes.",
basicTags,
Set.of(
createActionKey("list_available_meta_actions"),
@@ -324,8 +337,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
actionCapability.handleInterventions(List.of(intervention), target);
ExecutableAction executableAction = getExecutableAction(targetId);
executableAction.resume();
target.resume();
return JSONObject.of("ok", true).toJSONString();
};
@@ -5,7 +5,7 @@ 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.core.cognition.context.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;
@@ -28,21 +28,25 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
你会收到:
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
- 一条任务消息,其中包含:
- check_mode:当前识别模式,PROCESS_CHECK 表示过程纠偏识别,FINAL_CHECK 表示链路末尾完成度验收;
- 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。
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description、status 与 result。
你的任务:
- 基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移,或与最新语境不再一致的情况;
- 当 check_mode=PROCESS_CHECK 时,基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
- 当 check_mode=FINAL_CHECK 时,判断当前行动链即将结束时,original_tendency 是否已经被执行结果满足;
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移、目标未完成,或与最新语境不再一致的情况;
- 若需要引入 corrector,则返回 needCorrection=true,并给出简洁明确的 reason;
- 若当前仍可继续推进,则返回 needCorrection=false。
- 若当前仍可继续推进或已经满足目标,则返回 needCorrection=false。
识别原则:
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的判断必须围绕这条行动本身,而不是被无关历史带偏。
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构与已知阶段状态。
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构、已知阶段状态与执行结果。
- original_tendency 表示这条行动链最初要解决的问题;若当前链路明显偏离这一倾向,应优先视为异常信号。
- evaluation_passed_reason 与 description 表示该行动最初为何被判定为可推进;若当前状态已经与这些前提不再一致,应考虑触发纠正。
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常或整体失衡的主要依据。
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常、整体失衡或最终目标未满足的主要依据。
- PROCESS_CHECK 关注执行过程是否仍然合理;FINAL_CHECK 关注链路结果是否满足 original_tendency。
- FINAL_CHECK 时应重点查看各 meta_action 的 result;如果 result 显示对象不存在、权限不足、参数错误、输出为空、命令失败、目标未产生或结果明显不符合 original_tendency,应触发纠正。
- communication 域用于判断最新交流语境是否已发生明显变化,导致当前行动继续推进不再合适。
- memory 域只在与当前行动明显相关时作为辅助参考使用。
@@ -51,12 +55,14 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
- 当前执行显著偏离 original_tendency,开始围绕无关目标展开;
- 当前行动所依赖的前提已被新的交流内容或上下文推翻;
- 当前行动链反复遇到同类失败、阻塞或空转迹象,继续按原链推进意义不大;
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛。
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛;
- FINAL_CHECK 时,执行结果明确显示 original_tendency 尚未满足,且可能通过补救步骤继续推进。
不应轻易触发纠正的情形:
- 只是正常的多步推进;
- 存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
- PROCESS_CHECK 时,只是正常的多步推进;
- PROCESS_CHECK 时,存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
- 行动链整体仍与 original_tendency、evaluation_passed_reason 和当前语境保持一致;
- FINAL_CHECK 时,执行结果已经足以满足 original_tendency,只需要向用户报告结果;
- 仅凭旧对话、低相关记忆或轻微波动,不足以判定当前行动异常。
关于输出:
@@ -85,6 +91,7 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "check_mode", input.getCheckMode());
appendChildElement(document, root, "executable_action_info", block -> {
appendTextElement(document, block, "executing_action_id", input.getActionId());
appendTextElement(document, block, "original_tendency", input.getTendency());
@@ -100,6 +107,7 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
appendTextElement(document, metaActionElement, "result", metaActionData.getResult());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
@@ -111,6 +119,7 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
@@ -4,8 +4,9 @@ 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.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.context.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;
@@ -28,13 +29,17 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
你会收到:
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
- 一条任务消息,其中包含:
- check_mode:当前纠偏模式,PROCESS_CHECK 表示过程纠偏,FINAL_CHECK 表示链路末尾目标未满足后的补救;
- 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。
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description、stage_description、status 与 result;stage_description 是该阶段的具体执行目标,description 是 MetaAction 的通用能力说明。
- available_meta_action:当前系统真实可用的 MetaAction 候选,包含 meta_action_key 与 meta_action_description。
你的任务:
- 基于当前上下文、原始行动意图与当前行动链进展,判断后续行动是否仍然符合目的;
- 若当前链路仍可继续推进,则不要随意干预;
- 当 check_mode=PROCESS_CHECK 时,基于当前上下文、原始行动意图与当前行动链进展,判断后续行动是否仍然符合目的;
- 当 check_mode=FINAL_CHECK 时,基于当前行动链的最终结果判断 original_tendency 尚未满足时应如何补救;
- 若当前链路仍可继续推进或最终结果已足够满足目标,则不要随意干预;
- 若当前链路已明显跑偏、缺少必要步骤、顺序不合理、存在冗余、已经不再适合继续,或需要引入新的动作单元,则输出干预方案;
- FINAL_CHECK 下若结果显示目标未满足,应优先补入可继续执行的后续动作;如果无法补救,可以输出空 intervention 并在 correctionReason 中说明原因。
- correctionReason 用于简洁说明为何需要这些纠偏。
纠偏原则:
@@ -43,6 +48,7 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
- original_tendency 是这条行动链最初要解决的问题;后续行动的调整必须仍然围绕它。
- evaluation_passed_reason 与 description 表示该行动最初为何能够成立;若当前链路已经与这些前提不一致,可据此进行纠偏。
- current_action_chain_overview 是判断后续链路是否缺步骤、顺序失衡、重复冗余或整体方向错误的主要依据。
- available_meta_action 是你进行 APPEND、INSERT 或 REBUILD 时可选择的动作全集;不要使用其中不存在的 action_key。
- communication 域用于判断最新交流语境是否已经变化,导致当前链路需要调整。
- memory 域只在与当前行动明显相关时作为辅助参考使用。
@@ -88,6 +94,8 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private ActionCapability actionCapability;
@Override
protected @NotNull Result<CorrectorResult> doExecute(CorrectorInput input) {
@@ -102,6 +110,7 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "check_mode", input.getCheckMode());
appendChildElement(document, root, "executable_action_info", block -> {
appendTextElement(document, block, "executing_action_id", input.getActionId());
appendTextElement(document, block, "original_tendency", input.getTendency());
@@ -116,11 +125,26 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
if (metaActionData.getStageDescription() != null && !metaActionData.getStageDescription().isBlank()) {
appendTextElement(document, metaActionElement, "stage_description", metaActionData.getStageDescription());
}
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
appendTextElement(document, metaActionElement, "result", metaActionData.getResult());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
appendRepeatedElements(
document,
root,
"available_meta_action",
actionCapability.listAvailableMetaActions().entrySet(),
(block, value) -> {
appendTextElement(document, block, "meta_action_key", value.getKey());
appendTextElement(document, block, "meta_action_description", value.getValue().getDescription());
return Unit.INSTANCE;
}
);
}
}.encodeToMessage();
}
@@ -128,6 +152,7 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
public class ActionExecutor extends AbstractAgentModule.Standalone {
private static final int MAX_EXTRACTOR_ATTEMPTS = 3;
private static final int MAX_FINAL_CORRECTION_ATTEMPTS = 1;
private final AssemblyHelper assemblyHelper = new AssemblyHelper();
@@ -105,12 +106,12 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
if (action instanceof ExecutableAction executableAction) {
if (action.getStatus() == Action.Status.FAILED) {
ensureExecutableResult(executableAction, true, null);
blockManager.emitActionFinishedBlock(executableAction);
emitExecutableActionFinished(executableAction);
return;
}
action.setStatus(Action.Status.SUCCESS);
ensureExecutableResult(executableAction, false, null);
blockManager.emitActionFinishedBlock(executableAction);
emitExecutableActionFinished(executableAction);
return;
}
if (action.getStatus() == Action.Status.FAILED) {
@@ -122,7 +123,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
action.setStatus(Action.Status.FAILED);
if (action instanceof ExecutableAction executableAction) {
ensureExecutableResult(executableAction, true, e.getLocalizedMessage());
blockManager.emitActionFinishedBlock(executableAction);
emitExecutableActionFinished(executableAction);
}
}
};
@@ -133,6 +134,15 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
action.setStatus(Action.Status.FAILED);
}
private void emitExecutableActionFinished(ExecutableAction executableAction) {
blockManager.emitActionFinishedBlock(executableAction);
cognitionCapability.initiateTurn(
"An executable action has finished. Check the latest action-finished state and inform the user of the outcome if it is relevant to the current conversation.",
executableAction.getSource(),
"action_planner"
);
}
private void handleStateAction(StateAction stateAction) {
if (closed.get()) {
return;
@@ -145,7 +155,6 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
actionCapability.putAction(executableAction);
val actionChain = executableAction.getActionChain();
val phaser = new Phaser();
if (!prepareExecutableAction(executableAction, actionChain)) {
return;
}
@@ -153,6 +162,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
blockManager.emitActionLaunchedBlock(executableAction);
val stageCursor = initStageCursor(executableAction, actionChain);
int finalCorrectionAttempts = 0;
while (true) {
val stageSelection = selectCurrentStage(executableAction, actionChain);
if (stageSelection.shouldReturn()) {
@@ -161,12 +171,15 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
if (stageSelection.shouldStop()) {
break;
}
val stageExecution = runCurrentStage(executableAction, phaser, stageCursor, stageSelection.metaActions());
val stageExecution = runCurrentStage(executableAction, stageCursor, stageSelection.metaActions());
if (stageExecution.closed()) {
return;
}
if (!applyStageCorrectionAndAdvance(executableAction, stageCursor, stageExecution)) {
break;
if (finalCorrectionAttempts >= MAX_FINAL_CORRECTION_ATTEMPTS || !applyFinalCorrection(executableAction, stageCursor)) {
break;
}
finalCorrectionAttempts++;
}
}
finishExecutableAction(executableAction);
@@ -211,10 +224,10 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
private StageExecution runCurrentStage(
ExecutableAction executableAction,
Phaser phaser,
StageCursor stageCursor,
List<MetaAction> metaActions
) {
Phaser phaser = new Phaser();
val recognizerRecord = startRecognizerIfNeeded(executableAction, phaser);
val listeningRecord = executeAndListening(metaActions, phaser, executableAction);
phaser.awaitAdvance(listeningRecord.phase());
@@ -248,7 +261,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
shouldRunCorrector = recognizerResult != null && recognizerResult.isNeedCorrection();
}
if (shouldRunCorrector) {
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction);
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.PROCESS_CHECK);
actionCorrector.execute(correctorInput)
.onSuccess(correctorResult -> {
actionCapability.handleInterventions(correctorResult.getMetaInterventionList(), executableAction);
@@ -267,6 +280,59 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
}
}
private boolean applyFinalCorrection(ExecutableAction executableAction, StageCursor stageCursor) {
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.FINAL_CHECK);
val recognizerResult = actionCorrectionRecognizer.execute(recognizerInput)
.getOrDefault(new RecognizerResult());
if (!recognizerResult.isNeedCorrection()) {
return false;
}
val previousStage = executableAction.getExecutingStage();
val correctorResult = actionCorrector.execute(recognizerInput).getOrDefault(new CorrectorResult());
val interventions = correctorResult.getMetaInterventionList() == null
? new ArrayList<work.slhaf.partner.core.action.entity.intervention.MetaIntervention>()
: correctorResult.getMetaInterventionList();
if (!interventions.isEmpty()) {
actionCapability.handleInterventions(interventions, executableAction);
}
blockManager.emitActionCorrectionBlock(
executableAction,
recognizerResult.getReason() == null || recognizerResult.getReason().isBlank()
? correctorResult.getCorrectionReason()
: recognizerResult.getReason(),
interventions
);
synchronized (executableAction.getExecutionLock()) {
if (executableAction.getStatus() == Action.Status.FAILED) {
return false;
}
if (interventions.isEmpty() || executableAction.getActionChain().isEmpty()) {
executableAction.setStatus(Action.Status.FAILED);
ensureExecutableResult(executableAction, true, "行动未达到目标,且未能生成可继续执行的纠偏行动");
return false;
}
if (executableAction.getStatus() == Action.Status.PREPARE) {
normalizeExecutingStage(executableAction, executableAction.getActionChain());
executableAction.setStatus(Action.Status.EXECUTING);
} else {
Integer nextStage = executableAction.getActionChain().keySet().stream()
.filter(stage -> stage > previousStage)
.min(Integer::compareTo)
.orElse(null);
if (nextStage == null) {
executableAction.setStatus(Action.Status.FAILED);
ensureExecutableResult(executableAction, true, "行动未达到目标,纠偏未生成后续执行阶段");
return false;
}
executableAction.setExecutingStage(nextStage);
}
stageCursor.refresh();
return true;
}
}
private void finishExecutableAction(ExecutableAction executableAction) {
// 如果是 ScheduledActionData, 则重置 ActionData 内容,记录执行历史与最终结果
if (executableAction instanceof SchedulableExecutableAction scheduledActionData) {
@@ -339,7 +405,12 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
result.reset();
metaAction.getParams().clear();
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription());
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(
metaAction.getKey(),
actionData.getUuid(),
actionData.getDescription(),
resolveCurrentStageDescription(actionData, executingStage)
);
AgentRuntimeException exception = extractorInputResult.exceptionOrNull();
if (exception != null) {
failureReason.set(exception.getMessage());
@@ -413,7 +484,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
if (!shouldRunCorrectionRecognizer(executableAction)) {
return RecognizerTaskRecord.disabled();
}
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction);
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.PROCESS_CHECK);
val task = buildRecognizerTask(recognizerInput, phaser);
Future<RecognizerResult> future = virtualExecutor.submit(task);
return new RecognizerTaskRecord(true, future);
@@ -445,6 +516,10 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
}
}
private String resolveCurrentStageDescription(ExecutableAction executableAction, int executingStage) {
return executableAction.getStageDescriptions().get(executingStage);
}
private String buildAttemptFailureReason(String prefix, String detail) {
if (detail == null || detail.isBlank()) {
return prefix;
@@ -655,6 +730,17 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
return stageCount < actionChain.size();
}
private void refresh() {
val orderList = new ArrayList<>(actionChain.keySet());
orderList.sort(Integer::compareTo);
stageCount = orderList.indexOf(executableAction.getExecutingStage());
if (stageCount < 0) {
stageCount = 0;
}
executingStageUpdated = false;
stageCountUpdated = false;
}
private void update() {
val orderList = new ArrayList<>(actionChain.keySet());
orderList.sort(Integer::compareTo);
@@ -667,32 +753,36 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
private AssemblyHelper() {
}
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) {
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description, String currentStageDescription) {
return actionCapability.loadMetaActionInfo(actionKey).fold(
metaActionInfo -> {
ExtractorInput input = new ExtractorInput();
input.setMetaActionInfo(metaActionInfo);
input.setTargetActionId(uuid);
input.setTargetActionDesc(description);
input.setCurrentStageDesc(currentStageDescription);
return Result.success(input);
},
Result::failure
);
}
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) {
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction, CorrectorInput.CheckMode checkMode) {
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)
executableAction.getStageDescriptions().get(stage),
metaAction.getResult().getStatus().name().toLowerCase(Locale.ROOT),
metaAction.getResult().getData()
))
.toList();
overview.put(stage, overviewItems);
});
return CorrectorInput.builder()
.checkMode(checkMode)
.tendency(executableAction.getTendency())
.source(executableAction.getSource())
.reason(executableAction.getReason())
@@ -6,9 +6,10 @@ 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.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
import work.slhaf.partner.module.StateHintContent;
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
import java.time.ZonedDateTime;
@@ -98,8 +99,8 @@ class ExecutingActionBlockManager {
buildStateActionCompactBlock(snapshot, blockName, emittedAt, event),
buildStateActionAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
70,
18,
90,
30,
10
));
}
@@ -382,6 +383,7 @@ class ExecutingActionBlockManager {
14,
24
));
contextWorkspace.register(buildActionFinishedStateHintBlock(snapshot, emittedAt, event));
}
private @NotNull BlockContent buildActionFinishedAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
@@ -406,6 +408,24 @@ class ExecutingActionBlockManager {
};
}
private @NotNull ContextBlock buildActionFinishedStateHintBlock(ExecutableActionSnapshot snapshot, String emittedAt, String event) {
return StateHintContent.createBlock(new StateHintContent(
SOURCE,
"An executable action has finished."
) {
@Override
public void fillStateContent(@NotNull Document document, @NotNull Element stateElement) {
appendTextElement(document, stateElement, "event", event);
appendTextElement(document, stateElement, "emitted_at", emittedAt);
appendTextElement(document, stateElement, "action_id", snapshot.getUuid());
appendTextElement(document, stateElement, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
appendTextElement(document, stateElement, "description", snapshot.getDescription());
appendTextElement(document, stateElement, "result", snapshot.getResult());
appendTextElement(document, stateElement, "source_user", snapshot.getSource());
}
});
}
private String emittedAt() {
return ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
@@ -6,7 +6,7 @@ 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.core.cognition.context.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;
@@ -29,7 +29,7 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Res
你会收到:
- 一条结构化上下文消息,其中可能包含当前行动相关状态、近期交流轨迹、以及活跃记忆切片;
- 一条任务消息,其中包含:
- target_action:本次参数提取所面向的目标行动,用于帮助你判断上下文中哪些内容与当前提取直接相关;
- target_action:本次参数提取所面向的目标行动,用于帮助你判断上下文中哪些内容与当前提取直接相关;其中 current_stage_description 表示本次 MetaAction 所属阶段的具体执行目标,应优先用于约束参数提取;
- meta_action_info:该行动对应的说明,以及允许提取的参数列表。每个 <param name="..."> 节点的文本内容表示该参数的含义或期望内容。
你的任务:
@@ -78,6 +78,9 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Res
appendChildElement(document, root, "target_action", block -> {
appendTextElement(document, block, "uuid", input.getTargetActionId());
appendTextElement(document, block, "description", input.getTargetActionDesc());
if (input.getCurrentStageDesc() != null && !input.getCurrentStageDesc().isBlank()) {
appendTextElement(document, block, "current_stage_description", input.getCurrentStageDesc());
}
return Unit.INSTANCE;
});
appendChildElement(document, root, "meta_action_info", element -> {
@@ -99,6 +102,7 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Res
return cognitionCapability.contextWorkspace()
.resolve(List.of(
ContextBlock.FocusedDomain.ACTION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.MEMORY
))
@@ -10,6 +10,7 @@ import java.util.Map;
@Data
@Builder
public class CorrectorInput {
private CheckMode checkMode;
private String tendency;
private String source;
private String reason;
@@ -18,11 +19,18 @@ public class CorrectorInput {
private Map<Integer, List<ActionChainItem>> actionChainOverview;
public enum CheckMode {
PROCESS_CHECK,
FINAL_CHECK
}
@Data
@AllArgsConstructor
public static class ActionChainItem {
private String actionKey;
private String description;
private String stageDescription;
private String status;
private String result;
}
}
@@ -16,6 +16,11 @@ public class ExtractorInput {
*/
private String targetActionDesc;
/**
* 当前执行阶段的 description
*/
private String currentStageDesc;
/**
* 目标 MetaActionInfo
*/
@@ -2,6 +2,7 @@ package work.slhaf.partner.module.action.executor.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@@ -12,6 +13,7 @@ public class ExtractorResult {
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ParamEntry {
private String name;
private String value;
@@ -8,10 +8,9 @@ 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.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.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;
@@ -19,6 +18,7 @@ import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAg
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.StateHintContent;
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;
@@ -42,8 +42,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
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 static final String TENDENCIES_EVALUATING_BLOCK_NAME = "pending_action_intentions";
private final ActionAssemblyHelper assemblyHelper = new ActionAssemblyHelper();
@@ -83,99 +82,89 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
return;
}
appendTendencyBlock(tendencies);
evaluateTendency(context.getSource(), input, extractorResult);
evaluateTendency(context.getSource(), 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),
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
buildTendenciesEvaluatingAbstractBlock(tendencies, datetime),
Set.of(ContextBlock.FocusedDomain.ACTION),
60,
18,
4
60, 18, 4
));
cognitionCapability.contextWorkspace().register(StateHintContent.createBlock(new StateHintContent(
TENDENCIES_EVALUATING_BLOCK_NAME,
"Partner intends to handle the latest input through actions. When replying to the user, present this as Partner preparing to take action, not as an internal evaluation process."
) {
@Override
public void fillStateContent(@NotNull Document document, @NotNull Element stateElement) {
appendListElement(document, stateElement, "action_intentions", "action_intention", tendencies);
}
}));
}
private @NotNull BlockContent buildTendenciesEvaluatingAbstractBlock(List<String> tendencies, String datetime) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName()) {
@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.");
appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " action intentions that Partner is preparing to handle.");
}
};
}
private @NotNull BlockContent buildTendenciesEvaluatingCompactBlock(List<String> tendencies, String datetime) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName()) {
@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) {
private void evaluateTendency(String source, ExtractorResult extractorResult) {
executor.execute(() -> {
EvaluatorInput evaluatorInput = assemblyHelper.buildEvaluatorInput(extractorResult);
List<EvaluatorResult> evaluatorResults = actionEvaluator.execute(evaluatorInput); // 并发操作均为访问
handleEvaluatorResults(evaluatorResults, source, input);
handleEvaluatorResults(evaluatorResults, source);
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;
private void handleEvaluatorResults(List<EvaluatorResult> evaluatorResults, String source) {
boolean hasPendingConfirmation = false;
List<String> refusedTendencies = new ArrayList<>();
for (EvaluatorResult evaluatorResult : evaluatorResults) {
expireResolvedPending(evaluatorResult);
if (!evaluatorResult.isOk()) {
refusedTendencies.add(evaluatorResult.getTendency());
continue;
}
ExecutableAction executableAction = assemblyHelper.buildActionData(evaluatorResult, source);
if (executableAction == null) {
continue;
}
passedActions.add(executableAction);
if (evaluatorResult.isNeedConfirm()) {
registerPendingContextBlock(executableAction, evaluatorResult, input);
pendingConfirmCount++;
hasPendingConfirmation = true;
registerPendingContextBlock(executableAction, evaluatorResult);
continue;
}
executeOrSchedule(executableAction);
approvedExecutableCount++;
}
if (passedActions.isEmpty()) {
return;
if (!refusedTendencies.isEmpty()) {
cognitionCapability.initiateTurn("Some candidate action tendencies were refused during evaluation. Only reply if the user has not already received a suitable response and one of the refused tendencies was explicitly promised or implied as executable. If a response has already been given, or no promise was made, respond with NO_REPLY.", source, getModuleName());
}
if (hasPendingConfirmation) {
cognitionCapability.initiateTurn("Some actions are pending user confirmation. Ask the user to confirm before executing them.", source, getModuleName());
}
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) {
@@ -192,47 +181,30 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
);
}
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) {
private void registerPendingContextBlock(ExecutableAction executableAction, EvaluatorResult evaluatorResult) {
cognitionCapability.contextWorkspace().register(StateHintContent.createBlock(new StateHintContent(
"actions_need_confirmation",
"Partner is waiting for user confirmation before executing a pending action."
) {
@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());
public void fillStateContent(@NotNull Document document, @NotNull Element stateElement) {
appendTextElement(document, stateElement, "state", "need_user_confirm");
appendTextElement(document, stateElement, "action_type", evaluatorResult.getType());
appendTextElement(document, stateElement, "tendency", executableAction.getTendency());
appendTextElement(document, stateElement, "reason", executableAction.getReason());
appendTextElement(document, stateElement, "description", executableAction.getDescription());
appendTextElement(document, stateElement, "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());
appendTextElement(document, stateElement, "schedule_type", scheduleData.getType());
appendTextElement(document, stateElement, "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);
stateElement.appendChild(chainRoot);
List<Integer> orders = new ArrayList<>(primaryActionChain.keySet());
orders.sort(Integer::compareTo);
for (Integer order : orders) {
@@ -247,33 +219,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
);
}
}
};
}
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) {
@@ -306,7 +252,6 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
if (!notified.compareAndSet(false, true)) {
return Unit.INSTANCE;
}
watcherSelfTalk(action);
return Unit.INSTANCE;
})
);
@@ -314,34 +259,11 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
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() {
}
@@ -357,7 +279,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
if (actionChain == null) {
return null;
}
return switch (evaluatorResult.getType()) {
ExecutableAction executableAction = switch (evaluatorResult.getType()) {
case PLANNING -> new SchedulableExecutableAction(
evaluatorResult.getTendency(),
actionChain,
@@ -375,6 +297,27 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
userId
);
};
executableAction.getStageDescriptions().putAll(getStageDescriptions(evaluatorResult));
return executableAction;
}
private Map<Integer, String> getStageDescriptions(EvaluatorResult evaluatorResult) {
Map<Integer, String> descriptions = new HashMap<>();
List<EvaluatorResult.ChainElement> primaryActionChain = evaluatorResult.getPrimaryActionChain();
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return descriptions;
}
List<EvaluatorResult.ChainElement> orderedChain = new ArrayList<>(primaryActionChain);
orderedChain.sort(Comparator.comparing(EvaluatorResult.ChainElement::getOrder, Comparator.nullsLast(Integer::compareTo)));
int fixedOrder = 1;
for (EvaluatorResult.ChainElement chainElement : orderedChain) {
String description = chainElement.getDescription();
if (description != null && !description.isBlank()) {
descriptions.put(fixedOrder, description);
}
fixedOrder++;
}
return descriptions;
}
private Map<Integer, List<MetaAction>> getActionChain(EvaluatorResult evaluatorResult) {
@@ -399,6 +342,9 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
}
private boolean fixDependencies(Map<Integer, List<String>> primaryActionChain) {
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return false;
}
// 先将 primaryActionChain 的节点序号修正为从1开始依次增大
fixOrder(primaryActionChain);
List<Integer> fixedOrders = new ArrayList<>(primaryActionChain.keySet().stream().toList());
@@ -409,6 +355,9 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
for (Integer fixedOrder : fixedOrders) {
int lastOrder = fixedOrder - 1;
List<String> actionKeys = primaryActionChain.get(fixedOrder);
if (actionKeys == null || actionKeys.isEmpty()) {
continue;
}
for (String actionKey : actionKeys) {
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
@@ -451,9 +400,12 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
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));
List<Integer> orders = new ArrayList<>(tempChain.keySet());
orders.sort(Integer::compareTo);
int fixedOrder = 1;
for (Integer order : orders) {
List<String> actionKeys = tempChain.get(order);
primaryActionChain.put(fixedOrder++, actionKeys == null ? new ArrayList<>() : new ArrayList<>(actionKeys));
}
}
@@ -6,10 +6,10 @@ 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.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.core.cognition.context.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;
@@ -26,51 +26,174 @@ import java.util.concurrent.ExecutorService;
public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责评估单条行动倾向,并判断它是否值得进入后续行动链。
你负责评估单条行动倾向 tendency,并判断它在当前系统状态下是否可以进入后续行动链。
你会收到:
- 一条结构化上下文消息,其中可能包含当前活跃的行动相关状态、近期交流轨迹、认知相关上下文、以及当前活跃记忆切片;
- 一组 <available_meta_actions>,表示当前可用的 MetaAction 候选;
- 一条最新的 user message,其内容就是当前需要评估的单条 tendency。
- 一条结构化上下文消息,其中可能包含 action、communication、perceive、memory、cognition 等上下文;
- 一组 <available_meta_actions>,表示当前系统真实可用的 MetaAction 候选;
- 一条 user message,其内容是上游 ActionExtractor 提取出的单条 tendency。
重要前提:
- 你的输入 tendency 已经由 ActionExtractor 根据当前输入和上下文提取完成。
- 你不负责重新判断 tendency 是否来自最新用户输入。
- 你不负责重新做意图抽取、指代解析或上下文对齐。
- recent_chatmessage 中的最后一条不一定是最新输入,可能只是上一轮对话记录;不要依赖它来否定 tendency。
- 除非上下文中存在明确证据表明 tendency 已过期、已被取消、已被覆盖或与当前行动状态冲突,否则应把 tendency 当作当前待评估的候选行动。
你的任务:
- 判断该 tendency 是否成立、是否值得推进;
- 判断它更适合立即执行,还是先进入规划;
- 判断是否需要先向用户确认;
- 若可推进,则基于 available_meta_actions 生成 primaryActionChain;
- 若当前 tendency 与某个待处理 pending 明确对应,且本轮已完成承接或推进,应正确填写 resolvedPending;
- 若当前 tendency 明确包含可调度语义,再填写 scheduleData。
- 判断该 tendency 在当前系统状态下是否可推进;
- 判断完成该 tendency 是否需要行动链;
- 判断 available_meta_actions 是否能承接该 tendency;
- 判断是否需要用户确认;
- 判断该 tendency 更适合立即执行还是进入规划;
- 输出严格符合 EvaluatorResult 的评估结果。
评估原则:
- 结合上下文理解当前 tendency 与近期交流、当前行动状态、活跃记忆之间的关系。
- 若 tendency 与已有正在执行、等待确认、或已明确覆盖的行动完全等价,通常不应重复建立新的行动链。
- 若 tendency 是对已有待确认行动的确认、拒绝、补充条件、修改要求或继续推进,则应优先视为对原有行动状态的承接,而不是无关新任务。
- 若 action 相关上下文中存在等待确认的 block,且当前 tendency 明显与其相关,则必须显式承接这一点,不要因为其已存在于上下文中而省略。
- 只有在 tendency 明确具有可执行意义时,才返回 ok=true。
- 若 tendency 更适合直接交流回应,或尚不足以形成行动推进,则返回 ok=false。
- primaryActionChain 中只能使用 available_meta_actions 内出现的 action_key,不要编造不存在的动作。
- 不要输出完整执行细节、自然语言计划正文或额外解释,只输出 EvaluatorResult 对应结构。
核心原则:
- 不要把“需要确认”误判成“不可执行”。
- 不要把“有风险”误判成“用户没有要求”。
- 不要把“能力不足”误判成“用户没有要求”。
- 不要因为 recent_chatmessage 没有出现对应内容就否定 tendency。
- 不要根据上一轮聊天内容推翻当前 tendency。
- 只有在可行性、能力、风险、状态覆盖、信息完整性等评估层面拒绝 tendency。
关于字段:
- 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 通常应留空或保持无效默认值,不要强行填充。
决策流程:
0. 优先处理 pending action 的确认、拒绝、修改或取消
- 如果 action 上下文中存在等待用户确认的 pending action,且 tendency 表达的是确认、同意、继续、执行、可以、拒绝、取消、修改、补充条件等承接行为,应优先按 pending action 承接处理。
- 这类 tendency 不是新的独立行动,不要为“确认、拒绝、继续”本身寻找 available_meta_actions。
- 不要因为 pending action 有副作用就再次设置 needConfirm=true;用户确认 pending action 时,needConfirm 必须为 false,避免确认循环。
- 若 tendency 表示确认、同意、继续或执行 pending action:
- 应返回 ok=true;
- needConfirm=false;
- resolvedPending 应填写对应 pending block 的 blockName 与 source;
- primaryActionChain 应从 pending action 的 primary_action_chain 承接;
- type 应从 pending action 的 action_type 承接;
- scheduleData 应从 pending action 的 schedule 信息承接;若 pending action 没有调度信息则留空;
- reason 应说明用户已确认该 pending action,现在可以执行。
- 若 tendency 表示拒绝、取消或不执行 pending action:
- 应填写 resolvedPending;
- ok=false;
- reason 应说明用户取消了 pending action,因此不再推进;
- 不要生成 primaryActionChain。
- 若 tendency 表示修改 pending action、补充条件或改变目标:
- 应填写 resolvedPending;
- 不要把它当作全新无关任务;
- 若修改后的目标足够明确且 available_meta_actions 可承接,则按修改后的行动返回 ok=true;
- 若信息不足,则 ok=false,并说明需要澄清。
- 只有当 action 上下文中不存在相关 pending action,或 tendency 明显不是对 pending action 的承接时,才继续后续可行性评估。
1. 判断是否已经被现有 action 状态覆盖
- 若 tendency 与已有正在执行、等待确认、已完成且仍有效的 action 完全等价,通常不应重复建立行动链。
- 若 tendency 与现有 action 相关但不是重复,而是追加约束、改变目标或要求继续,应按新的行动需求评估。
- 若拒绝是因为已有 action 覆盖,ok=false,并在 reason 中说明覆盖关系。
- 不要仅因为 conversation 中存在相似历史内容就认为已覆盖;必须有 action 状态证据。
2. 判断是否需要行动链
按照完成条件判断,而不是按行为类别死板判断:
- 如果完成 tendency 需要系统获取当前状态、访问资源、调用能力、操作对象、改变状态、触发过程、安排未来动作或向外部系统发出请求,则通常需要行动链。
- 如果完成 tendency 只需要基于已有上下文进行解释、分析、总结、评价、翻译、改写、闲聊或普通交流,且不需要任何系统能力介入,则通常不需要行动链。
- 如果 tendency 的完成结果依赖“实际执行后发生什么”“当前对象真实状态是什么”“外部资源现在是什么结果”,则通常需要行动链。
- 如果用户目标本身是让系统代为完成某件事,而不是只要建议、说明或解释,则通常需要行动链。
这里的“对象”可以是任何可被系统能力作用的东西,包括本地资源、远程资源、应用状态、文件系统、运行环境、记忆、计划、任务、接口、页面、数据、设备或会话状态。
这里的“产生影响”包括改变对象状态、创建结果、触发过程、安排未来动作或向外部系统发出请求。
不要试图枚举所有用户行为;依据“是否需要系统能力介入才能完成”来判断。
3. 判断 available_meta_actions 是否能承接
ok=true 的必要条件:
- tendency 当前没有被已有 action 状态覆盖;
- 完成 tendency 需要行动链;
- available_meta_actions 中存在能承接该 tendency 的 action_key;
- 可以生成非空 primaryActionChain。
规则:
- ok=true 时 primaryActionChain 必须非空。
- primaryActionChain 只能使用 available_meta_actions 中真实存在的 action_key。
- 不要编造 action_key。
- 对 pending action 的确认行为,不要为“确认”本身寻找 action_key;应从 pending action 中承接原 action chain。
- 如果没有任何可用能力能承接,即使 tendency 本身合理,也必须 ok=false。
- 如果无法承接,reason 应说明缺少能力、能力不匹配、信息不足或策略限制,不要说用户没有要求。
4. 判断信息是否足够
- 若 tendency 的目标、对象、范围、参数或约束足够明确,可以继续评估可执行性。
- 若缺少必要信息,但可以通过向用户确认或澄清补足,应 ok=false,并说明需要澄清。
- 若缺少的信息可以通过已有 action/context/pending 状态明确补足,则可以继续推进。
- 不要因为 tendency 中的信息来自上游解析或上下文补全就认为信息不足;只判断当前 tendency 自身是否足以形成行动链。
5. 判断是否需要确认
needConfirm 判断的是“是否需要先获得用户确认”,不是“是否能执行”。
- needConfirm=true 仍然可以 ok=true。
- 如果当前 tendency 是对已有 pending action 的确认、同意、继续或执行,needConfirm 必须为 false;确认行为不能再次触发确认。
- 若行动可能产生副作用、不可逆影响、权限风险、隐私风险、安全风险、资源消耗、外部可见影响、长期运行影响,或用户可能希望先确认,则应 needConfirm=true。
- 若行动是低风险、短时、可逆、范围明确、授权清晰,且系统策略允许直接推进,则可 needConfirm=false。
- 如果风险或目标不确定到无法形成可确认行动,应 ok=false,并说明需要澄清或缺少必要信息。
- 不要因为行动有副作用就直接拒绝;先判断是否可以通过 needConfirm 承接。
- 不要把“需要确认”写成“无法执行”。
6. 判断 type
- IMMEDIATE:目标清楚,能力链清楚,可以直接由当前 action chain 推进。
- PLANNING:目标需要拆解、多步协调、长期推进、条件判断或中间决策。
- ok=false 时,type 不应表达可执行状态;若结构必须填默认值,不要在 reason/description 中暗示会执行。
7. 输出字段要求
primaryActionChain:
- 只在 ok=true 时填写。
- 每个元素包含 order、description 和 actionKeys。
- description 是该 stage 的具体执行目标,用一句短句说明本阶段要获得、检查、修改或触发什么;它不是用户回复文案,也不是完整推理过程。
- 不要写伪代码。
- 若 tendency 包含多个依赖步骤、需要先获取 A 再根据 A 决定 B、需要多次调用同一能力处理不同对象,或需要为后续汇报获取多类事实,应输出多 order 的 primaryActionChain。
- 不要为了减少步骤而把复杂诊断、查找、读取、分析全部压缩成单个 command::execute。
- 同一个 available_meta_action 可以在不同 order 中重复使用,只要每次对应的阶段目标、参数或对象不同。
- 对需要最终汇报的任务,行动链应负责获取支撑汇报所需的事实;最终自然语言汇报由 Communication 基于 action-finished state 完成,不需要把“汇报”本身作为一个 MetaAction。
scheduleData:
- 仅当 tendency 明确要求未来、周期、延迟、提醒、定时或计划安排时填写。
- 没有调度语义时留空。
- 不要为了完整性强行填写。
resolvedPending:
- 仅当当前 tendency 明确承接某个 pending block 时填写。
- 无法明确对应时留空。
- 不要猜测。
reason:
- 说明可行性判断依据。
- 若 ok=true,说明为什么当前可以推进、使用能力链是否充分、是否需要确认。
- 若 ok=false,必须说明真正失败的层级:
- 不需要行动链,直接交流即可;
- 已有 action 覆盖;
- 缺少可用 MetaAction;
- action_key 无法承接;
- 必要信息不足;
- 指代或目标不明确;
- 风险/权限/策略限制导致无法推进;
- 需要澄清。
- 不得把“能力不足”“需要确认”“有风险”“信息不足”“上下文缺失”写成“用户没有要求”。
- 对确认类 tendency,不得输出“缺少确认动作能力”“确认本身不需要行动链”作为拒绝理由;应判断是否能承接 pending action。
- 不得基于 recent_chatmessage 的上一轮内容否定 tendency。
- 不得输出“用户当前并未提出该要求”这类意图抽取判断,除非上下文中存在明确取消、冲突或过期证据。
description:
- 简短概括处理结论。
- 若 ok=true,描述将要推进的行动。
- 若 needConfirm=true,描述应体现该行动需要确认。
- 若 ok=false,描述应体现不推进的真实原因。
输出要求:
- 严格按照 EvaluatorResult 对应结构输出。
- 不要输出结构之外的解释、注释或额外文本。
- 不要输出结构之外的解释、注释、推理过程或额外文本。
""";
@InjectCapability
@@ -104,8 +227,9 @@ public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, Lis
cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.ACTION,
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.COGNITION,
ContextBlock.FocusedDomain.MEMORY
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.MEMORY,
ContextBlock.FocusedDomain.COGNITION
)).encodeToMessage(),
availableMetaActionContext(),
new Message(Message.Character.USER, tendency)
@@ -144,8 +268,8 @@ public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, Lis
"available_meta_action",
actionCapability.listAvailableMetaActions().entrySet(),
(block, value) -> {
appendTextElement(document, root, "action_key", value.getKey());
appendTextElement(document, root, "action_value", value.getValue().getDescription());
appendTextElement(document, block, "meta_action_key", value.getKey());
appendTextElement(document, block, "meta_action_description", value.getValue().getDescription());
return Unit.INSTANCE;
}
);
@@ -3,6 +3,7 @@ package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data;
import work.slhaf.partner.core.action.entity.Schedulable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -31,9 +32,15 @@ public class EvaluatorResult {
}
public Map<Integer, List<String>> getPrimaryActionChainAsMap() {
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return new LinkedHashMap<>();
}
return primaryActionChain.stream().collect(Collectors.toMap(
ChainElement::getOrder,
ChainElement::getActionKeys,
chainElement -> {
List<String> actionKeys = chainElement.getActionKeys();
return actionKeys == null ? new ArrayList<>() : new ArrayList<>(actionKeys);
},
(oldValue, newValue) -> newValue,
LinkedHashMap::new
));
@@ -48,6 +55,7 @@ public class EvaluatorResult {
@Data
public static class ChainElement {
private Integer order;
private String description;
private List<String> actionKeys;
}
}
@@ -2,7 +2,7 @@ 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.core.cognition.context.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;
@@ -17,19 +17,52 @@ import java.util.List;
public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责从当前输入中提取可能的行动倾向,供后续模块继续评估。
你负责从当前输入中提取“可能值得进入后续行动评估”的行动倾向。
你会收到:
- 一条结构化上下文消息,主要包含 communication 域与 action 域内容;
- 一条结构化上下文消息,主要包含 communication 域、perceive 域与 action 域内容;
- 一条最新输入,作为本轮重点分析对象。
规则:
- communication 域主要用于理解当前会话语境、主题延续与用户意图。
- action 域主要用于判断相关行动是否已经在执行、等待确认,或已被覆盖。
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤或工具细节。
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息或条件变化,应避免重复提出。
你的任务:
- 基于当前输入与上下文,提取少量、明确、值得继续进入行动评估的行动倾向;
- 这里只提取“可能需要通过行动推进”的倾向,而不是所有可回应的意图;
- 若当前输入更适合直接交流回应,而不需要额外行动推进,则返回空结果。
基本原则:
- communication 域主要用于理解当前会话语境、话题延续、指代对象与用户真实意图。
- perceive 域可用于理解当前系统状态、环境信息、时间信息等;若当前输入仅是在询问这些已可直接回答的信息,不应因此提取行动倾向。
- action 域主要用于判断相关行动是否已经在执行、等待确认、或已被覆盖。
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤、工具细节或命令内容。
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息、确认信息或条件变化,应避免重复提出。
- 若 action 域中存在等待确认的 block,且当前输入与其相关,则必须提取出对应的行动倾向;不要因为其已存在于上下文中而省略。
- 若当前输入没有明显行动倾向,可返回空结果。
复合任务合并规则:
- 若当前输入包含“先...再...最后...”“并且/然后/同时”等多个动作描述,但这些动作共同服务于同一个最终目标,应提取为一个整体行动倾向,而不是拆成多个并列倾向。
- 中间的信息获取、检查、读取、查询步骤如果只是为了支撑最终判断、汇报、生成结果,不应作为独立 tendency 输出。
- 最终的“总结、判断、汇报、说明结果”如果依赖前置行动结果,应并入同一个整体 tendency,而不是作为单独的 communication 类 tendency 输出。
- 只有当多个动作目标彼此独立、可以分别完成且不共享同一个最终目的时,才输出多个 tendency。
什么情况下应提取行动倾向:
- 用户明确要求系统执行某个动作、调用某项能力、访问某类外部信息、操作某个对象或推进某个待办;
- 用户要求修改、继续、取消、确认或补充某个已有待处理行动;
- 用户明确要求系统代其完成某件事,而不是仅作解释、说明或回答;
- 若不进入行动链,则该请求无法被真正推进或完成。
什么情况下不应提取行动倾向:
- 当前输入本质上是一个可直接回答的提问;
- 当前输入只是要求解释、分析、总结、说明、翻译、评价、闲聊或感叹;
- 当前输入只是询问当前上下文、系统状态、已知信息、记忆内容或当前可见内容,而这些内容可直接通过交流给出;
- 当前输入只是一般性的信息查询,但没有明确要求系统通过外部行动补充信息;
- 当前输入只是对已有回复的轻量追问、澄清或自然延续,仍然更适合直接回应;
- 当前输入语义模糊,仅凭“可能有东西可做”不足以进入行动评估;
- 当前输入虽然提到某个对象或任务,但并未形成明确的行动推进意图。
重要限制:
- 不要把“可以通过行动获得更多信息”当成提取理由。
- 不要把“询问信息”自动等同于“要求去执行查询动作”。
- 不要为了提高召回而泛化提取。
- 如果更适合 communication 直接回答,就返回空结果。
- 倾向应简洁明确,表达“要推进什么”,不要写成自然语言执行方案。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
@@ -44,6 +77,7 @@ public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<Extr
List<Message> messages = List.of(
cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.ACTION
)).encodeToMessage(),
new Message(Message.Character.USER, input)
@@ -15,12 +15,14 @@ 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.config.ConfigCenter
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.nio.file.Path
import java.time.Duration
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
@@ -70,8 +72,9 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
val doneCondition: (Schedulable) -> Boolean = { schedulable ->
if (schedulable is Action) {
schedulable.status == Action.Status.FAILED || schedulable.status == Action.Status.SUCCESS
} else {
true
}
true
}
timeWheel = TimeWheel(listScheduledActions, onTrigger, doneCondition)
}
@@ -126,6 +129,12 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
val onTrigger: (toTrigger: Set<Schedulable>) -> Unit,
val doneCondition: (schedulable: Schedulable) -> Boolean
) : Closeable {
private val tracePath: Path = ConfigCenter.paths.stateDir
.resolve("trace")
.resolve("time-wheel")
.normalize()
.toAbsolutePath()
private val schedulableGroupByHour = Array<MutableSet<Schedulable>>(24) { mutableSetOf() }
private val wheel = Array<MutableSet<Schedulable>>(60 * 60) { mutableSetOf() }
private var recordHour: Int = -1
@@ -151,15 +160,14 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
logFailedStatus(schedulableData)
return@checkThenExecute
}
log.debug("Action next execution time: {}", parseToZonedDateTime)
log.debug("${schedulableData.uuid} 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)
log.debug("${schedulableData.uuid} scheduled at wheel offset {}", wheelOffset)
}
}
}
@@ -201,8 +209,13 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
val shouldBreak: Boolean
)
fun collectToTrigger(tick: Int, previousTick: Int, triggerHour: Int): Set<Schedulable>? {
if (tick > previousTick) {
fun collectToTrigger(
tick: Int,
previousTick: Int,
triggerHour: Int,
includeRemainingHour: Boolean = false
): Set<Schedulable>? {
if (tick >= previousTick) {
val toTrigger = mutableSetOf<Schedulable>()
for (i in previousTick..tick) {
val bucket = wheel[i]
@@ -213,6 +226,13 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
bucket.clear() // 避免重复触发
}
}
if (includeRemainingHour) {
val remainingHourBucket = schedulableGroupByHour[triggerHour]
if (remainingHourBucket.isNotEmpty()) {
toTrigger.addAll(remainingHourBucket.filter { it.enabled })
remainingHourBucket.clear()
}
}
return toTrigger
}
return null
@@ -261,7 +281,12 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
checkThenExecute(false) {
if (it.hour != launchingHour) {
shouldBreak = true
toTrigger = collectToTrigger(wheel.lastIndex, previousTick, launchingHour)
toTrigger = collectToTrigger(
wheel.lastIndex,
previousTick,
launchingHour,
includeRemainingHour = true
)
log.debug(
"Hour changed, previousTick: {}, tick: {}, toTriggerSize: {}",
previousTick,
@@ -270,8 +295,13 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
)
return@checkThenExecute
}
toTrigger = collectToTrigger(tick, previousTick, launchingHour)
if (tick >= wheel.lastIndex || schedulableGroupByHour[launchingHour].isEmpty()) {
toTrigger = collectToTrigger(
tick,
previousTick,
launchingHour,
includeRemainingHour = tick >= wheel.lastIndex
)
if (tick >= wheel.lastIndex) {
state.value = WheelState.SLEEPING
shouldBreak = true
}
@@ -0,0 +1,50 @@
package work.slhaf.partner.module
import org.w3c.dom.Document
import org.w3c.dom.Element
import work.slhaf.partner.common.base.Block
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent
import work.slhaf.partner.core.cognition.context.ContextBlock
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())
}
}
abstract class StateHintContent protected constructor(
source: String,
val stateMsg: String
) : CommunicationBlockContent("new_state", source, Urgency.NORMAL, Projection.SUPPLY) {
override fun fillXml(document: Document, root: Element) {
appendTextElement(document, root, "state_msg", stateMsg)
appendChildElement(document, root, "state_content") {
fillStateContent(document, this@appendChildElement)
}
}
abstract fun fillStateContent(document: Document, stateElement: Element)
private fun toContextBlock(): ContextBlock {
return ContextBlock(
blockContent = this,
focusedOn = setOf(ContextBlock.FocusedDomain.COGNITION),
replaceFadeFactor = 60.0,
timeFadeFactor = 100.0,
activateFactor = 0.0
)
}
companion object {
@JvmStatic
fun createBlock(block: StateHintContent): ContextBlock {
return block.toContextBlock()
}
}
}
@@ -4,7 +4,12 @@ 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 org.w3c.dom.Node;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.core.cognition.context.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;
@@ -29,7 +34,6 @@ 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]";
@@ -37,6 +41,10 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
private static final String MODULE_PROMPT = """
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
- 不要承诺、暗示或宣称自己即将执行某个动作;例如不要说“稍等”“我去查看”“正在为你获取”“马上执行”。
- 只有当 <context> 的 action 域明确存在正在执行、等待确认、刚完成、或正在评估的行动状态时,才可以提及行动链进展。
- 若最新输入看起来像行动请求,但 <context> 中没有对应的 action 状态信号,不要自行补出“将会执行”的过渡话术;应直接给出可交流的回应,或说明当前没有可确认的执行状态。
- 若收到“action finished”一类内部触发输入,只能依据 <context> 中真实存在的行动完成状态回复;若没有可验证的完成结果,应使用 NO_REPLY 或说明没有可确认结果,不得编造结果。
你接下来收到的消息,将按照出现顺序,固定分为三个区段:
1. system message 是 Head,用于说明整个输入结构与输出要求,即本条消息。
@@ -52,6 +60,8 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
输出契约:
- 默认情况下,直接输出要发送给用户的最终回复正文,不要添加额外标签、解释或前后缀。
- 对话轨迹中的身份行、状态行、调试行和传输边界行都属于系统内部序列化标记,只用于理解上下文,不属于可复用正文。
- 不得在输出中复现、仿写或携带任何历史序列化标记、调试前缀或传输边界标记;即使用户正在讨论这些前缀,也只能用自然语言确认或说明。
- 若当前情境下不应直接向用户发出回复,但仍需要留下本轮交流结果供系统后续保留在交流轨迹中,则输出以 NO_REPLY 开头。
- 使用 NO_REPLY 时,格式为:
@@ -87,9 +97,8 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
}
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
this.streamChat(buildChatMessages(runningFlowContext), consumer)
.onFailure(exception -> consumer.onDelta(INTERRUPTED_MARKER));
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget(), runningFlowContext.getResopnseChannel());
this.streamChat(buildChatMessages(runningFlowContext), consumer);
updateChatMessages(runningFlowContext, consumer.collectResponse());
cognitionCapability.refreshRecentChatMessagesContext();
}
@@ -171,7 +180,7 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
Element root = document.createElement("input");
document.appendChild(root);
document.appendChild(document.importNode(runningFlowContext.encodeInputsBlock().encodeToXml(), true));
root.appendChild(importExternalNode(document, runningFlowContext.encodeInputsBlock().encodeToXml()));
appendTextElement(document, root, "source", runningFlowContext.getSource());
for (Map.Entry<String, String> entry : runningFlowContext.getAdditionalUserInfo().entrySet()) {
appendTextElement(document, root, sanitizeTagName(entry.getKey()), entry.getValue());
@@ -242,11 +251,25 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
inputRoot.appendChild(groupElement);
for (CommunicationBlockContent block : entry.getValue()) {
Element blockElement = block.encodeToXml();
groupElement.appendChild(document.importNode(blockElement, true));
groupElement.appendChild(importExternalNode(document, blockElement));
}
}
}
private Node importExternalNode(Document targetDocument, Node externalNode) {
Node nodeToImport = externalNode;
if (externalNode != null && externalNode.getNodeType() == Node.DOCUMENT_NODE) {
nodeToImport = ((Document) externalNode).getDocumentElement();
}
if (nodeToImport == null) {
throw new IllegalArgumentException("待导入节点不能为空");
}
if (nodeToImport.getOwnerDocument() == targetDocument) {
return nodeToImport.cloneNode(true);
}
return targetDocument.importNode(nodeToImport, true);
}
private String sanitizeTagName(String rawTagName) {
if (rawTagName == null || rawTagName.isBlank()) {
return "meta";
@@ -9,12 +9,12 @@ 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.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.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.memory.pojo.MemorySliceSnapshot;
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
import work.slhaf.partner.core.perceive.PerceiveCapability;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
@@ -31,6 +31,7 @@ import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -41,7 +42,8 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
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 static final int FULL_ROLLING_RETAIN_DIVISOR = Integer.MAX_VALUE;
private static final int DIALOG_ROLLING_TRIGGER_LIMIT = 72;
private final AtomicBoolean rolling = new AtomicBoolean(false);
@@ -119,7 +121,8 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
return;
}
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), CONTEXT_RETAIN_DIVISOR);
int retainDivisor = refreshMemoryId ? FULL_ROLLING_RETAIN_DIVISOR : CONTEXT_RETAIN_DIVISOR;
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), retainDivisor);
applyRolling(result);
afterRollingRegistry.trigger(result);
@@ -138,7 +141,7 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
if (memoryId.isBlank()) {
return fullChatSnapshot;
}
MemoryUnit existingUnit = memoryCapability.getMemoryUnit(memoryId);
MemoryUnitSnapshot existingUnit = memoryCapability.getMemoryUnit(memoryId);
if (existingUnit.getConversationMessages().isEmpty()) {
return fullChatSnapshot;
}
@@ -156,8 +159,9 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
@NotNull
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
messageCompressor.execute(chatSnapshot);
Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
List<Message> rollingMessages = new ArrayList<>(chatSnapshot);
messageCompressor.execute(rollingMessages);
Result<String> summaryResult = messageSummarizer.execute(rollingMessages);
String summary = summaryResult.fold(
value -> value,
exp -> "no summary, due to exception"
@@ -165,20 +169,20 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
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);
MemoryUnitSnapshot memoryUnit = memoryCapability.updateMemoryUnit(rollingMessages, summary);
MemorySliceSnapshot newSlice = memoryUnit.getSlices().getLast();
return new RollingResult(memoryUnit, newSlice, rollingSize, retainDivisor);
}
private void applyRolling(RollingResult result) {
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildDialogAbstractBlock(result.summary(), result.memoryUnit().getId(), result.memorySlice().getId()),
buildDialogAbstractBlock(result.getSummary(), result.getMemoryUnit().getId(), result.getMemorySlice().getId()),
Set.of(ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.COMMUNICATION),
20,
5,
10
));
cognitionCapability.rollChatMessagesWithSnapshot(result.rollingSize(), result.retainDivisor());
cognitionCapability.rollChatMessagesWithSnapshot(result.getRollingSize(), result.getRetainDivisor());
}
private @NotNull BlockContent buildDialogAbstractBlock(String summary, String unitId, String sliceId) {
@@ -1,166 +1,203 @@
package work.slhaf.partner.module.communication
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import work.slhaf.partner.api.InteractionEvent.EventStatus
import work.slhaf.partner.api.ReplyEvent
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler
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,
fun createConsumer(target: String, responseChannel: String?): StreamChatMessageConsumer = ReplyConsumer(
target = target,
responseChannel = responseChannel
)
private data class ReplyChunk(
val delta: String,
val target: String,
)
}
private class ReplyConsumer(
private val collectorChannel: Channel<ReplyChunk>,
private val target: String,
) : StreamChatMessageConsumer() {
private class ReplyConsumer(
private val target: String,
private val responseChannel: String?
) : StreamChatMessageConsumer {
private enum class VisibilityState {
UNDECIDED,
NO_REPLY,
VISIBLE
}
private enum class VisibilityState {
UNDECIDED,
NO_REPLY,
VISIBLE
}
private val rawResponse = StringBuilder()
private val undecidedBuffer = StringBuilder()
private var visibilityState = VisibilityState.UNDECIDED
private val rawResponse = StringBuilder()
private val undecidedBuffer = StringBuilder()
private val visibleBuffer = StringBuilder()
private val lock = Any()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun onDelta(delta: String) {
private var flushJob: Job? = null
private var visibilityState = VisibilityState.UNDECIDED
private var terminalEmitted = false
override fun onDelta(delta: String) {
synchronized(lock) {
rawResponse.append(delta)
routeDelta(delta)
}
}
override fun collectResponse(): String {
override fun onComplete() {
synchronized(lock) {
finalizeUndecidedBuffer()
flushVisibleBufferLocked()
dispatchTerminalLocked(EventStatus.DONE)
shutdownScopeLocked()
}
}
override fun onError(exception: AgentRuntimeException) {
synchronized(lock) {
ExceptionReporterHandler.report(exception)
rawResponse.append(INTERRUPTED_MARKER)
routeDelta(INTERRUPTED_MARKER)
finalizeUndecidedBuffer()
flushVisibleBufferLocked()
dispatchTerminalLocked(EventStatus.ERROR)
shutdownScopeLocked()
}
}
override fun collectResponse(): String {
synchronized(lock) {
finalizeUndecidedBuffer()
flushVisibleBufferLocked()
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 routeDelta(delta: String) {
when (visibilityState) {
VisibilityState.NO_REPLY -> return
VisibilityState.VISIBLE -> appendVisibleLocked(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)
private fun resolveVisibility() {
val content = undecidedBuffer.toString()
if (content.length <= NO_REPLY_MARKER.length) {
if (NO_REPLY_MARKER.startsWith(content)) {
return
}
revealBufferedContent()
return
}
private fun finalizeUndecidedBuffer() {
if (visibilityState != VisibilityState.UNDECIDED) {
return
}
if (undecidedBuffer.toString() == NO_REPLY_MARKER) {
visibilityState = VisibilityState.NO_REPLY
undecidedBuffer.setLength(0)
return
}
if (!content.startsWith(NO_REPLY_MARKER)) {
revealBufferedContent()
return
}
private fun revealBufferedContent() {
visibilityState = VisibilityState.VISIBLE
if (undecidedBuffer.isNotEmpty()) {
flushVisible(undecidedBuffer.toString())
undecidedBuffer.setLength(0)
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()) {
appendVisibleLocked(undecidedBuffer.toString())
undecidedBuffer.setLength(0)
}
}
private fun appendVisibleLocked(delta: String) {
if (delta.isEmpty()) {
return
}
visibleBuffer.append(delta)
scheduleFlushLocked()
}
private fun scheduleFlushLocked() {
flushJob?.cancel()
flushJob = scope.launch {
delay(AGGREGATE_WINDOW_MILLIS.milliseconds)
synchronized(lock) {
flushVisibleBufferLocked()
}
}
}
private fun flushVisible(delta: String) {
collectorChannel.trySend(ReplyChunk(delta, target)).isSuccess
private fun flushVisibleBufferLocked() {
flushJob?.cancel()
flushJob = null
if (visibleBuffer.isEmpty()) {
return
}
val content = visibleBuffer.toString()
visibleBuffer.setLength(0)
dispatchLocked(
ReplyEvent(
status = EventStatus.RUNNING,
target = target,
content = content,
mode = ReplyEvent.ContentMode.APPEND
)
)
}
override fun consumeDelta(delta: String?) {
private fun dispatchTerminalLocked(status: EventStatus) {
if (terminalEmitted) {
return
}
terminalEmitted = true
dispatchLocked(
ReplyEvent(
status = status,
target = target,
content = "",
mode = ReplyEvent.ContentMode.APPEND
)
)
}
private fun dispatchLocked(event: ReplyEvent) {
if (responseChannel == null) {
AgentRuntime.response(event)
} else {
AgentRuntime.response(event, responseChannel)
}
}
private fun shutdownScopeLocked() {
flushJob?.cancel()
flushJob = null
scope.cancel()
}
private companion object {
private const val AGGREGATE_WINDOW_MILLIS = 100L
private const val INTERRUPTED_MARKER = "\n[response interrupted due to internal exception]"
private const val NO_REPLY_MARKER = "NO_REPLY"
}
}
@@ -1,17 +0,0 @@
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
) {
}
@@ -0,0 +1,17 @@
package work.slhaf.partner.module.communication
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot
import work.slhaf.partner.framework.agent.model.pojo.Message
data class RollingResult(
val memoryUnit: MemoryUnitSnapshot,
val memorySlice: MemorySliceSnapshot,
val rollingSize: Int,
val retainDivisor: Int,
) {
val summary: String
get() = memorySlice.summary ?: ""
fun incrementMessages(): List<Message> = memoryUnit.messagesOf(memorySlice)
}
@@ -65,7 +65,7 @@ public class MessageSummarizer extends AbstractAgentModule.Sub<List<Message>, Re
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
document.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
root.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
appendListElement(document, root, "chat_messages", "message", messages, (element, message) -> {
element.setAttribute("role", message.roleValue());
element.setTextContent(message.getContent());
@@ -0,0 +1,54 @@
package work.slhaf.partner.module.impression;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.context.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.runtime.PartnerRunningFlowContext;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
public class ImpressionRecaller extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
@InjectCapability
private CognitionCapability cognitionCapability;
/**
* 从交互中积累谈论的内容的特征(证据),基于证据创建 ActiveEntity,然后交给 CognitionCapability 进行投影并更新上下文
*/
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
val contextWorkspace = cognitionCapability.contextWorkspace();
context.getInputs()
.stream()
.map(inputEntry -> {
val content = inputEntry.getContent();
return cognitionCapability.projectEntity(content);
})
.flatMap(Collection::stream)
.collect(Collectors.toSet())
.forEach(activeEntity -> {
contextWorkspace.register(new ContextBlock(
activeEntity,
activeEntity,
activeEntity,
Set.of(
ContextBlock.FocusedDomain.COGNITION,
ContextBlock.FocusedDomain.MEMORY
),
100,
0.5,
20
));
});
}
@Override
public int order() {
return 2;
}
}
@@ -0,0 +1,16 @@
package work.slhaf.partner.module.impression;
import java.util.List;
public record ImpressionUpdateApplyResult(
List<String> createdEntityUuids
) {
public ImpressionUpdateApplyResult {
createdEntityUuids = createdEntityUuids == null ? List.of() : List.copyOf(createdEntityUuids);
}
public static ImpressionUpdateApplyResult empty() {
return new ImpressionUpdateApplyResult(List.of());
}
}
@@ -0,0 +1,19 @@
package work.slhaf.partner.module.impression;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
public record ImpressionUpdateContext(
String memoryUnitId,
String memorySliceId,
String summary,
int rollingSize,
int retainDivisor,
int sliceStartIndex,
int sliceEndIndex,
long sliceTimestamp,
long unitTimestamp,
List<Message> incrementMessages
) {
}
@@ -0,0 +1,66 @@
package work.slhaf.partner.module.impression
/**
* A conservative, auditable plan produced after message rolling.
*
* The updater should treat this model as intent only: validation decides whether
* a step is safe to execute, and the applier performs mutations through
* CognitionCapability / ImpressionCore so indexes stay consistent.
*/
data class ImpressionUpdatePlan @JvmOverloads constructor(
val steps: List<ImpressionUpdateStep>,
val status: PlanStatus = PlanStatus.PREPARED,
val reason: String? = null,
)
enum class PlanStatus {
PREPARED,
CONFIRMED,
REJECTED,
}
sealed class ImpressionUpdateStep
data class UpdateExistingStep(
val entityUuid: String,
val updatePatch: UpdatePatch,
) : ImpressionUpdateStep()
data class CreateEntityStep(
val subject: String,
val impressions: List<ImpressionPatch> = emptyList(),
val features: List<FeaturePatch> = emptyList(),
val aliases: List<AliasPatch> = emptyList(),
val relations: List<RelationPatch> = emptyList(),
) : ImpressionUpdateStep()
sealed class UpdatePatch
data class ImpressionPatch @JvmOverloads constructor(
val impression: String,
val newImpression: String? = null,
val confidence: Double = 1.0,
) : UpdatePatch()
data class FeaturePatch @JvmOverloads constructor(
val feature: String,
val newFeature: String? = null,
val confidence: Double = 1.0,
) : UpdatePatch()
data class AliasPatch @JvmOverloads constructor(
val alias: String,
val deprecated: Boolean = false,
) : UpdatePatch()
data class SubjectPatch @JvmOverloads constructor(
val subject: String,
val keepOldSubjectAsAlias: Boolean = true,
) : UpdatePatch()
data class RelationPatch @JvmOverloads constructor(
val target: String,
val relation: String,
val strength: Double = 1.0,
) : UpdatePatch()
@@ -0,0 +1,96 @@
package work.slhaf.partner.module.impression;
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.support.Result;
import java.util.ArrayList;
import java.util.List;
public class ImpressionUpdatePlanApplier extends AbstractAgentModule.Sub<ImpressionUpdatePlan, Result<ImpressionUpdateApplyResult>> {
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected Result<ImpressionUpdateApplyResult> doExecute(ImpressionUpdatePlan plan) {
return apply(plan);
}
public Result<ImpressionUpdateApplyResult> apply(ImpressionUpdatePlan plan) {
return Result.runCatching(() -> {
if (plan == null || plan.getStatus() != PlanStatus.CONFIRMED) {
throw new IllegalArgumentException("only confirmed impression update plans can be applied");
}
List<String> createdEntityUuids = new ArrayList<>();
for (ImpressionUpdateStep step : plan.getSteps()) {
String createdEntityUuid = applyStep(step);
if (createdEntityUuid != null && !createdEntityUuid.isBlank()) {
createdEntityUuids.add(createdEntityUuid);
}
}
return new ImpressionUpdateApplyResult(createdEntityUuids);
});
}
private String applyStep(ImpressionUpdateStep step) {
if (step instanceof UpdateExistingStep updateStep) {
applyPatch(updateStep.getEntityUuid(), updateStep.getUpdatePatch());
return null;
}
if (step instanceof CreateEntityStep createStep) {
String entityUuid = cognitionCapability.createEntity(createStep.getSubject());
if (entityUuid == null || entityUuid.isBlank()) {
throw new IllegalStateException("created entity uuid is blank");
}
applyPatches(entityUuid, createStep.getImpressions());
applyPatches(entityUuid, createStep.getFeatures());
applyPatches(entityUuid, createStep.getAliases());
applyPatches(entityUuid, createStep.getRelations());
return entityUuid;
}
throw new IllegalArgumentException("unsupported impression update step: " + step);
}
private void applyPatches(String entityUuid, List<? extends UpdatePatch> patches) {
for (UpdatePatch patch : patches) {
applyPatch(entityUuid, patch);
}
}
private void applyPatch(String entityUuid, UpdatePatch patch) {
boolean applied = switch (patch) {
case SubjectPatch subjectPatch -> cognitionCapability.renameEntitySubject(
entityUuid,
subjectPatch.getSubject(),
subjectPatch.getKeepOldSubjectAsAlias()
);
case AliasPatch aliasPatch -> cognitionCapability.addEntityAlias(entityUuid, aliasPatch.getAlias(), aliasPatch.getDeprecated());
case ImpressionPatch impressionPatch -> cognitionCapability.updateEntityImpression(
entityUuid,
impressionPatch.getImpression(),
impressionPatch.getNewImpression(),
impressionPatch.getConfidence()
);
case FeaturePatch featurePatch -> cognitionCapability.updateEntityFeature(
entityUuid,
featurePatch.getFeature(),
featurePatch.getNewFeature(),
featurePatch.getConfidence()
);
case RelationPatch relationPatch -> cognitionCapability.updateEntityRelation(
entityUuid,
relationPatch.getTarget(),
relationPatch.getRelation(),
relationPatch.getStrength()
);
case null, default -> throw new IllegalArgumentException("unsupported impression update patch: " + patch);
};
if (!applied) {
throw new IllegalStateException("failed to apply impression update patch: " + patch);
}
}
}
@@ -0,0 +1,71 @@
package work.slhaf.partner.module.impression;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import java.util.List;
public class ImpressionUpdatePlanValidator extends AbstractAgentModule.Sub<ImpressionUpdatePlan, Boolean> {
@Override
protected @NotNull Boolean doExecute(ImpressionUpdatePlan plan) {
return isExecutable(plan);
}
public boolean isExecutable(ImpressionUpdatePlan plan) {
if (plan == null || plan.getStatus() != PlanStatus.PREPARED) {
return false;
}
List<ImpressionUpdateStep> steps = plan.getSteps();
if (steps.isEmpty()) {
return false;
}
for (ImpressionUpdateStep step : steps) {
if (!isValidStep(step)) {
return false;
}
}
return true;
}
private boolean isValidStep(ImpressionUpdateStep step) {
if (step instanceof UpdateExistingStep updateStep) {
return hasText(updateStep.getEntityUuid()) && isValidPatch(updateStep.getUpdatePatch());
}
if (step instanceof CreateEntityStep createStep) {
return hasText(createStep.getSubject())
&& (!createStep.getImpressions().isEmpty()
|| !createStep.getFeatures().isEmpty()
|| !createStep.getAliases().isEmpty()
|| !createStep.getRelations().isEmpty())
&& createStep.getImpressions().stream().allMatch(this::isValidPatch)
&& createStep.getFeatures().stream().allMatch(this::isValidPatch)
&& createStep.getAliases().stream().allMatch(this::isValidPatch)
&& createStep.getRelations().stream().allMatch(this::isValidPatch);
}
return false;
}
private boolean isValidPatch(UpdatePatch patch) {
if (patch instanceof ImpressionPatch impressionPatch) {
return hasText(impressionPatch.getImpression());
}
if (patch instanceof FeaturePatch featurePatch) {
return hasText(featurePatch.getFeature());
}
if (patch instanceof AliasPatch aliasPatch) {
return hasText(aliasPatch.getAlias());
}
if (patch instanceof SubjectPatch subjectPatch) {
return hasText(subjectPatch.getSubject());
}
if (patch instanceof RelationPatch relationPatch) {
return hasText(relationPatch.getTarget()) && hasText(relationPatch.getRelation());
}
return false;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}
@@ -0,0 +1,78 @@
package work.slhaf.partner.module.impression;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ModuleExecutionException;
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 ImpressionUpdatePlanner extends AbstractAgentModule.Sub<ImpressionUpdateContext, Result<ImpressionUpdatePlan>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在对话 rolling 后,根据新的 memory slice 证据生成保守的实体印象更新计划。
你只输出 ImpressionUpdatePlan 对应结构:
- 如果没有稳定、可复用的实体信息变化,返回 REJECTED 并说明原因。
- 只有当证据明确支持时,才返回 PREPARED 计划来创建实体或更新已有实体。
- 不要做复杂实体合并,不要发明不在证据中的事实。
- patch 字段必须使用简洁、稳定、可索引的表达。
- 不要输出 CONFIRMED;CONFIRMED 只能由代码 Validator 通过后设置。
""";
@Override
protected Result<ImpressionUpdatePlan> doExecute(ImpressionUpdateContext context) {
return plan(context);
}
public Result<ImpressionUpdatePlan> plan(ImpressionUpdateContext context) {
try {
return Result.success(formattedChat(List.of(buildTaskMessage(context)), ImpressionUpdatePlan.class).getOrThrow());
} catch (AgentRuntimeException e) {
return Result.failure(new ModuleExecutionException(
"planning impression update failed",
this.getClass(),
getModuleName()
));
}
}
private Message buildTaskMessage(ImpressionUpdateContext context) {
return new TaskBlock("impression_update_task") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "memory_unit_id", context.memoryUnitId());
appendTextElement(document, root, "memory_slice_id", context.memorySliceId());
appendTextElement(document, root, "summary", context.summary());
appendTextElement(document, root, "rolling_size", Integer.toString(context.rollingSize()));
appendTextElement(document, root, "retain_divisor", Integer.toString(context.retainDivisor()));
appendTextElement(document, root, "slice_start_index", Integer.toString(context.sliceStartIndex()));
appendTextElement(document, root, "slice_end_index", Integer.toString(context.sliceEndIndex()));
appendTextElement(document, root, "slice_timestamp", Long.toString(context.sliceTimestamp()));
appendTextElement(document, root, "unit_timestamp", Long.toString(context.unitTimestamp()));
appendListElement(document, root, "increment_messages", "message", context.incrementMessages(), (element, message) -> {
element.setAttribute("role", message.roleValue());
element.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
}.encodeToMessage();
}
@Override
public @NotNull String modelKey() {
return "impression_update_planner";
}
@Override
public @NotNull List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
}
@@ -0,0 +1,111 @@
package work.slhaf.partner.module.impression;
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.context.BlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.core.cognition.impression.ActiveEntity;
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
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.communication.AfterRolling;
import work.slhaf.partner.module.communication.AfterRollingRegistry;
import work.slhaf.partner.module.communication.RollingResult;
import java.util.List;
import java.util.Set;
public class ImpressionUpdater extends AbstractAgentModule.Standalone implements AfterRolling {
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private AfterRollingRegistry afterRollingRegistry;
@InjectModule
private ImpressionUpdatePlanner planner;
@InjectModule
private ImpressionUpdatePlanValidator validator;
@InjectModule
private ImpressionUpdatePlanApplier applier;
@Init
public void init() {
afterRollingRegistry.register(this);
}
@Override
public void consume(RollingResult result) {
ImpressionUpdateContext context = buildContext(result);
Result<ImpressionUpdatePlan> planResult = planner.execute(context);
ImpressionUpdatePlan plan = planResult.getOrDefault(null);
if (!validator.execute(plan)) {
return;
}
ImpressionUpdatePlan confirmedPlan = new ImpressionUpdatePlan(
plan.getSteps(),
PlanStatus.CONFIRMED,
plan.getReason()
);
Result<ImpressionUpdateApplyResult> applyResult = applier.execute(confirmedPlan);
applyResult.onFailure(exp -> applierFailure(context, exp.getMessage()))
.onSuccess(applySummary -> applySummary.createdEntityUuids().forEach(entityUuid -> {
ActiveEntity activeEntity = cognitionCapability.activateKnownEntity(entityUuid);
if (activeEntity != null) {
registerActiveEntity(activeEntity);
}
}));
}
private ImpressionUpdateContext buildContext(RollingResult result) {
MemoryUnitSnapshot unit = result.getMemoryUnit();
MemorySliceSnapshot slice = result.getMemorySlice();
return new ImpressionUpdateContext(
unit.getId(),
slice.getId(),
result.getSummary(),
result.getRollingSize(),
result.getRetainDivisor(),
slice.getStartIndex(),
slice.getEndIndex(),
slice.getTimestamp(),
unit.getTimestamp(),
List.copyOf(result.incrementMessages())
);
}
private void applierFailure(ImpressionUpdateContext context, String message) {
cognitionCapability.contextWorkspace().register(new ContextBlock(
new BlockContent("impression_update_apply_failure", "impression_updater", BlockContent.Urgency.LOW) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "memory_unit_id", context.memoryUnitId());
appendTextElement(document, root, "memory_slice_id", context.memorySliceId());
appendTextElement(document, root, "message", message == null ? "" : message);
}
},
Set.of(ContextBlock.FocusedDomain.COGNITION),
20,
20,
0
));
}
private void registerActiveEntity(ActiveEntity activeEntity) {
cognitionCapability.contextWorkspace().register(new ContextBlock(
activeEntity,
activeEntity,
activeEntity,
Set.of(ContextBlock.FocusedDomain.COGNITION, ContextBlock.FocusedDomain.MEMORY),
100,
0.5,
20
));
}
}
@@ -4,8 +4,8 @@ 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.MemorySliceSnapshot;
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
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;
@@ -52,11 +52,11 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
}
}
public void recordMemory(MemoryUnit memoryUnit,
public void recordMemory(MemoryUnitSnapshot memoryUnit,
String topicPath,
List<String> relatedTopicPaths,
ActivationProfile activationProfile) {
MemorySlice memorySlice = memoryUnit.getSlices().getLast();
MemorySliceSnapshot memorySlice = memoryUnit.getSlices().getLast();
SliceRef sliceRef = new SliceRef(memoryUnit.getId(), memorySlice.getId());
LocalDate date = toLocalDate(memorySlice.getTimestamp());
runtimeLock.lock();
@@ -96,6 +96,9 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
}
public String fixTopicPath(String topicPath) {
if (topicPath == null || topicPath.isBlank()) {
return "";
}
String[] parts = topicPath.split("->");
List<String> cleanedParts = new ArrayList<>();
for (String part : parts) {
@@ -156,13 +159,13 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
}
private ActivatedMemorySlice buildActivatedMemorySlice(SliceRef ref) {
MemoryUnit memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
Result<MemorySlice> memorySliceResult = memoryCapability.getMemorySlice(ref.getUnitId(), ref.getSliceId());
MemoryUnitSnapshot memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
Result<MemorySliceSnapshot> 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);
MemorySliceSnapshot memorySlice = memorySliceResult.getOrThrow();
List<Message> messages = memoryUnit.messagesOf(memorySlice);
LocalDate date = toLocalDate(memorySlice.getTimestamp());
return ActivatedMemorySlice.builder()
.unitId(ref.getUnitId())
@@ -174,19 +177,6 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
.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)
@@ -69,8 +69,8 @@ final class MemoryRuntimeStateCodec {
List<StateValue.Obj> dateIndexStates = dateIndex.entries().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> StateValue.obj(Map.of(
"date", StateValue.str(entry.getKey().toString()),
"refs", StateValue.arr(encodeSliceRefs(entry.getValue()))
"date", entry.getKey().toString(),
"refs", encodeSliceRefs(entry.getValue())
)))
.toList();
state.append("date_index", StateValue.arr(dateIndexStates));
@@ -82,8 +82,8 @@ final class MemoryRuntimeStateCodec {
TopicMemoryIndex.TopicTreeNode topicNode,
List<StateValue.Obj> topicStates) {
topicStates.add(StateValue.obj(Map.of(
"topic_path", StateValue.str(path),
"bindings", StateValue.arr(encodeTopicBindings(topicNode.bindings()))
"topic_path", path,
"bindings", encodeTopicBindings(topicNode.bindings())
)));
for (Map.Entry<String, TopicMemoryIndex.TopicTreeNode> childEntry : topicNode.children().entrySet()) {
collectTopicStates(path + "->" + childEntry.getKey(), childEntry.getValue(), topicStates);
@@ -93,18 +93,16 @@ final class MemoryRuntimeStateCodec {
private List<StateValue> encodeTopicBindings(List<TopicMemoryIndex.TopicBinding> bindings) {
return bindings.stream()
.map(binding -> (StateValue) StateValue.obj(Map.of(
"unit_id", StateValue.str(binding.sliceRef().getUnitId()),
"slice_id", StateValue.str(binding.sliceRef().getSliceId()),
"timestamp", StateValue.num(binding.timestamp()),
"unit_id", binding.sliceRef().getUnitId(),
"slice_id", binding.sliceRef().getSliceId(),
"timestamp", binding.timestamp(),
"activation_profile", StateValue.obj(Map.of(
"activation_weight", StateValue.num(binding.activationProfile().getActivationWeight()),
"diffusion_weight", StateValue.num(binding.activationProfile().getDiffusionWeight()),
"activation_weight", binding.activationProfile().getActivationWeight(),
"diffusion_weight", binding.activationProfile().getDiffusionWeight(),
"context_independence_weight",
StateValue.num(binding.activationProfile().getContextIndependenceWeight())
binding.activationProfile().getContextIndependenceWeight()
)),
"related_topic_paths", StateValue.arr(binding.relatedTopicPaths().stream()
.map(StateValue::str)
.toList())
"related_topic_paths", binding.relatedTopicPaths()
)))
.toList();
}
@@ -156,8 +154,8 @@ final class MemoryRuntimeStateCodec {
private List<StateValue> encodeSliceRefs(List<SliceRef> refs) {
return refs.stream()
.map(ref -> (StateValue) StateValue.obj(Map.of(
"unit_id", StateValue.str(ref.getUnitId()),
"slice_id", StateValue.str(ref.getSliceId())
"unit_id", ref.getUnitId(),
"slice_id", ref.getSliceId()
)))
.toList();
}
@@ -0,0 +1,15 @@
package work.slhaf.partner.module.memory.selector;
import lombok.AllArgsConstructor;
import lombok.Data;
import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext;
import java.time.LocalDateTime;
import java.util.List;
@Data
@AllArgsConstructor
public class MemoryInputEntry {
private LocalDateTime receivedDateTime;
private List<RunningFlowContext.InputEntry> inputs;
}
@@ -5,9 +5,11 @@ 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.BlockContent;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.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.factory.component.annotation.InjectModule;
@@ -23,7 +25,11 @@ import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResul
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
@@ -33,6 +39,7 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
@InjectCapability
private CognitionCapability cognitionCapability;
private final AtomicBoolean memoryCalling = new AtomicBoolean(false);
@InjectModule
private MemoryRuntime memoryRuntime;
@@ -40,24 +47,125 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
private MemoryRecallEvaluator memoryRecallEvaluator;
@InjectModule
private MemoryRecallCueExtractor memoryRecallCueExtractor;
private final Lock inputsLock = new ReentrantLock();
private final List<MemoryInputEntry> collectedInputs = new ArrayList<>();
@InjectCapability
private ActionCapability actionCapability;
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext runningFlowContext) {
List<RunningFlowContext.InputEntry> snapshotInputs = List.copyOf(runningFlowContext.getInputs());
collectInputs(runningFlowContext);
tryStartMemoryRecallWorker();
}
private void collectInputs(PartnerRunningFlowContext runningFlowContext) {
inputsLock.lock();
try {
collectedInputs.add(new MemoryInputEntry(
runningFlowContext.getFirstInputDateTime(),
List.copyOf(runningFlowContext.getInputs())
));
} finally {
inputsLock.unlock();
}
}
private void tryStartMemoryRecallWorker() {
if (!memoryCalling.compareAndSet(false, true)) {
return;
}
actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL).execute(() -> {
try {
drainMemoryRecall();
} finally {
memoryCalling.set(false);
if (hasCollectedInputs()) {
tryStartMemoryRecallWorker();
}
}
});
}
private void drainMemoryRecall() {
while (true) {
List<MemoryInputEntry> snapshotInputs = drainCollectedInputs();
if (snapshotInputs.isEmpty()) {
return;
}
try {
recallMemory(snapshotInputs);
} catch (Exception e) {
log.error("[MemorySelector] 记忆召回任务执行失败", e);
}
}
}
private void recallMemory(List<MemoryInputEntry> memoryInputEntries) {
ExtractorInput input = new ExtractorInput(
snapshotInputs,
memoryRuntime.getTopicTree(),
runningFlowContext.getFirstInputDateTime().toLocalDate()
memoryInputEntries,
memoryRuntime.getTopicTree()
);
ExtractorResult extractorResult = memoryRecallCueExtractor.execute(input);
if (extractorResult.getMatches().isEmpty()) {
return;
}
List<ActivatedMemorySlice> activatedSlices = selectAndEvaluateMemory(snapshotInputs, extractorResult);
List<ActivatedMemorySlice> activatedSlices = selectAndEvaluateMemory(flattenInputs(memoryInputEntries), extractorResult);
updateMemoryContext(activatedSlices);
}
private List<MemoryInputEntry> drainCollectedInputs() {
inputsLock.lock();
try {
if (collectedInputs.isEmpty()) {
return List.of();
}
List<MemoryInputEntry> snapshot = new ArrayList<>(collectedInputs);
collectedInputs.clear();
snapshot.sort(Comparator.comparing(MemoryInputEntry::getReceivedDateTime));
return snapshot;
} finally {
inputsLock.unlock();
}
}
private boolean hasCollectedInputs() {
inputsLock.lock();
try {
return !collectedInputs.isEmpty();
} finally {
inputsLock.unlock();
}
}
private List<RunningFlowContext.InputEntry> flattenInputs(List<MemoryInputEntry> memoryInputEntries) {
if (memoryInputEntries.isEmpty()) {
return List.of();
}
long firstEpochMillis = memoryInputEntries.stream()
.map(MemoryInputEntry::getReceivedDateTime)
.mapToLong(this::toEpochMillis)
.min()
.orElseThrow();
return memoryInputEntries.stream()
.flatMap(entry -> {
long entryEpochMillis = toEpochMillis(entry.getReceivedDateTime());
return entry.getInputs().stream()
.map(input -> new RunningFlowContext.InputEntry(
entryEpochMillis + input.getOffsetMillis() - firstEpochMillis,
input.getContent()
));
})
.sorted(Comparator.comparingLong(RunningFlowContext.InputEntry::getOffsetMillis))
.toList();
}
private long toEpochMillis(java.time.LocalDateTime dateTime) {
return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
private void updateMemoryContext(List<ActivatedMemorySlice> activatedSlices) {
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildMemoryFullBlock(activatedSlices),
@@ -155,6 +263,9 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
private void setMemoryCandidates(LinkedHashMap<String, ActivatedMemorySlice> candidates, List<ExtractorResult.ExtractorMatchData> matches) {
for (ExtractorResult.ExtractorMatchData match : matches) {
if (match == null || match.getType() == null || match.getText() == null || match.getText().isBlank()) {
continue;
}
try {
List<ActivatedMemorySlice> recalledSlices = switch (match.getType()) {
case ExtractorResult.ExtractorMatchData.Constant.TOPIC ->
@@ -7,7 +7,7 @@ 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.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.context.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.factory.component.annotation.Init;
@@ -5,7 +5,7 @@ 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.core.cognition.context.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.factory.component.annotation.InjectModule;
@@ -18,6 +18,8 @@ import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorInput
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResult;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorInput, ExtractorResult> implements ActivateModel {
@@ -28,12 +30,11 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
你会收到:
- 一条结构化上下文消息,其中包含当前活跃的 communication 域与 memory 域内容;
- 一条任务消息,其中包含:
- new_inputs:一组按时间顺序累积的新输入,每条输入附带 interval-to-first;
- current_date:当前日期;
- memory_input_entries:一组按接收时间排序的新输入批次;每个批次包含 received_date_time 与该批次内按时间顺序排列的 inputs;
- memory_topic_tree:当前可用的记忆主题树结构。
你的任务:
- 基于 new_inputs、当前语境与已有记忆主题树,提取本次记忆召回最值得尝试的匹配项;
- 基于 memory_input_entries、当前语境与已有记忆主题树,提取本次记忆召回最值得尝试的匹配项;
- 匹配项只允许有两类:topic 或 date;
- topic 用于表示应优先检索的记忆主题路径;
- date 用于表示应优先检索的具体日期;
@@ -41,7 +42,7 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
提取原则:
- 你的目标是提取“可用于后续召回”的线索,而不是复述输入内容本身。
- new_inputs 应整体理解,不要只抓最后一句;如果多条输入共同收敛到同一记忆方向,应提取更稳定的主题线索。
- memory_input_entries 应整体理解,不要只抓最后一个批次或最后一句;如果多个批次共同收敛到同一记忆方向,应提取更稳定的主题线索。
- communication 域用于判断当前输入是否在承接近期某段对话、某个旧话题或某个已出现过的指代对象。
- memory 域用于辅助判断当前输入与哪些已激活记忆方向明显相关;只有在这种相关性明确时才使用,不要机械复述 memory 域内容。
- memory_topic_tree 是 topic 提取的主要参照;topic 应尽量贴近主题树中已有的层级与命名,不要随意发明与主题树无关的新路径。
@@ -59,8 +60,8 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
关于 date:
- date 表示一个明确的记忆日期。
- date 的 text 必须是可被 Java LocalDate.parse 正常解析的日期文本,即 yyyy-MM-dd。
- 只有在输入中存在明确日期,或结合 current_date 后可以稳定推断出具体某一天时,才输出 date。
- 像“今天”“昨天”“前天”“上周六”这类表达,只有在能够稳定落到某个具体日期时才可输出。
- 只有在输入中存在明确日期,或结合相关输入批次的 received_date_time 后可以稳定推断出具体某一天时,才输出 date。
- 像“今天”“昨天”“前天”“上周六”这类表达,只有在能够根据对应批次 received_date_time 稳定落到某个具体日期时才可输出。
- 对于“最近”“前几天”“那段时间”“之前”“上次”这类无法稳定定位到某一天的表达,不要输出 date。
何时应提取 topic:
@@ -71,7 +72,7 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
何时应提取 date:
- 当前输入明确提到了某个具体日期;
- 当前输入使用相对日期表达,但结合 current_date 可以稳定还原到具体某一天;
- 当前输入使用相对日期表达,但结合对应批次的 received_date_time 可以稳定还原到具体某一天;
- 当前输入中的回忆目标明显依赖某个特定日期,且该日期能够被明确确定。
何时不应轻易输出:
@@ -134,15 +135,20 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "new_inputs", (inputsElement) -> {
appendListElement(document, inputsElement, "inputs", "input", input.getInputs(), (inputElement, entry) -> {
inputElement.setAttribute("interval-to-first", String.valueOf(entry.getOffsetMillis()));
inputElement.setTextContent(entry.getContent());
appendChildElement(document, root, "memory_input_entries", (entriesElement) -> {
appendRepeatedElements(document, entriesElement, "memory_input_entry", input.getMemoryInputEntries().stream()
.sorted(Comparator.comparing(work.slhaf.partner.module.memory.selector.MemoryInputEntry::getReceivedDateTime))
.toList(), (entryElement, memoryInputEntry) -> {
appendTextElement(document, entryElement, "received_date_time", memoryInputEntry.getReceivedDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
appendListElement(document, entryElement, "inputs", "input", memoryInputEntry.getInputs(), (inputElement, entry) -> {
inputElement.setAttribute("interval-to-first", String.valueOf(entry.getOffsetMillis()));
inputElement.setTextContent(entry.getContent());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
appendTextElement(document, root, "current_date", input.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
appendTextElement(document, root, "memory_topic_tree", input.getTopic_tree());
}
}.encodeToMessage();
@@ -155,17 +161,40 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
}
private ExtractorResult fix(ExtractorResult extractorResult) {
extractorResult.getMatches().forEach(m -> {
if (m.getType().equals(ExtractorResult.ExtractorMatchData.Constant.DATE)) {
return;
}
m.setText(memoryRuntime.fixTopicPath(m.getText()));
});
if (extractorResult.getMatches().isEmpty()) {
return extractorResult;
ExtractorResult safeResult = extractorResult == null ? new ExtractorResult() : extractorResult;
List<ExtractorResult.ExtractorMatchData> rawMatches = safeResult.getMatches();
if (rawMatches == null || rawMatches.isEmpty()) {
safeResult.setMatches(List.of());
return safeResult;
}
extractorResult.getMatches().removeIf(m -> m.getText().split("->")[0].isEmpty());
return extractorResult;
List<ExtractorResult.ExtractorMatchData> normalizedMatches = new ArrayList<>();
for (ExtractorResult.ExtractorMatchData match : rawMatches) {
if (match == null) {
continue;
}
String type = match.getType();
String text = match.getText();
if (text == null || text.isBlank()) {
continue;
}
if (ExtractorResult.ExtractorMatchData.Constant.TOPIC.equals(type)) {
text = memoryRuntime.fixTopicPath(text);
if (text.isBlank() || text.split("->")[0].isEmpty()) {
continue;
}
match.setText(text);
normalizedMatches.add(match);
continue;
}
if (ExtractorResult.ExtractorMatchData.Constant.DATE.equals(type)) {
normalizedMatches.add(match);
}
}
safeResult.setMatches(normalizedMatches);
return safeResult;
}
@Override
@@ -2,15 +2,13 @@ package work.slhaf.partner.module.memory.selector.extractor.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext;
import work.slhaf.partner.module.memory.selector.MemoryInputEntry;
import java.time.LocalDate;
import java.util.List;
@Data
@AllArgsConstructor
public class ExtractorInput {
private List<RunningFlowContext.InputEntry> inputs;
private List<MemoryInputEntry> memoryInputEntries;
private String topic_tree;
private LocalDate date;
}
@@ -149,7 +149,7 @@ public class MemoryRecallProfileExtractor extends AbstractAgentModule.Standalone
@Override
public void consume(RollingResult result) {
List<Message> slicedMessages = sliceMessages(result);
List<Message> slicedMessages = result.incrementMessages();
if (slicedMessages.isEmpty()) {
return;
}
@@ -169,31 +169,21 @@ public class MemoryRecallProfileExtractor extends AbstractAgentModule.Standalone
relatedTopicPaths,
slicedMessages
);
memoryRuntime.recordMemory(result.memoryUnit(), topicPath, relatedTopicPaths, activationProfile);
memoryRuntime.recordMemory(result.getMemoryUnit(), topicPath, relatedTopicPaths, activationProfile);
}).onFailure(exp -> memoryRuntime.recordMemory(
result.memoryUnit(),
result.getMemoryUnit(),
null,
List.of(),
defaultActivationProfile()
));
}
private List<Message> sliceMessages(RollingResult result) {
int size = result.memoryUnit().getConversationMessages().size();
int start = Math.clamp(result.memorySlice().getStartIndex(), 0, size);
int end = Math.clamp(result.memorySlice().getEndIndex(), start, size);
if (start >= end) {
return List.of();
}
return result.memoryUnit().getConversationMessages().subList(start, end);
}
private Message resolveTopicTaskMessage(RollingResult result, List<Message> slicedMessages) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "current_topic_tree", memoryRuntime.getTopicTree());
appendTextElement(document, root, "slice_summary", result.summary());
appendTextElement(document, root, "slice_summary", result.getSummary());
appendRepeatedElements(document, root, "message", slicedMessages, (messageElement, message) -> {
messageElement.setAttribute("role", message.roleValue());
messageElement.setTextContent(message.getContent());
@@ -3,10 +3,10 @@ package work.slhaf.partner.module.perceive;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.core.cognition.context.BlockContent;
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.core.perceive.PerceiveCapability;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
@@ -4,9 +4,9 @@ import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.context.BlockContent;
import work.slhaf.partner.core.cognition.context.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.runtime.PartnerRunningFlowContext;
@@ -0,0 +1,18 @@
package work.slhaf.partner.runtime;
import work.slhaf.partner.common.vector.VectorClientRegistry;
import work.slhaf.partner.framework.agent.Agent;
import work.slhaf.partner.runtime.gateway.WebSocketGatewayRegistration;
public final class PartnerAgentBootstrap extends Agent.AgentBootstrap {
public PartnerAgentBootstrap(Agent.AgentApp agentApp) {
super(agentApp);
}
@Override
protected void bootstrap() {
addGatewayRegistration(new WebSocketGatewayRegistration());
addConfigurable(new VectorClientRegistry());
}
}
@@ -8,8 +8,9 @@ import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext
class PartnerRunningFlowContext private constructor(
override val source: String,
inputs: List<InputEntry>,
firstInputEpochMillis: Long
) : RunningFlowContext(inputs, firstInputEpochMillis) {
firstInputEpochMillis: Long,
responseChannel: String? = null
) : RunningFlowContext(inputs, firstInputEpochMillis, responseChannel) {
companion object {
@@ -32,12 +33,18 @@ class PartnerRunningFlowContext private constructor(
@JvmStatic
@JvmOverloads
fun fromUser(userId: String, input: String, receivedAtMillis: Long = System.currentTimeMillis()) =
fun fromUser(
userId: String,
input: String,
receivedAtMillis: Long = System.currentTimeMillis(),
responseChannel: String? = null
) =
PartnerRunningFlowContext(
SourceTag.buildUserSource(userId),
listOf(InputEntry(0L, input)),
receivedAtMillis
)
receivedAtMillis,
responseChannel
).apply { this.target = userId }
@JvmStatic
@JvmOverloads
@@ -45,7 +52,8 @@ class PartnerRunningFlowContext private constructor(
PartnerRunningFlowContext(
SourceTag.buildAgentSource(),
listOf(InputEntry(0L, input)),
receivedAtMillis
receivedAtMillis,
null
).apply {
putUserInfo(InfoKeys.PLATFORM, SOURCE_SELF_PLATFORM)
putUserInfo(InfoKeys.NICKNAME, SOURCE_SELF_NICKNAME)
@@ -56,7 +64,8 @@ class PartnerRunningFlowContext private constructor(
return PartnerRunningFlowContext(
source = source,
inputs = inputs,
firstInputEpochMillis = System.currentTimeMillis()
firstInputEpochMillis = System.currentTimeMillis(),
resopnseChannel
)
}
@@ -3,9 +3,9 @@ package work.slhaf.partner.runtime.exception;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
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.context.BlockContent;
import work.slhaf.partner.core.cognition.context.ContextBlock;
import work.slhaf.partner.framework.agent.exception.AgentException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
import work.slhaf.partner.framework.agent.exception.ExceptionReporter;
@@ -1,5 +1,6 @@
package work.slhaf.partner.runtime.gateway;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@@ -8,11 +9,9 @@ import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.api.InputData;
import work.slhaf.partner.api.InteractionEvent;
import work.slhaf.partner.framework.agent.interaction.AgentGateway;
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistration;
import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
import work.slhaf.partner.framework.agent.interaction.data.InputData;
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.net.InetSocketAddress;
@@ -34,11 +33,13 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
// 记录最后一次收到Pong的时间
private final ConcurrentHashMap<WebSocket, Long> lastPongTimes = new ConcurrentHashMap<>();
public WebSocketGateway(int port, long heartbeatInterval) {
super(new InetSocketAddress(port));
public WebSocketGateway(int port, @NotNull String hostname, long heartbeatInterval) {
super(new InetSocketAddress(hostname, port));
this.heartbeatInterval = heartbeatInterval;
this.setReuseAddr(true);
this.executor = Executors.newSingleThreadExecutor();
log.info("WebSocketGateway will start on {}: {}", hostname, port);
}
public void launch() {
@@ -47,12 +48,11 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
}
this.start();
startHeartbeatThread();
AgentRuntime.INSTANCE.registerResponseChannel(getChannelName(), this);
}
@Override
public PartnerRunningFlowContext parseRunningFlowContext(InputData inputData) {
PartnerRunningFlowContext context = PartnerRunningFlowContext.fromUser(inputData.getSource(), inputData.getContent());
PartnerRunningFlowContext context = PartnerRunningFlowContext.fromUser(inputData.getSource(), inputData.getContent(),System.currentTimeMillis(), getChannelName());
inputData.getMeta().forEach(context::putUserInfo);
return context;
}
@@ -119,14 +119,18 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
@Override
public void onMessage(WebSocket webSocket, String s) {
InputData inputData = JSONObject.parseObject(s, InputData.class);
userSessions.put(inputData.getSource(), webSocket); // 注册连接
receive(inputData);
try {
InputData inputData = JSONObject.parseObject(s, InputData.class);
userSessions.put(inputData.getSource(), webSocket); // 注册连接
receive(inputData);
} catch (JSONException ignored) {
log.warn("Invalid input format: {}", s);
}
}
@Override
public void onError(WebSocket webSocket, Exception e) {
log.error(e.getLocalizedMessage());
log.error("WebSocketGateway error", e);
}
@Override
@@ -134,11 +138,6 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
log.info("WebSocketServer 已启动...");
}
@Override
public AgentGatewayRegistration registration() {
return WebSocketGatewayRegistration.INSTANCE;
}
@Override
@NotNull
public String getChannelName() {
@@ -3,16 +3,17 @@ package work.slhaf.partner.runtime.gateway
import work.slhaf.partner.framework.agent.interaction.AgentGateway
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistration
object WebSocketGatewayRegistration : AgentGatewayRegistration {
class WebSocketGatewayRegistration : AgentGatewayRegistration {
override val channelName: String = "websocket_channel"
override fun create(params: Map<String, String>): AgentGateway<*, *> {
val port = params["port"]?.toIntOrNull() ?: 29600
val heartbeatInterval = params["heartbeat_interval"]?.toLongOrNull() ?: 10_000L
val hostname = params["hostname"] ?: "127.0.0.1"
require(port > 0) { "port must be greater than 0" }
require(heartbeatInterval > 0) { "heartbeat_interval must be greater than 0" }
return WebSocketGateway(port, heartbeatInterval)
return WebSocketGateway(port, hostname, heartbeatInterval)
}
override fun shutdown(instance: AgentGateway<*, *>) {

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