2 Commits
Author SHA1 Message Date
slhaf 67499390b2 重构拆分原‘记忆图谱’以适应后续扩展
- 拆分原`MemoryGraph`为 MemoryCore, CacheCore, GraphCore, PerceiveCore几个部分.
- MemoryCore中将不再包含操作逻辑, 由MemoryManager统一处理, 序列化逻辑仍交给MemoryCore。
- 更新README
2025-06-06 19:25:12 +08:00
slhaf 8f0e62d6db 发布版本: 第二阶段 0.5.0 2025-06-06 10:36:41 +08:00
395 changed files with 4846 additions and 34744 deletions
-137
View File
@@ -1,137 +0,0 @@
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
@@ -1,161 +0,0 @@
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
@@ -0,0 +1,36 @@
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
@@ -1,137 +0,0 @@
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
@@ -1,70 +0,0 @@
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
@@ -1,38 +0,0 @@
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
+16 -28
View File
@@ -4,7 +4,10 @@ target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
@@ -33,31 +36,16 @@ build/
### Mac OS ###
.DS_Store
/backup/data/
/backup/config/
/Partner-Core/src/main/java/src/test/java/memory/test.json
/Partner-Core/src/main/java/src/test/java/memory/result/input1.json
/Partner-Core/src/main/java/src/test/java/memory/result/input2.json
/Partner-Core/src/main/java/src/test/java/memory/result/output1.json
/Partner-Core/src/main/java/src/test/java/memory/result/output2.json
/Partner-Core/src/main/java/src/test/java/memory/result/total_input.json
/Partner-Core/src/main/java/src/test/java/memory/result/input3.json
/Partner-Core/src/main/java/src/test/java/memory/result/input4.json
/Partner-Core/src/main/java/src/test/java/memory/result/primary_input.json
/Partner-Core/src/main/java/src/main/resources/prompt/module/memory/topic_extractor.json.bak
/backup/
/Partner-Main/src/test/java/text/test.json
/CLAUDE.md
/config/
/data/
/generated-classes/
/.idea/copilot.data.migration.ask2agent.xml
/Partner-Main/data/
/AGENTS.md
/.serena/
/Partner-Core/data/
/.ai/mcp/mcp.json
/.codex
# Maven / build outputs
dependency-reduced-pom.xml
/config/
/src/test/java/memory/test.json
/src/test/java/memory/result/input1.json
/src/test/java/memory/result/input2.json
/src/test/java/memory/result/output1.json
/src/test/java/memory/result/output2.json
/src/test/java/memory/result/total_input.json
/src/test/java/memory/result/input3.json
/src/test/java/memory/result/input4.json
/src/test/java/memory/result/primary_input.json
/src/main/resources/prompt/module/memory/topic_extractor.json.bak
/backup/
-1
View File
@@ -6,4 +6,3 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
/inspectionProfiles/Project_Default.xml
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
-11
View File
@@ -1,11 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>Onebot</w>
<w>onebot</w>
<w>openai</w>
<w>partnerctl</w>
<w>zuper</w>
</words>
</dictionary>
</component>
-14
View File
@@ -1,20 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<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-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>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.2.0" />
</component>
</project>
-41
View File
@@ -1,53 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<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="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" />
<writeAnnotation name="work.slhaf.partner.api.agent.factory.component.annotation.InjectModule" />
<writeAnnotation name="work.slhaf.partner.api.agent.factory.module.annotation.InjectModule" />
<writeAnnotation name="work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability" />
<writeAnnotation name="work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule" />
</writeAnnotations>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
<option value="$PROJECT_DIR$/PartnerExecutor/pom.xml" />
</list>
</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>
</component>
<component name="PWA">
<option name="enabled" value="true" />
-79
View File
@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
-135
View File
@@ -1,135 +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.partner</groupId>
<artifactId>partner</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>partner-core</artifactId>
<version>0.5.0</version>
<dependencies>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>work.slhaf.partner</groupId>
<artifactId>partner-framework</artifactId>
<version>${partner.runtime.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api -->
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-api</artifactId>
<version>1.0.0-M2.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.onnxruntime</groupId>
<artifactId>onnxruntime</artifactId>
<version>1.23.1</version>
</dependency>
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>0.34.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.modelcontextprotocol.sdk/mcp -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.17.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.20.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.2.1</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>work.slhaf.partner.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>${maven.compiler.target}</jvmTarget>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,12 +0,0 @@
package work.slhaf.partner;
import work.slhaf.partner.framework.agent.Agent;
public class Main {
public static void main(String[] args) {
boolean launched = Agent.newAgent(Main.class).launch();
if (!launched) {
System.exit(1);
}
}
}
@@ -1,123 +0,0 @@
package work.slhaf.partner.common.base
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
abstract class Block(
val blockName: String
) {
fun encodeToXml(): Element {
val document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument()
val root = document.createElement(blockName)
document.appendChild(root)
appendRootAttributes().forEach { attribute, value -> root.setAttribute(attribute, value) }
fillXml(document, root)
return root
}
protected open fun appendRootAttributes(): Map<String, String> {
return emptyMap()
}
fun encodeToXmlString(): String {
val transformer = TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
}
return StringWriter().use { writer ->
transformer.transform(DOMSource(encodeToXml()), StreamResult(writer))
writer.toString()
}
}
protected abstract fun fillXml(document: Document, root: Element)
protected fun appendTextElement(
document: Document,
parent: Element,
tagName: String,
value: Any?
): Element {
val element = document.createElement(tagName)
element.textContent = value?.toString() ?: ""
parent.appendChild(element)
return element
}
protected fun appendChildElement(
document: Document,
parent: Element,
tagName: String,
block: Element.() -> Unit = {}
): Element {
val element = document.createElement(tagName)
parent.appendChild(element)
element.block()
return element
}
protected fun appendCDataElement(
document: Document,
parent: Element,
tagName: String,
value: String?
): Element {
val element = document.createElement(tagName)
element.appendChild(document.createCDATASection(value ?: ""))
parent.appendChild(element)
return element
}
@JvmOverloads
protected fun <T> appendListElement(
document: Document,
parent: Element,
wrapperTagName: String,
itemTagName: String,
values: Iterable<T>,
block: Element.(T) -> Unit = { value ->
textContent = value?.toString() ?: ""
}
): Element {
val wrapper = document.createElement(wrapperTagName)
parent.appendChild(wrapper)
for (value in values) {
val item = document.createElement(itemTagName)
wrapper.appendChild(item)
item.block(value)
}
return wrapper
}
@JvmOverloads
protected fun <T> appendRepeatedElements(
document: Document,
parent: Element,
itemTagName: String,
values: Iterable<T>,
block: Element.(T) -> Unit = { value ->
textContent = value?.toString() ?: ""
}
) {
for (value in values) {
val item = document.createElement(itemTagName)
parent.appendChild(item)
item.block(value)
}
}
}
@@ -1,59 +0,0 @@
package work.slhaf.partner.common.vector;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
import java.util.Map;
@Slf4j
public class OllamaVectorClient extends VectorClient {
private final String ollamaEmbeddingUrl;
private final String ollamaEmbeddingModel;
protected OllamaVectorClient(String url, String model) {
this.ollamaEmbeddingUrl = url;
this.ollamaEmbeddingModel = model;
compute("test");
}
@Override
protected float[] doCompute(String input) {
Map<String, String> param = Map.of("model", ollamaEmbeddingModel, "input", input);
HttpRequest request = HttpRequest.get(ollamaEmbeddingUrl).body(JSONObject.toJSONString(param));
try (HttpResponse response = request.execute()) {
if (!response.isOk())
throw new VectorClientExecutionException(
"Failed to execute embedding model",
"ollama",
"COMPUTE",
ollamaEmbeddingUrl
);
String resStr = response.body();
EmbeddingModelResponse embeddingResponse = JSONObject.parseObject(resStr, EmbeddingModelResponse.class);
return embeddingResponse.getEmbeddings()[0];
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to execute embedding model",
"ollama",
"COMPUTE",
ollamaEmbeddingUrl,
e
);
}
}
@Data
private static class EmbeddingModelResponse {
private String model;
private float[][] embeddings;
private long total_duration;
private long load_duration;
private int prompt_eval_count;
}
}
@@ -1,100 +0,0 @@
package work.slhaf.partner.common.vector;
import ai.djl.huggingface.tokenizers.Encoding;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.onnxruntime.OnnxTensor;
import ai.onnxruntime.OrtEnvironment;
import ai.onnxruntime.OrtSession;
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("FieldMayBeFinal")
public class OnnxVectorClient extends VectorClient {
private String tokenizerPath;
private String modelPath;
private HuggingFaceTokenizer tokenizer;
private OrtSession session;
private OrtEnvironment env;
protected OnnxVectorClient(String tokenizer, String model) {
this.tokenizerPath = tokenizer;
this.modelPath = model;
loadTokenizer();
loadModel();
compute("test");
}
private void loadModel() {
try {
env = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions ops = new OrtSession.SessionOptions();
session = env.createSession(modelPath, ops);
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to load ONNX model",
"onnx",
"MODEL_LOAD",
modelPath,
e
);
}
}
private void loadTokenizer() {
try {
tokenizer = HuggingFaceTokenizer.newInstance(Path.of(tokenizerPath));
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to load tokenizer",
"onnx",
"TOKENIZER_LOAD",
tokenizerPath,
e
);
}
}
@Override
protected float[] doCompute(String input) {
try {
Encoding encode = tokenizer.encode(input);
long[] ids = encode.getIds();
long[] attentionMask = encode.getAttentionMask();
long[][] inputIdsBatch = {ids};
long[][] attentionMaskBatch = {attentionMask};
long[][] tokenTypeIdsBatch = {new long[ids.length]}; // 初始化全 0
for (int i = 0; i < ids.length; i++)
tokenTypeIdsBatch[0][i] = 0;
OnnxTensor inputTensor = OnnxTensor.createTensor(env, inputIdsBatch);
OnnxTensor maskTensor = OnnxTensor.createTensor(env, attentionMaskBatch);
OnnxTensor tokenTypeTensor = OnnxTensor.createTensor(env, tokenTypeIdsBatch);
Map<String, OnnxTensor> inputs = new HashMap<>();
inputs.put("input_ids", inputTensor);
inputs.put("attention_mask", maskTensor);
inputs.put("token_type_ids", tokenTypeTensor);
OrtSession.Result result = session.run(inputs);
OnnxTensor embeddingTensor = (OnnxTensor) result.get(0);
return embeddingTensor.getFloatBuffer().array();
} catch (Exception e) {
throw new VectorClientExecutionException(
"Failed to execute embedding model",
"onnx",
"COMPUTE",
modelPath,
e
);
}
}
}
@@ -1,105 +0,0 @@
package work.slhaf.partner.common.vector;
import lombok.extern.slf4j.Slf4j;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.ops.transforms.Transforms;
import work.slhaf.partner.common.vector.exception.VectorClientExecutionException;
import work.slhaf.partner.common.vector.exception.VectorClientStartupException;
@Slf4j
public abstract class VectorClient {
public static boolean status = false;
public static VectorClient INSTANCE;
public static void startClient(VectorConfig config) {
try {
if (config instanceof VectorConfig.Ollama ollama) {
INSTANCE = new OllamaVectorClient(ollama.ollamaEmbeddingUrl, ollama.ollamaEmbeddingModel);
} else if (config instanceof VectorConfig.Onnx onnx) {
INSTANCE = new OnnxVectorClient(onnx.tokenizerPath, onnx.embeddingModelPath);
} else {
return;
}
status = true;
} catch (VectorClientStartupException e) {
throw e;
} catch (VectorClientExecutionException e) {
throw new VectorClientStartupException(
"Vector client startup failed",
e.getClientType(),
"COMPUTE".equals(e.getPhase()) ? "STARTUP_SELF_TEST" : e.getPhase(),
e.getTarget(),
e
);
} catch (Exception e) {
throw new VectorClientStartupException(
"Vector client startup failed",
resolveClientType(config),
"STARTUP",
resolveTarget(config),
e
);
}
}
public float[] compute(String input) {
if (!status) {
return null;
}
return doCompute(input);
}
protected abstract float[] doCompute(String input);
private static String resolveClientType(VectorConfig config) {
if (config instanceof VectorConfig.Onnx) {
return "onnx";
}
if (config instanceof VectorConfig.Ollama) {
return "ollama";
}
return "unknown";
}
private static String resolveTarget(VectorConfig config) {
if (config instanceof VectorConfig.Onnx onnx) {
return onnx.embeddingModelPath;
}
if (config instanceof VectorConfig.Ollama ollama) {
return ollama.ollamaEmbeddingUrl;
}
return null;
}
public double compare(float[] v1, float[] v2) {
if (!status) {
return 0;
}
try (INDArray a1 = Nd4j.create(v1); INDArray a2 = Nd4j.create(v2)) {
return Transforms.cosineSim(a1, a2);
}
}
public float[] weightedAverage(float[] newVector, float[] primaryVector) {
try (INDArray primary = Nd4j.create(primaryVector);
INDArray latest = Nd4j.create(newVector)) {
// 1️⃣ 计算余弦相似度
double similarity = Transforms.cosineSim(primary, latest);
// 2️⃣ 根据相似度决定更新比例 α(差异越大,新输入影响越强)
double alpha = (1.0 - similarity) * 0.5;
alpha = Math.clamp(alpha, 0.05, 0.5);
// 3️⃣ 按比例混合旧向量与新向量
INDArray updated = primary.mul(1 - alpha).add(latest.mul(alpha));
// 4️⃣ 归一化结果(保持方向空间一致)
updated = updated.div(updated.norm2Number());
return updated.toFloatVector();
}
}
}
@@ -1,50 +0,0 @@
package work.slhaf.partner.common.vector;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.config.Config;
import work.slhaf.partner.framework.agent.config.ConfigRegistration;
import work.slhaf.partner.framework.agent.config.Configurable;
import java.nio.file.Path;
import java.util.Map;
public class VectorClientRegistry implements Configurable, ConfigRegistration<VectorConfig> {
@Override
public void init(@NotNull VectorConfig config, @Nullable JSONObject json) {
if (!config.enabled) {
return;
}
if (config.type == null) {
return;
}
if (json == null) {
return;
}
config = switch (config.type) {
case ONNX -> json.toJavaObject(VectorConfig.Onnx.class);
case OLLAMA -> json.toJavaObject(VectorConfig.Ollama.class);
};
VectorClient.startClient(config);
}
@Override
@NotNull
public Class<VectorConfig> type() {
return VectorConfig.class;
}
@Nullable
@Override
public VectorConfig defaultConfig() {
return new VectorConfig(false, null);
}
@Override
public @NotNull Map<Path, ConfigRegistration<? extends Config>> declare() {
return Map.of(Path.of("vector.json"), this);
}
}
@@ -1,44 +0,0 @@
package work.slhaf.partner.common.vector;
import work.slhaf.partner.framework.agent.config.Config;
public sealed class VectorConfig extends Config permits VectorConfig.Ollama, VectorConfig.Onnx {
final boolean enabled;
final Type type;
public VectorConfig(boolean enabled, Type type) {
this.enabled = enabled;
this.type = type;
}
public enum Type {
ONNX,
OLLAMA
}
static final class Onnx extends VectorConfig {
final String tokenizerPath;
final String embeddingModelPath;
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath) {
super(enabled, type);
this.tokenizerPath = tokenizerPath;
this.embeddingModelPath = embeddingModelPath;
}
}
static final class Ollama extends VectorConfig {
final String ollamaEmbeddingUrl;
final String ollamaEmbeddingModel;
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel) {
super(enabled, type);
this.ollamaEmbeddingUrl = ollamaEmbeddingUrl;
this.ollamaEmbeddingModel = ollamaEmbeddingModel;
}
}
}
@@ -1,49 +0,0 @@
package work.slhaf.partner.common.vector.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class VectorClientExecutionException extends AgentRuntimeException {
private final String clientType;
private final String phase;
private final String target;
public VectorClientExecutionException(String message, String clientType, String phase, @Nullable String target) {
super(message);
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
public VectorClientExecutionException(String message, String clientType, String phase, @Nullable String target, Throwable cause) {
super(message, cause);
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
public String getClientType() {
return clientType;
}
public String getPhase() {
return phase;
}
public String getTarget() {
return target;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("clientType", clientType);
report.getExtra().put("phase", phase);
if (target != null) {
report.getExtra().put("target", target);
}
return report;
}
}
@@ -1,37 +0,0 @@
package work.slhaf.partner.common.vector.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class VectorClientStartupException extends AgentStartupException {
private final String clientType;
private final String phase;
private final String target;
public VectorClientStartupException(String message, String clientType, String phase, @Nullable String target) {
super(message, "vector-client-registry");
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
public VectorClientStartupException(String message, String clientType, String phase, @Nullable String target, Throwable cause) {
super(message, "vector-client-registry", cause);
this.clientType = clientType;
this.phase = phase;
this.target = target;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("clientType", clientType);
report.getExtra().put("phase", phase);
if (target != null) {
report.getExtra().put("target", target);
}
return report;
}
}
@@ -1,41 +0,0 @@
package work.slhaf.partner.core.action;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.ExecutableAction;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@Capability(value = "action")
public interface ActionCapability {
void putAction(@NonNull ExecutableAction executableAction);
Set<ExecutableAction> listActions(@Nullable ExecutableAction.Status status, @Nullable String source);
ExecutorService getExecutor(ActionCore.ExecutorType type);
Result<MetaAction> loadMetaAction(@NonNull String actionKey);
Result<MetaActionInfo> loadMetaActionInfo(@NonNull String actionKey);
void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions);
Map<String, MetaActionInfo> listAvailableMetaActions();
boolean checkExists(String... actionKeys);
RunnerClient runnerClient();
void handleInterventions(List<MetaIntervention> interventions, ExecutableAction data);
}
@@ -1,313 +0,0 @@
package work.slhaf.partner.core.action;
import com.alibaba.fastjson2.JSONObject;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.ExecutableAction;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.action.exception.ActionLookupException;
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.framework.agent.config.ConfigCenter;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.factory.context.Shutdown;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.framework.agent.support.Result;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@SuppressWarnings("FieldMayBeFinal")
@CapabilityCore(value = "action")
@Slf4j
public class ActionCore implements StateSerializable {
public static final String BUILTIN_LOCATION = "builtin";
public static final String ORIGIN_LOCATION = "origin";
// 由于当前的执行器逻辑实现,平台线程池大小不得小于 2,这里规定为最小为 4
private final ExecutorService platformExecutor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4));
private final ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
/**
* 已存在的行动程序,键格式为‘<MCP-ServerName>::<Tool-Name>’,值为 MCP Server 通过 Resources 相关渠道传递的行动程序元信息
*/
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
/**
* 持久行动池
*/
private CopyOnWriteArraySet<ExecutableAction> actionPool = new CopyOnWriteArraySet<>();
private RunnerClient runnerClient;
public ActionCore() throws IOException, ClassNotFoundException {
String baseActionPath = ConfigCenter.INSTANCE.getPaths().getResourcesDir().resolve("action").normalize().toAbsolutePath().toString();
// TODO 通过 Config 指定采用何种 runnerClient,当前只提供 LocalRunnerClient
runnerClient = new LocalRunnerClient(existedMetaActions, virtualExecutor, baseActionPath);
register();
}
@Shutdown
public void shutdown() {
try {
runnerClient.close();
} catch (Exception e) {
log.warn("runner client close error", e);
}
try {
platformExecutor.shutdown();
virtualExecutor.shutdown();
int count = 0;
if (!platformExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
count += platformExecutor.shutdownNow().size();
}
if (!virtualExecutor.awaitTermination(8, TimeUnit.SECONDS)) {
count += virtualExecutor.shutdownNow().size();
}
if (count != 0) {
log.warn("{} tasks still running", count);
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
@CapabilityMethod
public void putAction(@NonNull ExecutableAction executableAction) {
actionPool.removeIf(data -> data.getUuid().equals(executableAction.getUuid())); // 用来应对 ScheduledActionData 的重新排列
actionPool.add(executableAction);
}
@CapabilityMethod
public Set<ExecutableAction> listActions(@Nullable ExecutableAction.Status status, @Nullable String source) {
return actionPool.stream()
.filter(actionData -> status == null || actionData.getStatus().equals(status))
.filter(actionData -> source == null || actionData.getSource().equals(source))
.collect(Collectors.toSet());
}
@CapabilityMethod
public ExecutorService getExecutor(ExecutorType type) {
return switch (type) {
case VIRTUAL -> virtualExecutor;
case PLATFORM -> platformExecutor;
};
}
@CapabilityMethod
public void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions) {
existedMetaActions.putAll(metaActions);
}
@CapabilityMethod
public Map<String, MetaActionInfo> listAvailableMetaActions() {
return existedMetaActions;
}
@CapabilityMethod
public Result<MetaAction> loadMetaAction(@NonNull String actionKey) {
MetaActionInfo metaActionInfo = existedMetaActions.get(actionKey);
if (metaActionInfo == null) {
return Result.failure(new ActionLookupException(
"Meta action info not found for action key: " + actionKey,
actionKey,
"META_ACTION"
));
}
String[] split = actionKey.split("::", 2);
if (split.length < 2) {
return Result.failure(new ActionLookupException(
"Invalid action key format: " + actionKey,
actionKey,
"META_ACTION"
));
}
MetaAction.Type type = switch (split[0]) {
case BUILTIN_LOCATION -> MetaAction.Type.BUILTIN;
case ORIGIN_LOCATION -> MetaAction.Type.ORIGIN;
default -> MetaAction.Type.MCP;
};
return Result.success(new MetaAction(
split[1],
metaActionInfo.getIo(),
metaActionInfo.getLauncher(),
type,
split[0]
));
}
@CapabilityMethod
public Result<MetaActionInfo> loadMetaActionInfo(@NonNull String actionKey) {
MetaActionInfo info = existedMetaActions.get(actionKey);
if (info == null) {
return Result.failure(new ActionLookupException(
"Meta action description not found for action key: " + actionKey,
actionKey,
"META_ACTION_INFO"
));
}
return Result.success(info);
}
@CapabilityMethod
public boolean checkExists(String... actionKeys) {
return existedMetaActions.keySet().containsAll(Arrays.asList(actionKeys));
}
@CapabilityMethod
public RunnerClient runnerClient() {
return runnerClient;
}
@CapabilityMethod
public void handleInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
// 加载数据
if (executableAction == null) {
return;
}
// 加锁确保同步
synchronized (executableAction.getExecutionLock()) {
applyInterventions(interventions, executableAction);
}
}
private void applyInterventions(List<MetaIntervention> interventions, ExecutableAction executableAction) {
boolean[] rebuildCleanTag = {false};
interventions.sort(Comparator.comparingInt(MetaIntervention::getOrder));
for (MetaIntervention intervention : interventions) {
Result<List<MetaAction>> actionsResult = resolveInterventionActions(intervention);
actionsResult
.onFailure(ExceptionReporterHandler.INSTANCE::report)
.onSuccess(actions -> {
switch (intervention.getType()) {
case InterventionType.APPEND ->
handleAppend(executableAction, intervention.getOrder(), actions);
case InterventionType.INSERT ->
handleInsert(executableAction, intervention.getOrder(), actions);
case InterventionType.DELETE ->
handleDelete(executableAction, intervention.getOrder(), actions);
case InterventionType.CANCEL -> handleCancel(executableAction);
case InterventionType.REBUILD -> {
if (!rebuildCleanTag[0]) {
cleanActionData(executableAction);
rebuildCleanTag[0] = true;
}
handleRebuild(executableAction, intervention.getOrder(), actions);
}
}
});
}
}
private Result<List<MetaAction>> resolveInterventionActions(MetaIntervention intervention) {
List<MetaAction> actions = new ArrayList<>();
for (String actionKey : intervention.getActions()) {
Result<MetaAction> metaActionResult = loadMetaAction(actionKey);
AgentRuntimeException failure = metaActionResult.onSuccess(actions::add).exceptionOrNull();
if (failure != null) {
return Result.failure(failure);
}
}
return Result.success(actions);
}
/**
* 在指定阶段之后追加新的行动单元组
*/
private void handleAppend(ExecutableAction executableAction, int order, List<MetaAction> actions) {
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
int targetOrder = nextAvailableOrder(actionChain, order + 1);
actionChain.put(targetOrder, actions);
}
private int nextAvailableOrder(Map<Integer, List<MetaAction>> actionChain, int startOrder) {
int order = startOrder;
while (actionChain.containsKey(order)) {
order++;
}
return order;
}
/**
* 在未进入执行阶段和正处于行动阶段的行动单元组插入新的行动
*/
private void handleInsert(ExecutableAction executableAction, int order, List<MetaAction> actions) {
if (order < executableAction.getExecutingStage())
return;
List<MetaAction> stageActions = executableAction.getActionChain().computeIfAbsent(order, k -> new ArrayList<>());
synchronized (stageActions) {
stageActions.addAll(actions);
}
}
private void handleDelete(ExecutableAction executableAction, int order, List<MetaAction> actions) {
if (order <= executableAction.getExecutingStage())
return;
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
if (actionChain.containsKey(order)) {
actionChain.get(order).removeAll(actions);
if (actionChain.get(order).isEmpty()) {
actionChain.remove(order);
}
}
}
private void handleCancel(ExecutableAction executableAction) {
executableAction.setStatus(ExecutableAction.Status.FAILED);
executableAction.setResult("行动取消");
}
private void handleRebuild(ExecutableAction executableAction, int order, List<MetaAction> actions) {
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
actionChain.put(order, actions);
}
private void cleanActionData(ExecutableAction executableAction) {
executableAction.getActionChain().clear();
executableAction.setExecutingStage(0);
executableAction.setStatus(ExecutableAction.Status.PREPARE);
executableAction.getHistory().clear();
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "action.json");
}
@Override
public void load(@NotNull JSONObject state) {
actionPool = ActionPoolStateCodec.decode(state.getJSONArray("action_pool"));
}
@Override
public @NotNull State convert() {
State state = new State();
List<StateValue.Obj> actionPoolState = ActionPoolStateCodec.encode(actionPool);
state.append("action_pool", StateValue.arr(actionPoolState));
return state;
}
public enum ExecutorType {
VIRTUAL, PLATFORM
}
}
@@ -1,357 +0,0 @@
package work.slhaf.partner.core.action;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
final class ActionPoolStateCodec {
private ActionPoolStateCodec() {
}
static List<StateValue.Obj> encode(CopyOnWriteArraySet<ExecutableAction> actionPool) {
return actionPool.stream()
.map(ActionPoolStateCodec::encodeExecutableAction)
.toList();
}
static CopyOnWriteArraySet<ExecutableAction> decode(@Nullable JSONArray actionPoolArray) {
CopyOnWriteArraySet<ExecutableAction> restored = new CopyOnWriteArraySet<>();
if (actionPoolArray == null) {
return restored;
}
for (int i = 0; i < actionPoolArray.size(); i++) {
JSONObject actionObject = actionPoolArray.getJSONObject(i);
if (actionObject == null) {
continue;
}
try {
ExecutableAction executableAction = decodeExecutableAction(actionObject);
if (executableAction != null) {
restored.add(executableAction);
}
} catch (Exception e) {
log.warn("Skip invalid action_pool item at index {}", i, e);
}
}
return restored;
}
private static StateValue.Obj encodeExecutableAction(ExecutableAction action) {
Map<String, StateValue> actionMap = new LinkedHashMap<>();
actionMap.put("kind", StateValue.str(action instanceof SchedulableExecutableAction ? "schedulable" : "immediate"));
actionMap.put("uuid", StateValue.str(action.getUuid()));
actionMap.put("source", StateValue.str(action.getSource()));
actionMap.put("reason", StateValue.str(action.getReason()));
actionMap.put("description", StateValue.str(action.getDescription()));
actionMap.put("status", StateValue.str(action.getStatus().name()));
actionMap.put("tendency", StateValue.str(action.getTendency()));
actionMap.put("executing_stage", StateValue.num(action.getExecutingStage()));
String result = resolveExecutableResult(action);
if (result != null) {
actionMap.put("result", StateValue.str(result));
}
if (action instanceof SchedulableExecutableAction schedulableAction) {
actionMap.put("schedule_type", StateValue.str(schedulableAction.getScheduleType().name()));
actionMap.put("schedule_content", StateValue.str(schedulableAction.getScheduleContent()));
actionMap.put("enabled", StateValue.bool(schedulableAction.getEnabled()));
actionMap.put("schedule_histories", StateValue.arr(encodeScheduleHistories(schedulableAction)));
}
List<StateValue> chainStates = action.getActionChain().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.<StateValue>map(entry -> {
Map<String, StateValue> stageMap = new LinkedHashMap<>();
stageMap.put("stage", StateValue.num(entry.getKey()));
String stageDescription = action.getStageDescriptions().get(entry.getKey());
if (stageDescription != null && !stageDescription.isBlank()) {
stageMap.put("description", StateValue.str(stageDescription));
}
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
.map(metaAction -> (StateValue) encodeMetaAction(metaAction))
.toList()));
return StateValue.obj(stageMap);
}).toList();
actionMap.put("action_chain", StateValue.arr(chainStates));
actionMap.put("history", StateValue.arr(encodeHistoryStages(action.getHistory())));
return StateValue.obj(actionMap);
}
private static StateValue.Obj encodeMetaAction(MetaAction metaAction) {
Map<String, StateValue> metaMap = new LinkedHashMap<>();
metaMap.put("name", StateValue.str(metaAction.getName()));
metaMap.put("io", StateValue.bool(metaAction.getIo()));
if (metaAction.getLauncher() != null) {
metaMap.put("launcher", StateValue.str(metaAction.getLauncher()));
}
metaMap.put("type", StateValue.str(metaAction.getType().name()));
metaMap.put("location", StateValue.str(metaAction.getLocation()));
metaMap.put("params_json", StateValue.str(JSONObject.toJSONString(metaAction.getParams())));
metaMap.put("result_status", StateValue.str(metaAction.getResult().getStatus().name()));
if (metaAction.getResult().getData() != null) {
metaMap.put("result_data", StateValue.str(metaAction.getResult().getData()));
}
return StateValue.obj(metaMap);
}
private static StateValue.Obj encodeHistoryAction(HistoryAction historyAction) {
Map<String, StateValue> historyMap = new LinkedHashMap<>();
historyMap.put("action_key", StateValue.str(historyAction.actionKey()));
historyMap.put("description", StateValue.str(historyAction.description()));
historyMap.put("result", StateValue.str(historyAction.result()));
return StateValue.obj(historyMap);
}
private static ExecutableAction decodeExecutableAction(JSONObject actionObject) {
String kind = actionObject.getString("kind");
String uuid = actionObject.getString("uuid");
String source = actionObject.getString("source");
String reason = actionObject.getString("reason");
String description = actionObject.getString("description");
String tendency = actionObject.getString("tendency");
String status = actionObject.getString("status");
Integer executingStage = actionObject.getInteger("executing_stage");
if (kind == null || uuid == null || source == null || reason == null || description == null || tendency == null) {
return null;
}
Map<Integer, 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");
String scheduleContent = actionObject.getString("schedule_content");
if (scheduleType == null || scheduleContent == null) {
return null;
}
SchedulableExecutableAction schedulableAction = new SchedulableExecutableAction(
tendency,
restoredChain,
reason,
description,
source,
Schedulable.ScheduleType.valueOf(scheduleType),
scheduleContent,
uuid
);
Boolean enabled = actionObject.getBoolean("enabled");
if (enabled != null) {
schedulableAction.setEnabled(enabled);
}
schedulableAction.getScheduleHistories().addAll(decodeScheduleHistories(actionObject.getJSONArray("schedule_histories")));
executableAction = schedulableAction;
} else if ("immediate".equals(kind)) {
executableAction = new ImmediateExecutableAction(
tendency,
restoredChain,
reason,
description,
source,
uuid
);
} else {
return null;
}
if (status != null) {
executableAction.setStatus(Action.Status.valueOf(status));
}
if (executingStage != null) {
executableAction.setExecutingStage(executingStage);
}
String result = actionObject.getString("result");
if (result != null) {
executableAction.setResult(result);
}
executableAction.getStageDescriptions().putAll(restoredStageDescriptions);
executableAction.getHistory().putAll(decodeHistory(actionObject.getJSONArray("history")));
return executableAction;
}
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);
}
for (int i = 0; i < actionChainArray.size(); i++) {
JSONObject stageObject = actionChainArray.getJSONObject(i);
if (stageObject == null) {
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);
MetaAction metaAction = decodeMetaAction(actionObject);
if (metaAction != null) {
metaActions.add(metaAction);
}
}
restored.put(stage, metaActions);
}
return toMutableActionChain(restored);
}
private static MetaAction decodeMetaAction(@Nullable JSONObject actionObject) {
if (actionObject == null) {
return null;
}
String name = actionObject.getString("name");
Boolean io = actionObject.getBoolean("io");
String type = actionObject.getString("type");
String location = actionObject.getString("location");
if (name == null || io == null || type == null || location == null) {
return null;
}
MetaAction metaAction = new MetaAction(
name,
io,
actionObject.getString("launcher"),
MetaAction.Type.valueOf(type),
location
);
String paramsJson = actionObject.getString("params_json");
if (paramsJson != null && !paramsJson.isBlank()) {
JSONObject paramsObject = JSONObject.parseObject(paramsJson);
if (paramsObject != null) {
metaAction.getParams().putAll(paramsObject);
}
}
String resultStatus = actionObject.getString("result_status");
if (resultStatus != null) {
metaAction.getResult().setStatus(MetaAction.Result.Status.valueOf(resultStatus));
}
metaAction.getResult().setData(actionObject.getString("result_data"));
return metaAction;
}
private static Map<Integer, List<HistoryAction>> decodeHistory(@Nullable JSONArray historyArray) {
Map<Integer, List<HistoryAction>> restored = new LinkedHashMap<>();
if (historyArray == null) {
return restored;
}
for (int i = 0; i < historyArray.size(); i++) {
JSONObject stageObject = historyArray.getJSONObject(i);
if (stageObject == null) {
continue;
}
Integer stage = stageObject.getInteger("stage");
JSONArray actions = stageObject.getJSONArray("actions");
if (stage == null || actions == null) {
continue;
}
List<HistoryAction> historyActions = new ArrayList<>();
for (int j = 0; j < actions.size(); j++) {
JSONObject historyObject = actions.getJSONObject(j);
if (historyObject == null) {
continue;
}
String actionKey = historyObject.getString("action_key");
String description = historyObject.getString("description");
String result = historyObject.getString("result");
if (actionKey == null || description == null || result == null) {
continue;
}
historyActions.add(new HistoryAction(actionKey, description, result));
}
restored.put(stage, historyActions);
}
return restored;
}
private static List<StateValue> encodeHistoryStages(Map<Integer, ? extends List<HistoryAction>> historyMap) {
return historyMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.<StateValue>map(entry -> {
Map<String, StateValue> stageMap = new LinkedHashMap<>();
stageMap.put("stage", StateValue.num(entry.getKey()));
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
.map(historyAction -> (StateValue) encodeHistoryAction(historyAction))
.toList()));
return StateValue.obj(stageMap);
}).toList();
}
private static List<StateValue> encodeScheduleHistories(SchedulableExecutableAction schedulableAction) {
return schedulableAction.getScheduleHistories().stream()
.<StateValue>map(scheduleHistory -> {
Map<String, StateValue> historyMap = new LinkedHashMap<>();
historyMap.put("end_time", StateValue.str(scheduleHistory.getEndTime().toString()));
historyMap.put("result", StateValue.str(scheduleHistory.getResult()));
historyMap.put("history", StateValue.arr(encodeHistoryStages(scheduleHistory.getHistory())));
return StateValue.obj(historyMap);
})
.toList();
}
private static List<SchedulableExecutableAction.ScheduleHistory> decodeScheduleHistories(@Nullable JSONArray scheduleHistoriesArray) {
List<SchedulableExecutableAction.ScheduleHistory> restored = new ArrayList<>();
if (scheduleHistoriesArray == null) {
return restored;
}
for (int i = 0; i < scheduleHistoriesArray.size(); i++) {
JSONObject historyObject = scheduleHistoriesArray.getJSONObject(i);
if (historyObject == null) {
continue;
}
try {
String endTime = historyObject.getString("end_time");
String result = historyObject.getString("result");
if (endTime == null || result == null) {
continue;
}
restored.add(new SchedulableExecutableAction.ScheduleHistory(
ZonedDateTime.parse(endTime),
result,
decodeHistory(historyObject.getJSONArray("history"))
));
} catch (Exception e) {
log.warn("Skip invalid schedule_history item at index {}", i, e);
}
}
return restored;
}
private static Map<Integer, List<MetaAction>> toMutableActionChain(Map<Integer, List<MetaAction>> actionChain) {
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
actionChain.forEach((stage, actions) -> restored.put(stage, new ArrayList<>(actions)));
return restored;
}
private static String resolveExecutableResult(ExecutableAction action) {
try {
return action.getResult();
} catch (RuntimeException ignored) {
return null;
}
}
}
@@ -1,297 +0,0 @@
package work.slhaf.partner.core.action.entity
import work.slhaf.partner.module.action.executor.entity.HistoryAction
import java.time.Instant
import java.time.ZonedDateTime
import java.util.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
sealed class Action(
open val uuid: String = UUID.randomUUID().toString()
) {
/**
* 行动ID
*/
abstract val source: String
/**
* 行动原因
*/
abstract val reason: String
/**
* 行动描述
*/
abstract val description: String
abstract val timeout: Duration
val timeoutMills: Long
get() = timeout.inWholeMilliseconds
/**
* 行动状态
*/
var status: Status = Status.PREPARE
enum class Status {
/**
* 执行成功
*/
SUCCESS,
/**
* 执行失败
*/
FAILED,
/**
* 执行中
*/
EXECUTING,
/**
* 暂时中断
*/
INTERRUPTED,
/**
* 预备执行
*/
PREPARE
}
}
sealed interface Schedulable {
val scheduleType: ScheduleType
val scheduleContent: String
val uuid: String
val timeout: Duration
val timeoutMills: Long
var enabled: Boolean
enum class ScheduleType {
CYCLE,
ONCE
}
}
/**
* 行动模块传递的行动数据,包含行动uuid、倾向、状态、行动链、结果、发起原因、行动描述等信息。
*/
sealed class ExecutableAction(
override val uuid: String = UUID.randomUUID().toString()
) : Action(uuid) {
val executionLock: Any = Any()
/**
* 行动倾向
*/
abstract val tendency: String
/**
* 行动链
*/
abstract val actionChain: MutableMap<Int, MutableList<MetaAction>>
/**
* 行动阶段描述,用于为每个 stage 提供可落地的执行目标说明
*/
val stageDescriptions: MutableMap<Int, String> = mutableMapOf()
/**
* 行动阶段(当前阶段)
*/
var executingStage: Int = 0
/**
* 行动结果
*/
var result: String? = null
val history: MutableMap<Int, MutableList<HistoryAction>> = mutableMapOf()
override val timeout: Duration = 10.minutes
/**
* @param timeout 最长打断时间
* @return 是否超时结束
*/
fun interrupt(timeout: Int): Boolean {
status = Status.INTERRUPTED
val interruptAt = Instant.now().epochSecond
while (status == Status.INTERRUPTED) {
Thread.sleep(500)
if (Instant.now().epochSecond - interruptAt > timeout) {
return false
}
}
return true
}
fun resume() {
status = Status.EXECUTING
}
fun snapshot(): ExecutableActionSnapshot {
val schedulable = this as? Schedulable
return ExecutableActionSnapshot(
uuid = uuid,
source = source,
reason = reason,
description = description,
timeoutMills = timeoutMills,
status = status,
tendency = tendency,
actionChainSize = actionChain.size,
stageDescriptions = stageDescriptions.toMap(),
executingStage = executingStage,
result = result,
history = history.mapValues { (_, value) -> value.toList() },
scheduleType = schedulable?.scheduleType,
scheduleContent = schedulable?.scheduleContent,
enabled = schedulable?.enabled
)
}
}
/**
* 计划行动数据类,继承自[Action],扩展了[Schedulable]相关调度属性,用于标识计划类型(单次还是周期性任务)和计划内容
*/
data class SchedulableExecutableAction @JvmOverloads constructor(
override val tendency: String,
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
override val reason: String,
override val description: String,
override val source: String,
override val scheduleType: Schedulable.ScheduleType,
override val scheduleContent: String,
override val uuid: String = UUID.randomUUID().toString(),
) : ExecutableAction(uuid), Schedulable {
override var enabled = true
val scheduleHistories = ArrayList<ScheduleHistory>()
fun recordAndReset() {
val newHistory = ScheduleHistory(ZonedDateTime.now(), result ?: "null", history.toMap())
scheduleHistories.add(newHistory)
executingStage = 0
for (entry in actionChain) {
for (action in entry.value) {
action.params.clear()
action.result.reset()
}
}
}
data class ScheduleHistory(
val endTime: ZonedDateTime,
val result: String,
val history: Map<Int, List<HistoryAction>>
)
}
/**
* 即时行动数据类
*/
data class ImmediateExecutableAction @JvmOverloads constructor(
override val tendency: String,
override val actionChain: MutableMap<Int, MutableList<MetaAction>>,
override val reason: String,
override val description: String,
override val source: String,
override val uuid: String = UUID.randomUUID().toString(),
) : ExecutableAction(uuid)
/**
* 用于计时的一次性或周期性触发或者针对某一数据源进行内容更新的行动
*/
data class StateAction @JvmOverloads constructor(
override val source: String,
override val reason: String,
override val description: String,
override val scheduleType: Schedulable.ScheduleType,
override val scheduleContent: String,
val trigger: Trigger,
override var enabled: Boolean = true,
override val timeout: Duration = 5.minutes,
) : Action(), Schedulable {
fun snapshot(): StateActionSnapshot {
return StateActionSnapshot(
uuid = uuid,
source = source,
reason = reason,
description = description,
timeoutMills = timeoutMills,
status = status,
scheduleType = scheduleType,
scheduleContent = scheduleContent,
enabled = enabled,
)
}
sealed interface Trigger {
fun onTrigger()
/**
* State 更新触发
*/
class Update<T>(val stateSource: T, val update: (stateSource: T) -> Unit) : Trigger {
override fun onTrigger() {
update(stateSource)
}
}
/**
* 常规逻辑触发
*/
class Call(val call: () -> Unit) : Trigger {
override fun onTrigger() {
call()
}
}
}
}
data class ExecutableActionSnapshot(
val uuid: String,
val source: String,
val reason: String,
val description: String,
val timeoutMills: Long,
val status: Action.Status,
val tendency: String,
val actionChainSize: Int,
val stageDescriptions: Map<Int, String>,
val executingStage: Int,
val result: String?,
val history: Map<Int, List<HistoryAction>>,
val scheduleType: Schedulable.ScheduleType? = null,
val scheduleContent: String? = null,
val enabled: Boolean? = null
)
data class StateActionSnapshot(
val uuid: String,
val source: String,
val reason: String,
val description: String,
val timeoutMills: Long,
val status: Action.Status,
val scheduleType: Schedulable.ScheduleType,
val scheduleContent: String,
val enabled: Boolean
)
@@ -1,10 +0,0 @@
package work.slhaf.partner.core.action.entity;
import lombok.Data;
@Data
public class ActionFileMetaData {
private String content;
private String name;
private String ext;
}
@@ -1,84 +0,0 @@
package work.slhaf.partner.core.action.entity
/**
* 行动链中的单一元素,封装了调用外部行动程序的必要信息与结果容器,可被[work.slhaf.partner.core.action.ActionCapability]执行
*/
data class MetaAction(
/**
* 行动name,用于标识行动程序
*/
val name: String,
/**
* 是否IO密集,用于决定使用何种线程池
*/
val io: Boolean = false,
/**
* 启动器/解释器,对于原生 MCP Tool 、Dynamic Action 来说可忽略,目前仅用于 ORIGIN 类型
*/
val launcher: String? = null,
/**
* 行动程序类型,可分为 MCP、ORIGIN、BUILTIN 三种,
* 分别对应读取到的 MCP Tool、生成的临时行动程序、内置行动
*/
val type: Type,
/**
* 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id;
* 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串;
* 当类型为 BUILTIN 时,该字段固定为 builtin
*/
val location: String,
) {
/**
* 行动程序可接受的参数,由调用处设置
*/
val params: MutableMap<String, Any> = mutableMapOf()
/**
* 行动结果,包括执行状态和相应内容(执行结果或者错误信息)
*/
val result = Result()
val key: String
/**
* actionKey 将由 location+name 共同定位
*
* @return actionKey
*/
get() = "$location::$name"
class Result {
var status = Status.WAITING
var data: String? = null
fun reset() {
status = Status.WAITING
data = null
}
enum class Status {
SUCCESS,
FAILED,
WAITING
}
}
enum class Type {
/**
* 将调用的 MCP 工具,可包括远程、本地任意服务
*/
MCP,
/**
* 适用于‘临时生成’的行动程序,在生成后根据序列化选项及执行情况,进行持久化
*/
ORIGIN,
/**
* 由本地内置注册表直接执行的行动
*/
BUILTIN
}
}
@@ -1,44 +0,0 @@
package work.slhaf.partner.core.action.entity
import com.alibaba.fastjson2.JSONObject
data class MetaActionInfo(
/**
* 是否 IO 密集
*/
val io: Boolean,
/**
* 所需的启动器/解释器
*/
val launcher: String?,
/**
* 参数描述
*/
val params: Map<String, String>,
/**
* 行动功能描述
*/
val description: String,
/**
* 行动标签
*/
val tags: Set<String>,
/**
* 前置行动依赖
*/
val preActions: Set<String>,
/**
* 后置行动依赖
*/
val postActions: Set<String>,
/**
* 是否严格依赖前置行动的成功执行,若为true且前置行动失败则不执行该行动,后置任务多为触发式。默认即执行。
*/
val strictDependencies: Boolean,
/**
* 响应格式说明
*/
val responseSchema: JSONObject,
)
@@ -1,28 +0,0 @@
package work.slhaf.partner.core.action.entity.intervention;
public enum InterventionType {
/**
* 追加行动: 追加至指定行动链序列之后才执行
*/
APPEND,
/**
* 插入行动: 指定行动链序列执行过程中即时新增并执行
*/
INSERT,
/**
* 重建行动: 重建指定行动链序列之后的所有行动内容
*/
REBUILD,
/**
* 删除行动: 删除指定行动链序列上的指定行动单元
*/
DELETE,
/**
* 取消行动链: 中断并取消指定行动链的执行
*/
CANCEL
}
@@ -1,21 +0,0 @@
package work.slhaf.partner.core.action.entity.intervention;
import lombok.Data;
import java.util.List;
@Data
public class MetaIntervention {
/**
* 干预数据类型
*/
private InterventionType type;
/**
* 干预数据对应的行动链序列
*/
private int order;
/**
* 干预数据所需的行动key列表
*/
private List<String> actions;
}
@@ -1,39 +0,0 @@
package work.slhaf.partner.core.action.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class ActionInfrastructureStartupException extends AgentStartupException {
private final String component;
private final String path;
private final String command;
public ActionInfrastructureStartupException(String message, String component, @Nullable String path, @Nullable String command) {
super(message, "action-core");
this.component = component;
this.path = path;
this.command = command;
}
public ActionInfrastructureStartupException(String message, String component, @Nullable String path, @Nullable String command, Throwable cause) {
super(message, "action-core", cause);
this.component = component;
this.path = path;
this.command = command;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("component", component);
if (path != null) {
report.getExtra().put("path", path);
}
if (command != null) {
report.getExtra().put("command", command);
}
return report;
}
}
@@ -1,30 +0,0 @@
package work.slhaf.partner.core.action.exception;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class ActionLookupException extends AgentRuntimeException {
private final String actionKey;
private final String lookupTarget;
public ActionLookupException(String message, String actionKey, String lookupTarget) {
super(message);
this.actionKey = actionKey;
this.lookupTarget = lookupTarget;
}
public ActionLookupException(String message, String actionKey, String lookupTarget, Throwable cause) {
super(message, cause);
this.actionKey = actionKey;
this.lookupTarget = lookupTarget;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("actionKey", actionKey);
report.getExtra().put("lookupTarget", lookupTarget);
return report;
}
}
@@ -1,58 +0,0 @@
package work.slhaf.partner.core.action.exception;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
public class ActionSerializationException extends AgentRuntimeException {
private final String actionName;
private final String baseDir;
private final String fileExt;
private final String stage;
public ActionSerializationException(
String message,
@Nullable String actionName,
@Nullable String baseDir,
@Nullable String fileExt,
String stage
) {
super(message);
this.actionName = actionName;
this.baseDir = baseDir;
this.fileExt = fileExt;
this.stage = stage;
}
public ActionSerializationException(
String message,
@Nullable String actionName,
@Nullable String baseDir,
@Nullable String fileExt,
String stage,
Throwable cause
) {
super(message, cause);
this.actionName = actionName;
this.baseDir = baseDir;
this.fileExt = fileExt;
this.stage = stage;
}
@Override
public ExceptionReport toReport() {
ExceptionReport report = super.toReport();
report.getExtra().put("stage", stage);
if (actionName != null) {
report.getExtra().put("actionName", actionName);
}
if (baseDir != null) {
report.getExtra().put("baseDir", baseDir);
}
if (fileExt != null) {
report.getExtra().put("fileExt", fileExt);
}
return report;
}
}
@@ -1,207 +0,0 @@
package work.slhaf.partner.core.action.runner;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.Nullable;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
import work.slhaf.partner.core.action.runner.execution.McpActionExecutor;
import work.slhaf.partner.core.action.runner.execution.OriginExecutionService;
import work.slhaf.partner.core.action.runner.mcp.*;
import work.slhaf.partner.core.action.runner.policy.BwrapPolicyProvider;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.support.ActionSerializer;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class LocalRunnerClient extends RunnerClient {
public static final String MCP_NAME_DESC = "mcp-desc";
public static final String MCP_NAME_DYNAMIC = "mcp-dynamic";
private final String tmpActionPath;
private final String dynamicActionPath;
private final String mcpServerPath;
private final String mcpDescPath;
private final McpClientRegistry mcpClientRegistry;
private final McpTransportFactory mcpTransportFactory;
private final ActionSerializer actionSerializer;
private final OriginExecutionService originExecutionService;
private final McpActionExecutor mcpActionExecutor;
private final McpMetaRegistry mcpMetaRegistry;
private final McpDescWatcher mcpDescWatcher;
private final DynamicActionMcpManager dynamicActionMcpManager;
private final McpConfigWatcher mcpConfigWatcher;
private final AtomicBoolean closed = new AtomicBoolean(false);
public LocalRunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @Nullable String baseActionPath) {
super(existedMetaActions, executor, baseActionPath);
this.tmpActionPath = buildPathStr(ACTION_PATH, "tmp");
this.dynamicActionPath = buildPathStr(ACTION_PATH, "dynamic");
this.mcpServerPath = buildPathStr(ACTION_PATH, "mcp");
this.mcpDescPath = buildPathStr(mcpServerPath, "desc");
createPath(tmpActionPath);
createPath(dynamicActionPath);
createPath(mcpServerPath);
createPath(mcpDescPath);
McpClientRegistry clientRegistry = new McpClientRegistry();
McpTransportFactory transportFactory = new McpTransportFactory();
ActionSerializer serializer = new ActionSerializer(tmpActionPath, dynamicActionPath);
OriginExecutionService originService = new OriginExecutionService();
McpActionExecutor actionExecutor = new McpActionExecutor(clientRegistry);
McpMetaRegistry metaRegistry = null;
McpDescWatcher descWatcher = null;
DynamicActionMcpManager dynamicManager = null;
McpConfigWatcher configWatcher = null;
try {
registerPolicyProviders();
metaRegistry = new McpMetaRegistry(existedMetaActions);
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DESC, metaRegistry.clientConfig(MCP_NAME_DESC, 10));
log.info("DescMcp 注册完毕");
descWatcher = new McpDescWatcher(Path.of(mcpDescPath), metaRegistry, executor);
descWatcher.start();
dynamicManager = new DynamicActionMcpManager(
Path.of(dynamicActionPath),
existedMetaActions,
executor
);
registerMcpClient(clientRegistry, transportFactory, MCP_NAME_DYNAMIC, dynamicManager.clientConfig(10));
log.info("DynamicActionMcp 注册完毕");
dynamicManager.start();
configWatcher = new McpConfigWatcher(
Path.of(mcpServerPath),
existedMetaActions,
clientRegistry,
transportFactory,
metaRegistry,
executor
);
configWatcher.start();
configWatcher.registerPolicyListener();
} catch (ActionInfrastructureStartupException e) {
closeQuietly(configWatcher);
closeQuietly(dynamicManager);
closeQuietly(descWatcher);
closeQuietly(metaRegistry);
closeQuietly(clientRegistry);
throw e;
} catch (Exception e) {
closeQuietly(configWatcher);
closeQuietly(dynamicManager);
closeQuietly(descWatcher);
closeQuietly(metaRegistry);
closeQuietly(clientRegistry);
throw new ActionInfrastructureStartupException(
"LocalRunnerClient initialization failed",
"local-runner-client",
ACTION_PATH,
null,
e
);
}
this.mcpClientRegistry = clientRegistry;
this.mcpTransportFactory = transportFactory;
this.actionSerializer = serializer;
this.originExecutionService = originService;
this.mcpActionExecutor = actionExecutor;
this.mcpMetaRegistry = metaRegistry;
this.mcpDescWatcher = descWatcher;
this.dynamicActionMcpManager = dynamicManager;
this.mcpConfigWatcher = configWatcher;
}
private void registerPolicyProviders() {
OsInfo os = SystemUtil.getOsInfo();
if (os.isLinux()) {
ExecutionPolicyRegistry.INSTANCE.registerPolicyProvider(BwrapPolicyProvider.INSTANCE);
}
}
@Override
protected RunnerResponse doRun(MetaAction metaAction) {
RunnerResponse response;
response = switch (metaAction.getType()) {
case MCP -> mcpActionExecutor.run(metaAction);
case ORIGIN -> originExecutionService.run(metaAction);
case BUILTIN -> doRunWithBuiltin(metaAction);
};
return response;
}
@Override
public String buildTmpPath(String actionKey, String codeType) {
return actionSerializer.buildTmpPath(actionKey, codeType);
}
@Override
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
actionSerializer.tmpSerialize(tempAction, code, codeType);
}
@Override
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
actionSerializer.persistSerialize(metaActionInfo, fileMetaData);
}
private void registerMcpClient(McpClientRegistry clientRegistry, McpTransportFactory transportFactory, String id, McpTransportConfig transportConfig) {
val client = io.modelcontextprotocol.client.McpClient.sync(transportFactory.create(transportConfig))
.requestTimeout(java.time.Duration.ofSeconds(transportConfig.timeout()))
.clientInfo(new io.modelcontextprotocol.spec.McpSchema.Implementation(id, "PARTNER"))
.build();
clientRegistry.register(id, client);
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
mcpConfigWatcher.unregisterPolicyListener();
closeQuietly(mcpConfigWatcher);
closeQuietly(dynamicActionMcpManager);
closeQuietly(mcpDescWatcher);
closeQuietly(mcpMetaRegistry);
closeQuietly(mcpClientRegistry);
}
private void closeQuietly(AutoCloseable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (Exception ignored) {
}
}
public String buildPathStr(String... path) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < path.length; i++) {
str.append(path[i]);
if (i < path.length - 1) {
str.append("/");
}
}
return str.toString();
}
}
@@ -1,126 +0,0 @@
package work.slhaf.partner.core.action.runner;
import lombok.Data;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaAction.Result;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
import work.slhaf.partner.module.action.builtin.BuiltinActionRegistry;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
/**
* 执行客户端抽象类
* <br/>
* 只负责暴露序列化、执行等相应接口,具体逻辑交给下游实现
* <br/>
* 默认存在两类实现,{@link LocalRunnerClient} 和 {@link SandboxRunnerClient}
* <ol>
* LocalRunnerClient:
* <li>
* 对应本地运行环境,可在本地启动 MCP 客户端将 RunnerClient 暴露的能力接口转发至本地 MCP Client 并执行
* </li>
* SandboxRunnerClient:
* <li>
* 对应沙盒运行环境,该 Client 仅作为沙盒环境的客户端,不持有额外能力,仅保持远端连接已存在行动的内容更新
* </li>
* </ol>
*/
@Slf4j
public abstract class RunnerClient implements AutoCloseable {
protected final String ACTION_PATH;
protected final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
protected final ExecutorService executor;
@Setter
protected BuiltinActionRegistry builtinActionRegistry;
/**
* ActionCore 将注入虚拟线程池
*/
public RunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor, @NotNull String baseActionPath) {
this.existedMetaActions = existedMetaActions;
this.executor = executor;
this.ACTION_PATH = baseActionPath;
createPath(ACTION_PATH);
}
/**
* 执行行动程序
*/
public void submit(MetaAction metaAction) {
log.debug("执行行动: {}", metaAction);
// 获取已存在行动列表
Result result = metaAction.getResult();
if (!result.getStatus().equals(Result.Status.WAITING)) {
return;
}
RunnerResponse response = work.slhaf.partner.framework.agent.support.Result.runCatching(() -> doRun(metaAction)).fold(
runnerResponse -> runnerResponse,
ex -> {
RunnerResponse r = new RunnerResponse();
r.setOk(false);
r.setData(ex.getLocalizedMessage());
return r;
}
);
result.setData(response.getData());
result.setStatus(response.isOk() ? Result.Status.SUCCESS : Result.Status.FAILED);
log.debug("行动执行结果: {}", response);
}
protected abstract RunnerResponse doRun(MetaAction metaAction);
public abstract String buildTmpPath(String actionKey, String codeType);
public abstract void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException;
public abstract void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData);
protected RunnerResponse doRunWithBuiltin(MetaAction metaAction) {
RunnerResponse response = new RunnerResponse();
if (builtinActionRegistry == null) {
response.setOk(false);
response.setData("BuiltinActionRegistry 未初始化");
return response;
}
response.setData(builtinActionRegistry.call(metaAction.getKey(), metaAction.getParams()));
response.setOk(true);
return response;
}
protected void createPath(String pathStr) {
val path = Path.of(pathStr);
try {
Files.createDirectory(path);
} catch (IOException e) {
if (!Files.exists(path)) {
throw new ActionInfrastructureStartupException(
"Failed to create action directory: " + pathStr,
"runner-client",
pathStr,
null,
e
);
}
}
}
@Data
public static class RunnerResponse {
private boolean ok;
private String data;
}
}
@@ -1,60 +0,0 @@
package work.slhaf.partner.core.action.runner;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
/**
* 基于 Http 与 WebSocket 的沙盒执行器客户端,负责:
* <ul>
* <li>
* 发送行动单元数据
* </li>
* <li>
* 实时更新获取已存在行动列表
* </li>
* <li>
* 向传入的 MetaAction 回写执行结果
* </li>
* </ul>
*/
public class SandboxRunnerClient extends RunnerClient {
public SandboxRunnerClient(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ExecutorService executor) { // 连接沙盒执行器(websocket)
super(existedMetaActions, executor, null);
}
protected RunnerResponse doRun(MetaAction metaAction) {
return switch (metaAction.getType()) {
case BUILTIN -> doRunWithBuiltin(metaAction);
case MCP, ORIGIN -> {
// 调用沙盒执行器
yield null;
}
};
}
@Override
public String buildTmpPath(String actionKey, String codeType) {
throw new UnsupportedOperationException("Unimplemented method 'buildTmpPath'");
}
@Override
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
throw new UnsupportedOperationException("Unimplemented method 'tmpSerialize'");
}
@Override
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
throw new UnsupportedOperationException("Unimplemented method 'persistSerialize'");
}
@Override
public void close() throws Exception {
}
}
@@ -1,185 +0,0 @@
package work.slhaf.partner.core.action.runner.execution;
import lombok.Data;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class CommandExecutionService {
public static final CommandExecutionService INSTANCE = new CommandExecutionService();
private CommandExecutionService() {
}
public String[] buildFileExecutionCommands(String launcher, Map<String, Object> params, String absolutePath) {
int paramSize = params == null ? 0 : params.size();
String[] commands = new String[paramSize + 2];
commands[0] = launcher;
commands[1] = absolutePath;
AtomicInteger paramCount = new AtomicInteger(2);
if (params != null) {
params.forEach((param, value) -> commands[paramCount.getAndIncrement()] = "--" + param + "=" + value);
}
return commands;
}
public Result exec(List<String> commands) {
return exec(commands.toArray(new String[0]));
}
public Result exec(WrappedLaunchSpec launchSpec) {
Result result = new Result();
List<String> output = Collections.synchronizedList(new ArrayList<>());
List<String> error = Collections.synchronizedList(new ArrayList<>());
try {
Process process = startProcess(launchSpec);
Thread stdoutThread = Thread.startVirtualThread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.add(line);
}
} catch (Exception ignored) {
}
});
Thread stderrThread = Thread.startVirtualThread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
error.add(line);
}
} catch (Exception ignored) {
}
});
int exitCode = process.waitFor();
stdoutThread.join();
stderrThread.join();
result.setOk(exitCode == 0);
List<String> stdoutLines = List.copyOf(output);
List<String> stderrLines = List.copyOf(error);
result.setStdoutLines(stdoutLines);
result.setStderrLines(stderrLines);
result.setResultList(stdoutLines.isEmpty() ? stderrLines : stdoutLines);
result.setTotal(buildDisplayText(stdoutLines, stderrLines));
} catch (Exception e) {
result.setOk(false);
result.setTotal(e.getMessage());
result.setStdoutLines(List.of());
result.setStderrLines(List.of(e.getMessage()));
result.setResultList(result.getStderrLines());
}
return result;
}
public Result exec(String... commands) {
return exec(defaultLaunchSpec(commands));
}
public CommandSession createSessionTask(List<String> commands) {
return createSessionTask(commands.toArray(new String[0]));
}
public CommandSession createSessionTask(WrappedLaunchSpec launchSpec) {
try {
Process process = startProcess(launchSpec);
CommandSession session = new CommandSession();
StringBuilder stdoutBuffer = new StringBuilder();
StringBuilder stderrBuffer = new StringBuilder();
session.setProcess(process);
session.setStdoutBuffer(stdoutBuffer);
session.setStderrBuffer(stderrBuffer);
Thread.startVirtualThread(() -> readToBuffer(process.getInputStream(), stdoutBuffer));
Thread.startVirtualThread(() -> readToBuffer(process.getErrorStream(), stderrBuffer));
return session;
} catch (Exception e) {
throw new IllegalStateException("创建命令会话失败", e);
}
}
public CommandSession createSessionTask(String... commands) {
return createSessionTask(defaultLaunchSpec(commands));
}
private void readToBuffer(java.io.InputStream inputStream, StringBuilder buffer) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
synchronized (buffer) {
if (!buffer.isEmpty()) {
buffer.append('\n');
}
buffer.append(line);
}
}
} catch (Exception ignored) {
}
}
private Process startProcess(WrappedLaunchSpec launchSpec) throws Exception {
ProcessBuilder processBuilder = new ProcessBuilder();
List<String> command = new ArrayList<>();
command.add(launchSpec.getCommand());
command.addAll(launchSpec.getArgs());
processBuilder.command(command);
processBuilder.redirectErrorStream(false);
if (launchSpec.getWorkingDirectory() != null && !launchSpec.getWorkingDirectory().isBlank()) {
processBuilder.directory(new File(launchSpec.getWorkingDirectory()));
}
Map<String, String> environment = processBuilder.environment();
environment.clear();
environment.putAll(launchSpec.getEnvironment());
return processBuilder.start();
}
private WrappedLaunchSpec defaultLaunchSpec(String... commands) {
return new WrappedLaunchSpec(
commands[0],
List.of(commands).subList(1, commands.length),
null,
System.getenv()
);
}
private String buildDisplayText(List<String> stdoutLines, List<String> stderrLines) {
if (stdoutLines.isEmpty()) {
return String.join("\n", stderrLines);
}
if (stderrLines.isEmpty()) {
return String.join("\n", stdoutLines);
}
return String.join("\n", stdoutLines) + "\n" + String.join("\n", stderrLines);
}
@Data
public static class Result {
private boolean ok;
private String total;
private List<String> resultList;
private List<String> stdoutLines;
private List<String> stderrLines;
}
@Data
public static class CommandSession {
private Process process;
private StringBuilder stdoutBuffer;
private StringBuilder stderrBuffer;
}
}
@@ -1,72 +0,0 @@
package work.slhaf.partner.core.action.runner.execution;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
import java.util.List;
import java.util.stream.Collectors;
public class McpActionExecutor {
private final McpClientRegistry mcpClientRegistry;
public McpActionExecutor(McpClientRegistry mcpClientRegistry) {
this.mcpClientRegistry = mcpClientRegistry;
}
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation());
if (mcpClient == null) {
response.setOk(false);
response.setData("MCP client not found: " + metaAction.getLocation());
return response;
}
McpSchema.CallToolRequest callToolRequest = McpSchema.CallToolRequest.builder()
.name(metaAction.getName())
.arguments(metaAction.getParams())
.build();
McpSchema.CallToolResult callToolResult;
try {
callToolResult = mcpClient.callTool(callToolRequest);
} catch (Exception e) {
response.setOk(false);
response.setData("MCP tool call failed: " + e.getMessage());
return response;
}
Boolean error = callToolResult.isError();
response.setOk(error == null || !error);
response.setData(extractResponseData(callToolResult));
return response;
}
private String extractResponseData(McpSchema.CallToolResult callToolResult) {
Object structuredContent = callToolResult.structuredContent();
if (structuredContent != null) {
return String.valueOf(structuredContent);
}
List<McpSchema.Content> contents = callToolResult.content();
if (contents != null && !contents.isEmpty()) {
String contentSummary = contents.stream()
.map(this::renderContent)
.filter(text -> text != null && !text.isBlank())
.collect(Collectors.joining("\n"));
if (!contentSummary.isBlank()) {
return contentSummary;
}
}
return callToolResult.toString();
}
private String renderContent(McpSchema.Content content) {
if (content instanceof McpSchema.TextContent textContent) {
return textContent.text();
}
return String.valueOf(content);
}
}
@@ -1,35 +0,0 @@
package work.slhaf.partner.core.action.runner.execution;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.io.File;
import java.util.Arrays;
import static work.slhaf.partner.core.action.ActionCore.ORIGIN_LOCATION;
public class OriginExecutionService {
public OriginExecutionService() {
}
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
File file = new File(resolveOriginPath(metaAction));
String[] commands = CommandExecutionService.INSTANCE.buildFileExecutionCommands(metaAction.getLauncher(), metaAction.getParams(), file.getAbsolutePath());
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(Arrays.stream(commands).toList());
CommandExecutionService.Result execResult = CommandExecutionService.INSTANCE.exec(wrapped);
response.setOk(execResult.isOk());
response.setData(execResult.getTotal());
return response;
}
private String resolveOriginPath(MetaAction metaAction) {
if (ORIGIN_LOCATION.equals(metaAction.getLocation())) {
return metaAction.getName();
}
return metaAction.getLocation();
}
}
@@ -1,360 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class DynamicActionMcpManager implements AutoCloseable {
private final Path root;
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final CommandExecutionService commandExecutionService;
private final McpStatelessAsyncServer server;
private final InProcessMcpTransport clientTransport;
private final DirectoryWatchSupport watchSupport;
public DynamicActionMcpManager(Path root,
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
ExecutorService executor) throws IOException {
this.root = root;
this.existedMetaActions = existedMetaActions;
this.commandExecutionService = CommandExecutionService.INSTANCE;
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
this.clientTransport = pair.clientSide();
McpSchema.ServerCapabilities serverCapabilities = McpSchema.ServerCapabilities.builder()
.tools(true)
.build();
this.server = McpServer.async(pair.serverSide())
.capabilities(serverCapabilities)
.jsonMapper(McpJsonMapper.getDefault())
.build();
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 1, this::loadExisting)
.onCreate(this::handleCreate)
.onModify(this::handleModify)
.onDelete(this::handleDelete)
.onOverflow((thisDir, context) -> reconcile());
}
public McpTransportConfig.InProcess clientConfig(int timeout) {
return new McpTransportConfig.InProcess(timeout, clientTransport);
}
public void start() {
watchSupport.start();
log.info("DynamicActionMcp 文件监听注册完毕");
}
private void loadExisting() {
File file = root.toFile();
if (file.isFile()) {
throw new ActionInfrastructureStartupException(
"Expected a directory but found a file: " + root,
"dynamic-action-mcp-manager",
root.toString(),
null
);
}
File[] files = file.listFiles();
if (files == null) {
throw new ActionInfrastructureStartupException(
"Failed to read action directory: " + root,
"dynamic-action-mcp-manager",
root.toString(),
null
);
}
for (File dir : files) {
if (!normalPath(dir.toPath())) {
continue;
}
addAction(dir.getName(), dir.toPath());
}
}
private boolean isTmp(Path context) {
return context.getFileName().endsWith(".tmp");
}
private void handleModify(Path thisDir, Path context) {
if (context == null || isTmp(context) || !normalPath(thisDir)) {
return;
}
modify(thisDir, context);
}
private void handleCreate(Path thisDir, Path context) {
if (context == null || isTmp(context)) {
return;
}
if (thisDir.equals(root) && Files.isDirectory(context)) {
try {
watchSupport.registerDirectory(context);
} catch (IOException e) {
log.error("监听目录注册失败: {}", context);
}
}
if (normalPath(thisDir)) {
modify(thisDir, context);
}
if (Files.isDirectory(context) && normalPath(context)) {
File[] files = context.toFile().listFiles();
if (files == null) {
log.warn("目录无法访问: {}", context);
return;
}
for (File file : files) {
modify(context, file.toPath());
}
}
}
private void handleDelete(Path thisDir, Path context) {
if (context == null || isTmp(context)) {
return;
}
if (thisDir.equals(root)) {
String name = context.getFileName().toString();
Path candidate = root.resolve(name);
if (Files.isDirectory(candidate)) {
return;
}
removeAction(name);
AtomicReference<java.nio.file.WatchKey> toRemove = new AtomicReference<>();
watchSupport.context().watchKeys().forEach((key, path) -> {
if (path.getFileName().toString().equals(name)) {
key.cancel();
toRemove.set(key);
}
});
if (toRemove.get() != null) {
watchSupport.context().watchKeys().remove(toRemove.get());
}
return;
}
if (!thisDir.equals(root) && !normalPath(thisDir)) {
removeAction(thisDir.getFileName().toString());
}
}
private void reconcile() {
Set<String> existed = existedMetaActions.keySet().stream()
.filter(actionKey -> actionKey.startsWith("local::"))
.map(actionKey -> actionKey.split("::")[1])
.collect(Collectors.toSet());
Set<String> currentDirs = new HashSet<>();
try (Stream<Path> stream = Files.list(root).filter(Files::isDirectory)) {
stream.forEach(path -> {
String name = path.getFileName().toString();
currentDirs.add(name);
boolean contains = existed.contains(name);
boolean normal = normalPath(path);
if (contains && !normal) {
removeAction(name);
}
if (!contains) {
boolean alreadyWatching = watchSupport.isWatching(path);
if (!alreadyWatching) {
try {
watchSupport.registerDirectory(path);
} catch (IOException e) {
log.error("监听目录注册失败: {}", path);
}
}
if (normal) {
addAction(name, path);
}
}
});
} catch (IOException e) {
log.error("目录无法读取: {}", root);
return;
}
for (String existedName : existed) {
if (!currentDirs.contains(existedName)) {
removeAction(existedName);
}
}
}
private void modify(Path thisDir, Path context) {
String fileName = context.getFileName().toString();
if (fileName.equals("desc.json")) {
handleMetaModify(thisDir);
}
if (fileName.startsWith("run.")) {
handleProgramModify(thisDir);
}
}
private void handleProgramModify(Path thisDir) {
String name = thisDir.getFileName().toString();
String actionKey = "local::" + name;
if (existedMetaActions.containsKey(actionKey)) {
return;
}
if (!addAction(name, thisDir)) {
removeAction(name);
}
}
private void handleMetaModify(Path thisDir) {
String name = thisDir.getFileName().toString();
if (!addAction(name, thisDir)) {
removeAction(name);
}
}
private boolean addAction(String name, Path dir) {
File program = null;
try (Stream<Path> stream = Files.list(dir)) {
for (Path path : stream.toList()) {
if (isTmp(path)) {
continue;
}
if (path.getFileName().toString().startsWith("run.")) {
program = path.toFile();
}
}
} catch (Exception e) {
log.error("添加 action 失败", e);
return false;
}
MetaActionInfo info;
try {
info = JSON.parseObject(Files.readString(dir.resolve("desc.json"), StandardCharsets.UTF_8), MetaActionInfo.class);
} catch (Exception e) {
log.error("desc.json 加载失败: {}", dir);
return false;
}
String actionKey = "local::" + name;
existedMetaActions.put(actionKey, info);
server.addTool(buildAsyncToolSpecification(info, program, actionKey, name)).subscribe();
return true;
}
private void removeAction(String name) {
existedMetaActions.remove("local::" + name);
server.removeTool(name).subscribe();
}
private boolean normalPath(Path path) {
File[] files = loadFiles(path);
if (files == null || files.length < 2) {
return false;
}
boolean desc = false;
int run = 0;
for (File file : files) {
String fileName = file.getName();
if (fileName.endsWith(".tmp")) {
continue;
}
if (fileName.equals("desc.json")) {
desc = true;
}
if (fileName.startsWith("run.")) {
run++;
}
}
return run == 1 && desc;
}
private File[] loadFiles(Path path) {
if (!Files.isDirectory(path)) {
return null;
}
return path.toFile().listFiles();
}
private McpStatelessServerFeatures.AsyncToolSpecification buildAsyncToolSpecification(MetaActionInfo info, File program, String actionKey, String name) {
Map<String, Object> additional = Map.of(
"pre", info.getPreActions(),
"post", info.getPostActions(),
"strict_pre", info.getStrictDependencies(),
"io", info.getIo()
);
McpSchema.Tool tool = McpSchema.Tool.builder()
.name(name)
.description(info.getDescription())
.inputSchema(McpJsonMapper.getDefault(), JSONObject.toJSONString(info.getParams()))
.outputSchema(info.getResponseSchema())
.title(actionKey)
.meta(additional)
.build();
return McpStatelessServerFeatures.AsyncToolSpecification.builder()
.tool(tool)
.callHandler(buildToolHandler(program, info.getLauncher()))
.build();
}
private BiFunction<McpTransportContext, McpSchema.CallToolRequest, Mono<McpSchema.CallToolResult>> buildToolHandler(File program, String launcher) {
return (mcpTransportContext, callToolRequest) -> {
Map<String, Object> arguments = callToolRequest.arguments();
if (arguments == null) {
arguments = Map.of();
}
String[] commands = commandExecutionService.buildFileExecutionCommands(launcher, arguments, program.getAbsolutePath());
if (commands == null) {
return Mono.just(McpSchema.CallToolResult.builder()
.addTextContent("未知文件类型: " + program.getName())
.isError(true)
.build());
}
return Mono.fromCallable(() -> {
CommandExecutionService.Result execResult = commandExecutionService.exec(
ExecutionPolicyRegistry.INSTANCE.prepare(List.of(commands))
);
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder()
.isError(!execResult.isOk());
List<String> resultList = execResult.getResultList();
if (resultList != null && !resultList.isEmpty()) {
builder.textContent(resultList);
builder.structuredContent(resultList);
} else {
builder.addTextContent(execResult.getTotal());
builder.structuredContent(execResult.getTotal());
}
return builder.build();
}).subscribeOn(Schedulers.boundedElastic());
};
}
@Override
public void close() {
try {
watchSupport.close();
} catch (IOException ignored) {
}
server.close();
}
}
@@ -1,155 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.server.McpStatelessServerHandler;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
public final class InProcessMcpTransport implements McpClientTransport, McpStatelessServerTransport {
// 每个 transport 只处理一条 inbound 流
private final Sinks.Many<McpSchema.JSONRPCMessage> inbound =
Sinks.many().unicast().onBackpressureBuffer();
private final AtomicBoolean clientConnected = new AtomicBoolean(false);
private final AtomicBoolean serverConnected = new AtomicBoolean(false);
/**
* 对端
*/
private volatile InProcessMcpTransport peer;
private volatile McpStatelessServerHandler serverHandler;
public static Pair pair() {
InProcessMcpTransport client = new InProcessMcpTransport();
InProcessMcpTransport server = new InProcessMcpTransport();
client.peer = server;
server.peer = client;
return new Pair(client, server);
}
/* ======================================================
* Internal receive: peer.sendMessage -> this.receive
* ====================================================== */
private void receive(McpSchema.JSONRPCMessage message) {
if (inbound.tryEmitNext(message).isFailure()) {
throw new RuntimeException("Failed to receive message: " + message);
}
}
/* ======================================================
* Client → Server sendMessage
* ====================================================== */
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
InProcessMcpTransport p = this.peer;
if (p == null) {
return Mono.error(new IllegalStateException("Transport is not linked"));
}
return Mono.fromRunnable(() -> p.receive(message));
}
/* ======================================================
* Client connect(handler) 处理 server → client 消息
* ====================================================== */
@Override
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
if (!clientConnected.compareAndSet(false, true)) {
return Mono.error(new IllegalStateException("Client already connected"));
}
return inbound.asFlux()
.concatMap(msg ->
handler.apply(Mono.just(msg))
// handler may emit response message → send back to server
.flatMap(resp -> resp != null ? sendMessage(resp) : Mono.empty())
)
.doFinally(sig -> clientConnected.set(false))
.then();
}
@Override
public void setExceptionHandler(Consumer<Throwable> handler) {
McpClientTransport.super.setExceptionHandler(handler);
}
/* ======================================================
* Server: bind stateless handler = process client → server inbound
* ====================================================== */
@Override
public void setMcpHandler(McpStatelessServerHandler handler) {
this.serverHandler = handler;
if (!serverConnected.compareAndSet(false, true)) {
throw new IllegalStateException("Server already connected");
}
// 订阅 client → server 消息
inbound.asFlux()
.concatMap(this::handleServerMessage)
.doFinally(sig -> serverConnected.set(false))
.subscribe();
}
/**
* Server 端处理 JSONRPCMessage
*/
private Mono<Void> handleServerMessage(McpSchema.JSONRPCMessage msg) {
// 创建 transport context(简单实现即可)
McpTransportContext ctx = key -> null;
if (msg instanceof McpSchema.JSONRPCRequest req) {
return serverHandler.handleRequest(ctx, req)
.flatMap(this::sendMessage);
}
if (msg instanceof McpSchema.JSONRPCNotification noti) {
return serverHandler.handleNotification(ctx, noti);
}
return Mono.empty();
}
@Override
public void close() {
McpClientTransport.super.close();
}
/* ======================================================
* other boilerplate
* ====================================================== */
@Override
public Mono<Void> closeGracefully() {
inbound.tryEmitComplete();
clientConnected.set(false);
serverConnected.set(false);
return Mono.empty();
}
@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return McpJsonMapper.getDefault().convertValue(data, typeRef);
}
@Override
public List<String> protocolVersions() {
return McpClientTransport.super.protocolVersions();
}
public record Pair(InProcessMcpTransport clientSide, InProcessMcpTransport serverSide) {
}
}
@@ -1,49 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.client.McpSyncClient;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class McpClientRegistry implements AutoCloseable {
private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>();
public McpSyncClient get(String serverName) {
return clients.get(serverName);
}
public void register(String serverName, McpSyncClient client) {
McpSyncClient old = clients.put(serverName, client);
if (old != null && old != client) {
old.close();
}
}
public McpSyncClient remove(String serverName) {
McpSyncClient client = detach(serverName);
if (client != null) {
client.close();
}
return client;
}
public McpSyncClient detach(String serverName) {
return clients.remove(serverName);
}
public boolean contains(String serverName) {
return clients.containsKey(serverName);
}
public Set<String> listIds() {
return new HashSet<>(clients.keySet());
}
@Override
public void close() {
clients.forEach((id, client) -> client.close());
clients.clear();
}
}
@@ -1,335 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import cn.hutool.json.JSONUtil;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
import work.slhaf.partner.core.action.runner.policy.RunnerExecutionPolicyListener;
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
@Slf4j
public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyListener {
private final Path root;
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final McpClientRegistry mcpClientRegistry;
private final McpTransportFactory mcpTransportFactory;
private final McpMetaRegistry mcpMetaRegistry;
private final DirectoryWatchSupport watchSupport;
private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>();
public McpConfigWatcher(Path root,
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
McpClientRegistry mcpClientRegistry,
McpTransportFactory mcpTransportFactory,
McpMetaRegistry mcpMetaRegistry,
ExecutorService executor) throws IOException {
this.root = root;
this.existedMetaActions = existedMetaActions;
this.mcpClientRegistry = mcpClientRegistry;
this.mcpTransportFactory = mcpTransportFactory;
this.mcpMetaRegistry = mcpMetaRegistry;
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, this::loadInitial)
.onCreate(this::handleCreate)
.onModify((thisDir, context) -> checkAndReload(true))
.onDelete(this::handleDelete)
.onOverflow((thisDir, context) -> checkAndReload(false));
}
public void start() {
watchSupport.start();
log.info("CommonMcp 文件监听注册完毕");
}
private void loadInitial() {
File[] files = loadFiles(root);
if (files == null) {
return;
}
for (File file : files) {
if (!normalFile(file)) {
continue;
}
loadAndRegisterMcpClientsFromFile(file);
}
}
private void handleCreate(Path thisDir, Path context) {
if (context == null) {
return;
}
File file = context.toFile();
if (!normalFile(file)) {
return;
}
loadAndRegisterMcpClientsFromFile(file);
}
private void handleDelete(Path thisDir, Path context) {
if (context == null) {
return;
}
File file = context.toFile();
if (!file.getName().endsWith(".json")) {
return;
}
McpConfigFileRecord fileRecord = mcpConfigFileCache.remove(file);
if (fileRecord == null) {
return;
}
for (String clientId : fileRecord.paramsCacheMap().keySet()) {
unregisterClient(clientId);
}
}
private boolean normalFile(File file) {
return file.exists() && file.isFile() && file.getName().endsWith(".json");
}
private void registerMcpClient(String id, McpTransportConfig transportConfig) {
McpSyncClient client = McpClient.sync(mcpTransportFactory.create(transportConfig))
.requestTimeout(Duration.ofSeconds(transportConfig.timeout()))
.clientInfo(new McpSchema.Implementation(id, "PARTNER"))
.build();
try {
for (McpSchema.Tool tool : client.listTools().tools()) {
existedMetaActions.put(id + "::" + tool.name(), mcpMetaRegistry.buildMetaActionInfo(id, tool));
}
mcpClientRegistry.register(id, client);
} catch (Exception e) {
log.warn("[{}] MCP client init failed, skipped (probably non-stdio-safe)", id, e);
client.close();
}
}
private void unregisterClient(String clientId) {
McpSyncClient client = mcpClientRegistry.detach(clientId);
removeClientActions(clientId, client);
if (client != null) {
try {
client.close();
} catch (Exception e) {
log.warn("[{}] MCP client close failed", clientId, e);
}
}
}
private void removeClientActions(String clientId, McpSyncClient client) {
boolean removedByListing = false;
if (client != null) {
try {
List<McpSchema.Tool> tools = client.listTools().tools();
if (tools != null) {
for (McpSchema.Tool tool : tools) {
existedMetaActions.remove(clientId + "::" + tool.name());
}
removedByListing = true;
}
} catch (Exception e) {
log.warn("[{}] MCP client listTools failed during unregister, fallback to key scan", clientId, e);
}
}
if (!removedByListing) {
String prefix = clientId + "::";
existedMetaActions.keySet().removeIf(actionKey -> actionKey.startsWith(prefix));
}
}
private cn.hutool.json.JSONObject readJson(File file) {
try {
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
} catch (Exception ignored) {
return null;
}
}
private cn.hutool.json.JSONObject readMcp(cn.hutool.json.JSONObject json, String id) {
try {
return json.getJSONObject(id);
} catch (Exception ignored) {
return null;
}
}
@SuppressWarnings("unchecked")
private McpTransportConfig readParams(cn.hutool.json.JSONObject mcp) {
Set<String> keys = mcp.keySet();
int timeout = mcp.getInt("timeout", 30);
if (matchesKeys(keys, Set.of("command", "args", "env"), Set.of("timeout"))) {
String command = mcp.getStr("command");
Map<String, String> env = mcp.getBean("env", Map.class);
java.util.List<String> args = mcp.getBeanList("args", String.class);
if (command == null || env == null || args == null) {
return null;
}
return new McpTransportConfig.Stdio(timeout, command, env, args);
}
if (matchesKeys(keys, Set.of("uri", "endpoint", "headers"), Set.of("timeout"))) {
String uri = mcp.getStr("uri");
String endpoint = mcp.getStr("endpoint");
Map<String, String> headers = mcp.getBean("headers", Map.class);
if (uri == null || endpoint == null || headers == null) {
return null;
}
return new McpTransportConfig.Http(timeout, uri, endpoint, headers);
}
if (matchesKeys(keys, Set.of("url"), Set.of("timeout"))) {
String url = mcp.getStr("url");
if (url == null) {
return null;
}
return new McpTransportConfig.Http(timeout, url, "", Map.of());
}
return null;
}
private boolean matchesKeys(Set<String> actualKeys, Set<String> requiredKeys, Set<String> optionalKeys) {
if (!actualKeys.containsAll(requiredKeys)) {
return false;
}
Set<String> allowedKeys = new HashSet<>(requiredKeys);
allowedKeys.addAll(optionalKeys);
return allowedKeys.containsAll(actualKeys);
}
private void checkAndReload(boolean trustCache) {
HashMap<String, McpTransportConfig> changedMap = new HashMap<>();
HashSet<String> existingMcpIdSet = new HashSet<>();
File[] files = loadFiles(root);
if (files == null) {
return;
}
for (File file : files) {
if (!normalFile(file)) {
continue;
}
McpConfigFileRecord fileRecord = mcpConfigFileCache.get(file);
boolean fileRecordExists = fileRecord != null;
if (fileRecordExists && !fileChanged(file, fileRecord) && trustCache) {
existingMcpIdSet.addAll(fileRecord.paramsCacheMap().keySet());
continue;
}
cn.hutool.json.JSONObject mcpConfigJson = readJson(file);
if (mcpConfigJson == null) {
if (fileRecordExists) {
existingMcpIdSet.addAll(fileRecord.paramsCacheMap().keySet());
}
continue;
}
McpConfigFileRecord newFileRecord = new McpConfigFileRecord(file.lastModified(), file.length(), new HashMap<>());
for (String id : mcpConfigJson.keySet()) {
cn.hutool.json.JSONObject mcp = readMcp(mcpConfigJson, id);
if (mcp == null) {
continue;
}
McpTransportConfig params = readParams(mcp);
if (params == null) {
continue;
}
existingMcpIdSet.add(id);
newFileRecord.paramsCacheMap().put(id, params);
if (fileRecordExists) {
McpTransportConfig paramsCache = fileRecord.paramsCacheMap().get(id);
if (paramsCache != null && paramsCache.equals(params)) {
continue;
}
}
changedMap.put(id, params);
}
mcpConfigFileCache.put(file, newFileRecord);
}
updateMcpClients(changedMap, existingMcpIdSet);
}
private void updateMcpClients(HashMap<String, McpTransportConfig> changedMap, HashSet<String> existingMcpIdSet) {
changedMap.forEach((clientId, config) -> {
unregisterClient(clientId);
registerMcpClient(clientId, config);
});
for (String clientId : mcpClientRegistry.listIds()) {
if (clientId.equals(LocalRunnerClient.MCP_NAME_DESC) || clientId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)) {
continue;
}
if (!existingMcpIdSet.contains(clientId)) {
unregisterClient(clientId);
}
}
existedMetaActions.keySet().removeIf(actionKey -> {
String serverId = actionKey.split("::")[0];
return !serverId.equals("local")
&& !serverId.equals(LocalRunnerClient.MCP_NAME_DESC)
&& !serverId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)
&& !existingMcpIdSet.contains(serverId);
});
}
private boolean fileChanged(File file, McpConfigFileRecord fileRecord) {
return fileRecord.lastModified() != file.lastModified() || fileRecord.length() != file.length();
}
private void loadAndRegisterMcpClientsFromFile(File file) {
cn.hutool.json.JSONObject mcpConfigJson = readJson(file);
if (mcpConfigJson == null) {
return;
}
McpConfigFileRecord newFileRecord = new McpConfigFileRecord(file.lastModified(), file.length());
for (String id : mcpConfigJson.keySet()) {
cn.hutool.json.JSONObject mcp = readMcp(mcpConfigJson, id);
if (mcp == null) {
continue;
}
McpTransportConfig params = readParams(mcp);
if (params == null) {
continue;
}
registerMcpClient(id, params);
newFileRecord.paramsCacheMap().put(id, params);
}
mcpConfigFileCache.put(file, newFileRecord);
}
private File[] loadFiles(Path path) {
if (!path.toFile().isDirectory()) {
return null;
}
return path.toFile().listFiles();
}
@Override
public void close() throws Exception {
watchSupport.close();
}
@Override
public void onPolicyChanged(@NotNull ExecutionPolicy policy) {
checkAndReload(false);
}
private record McpConfigFileRecord(long lastModified, long length, Map<String, McpTransportConfig> paramsCacheMap) {
private McpConfigFileRecord(long lastModified, long length) {
this(lastModified, length, new HashMap<>());
}
}
}
@@ -1,53 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
@Slf4j
public class McpDescWatcher implements AutoCloseable {
private final Path root;
private final McpMetaRegistry mcpMetaRegistry;
private final DirectoryWatchSupport watchSupport;
public McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
this.root = root;
this.mcpMetaRegistry = mcpMetaRegistry;
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, () -> mcpMetaRegistry.loadDirectory(root))
.onCreate(this::handleUpsert)
.onModify(this::handleUpsert)
.onDelete(this::handleDelete)
.onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root));
}
public void start() {
watchSupport.start();
log.info("DescMcp 文件监听注册完毕");
}
private void handleUpsert(Path thisDir, Path context) {
if (context == null || Files.isDirectory(context) || !mcpMetaRegistry.isValidDescFile(context.getFileName().toString())) {
return;
}
if (!mcpMetaRegistry.addOrUpdate(context)) {
mcpMetaRegistry.remove(context);
}
}
private void handleDelete(Path thisDir, Path context) {
if (context == null || !mcpMetaRegistry.isValidDescFile(context.getFileName().toString())) {
return;
}
mcpMetaRegistry.remove(context);
}
@Override
public void close() throws Exception {
watchSupport.close();
}
}
@@ -1,260 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import javassist.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
@Slf4j
public class McpMetaRegistry implements AutoCloseable {
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, MetaActionInfo> descInfoCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, MetaActionInfo> originalInfoCache = new ConcurrentHashMap<>();
private final McpStatelessAsyncServer descServer;
private final InProcessMcpTransport clientTransport;
public McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) {
this.existedMetaActions = existedMetaActions;
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
this.clientTransport = pair.clientSide();
McpSchema.ServerCapabilities serverCapabilities = McpSchema.ServerCapabilities.builder()
.resources(true, true)
.build();
this.descServer = McpServer.async(pair.serverSide())
.capabilities(serverCapabilities)
.jsonMapper(McpJsonMapper.getDefault())
.build();
}
public McpTransportConfig.InProcess clientConfig(String serverName, int timeout) {
return new McpTransportConfig.InProcess(timeout, clientTransport);
}
public void loadDirectory(Path root) {
File[] files = loadFiles(root);
if (files == null) {
return;
}
for (File file : files) {
addOrUpdate(file);
}
}
public boolean addOrUpdate(Path path) {
return addOrUpdate(path.toFile());
}
public boolean addOrUpdate(File file) {
String name = file.getName();
if (!isValidDescFile(name)) {
return false;
}
try {
MetaActionInfo info = JSON.parseObject(Files.readString(file.toPath(), StandardCharsets.UTF_8), MetaActionInfo.class);
String uri = file.toPath().toUri().toString();
descCache.put(uri, JSONObject.toJSONString(info));
String actionKey = name.replace(".desc.json", "");
descInfoCache.put(actionKey, copyMetaActionInfo(info));
descServer.addResource(buildAsyncResourceSpecification(name, uri)).block();
if (existedMetaActions.containsKey(actionKey)) {
existedMetaActions.put(actionKey, mergeWithOriginal(actionKey, info));
}
return true;
} catch (Exception e) {
log.error("desc.json 解析失败: {}", file.getAbsolutePath());
return false;
}
}
public void remove(Path path) {
String uri = path.toUri().toString();
String actionKey = path.getFileName().toString().replace(".desc.json", "");
descCache.remove(uri);
descInfoCache.remove(actionKey);
descServer.removeResource(uri).block();
MetaActionInfo originalInfo = originalInfoCache.get(actionKey);
if (originalInfo != null) {
existedMetaActions.put(actionKey, copyMetaActionInfo(originalInfo));
return;
}
MetaActionInfo info = existedMetaActions.get(actionKey);
if (info != null) {
existedMetaActions.put(actionKey, resetMetaActionInfo(info));
}
}
public void reconcile(Path root) {
File[] files = loadFiles(root);
if (files == null) {
return;
}
Set<String> currentUris = ConcurrentHashMap.newKeySet();
for (File file : files) {
if (!isValidDescFile(file.getName())) {
continue;
}
currentUris.add(file.toURI().toString());
if (!addOrUpdate(file)) {
remove(file.toPath());
}
}
List<String> serverUris = descServer.listResources()
.map(McpSchema.Resource::uri)
.collectList()
.block();
if (serverUris == null) {
log.error("无法获取 DescMcpServer 持有的资源列表");
return;
}
for (String uri : serverUris) {
if (!currentUris.contains(uri)) {
remove(Path.of(java.net.URI.create(uri)));
}
}
}
public MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) {
String actionKey = serverId + "::" + tool.name();
MetaActionInfo baseInfo = buildToolMetaActionInfo(tool);
originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo));
MetaActionInfo override = descInfoCache.get(actionKey);
return override == null ? baseInfo : mergeWithOriginal(actionKey, override);
}
private McpStatelessServerFeatures.AsyncResourceSpecification buildAsyncResourceSpecification(String name, String uri) {
McpSchema.Resource resource = McpSchema.Resource.builder()
.name(name)
.title(name)
.description("Action descriptor for " + name)
.mimeType("application/json")
.uri(uri)
.build();
BiFunction<McpTransportContext, McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler = (context, request) -> {
String result = descCache.get(request.uri());
if (result == null) {
return Mono.error(new NotFoundException("未找到 Resource: " + request.uri()));
}
return Mono.just(new McpSchema.ReadResourceResult(List.of(
new McpSchema.TextResourceContents(request.uri(), "application/json", result)
)));
};
return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler);
}
public boolean isValidDescFile(String fileName) {
return fileName.endsWith(".desc.json") && fileName.contains("::");
}
private File[] loadFiles(Path root) {
if (!Files.isDirectory(root)) {
return null;
}
return root.toFile().listFiles();
}
private MetaActionInfo resetMetaActionInfo(@NotNull MetaActionInfo info) {
return new MetaActionInfo(
false,
info.getLauncher(),
copyParams(info.getParams()),
info.getDescription(),
new LinkedHashSet<>(),
new LinkedHashSet<>(),
new LinkedHashSet<>(),
false,
copyResponseSchema(info.getResponseSchema())
);
}
@Override
public void close() {
descServer.close();
}
private MetaActionInfo buildToolMetaActionInfo(McpSchema.Tool tool) {
Map<String, Object> outputSchema = tool.outputSchema();
boolean io = false;
Set<String> preActions = new LinkedHashSet<>();
Set<String> postActions = new LinkedHashSet<>();
boolean strictDependencies = false;
Set<String> tags = new LinkedHashSet<>();
Map<String, Object> meta = tool.meta();
if (meta != null) {
JSONObject metaJson = JSONObject.from(meta);
io = Boolean.TRUE.equals(metaJson.getBoolean("io"));
preActions = toOrderedSet(metaJson.getList("pre", String.class));
postActions = toOrderedSet(metaJson.getList("post", String.class));
strictDependencies = Boolean.TRUE.equals(metaJson.getBoolean("strict"));
tags = toOrderedSet(metaJson.getList("tag", String.class));
}
return new MetaActionInfo(
io,
null,
copyParams(tool.inputSchema().properties()),
tool.description(),
tags,
preActions,
postActions,
strictDependencies,
outputSchema == null ? JSONObject.of() : JSONObject.from(outputSchema)
);
}
private MetaActionInfo mergeWithOriginal(String actionKey, MetaActionInfo override) {
MetaActionInfo original = originalInfoCache.get(actionKey);
return override == null ? copyMetaActionInfo(original) : copyMetaActionInfo(override);
}
private MetaActionInfo copyMetaActionInfo(MetaActionInfo source) {
if (source == null) {
return null;
}
return new MetaActionInfo(
source.getIo(),
source.getLauncher(),
copyParams(source.getParams()),
source.getDescription(),
toOrderedSet(source.getTags()),
toOrderedSet(source.getPreActions()),
toOrderedSet(source.getPostActions()),
source.getStrictDependencies(),
copyResponseSchema(source.getResponseSchema())
);
}
private <T> LinkedHashSet<T> toOrderedSet(Collection<T> source) {
return source == null ? new LinkedHashSet<>() : new LinkedHashSet<>(source);
}
private Map<String, String> copyParams(Map<String, ?> params) {
if (params == null) {
return null;
}
Map<String, String> copied = new LinkedHashMap<>();
params.forEach((key, value) -> copied.put(key, value == null ? null : String.valueOf(value)));
return copied;
}
private JSONObject copyResponseSchema(JSONObject responseSchema) {
return responseSchema == null ? JSONObject.of() : JSONObject.from(responseSchema);
}
}
@@ -1,20 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import java.util.List;
import java.util.Map;
public sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess {
int timeout();
record Http(int timeout, String baseUri, String endpoint,
Map<String, String> headers) implements McpTransportConfig {
}
record Stdio(int timeout, String command, Map<String, String> env,
List<String> args) implements McpTransportConfig {
}
record InProcess(int timeout, InProcessMcpTransport clientTransport) implements McpTransportConfig {
}
}
@@ -1,44 +0,0 @@
package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.spec.McpClientTransport;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class McpTransportFactory {
public McpClientTransport create(McpTransportConfig config) {
return switch (config) {
case McpTransportConfig.Stdio stdio -> {
List<String> commands = new ArrayList<>();
commands.add(stdio.command());
commands.addAll(stdio.args());
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(commands);
Map<String, String> env = new HashMap<>(stdio.env());
env.putAll(wrapped.getEnvironment());
ServerParameters serverParameters = ServerParameters.builder(wrapped.getCommand())
.args(wrapped.getArgs())
.env(env)
.build();
yield new StdioClientTransport(serverParameters, McpJsonMapper.getDefault());
}
case McpTransportConfig.Http http -> {
McpSyncHttpClientRequestCustomizer customizer = (builder, method, endpoint, body, context) -> http.headers().forEach(builder::setHeader);
yield HttpClientSseClientTransport.builder(http.baseUri())
.httpRequestCustomizer(customizer)
.sseEndpoint(http.endpoint())
.build();
}
case McpTransportConfig.InProcess inProcess -> inProcess.clientTransport();
};
}
}
@@ -1,82 +0,0 @@
package work.slhaf.partner.core.action.runner.policy
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException
private const val BWRAP_COMMAND = "bwrap"
object BwrapPolicyProvider : PolicyProvider(
policyName = "bwrap"
) {
init {
requireBwrapAvailable()
}
override fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec {
val (command, args) = splitCommands(commands)
val wrappedArgs = buildList {
add("--ro-bind")
add("/")
add("/")
add("--proc")
add("/proc")
add("--dev")
add("/dev")
if (policy.net == ExecutionPolicy.Network.DISABLE) {
add("--unshare-net")
}
if (!policy.workingDirectory.isNullOrBlank()) {
add("--chdir")
add(policy.workingDirectory)
}
policy.readOnlyPaths.forEach { path ->
add("--ro-bind")
add(path)
add(path)
}
policy.writablePaths.forEach { path ->
add("--bind")
add(path)
add(path)
}
add("--")
add(command)
addAll(args)
}
return WrappedLaunchSpec(
command = BWRAP_COMMAND,
args = wrappedArgs,
workingDirectory = policy.workingDirectory,
environment = resolveEnvironment(policy)
)
}
private fun requireBwrapAvailable() {
val available = try {
val process = ProcessBuilder(BWRAP_COMMAND, "--version")
.redirectErrorStream(true)
.start()
val exitCode = process.waitFor()
exitCode == 0
} catch (e: Exception) {
throw ActionInfrastructureStartupException(
"Failed to detect executable command '$BWRAP_COMMAND'",
"bwrap-policy-provider",
null,
BWRAP_COMMAND,
e
)
}
if (!available) {
throw ActionInfrastructureStartupException(
"Executable command '$BWRAP_COMMAND' is not available",
"bwrap-policy-provider",
null,
BWRAP_COMMAND
)
}
}
}
@@ -1,20 +0,0 @@
package work.slhaf.partner.core.action.runner.policy
object DirectPolicyProvider : PolicyProvider(
policyName = "direct"
) {
override fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec {
val (command, args) = splitCommands(commands)
return WrappedLaunchSpec(
command = command,
args = args,
workingDirectory = policy.workingDirectory,
environment = resolveEnvironment(policy)
)
}
}
@@ -1,157 +0,0 @@
package work.slhaf.partner.core.action.runner.policy
import com.alibaba.fastjson2.JSONObject
import work.slhaf.partner.framework.agent.config.Config
import work.slhaf.partner.framework.agent.config.ConfigRegistration
import work.slhaf.partner.framework.agent.config.Configurable
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
object ExecutionPolicyRegistry : Configurable, ConfigRegistration<ExecutionPolicy> {
private const val DEFAULT_PROVIDER = "direct"
private val policyProviders = ConcurrentHashMap<String, PolicyProvider>().apply {
put(DEFAULT_PROVIDER, DirectPolicyProvider)
}
private val listeners = CopyOnWriteArraySet<RunnerExecutionPolicyListener>()
init {
register()
}
@Volatile
private lateinit var currentPolicy: ExecutionPolicy
fun prepare(commands: List<String>): WrappedLaunchSpec {
val policy = currentPolicy
val provider = policyProviders[policy.provider]
?: policyProviders[DEFAULT_PROVIDER]
?: error("Default provider '${DEFAULT_PROVIDER}' is not registered")
return provider.prepare(policy, commands)
}
fun updatePolicy(policy: ExecutionPolicy) {
currentPolicy = policy
listeners.forEach { it.onPolicyChanged(policy) }
}
fun addListener(listener: RunnerExecutionPolicyListener) {
listeners += listener
}
fun removeListener(listener: RunnerExecutionPolicyListener) {
listeners -= listener
}
fun registerPolicyProvider(policyProvider: PolicyProvider) {
val name = policyProvider.policyName
if (policyProviders.containsKey(name)) {
return
}
policyProviders[name] = policyProvider
}
override fun declare(): Map<Path, ConfigRegistration<out Config>> {
return mapOf(Path.of("action", "runner_policy.json") to this)
}
override fun type(): Class<ExecutionPolicy> {
return ExecutionPolicy::class.java
}
override fun init(
config: ExecutionPolicy,
json: JSONObject?
) {
this.currentPolicy = config
}
override fun defaultConfig(): ExecutionPolicy {
return ExecutionPolicy(
provider = "direct",
mode = ExecutionPolicy.Mode.DIRECT,
net = ExecutionPolicy.Network.ENABLE,
inheritEnv = true,
env = emptyMap(),
workingDirectory = null,
readOnlyPaths = emptySet(),
writablePaths = emptySet(),
)
}
override fun onReload(
config: ExecutionPolicy,
json: JSONObject?
) {
this.currentPolicy = config
}
}
data class ExecutionPolicy(
val mode: Mode,
val provider: String,
val net: Network,
val inheritEnv: Boolean,
val env: Map<String, String>,
val workingDirectory: String?,
val readOnlyPaths: Set<String>,
val writablePaths: Set<String>,
) : Config() {
enum class Mode {
DIRECT,
SANDBOX
}
enum class Network {
DISABLE,
ENABLE
}
}
data class WrappedLaunchSpec(
val command: String,
val args: List<String>,
val workingDirectory: String? = null,
val environment: Map<String, String> = emptyMap()
)
abstract class PolicyProvider(
val policyName: String
) {
abstract fun prepare(
policy: ExecutionPolicy,
commands: List<String>
): WrappedLaunchSpec
protected fun resolveEnvironment(policy: ExecutionPolicy): Map<String, String> {
val result = LinkedHashMap<String, String>()
if (policy.inheritEnv) {
result.putAll(System.getenv())
}
result.putAll(policy.env)
return result
}
protected fun splitCommands(commands: List<String>): Pair<String, List<String>> {
require(commands.isNotEmpty()) { "commands must not be empty" }
return commands.first() to commands.drop(1)
}
}
interface RunnerExecutionPolicyListener {
fun onPolicyChanged(policy: ExecutionPolicy)
fun registerPolicyListener() {
ExecutionPolicyRegistry.addListener(this)
}
fun unregisterPolicyListener() {
ExecutionPolicyRegistry.removeListener(this)
}
}
@@ -1,131 +0,0 @@
package work.slhaf.partner.core.action.runner.support;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.entity.ActionFileMetaData;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionSerializationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@Slf4j
public class ActionSerializer {
private final String tmpActionPath;
private final String dynamicActionPath;
public ActionSerializer(String tmpActionPath, String dynamicActionPath) {
this.tmpActionPath = tmpActionPath;
this.dynamicActionPath = dynamicActionPath;
}
public static String normalizeCodeType(String codeType) {
if (codeType == null || codeType.isBlank()) {
throw new IllegalArgumentException("codeType 不能为空");
}
return codeType.startsWith(".") ? codeType : "." + codeType;
}
private static @NotNull Path createActionDir(String baseName, Path baseDir, String fileExt) {
for (int i = 0; ; i++) {
String dirName = i == 0 ? baseName : baseName + "(" + i + ")";
Path candidate = baseDir.resolve(dirName);
try {
Files.createDirectory(candidate);
return candidate;
} catch (FileAlreadyExistsException ignored) {
} catch (IOException e) {
throw new ActionSerializationException(
"Failed to create action directory: " + candidate.toAbsolutePath(),
baseName,
baseDir.toAbsolutePath().toString(),
fileExt,
"CREATE_DIRECTORY",
e
);
}
}
}
public String buildTmpPath(String actionKey, String codeType) {
return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString();
}
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
log.debug("行动程序临时序列化: {}", tempAction);
Path path = Path.of(tempAction.getLocation());
validateTmpLocation(path, codeType);
File file = path.toFile();
file.createNewFile();
Files.writeString(path, code);
log.debug("临时序列化完毕");
}
private void validateTmpLocation(Path path, String codeType) throws IOException {
String normalizedCodeType = normalizeCodeType(codeType);
String fileName = path.getFileName().toString();
if (!fileName.endsWith(normalizedCodeType)) {
throw new IOException("临时文件路径与 codeType 不匹配: " + path);
}
}
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
log.debug("行动程序持久序列化: {}", metaActionInfo);
val baseDir = Path.of(dynamicActionPath);
if (!Files.isDirectory(baseDir)) {
throw new ActionSerializationException(
"Action base directory is not available: " + baseDir.toAbsolutePath(),
fileMetaData.getName(),
baseDir.toAbsolutePath().toString(),
fileMetaData.getExt(),
"VALIDATE_BASE_DIR"
);
}
val actionDir = createActionDir(fileMetaData.getName(), baseDir, fileMetaData.getExt());
val runTmp = actionDir.resolve("run." + fileMetaData.getExt() + ".tmp");
val descTmp = actionDir.resolve("desc.json.tmp");
val runFinal = actionDir.resolve("run." + fileMetaData.getExt());
val descFinal = actionDir.resolve("desc.json");
try {
Files.writeString(runTmp, fileMetaData.getContent());
Files.writeString(descTmp, JSONObject.toJSONString(metaActionInfo));
Files.move(runTmp, runFinal, StandardCopyOption.ATOMIC_MOVE);
Files.move(descTmp, descFinal, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
safeDelete(runTmp);
safeDelete(descTmp);
safeDelete(runFinal);
safeDelete(descFinal);
safeDelete(actionDir);
throw new ActionSerializationException(
"Failed to persist action files",
fileMetaData.getName(),
baseDir.toAbsolutePath().toString(),
fileMetaData.getExt(),
"WRITE_FILES",
e
);
}
log.debug("持久序列化结束");
}
private void safeDelete(Path path) {
try {
if (Files.exists(path)) {
Files.delete(path);
}
} catch (IOException ignored) {
}
}
}
@@ -1,29 +0,0 @@
package work.slhaf.partner.core.cognition;
import org.w3c.dom.Element;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
import java.util.concurrent.locks.Lock;
@Capability("cognition")
public interface CognitionCapability {
void initiateTurn(String input, String target, String... skippedModules);
ContextWorkspace contextWorkspace();
List<Message> getChatMessages();
List<Message> snapshotChatMessages();
void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor);
void refreshRecentChatMessagesContext();
Element messageNotesElement();
Lock getMessageLock();
}
@@ -1,214 +0,0 @@
package work.slhaf.partner.core.cognition;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import kotlin.Unit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.common.base.Block;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@CapabilityCore(value = "cognition")
public class CognitionCore implements StateSerializable {
private static final String RECENT_CHAT_MESSAGE_NOTES = """
消息格式:
- 所有消息统一写为“标记行 + 空行 + 正文”,比如:
[[AGENT]: self]: [NOT_REPLIED][COMPRESSED]:
正文内容
- 标记行一定包含身份标签,通常格式为 [[USER]: <userName>] 或 [[AGENT]: self]
- 若身份标签提取失败,可能回退为 [[Unknown]: Unknown]
- 若存在其他标签,则写为“身份标签: 状态标签串:”
- 正文永远从空行后开始
标记含义:
- [USER]: 外部用户来源
- [AGENT]: 系统内部来源
- [NOT_REPLIED]: 仅保留在历史中的未直接回复结果
- [COMPRESSED]: 该消息正文经过压缩
""";
private final ReentrantLock messageLock = new ReentrantLock();
/**
* 主模型的聊天记录
*/
private List<Message> chatMessages = new ArrayList<>();
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
public CognitionCore() {
register();
}
@CapabilityMethod
public ContextWorkspace contextWorkspace() {
return contextWorkspace;
}
@CapabilityMethod
public void initiateTurn(String input, String target, String... skippedModules) {
PartnerRunningFlowContext primaryContext = PartnerRunningFlowContext.fromSelf(input);
primaryContext.setTarget(target);
if (skippedModules != null) {
for (String skippedModule : skippedModules) {
primaryContext.addSkippedModule(skippedModule);
}
}
AgentRuntime.INSTANCE.submit(primaryContext);
}
@CapabilityMethod
public List<Message> getChatMessages() {
return chatMessages;
}
@CapabilityMethod
public List<Message> snapshotChatMessages() {
messageLock.lock();
try {
return List.copyOf(chatMessages);
} finally {
messageLock.unlock();
}
}
@CapabilityMethod
public void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor) {
messageLock.lock();
try {
int safeSnapshotSize = Math.clamp(snapshotSize, 0, chatMessages.size());
if (safeSnapshotSize == 0) {
return;
}
int safeDivisor = Math.max(retainDivisor, 1);
int retainCount = safeSnapshotSize / safeDivisor;
int retainStart = Math.max(0, safeSnapshotSize - retainCount);
List<Message> rolled = new ArrayList<>(chatMessages.subList(retainStart, safeSnapshotSize));
if (chatMessages.size() > safeSnapshotSize) {
rolled.addAll(chatMessages.subList(safeSnapshotSize, chatMessages.size()));
}
chatMessages = rolled;
} finally {
messageLock.unlock();
}
}
@CapabilityMethod
public Lock getMessageLock() {
return messageLock;
}
@CapabilityMethod
public void refreshRecentChatMessagesContext() {
ContextBlock block = new ContextBlock(
new BlockContent("recent_chat_messages", "communication_producer") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.appendChild(document.importNode(messageNotesElement(), true));
Element chatMessagesElement = document.createElement("chat_messages");
root.appendChild(chatMessagesElement);
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
messageElement.setAttribute("role", message.roleValue());
messageElement.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
},
Set.of(ContextBlock.FocusedDomain.COMMUNICATION),
100,
10,
4
);
contextWorkspace.register(block);
}
@CapabilityMethod
public Element messageNotesElement() {
return new Block("message_tag_notes") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setTextContent(RECENT_CHAT_MESSAGE_NOTES);
}
}.encodeToXml();
}
private List<Message> resolveRecentChatMessages() {
int exclusiveEnd = Math.max(chatMessages.size() - 1, 0);
if (exclusiveEnd == 0) {
return List.of();
}
int start = Math.max(exclusiveEnd - 6, 0);
return chatMessages.subList(start, exclusiveEnd);
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "cognition.json");
}
@Override
public void load(@NotNull JSONObject state) {
JSONArray messageArray = state.getJSONArray("chat_messages");
if (messageArray == null) {
log.warn("chat_messages is missing");
return;
}
for (int i = 0; i < messageArray.size(); i++) {
JSONObject messageObject = messageArray.getJSONObject(i);
if (messageObject == null) {
continue;
}
String role = messageObject.getString("role");
String content = messageObject.getString("content");
if (role == null || content == null) {
continue;
}
this.chatMessages.add(new Message(Message.Character.fromValue(role), content));
}
refreshRecentChatMessagesContext();
}
@Override
public @NotNull State convert() {
State state = new State();
List<StateValue.Obj> convertedMessageList = chatMessages.stream().map(message -> {
Map<String, StateValue> convertedMap = Map.of(
"role", StateValue.str(message.roleValue()),
"content", StateValue.str(message.getContent())
);
return StateValue.obj(convertedMap);
}).toList();
state.append("chat_messages", StateValue.arr(convertedMessageList));
return state;
}
}
@@ -1,401 +0,0 @@
package work.slhaf.partner.core.cognition
import com.alibaba.fastjson2.JSONObject
import org.w3c.dom.Document
import org.w3c.dom.Element
import work.slhaf.partner.common.base.Block
import work.slhaf.partner.framework.agent.log.TraceEvent
import work.slhaf.partner.framework.agent.log.TraceRecorder
import java.time.Duration
import java.time.Instant
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
import kotlin.math.max
import kotlin.math.min
class ContextWorkspace {
private val stateSet = mutableSetOf<ContextBlock>()
private val lock = ReentrantReadWriteLock()
/**
* 根据传入的 [ContextBlock.FocusedDomain] 列表,获取上下文块
* @param domains 需要获取上下文的域列表,顺序将决定权重优先级,按照列表排序将具备线性权重分层,最终反映到 blockContent 列表的排序上
*/
fun resolve(domains: List<ContextBlock.FocusedDomain>): ResolvedContext = lock.write {
if (domains.isEmpty()) {
return@write ResolvedContext(emptyList())
}
val primaryDomain = domains.first()
val domainWeights = domains
.distinct()
.withIndex()
.associate { (index, domain) -> domain to (domains.size - index) }
val activeBlocks = mutableListOf<ResolvedContextBlock>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val block = iterator.next()
val fadedScore = block.applyTimeFade()
if (fadedScore <= 0.0) {
iterator.remove()
continue
}
val matchedDomains = block.focusedOn.intersect(domainWeights.keys)
if (matchedDomains.isEmpty()) {
continue
}
val exposure = if (primaryDomain in matchedDomains) {
ContextBlock.Exposure.PRIMARY
} else {
ContextBlock.Exposure.SECONDARY
}
val activationScore = block.activate(exposure)
if (activationScore <= 0.0) {
iterator.remove()
continue
}
activeBlocks += ResolvedContextBlock(
block = block,
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
activationScore = activationScore,
renderedBlock = block.render(exposure)
)
}
val blocks = activeBlocks
.sortedWith(
compareByDescending<ResolvedContextBlock> { it.domainWeight }
.thenByDescending { it.activationScore }
.thenBy { it.block.sourceKey.blockName }
.thenBy { it.block.sourceKey.source }
)
.groupBy { it.block.sourceKey }
.values
.map { groupedBlocks ->
if (groupedBlocks.size == 1) {
renderResolvedBlock(groupedBlocks.first())
} else {
AggregatedBlockContent(groupedBlocks)
}
}
ResolvedContext(blocks)
}
private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent {
return resolved.renderedBlock
}
/**
* @param contextBlock 注册的新上下文块
*/
fun register(contextBlock: ContextBlock) = lock.write {
val changedSourceKeys = linkedSetOf<ContextBlock.SourceKey>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val currentBlock = iterator.next()
if (!currentBlock.sameWith(contextBlock)) {
continue
}
if (currentBlock.applyReplaceFade() <= 0.0) {
iterator.remove()
changedSourceKeys.add(currentBlock.sourceKey)
}
}
stateSet += contextBlock
changedSourceKeys.add(contextBlock.sourceKey)
record("register", changedSourceKeys)
}
fun expire(blockName: String, source: String) = lock.write {
val sourceKey = ContextBlock.SourceKey(blockName, source)
val changedSourceKeys = linkedSetOf<ContextBlock.SourceKey>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val block = iterator.next()
if (block.sourceKey == sourceKey) {
iterator.remove()
changedSourceKeys.add(block.sourceKey)
}
}
if (changedSourceKeys.isNotEmpty()) {
record("expire", changedSourceKeys)
}
}
private fun record(action: String, changedSourceKeys: Set<ContextBlock.SourceKey>) {
val payload = JSONObject()
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 {
val payload = JSONObject()
payload["blockName"] = block.sourceKey.blockName
payload["source"] = block.sourceKey.source
payload["focusedOn"] = block.focusedOn.map { it.name }
payload["fullRendered"] = block.blockContent.encodeToXmlString()
payload["compactRendered"] = block.compactBlock.encodeToXmlString()
payload["abstractRendered"] = block.abstractBlock.encodeToXmlString()
return payload
}
}
private data class ResolvedContextBlock(
val block: ContextBlock,
val domainWeight: Int,
val activationScore: Double,
val renderedBlock: BlockContent
)
data class ContextBlock @JvmOverloads constructor(
val blockContent: BlockContent,
val compactBlock: BlockContent = blockContent,
val abstractBlock: BlockContent = compactBlock,
/**
* 该 block 集中在哪些域
*/
val focusedOn: Set<FocusedDomain>,
/**
* 新的 [blockContent] 属性与其相同时,发生的衰退步长
*/
private val replaceFadeFactor: Double,
/**
* 随时间发生的衰退步长,按照分钟定义
*/
private val timeFadeFactor: Double,
/**
* 触发一次激活时,发生的强化步长
*/
private val activateFactor: Double
) {
internal enum class Exposure {
PRIMARY,
SECONDARY
}
private enum class ProjectionLevel {
ABSTRACT,
COMPACT,
FULL
}
/**
* 默认活跃分数,降低至0时将在 [ContextWorkspace] 中移除该 block
* 此外还参与到当同源 block 存在时的排序,按该分数升序排列,只影响同源 block 间的顺序
*/
private var activationScore = 100.0
private var lastTouchedAt = Instant.now()
enum class FocusedDomain {
ACTION,
MEMORY,
PERCEIVE,
COGNITION,
COMMUNICATION,
}
internal val sourceKey: SourceKey
get() = SourceKey(blockContent.blockName, blockContent.source)
fun applyTimeFade(): Double {
refreshByElapsedTime()
return activationScore
}
fun applyReplaceFade(): Double {
refreshByElapsedTime()
activationScore = max(0.0, activationScore - replaceFadeFactor)
return activationScore
}
internal fun activate(exposure: Exposure): Double {
refreshByElapsedTime()
val currentLevel = currentProjectionLevel()
val increasedScore = when (exposure) {
Exposure.PRIMARY -> activationScore + when (currentLevel) {
ProjectionLevel.FULL -> activateFactor
ProjectionLevel.COMPACT -> activateFactor * 0.6
ProjectionLevel.ABSTRACT -> activateFactor * 0.6
}
Exposure.SECONDARY -> activationScore + when (currentLevel) {
ProjectionLevel.COMPACT -> activateFactor * 0.2
ProjectionLevel.ABSTRACT -> activateFactor * 0.1
ProjectionLevel.FULL -> 0.0
}
}
activationScore = min(activationCeiling(exposure, currentLevel), increasedScore)
return activationScore
}
private fun refreshByElapsedTime() {
val now = Instant.now()
val elapsedSeconds = Duration.between(lastTouchedAt, now).toMillis() / 1000.0
activationScore = max(0.0, activationScore - elapsedSeconds * (timeFadeFactor / 60.0))
lastTouchedAt = now
}
fun sameWith(contextBlock: ContextBlock): Boolean {
return this.sourceKey == contextBlock.sourceKey
}
internal fun render(exposure: Exposure): BlockContent {
return when (exposure) {
Exposure.PRIMARY -> when (currentProjectionLevel()) {
ProjectionLevel.FULL -> blockContent
ProjectionLevel.COMPACT, ProjectionLevel.ABSTRACT -> compactBlock
}
Exposure.SECONDARY -> when (currentProjectionLevel()) {
ProjectionLevel.ABSTRACT -> abstractBlock
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> compactBlock
}
}
}
fun render(): BlockContent {
return when (currentProjectionLevel()) {
ProjectionLevel.ABSTRACT -> abstractBlock
ProjectionLevel.COMPACT -> compactBlock
ProjectionLevel.FULL -> blockContent
}
}
private fun currentProjectionLevel(): ProjectionLevel {
return when {
activationScore < ABSTRACT_TO_COMPACT_THRESHOLD -> ProjectionLevel.ABSTRACT
activationScore < COMPACT_TO_FULL_THRESHOLD -> ProjectionLevel.COMPACT
else -> ProjectionLevel.FULL
}
}
private fun activationCeiling(exposure: Exposure, currentLevel: ProjectionLevel): Double {
return when (exposure) {
Exposure.PRIMARY -> when (currentLevel) {
ProjectionLevel.ABSTRACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
}
Exposure.SECONDARY -> when (currentLevel) {
ProjectionLevel.ABSTRACT -> ABSTRACT_TO_COMPACT_THRESHOLD - PROJECTION_EPSILON
ProjectionLevel.COMPACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
}
}
}
data class SourceKey(
val blockName: String,
val source: String
)
companion object {
private const val MAX_ACTIVATION_SCORE = 100.0
private const val ABSTRACT_TO_COMPACT_THRESHOLD = 30.0
private const val COMPACT_TO_FULL_THRESHOLD = 70.0
private const val PROJECTION_EPSILON = 0.000001
}
}
private class AggregatedBlockContent(
private val groupedBlocks: List<ResolvedContextBlock>
) : BlockContent(
groupedBlocks.first().block.sourceKey.blockName,
groupedBlocks.first().block.sourceKey.source,
groupedBlocks.maxByOrNull { it.renderedBlock.urgency.ordinal }?.renderedBlock?.urgency ?: Urgency.NORMAL
) {
override fun fillXml(document: Document, root: Element) {
val snapshotIndex = groupedBlocks.withIndex()
.maxWithOrNull(
compareBy<IndexedValue<ResolvedContextBlock>> { it.value.activationScore }
.thenBy { it.index }
)?.index ?: 0
groupedBlocks.forEachIndexed { index, groupedBlock ->
val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot"
val wrapper = document.createElement(tagName)
val renderedBlock = groupedBlock.renderedBlock
val encoded = renderedBlock.encodeToXml()
val attributes = encoded.attributes
for (attributeIndex in 0 until attributes.length) {
val attribute = attributes.item(attributeIndex)
wrapper.setAttribute(attribute.nodeName, attribute.nodeValue)
}
root.appendChild(wrapper)
val childNodes = encoded.childNodes
for (childIndex in 0 until childNodes.length) {
wrapper.appendChild(document.importNode(childNodes.item(childIndex), true))
}
}
}
}
abstract class BlockContent @JvmOverloads protected constructor(
blockName: String,
val source: String,
val urgency: Urgency = Urgency.NORMAL
) : Block(blockName) {
enum class Urgency {
LOW,
NORMAL,
HIGH,
CRITICAL
}
override fun appendRootAttributes(): Map<String, String> {
return mapOf(
"source" to source,
"urgency" to urgency.name.lowercase(Locale.ROOT)
)
}
}
abstract class CommunicationBlockContent @JvmOverloads constructor(
blockName: String,
source: String,
urgency: Urgency = Urgency.NORMAL,
val type: Projection = Projection.CONTEXT,
) : BlockContent(
blockName,
source,
urgency
) {
enum class Projection {
SUPPLY,
CONTEXT
}
}
@@ -1,61 +0,0 @@
package work.slhaf.partner.core.cognition
import org.w3c.dom.Document
import work.slhaf.partner.framework.agent.model.pojo.Message
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
data class ResolvedContext(
val blocks: List<BlockContent>
) {
fun encodeToMessage(): Message {
val content = if (blocks.isEmpty()) {
"<no_context></no_context>"
} else {
buildContextXml(blocks)
}
return Message(Message.Character.USER, content)
}
private fun buildContextXml(blocks: List<BlockContent>): String {
return try {
val document = newDocument()
val root = document.createElement("context")
document.appendChild(root)
blocks.stream()
.map(BlockContent::encodeToXml)
.forEach { blockElement ->
root.appendChild(document.importNode(blockElement, true))
}
toXmlString(document)
} catch (e: Exception) {
throw IllegalStateException("构建 context 区段失败", e)
}
}
private fun newDocument(): Document {
return DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument()
}
private fun toXmlString(document: Document): String {
val transformer = TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
}
return StringWriter().use { writer ->
transformer.transform(DOMSource(document), StreamResult(writer))
writer.toString()
}
}
}
@@ -1,27 +0,0 @@
package work.slhaf.partner.core.memory;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import java.util.Collection;
import java.util.List;
@Capability(value = "memory")
public interface MemoryCapability {
MemoryUnit getMemoryUnit(String unitId);
Result<MemorySlice> getMemorySlice(String unitId, String sliceId);
MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary);
Collection<MemoryUnit> listMemoryUnits();
void refreshMemorySession();
String getMemorySessionId();
}
@@ -1,184 +0,0 @@
package work.slhaf.partner.core.memory;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@CapabilityCore(value = "memory")
@Slf4j
public class MemoryCore implements StateSerializable {
private final Lock memoryLock = new ReentrantLock();
private final ConcurrentHashMap<String, MemoryUnit> memoryUnits = new ConcurrentHashMap<>();
// 默认值一般只存在于智能体初次启动时
private String memorySessionId = UUID.randomUUID().toString();
public MemoryCore() {
register();
}
@CapabilityMethod
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
memoryLock.lock();
try {
MemoryUnit unit = getMemoryUnit(memorySessionId);
unit.updateTimestamp();
List<Message> conversationMessages = unit.getConversationMessages();
int startIndex = conversationMessages.size();
MemorySlice memorySlice = new MemorySlice(
startIndex,
startIndex + chatMessages.size(),
summary
);
conversationMessages.addAll(chatMessages);
unit.getSlices().add(memorySlice);
normalizeMemoryUnit(unit);
return unit;
} finally {
memoryLock.unlock();
}
}
@CapabilityMethod
public MemoryUnit getMemoryUnit(String unitId) {
MemoryUnit unit = memoryUnits.computeIfAbsent(unitId, id -> {
MemoryUnit newUnit = new MemoryUnit(id);
newUnit.register();
return newUnit;
});
unit.load();
return unit;
}
@CapabilityMethod
public Result<MemorySlice> getMemorySlice(String unitId, String sliceId) {
MemoryUnit memoryUnit = memoryUnits.get(unitId);
if (memoryUnit == null) {
return Result.failure(new MemoryLookupException(
"Memory slice not found: " + unitId + ":" + sliceId,
unitId + ":" + sliceId,
"MEMORY_SLICE"
));
}
for (MemorySlice slice : memoryUnit.getSlices()) {
if (sliceId.equals(slice.getId())) {
return Result.success(slice);
}
}
return Result.failure(new MemoryLookupException(
"Memory slice not found: " + unitId + ":" + sliceId,
unitId + ":" + sliceId,
"MEMORY_SLICE"
));
}
@CapabilityMethod
public Collection<MemoryUnit> listMemoryUnits() {
return new ArrayList<>(memoryUnits.values());
}
@CapabilityMethod
public void refreshMemorySession() {
memorySessionId = UUID.randomUUID().toString();
}
@CapabilityMethod
public String getMemorySessionId() {
return memorySessionId;
}
private void normalizeMemoryUnit(MemoryUnit memoryUnit) {
if (memoryUnit.getTimestamp() == null || memoryUnit.getTimestamp() <= 0) {
memoryUnit.updateTimestamp();
}
int maxEndExclusive = memoryUnit.getConversationMessages().size();
List<MemorySlice> normalizedSlices = new ArrayList<>(memoryUnit.getSlices().size());
for (MemorySlice slice : memoryUnit.getSlices()) {
if (slice == null) {
continue;
}
String sliceId = slice.getId();
if (sliceId == null || sliceId.isBlank()) {
sliceId = UUID.randomUUID().toString();
}
long sliceTimestamp = slice.getTimestamp() == null || slice.getTimestamp() <= 0
? memoryUnit.getTimestamp()
: slice.getTimestamp();
int startIndex = slice.getStartIndex() == null || slice.getStartIndex() < 0
? 0
: Math.min(slice.getStartIndex(), maxEndExclusive);
int endIndex = slice.getEndIndex() == null || slice.getEndIndex() < startIndex
? maxEndExclusive
: Math.min(slice.getEndIndex(), maxEndExclusive);
normalizedSlices.add(MemorySlice.restore(
sliceId,
startIndex,
endIndex,
slice.getSummary(),
sliceTimestamp
));
}
memoryUnit.getSlices().clear();
memoryUnit.getSlices().addAll(normalizedSlices);
memoryUnit.getSlices().sort(Comparator.naturalOrder());
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "memory.json");
}
@Override
public void load(@NotNull JSONObject state) {
String memorySessionId = state.getString("memory_session_id");
if (memorySessionId == null) {
throw new IllegalStateException("Memory session id is missing");
}
JSONArray array = state.getJSONArray("memory_unit_uuid_set");
if (array == null) {
throw new IllegalStateException("Memory unit uuid set is missing");
}
for (int i = 0; i < array.size(); i++) {
String unitUuid = array.getString(i);
if (unitUuid == null) {
throw new IllegalStateException("memory_unit_uuid_set is not a uuid array, index: " + i);
}
MemoryUnit memoryUnit = new MemoryUnit(unitUuid);
memoryUnits.put(unitUuid, memoryUnit);
}
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("memory_session_id", StateValue.str(memorySessionId));
List<StateValue.Str> unitOverview = memoryUnits.keySet().stream()
.map(StateValue::str)
.toList();
state.append("memory_unit_uuid_set", StateValue.arr(unitOverview));
return state;
}
}
@@ -1,45 +0,0 @@
package work.slhaf.partner.core.memory.pojo;
import lombok.Getter;
import java.util.UUID;
@Getter
public class MemorySlice implements Comparable<MemorySlice> {
private final String id;
private final Integer startIndex;
private final Integer endIndex;
private final String summary;
private final Long timestamp;
public MemorySlice(Integer startIndex, Integer endIndex, String summary) {
this.id = UUID.randomUUID().toString();
this.timestamp = System.currentTimeMillis();
this.startIndex = startIndex;
this.endIndex = endIndex;
this.summary = summary;
}
private MemorySlice(String id, Integer startIndex, Integer endIndex, String summary, Long timestamp) {
this.id = id;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.summary = summary;
this.timestamp = timestamp;
}
public static MemorySlice restore(String id, Integer startIndex, Integer endIndex, String summary, Long timestamp) {
return new MemorySlice(id, startIndex, endIndex, summary, timestamp);
}
@Override
public int compareTo(MemorySlice memorySlice) {
if (memorySlice.getTimestamp() > this.getTimestamp()) {
return -1;
} else if (memorySlice.getTimestamp() < this.timestamp) {
return 1;
}
return 0;
}
}
@@ -1,125 +0,0 @@
package work.slhaf.partner.core.memory.pojo;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Getter
public class MemoryUnit implements StateSerializable {
private final String id;
private final List<Message> conversationMessages = new ArrayList<>();
private Long timestamp = 0L;
private final List<MemorySlice> slices = new ArrayList<>();
public MemoryUnit(String id) {
this.id = id;
this.register();
}
public void updateTimestamp() {
timestamp = System.currentTimeMillis();
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "memory", "memory-unit" + id + ".json");
}
@Override
public void load(@NotNull JSONObject state) {
Long loadedTimestamp = state.getLong("update_timestamp");
this.timestamp = loadedTimestamp != null ? loadedTimestamp : 0L;
this.conversationMessages.clear();
this.slices.clear();
JSONArray messageArray = state.getJSONArray("conversation_messages");
if (messageArray != null) {
for (int i = 0; i < messageArray.size(); i++) {
JSONObject messageObject = messageArray.getJSONObject(i);
if (messageObject == null) {
continue;
}
String role = messageObject.getString("role");
String content = messageObject.getString("content");
if (role == null || content == null) {
continue;
}
Message message = new Message(Message.Character.fromValue(role), content);
this.conversationMessages.add(message);
}
}
var sliceArray = state.getJSONArray("memory_slices");
if (sliceArray != null) {
for (int i = 0; i < sliceArray.size(); i++) {
JSONObject sliceObject = sliceArray.getJSONObject(i);
if (sliceObject == null) {
continue;
}
String sliceId = sliceObject.getString("id");
Integer startIndex = sliceObject.getInteger("start_index");
Integer endIndex = sliceObject.getInteger("end_index");
String summary = sliceObject.getString("summary");
Long createdTimestamp = sliceObject.getLong("created_timestamp");
if (sliceId == null || startIndex == null || endIndex == null || summary == null || createdTimestamp == null) {
continue;
}
MemorySlice slice = MemorySlice.restore(sliceId, startIndex, endIndex, summary, createdTimestamp);
this.slices.add(slice);
}
}
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("id", StateValue.str(id));
state.append("update_timestamp", StateValue.num(timestamp));
List<StateValue.Obj> convertedMessageList = conversationMessages.stream().map(message -> {
Map<String, StateValue> convertedMap = Map.of(
"role", StateValue.str(message.roleValue()),
"content", StateValue.str(message.getContent())
);
return StateValue.obj(convertedMap);
}).toList();
state.append("conversation_messages", StateValue.arr(convertedMessageList));
List<StateValue.Obj> convertedSliceList = slices.stream().map(slice -> {
Map<String, StateValue> convertedMap = Map.of(
"id", StateValue.str(slice.getId()),
"start_index", StateValue.num(slice.getStartIndex()),
"end_index", StateValue.num(slice.getEndIndex()),
"summary", StateValue.str(slice.getSummary()),
"created_timestamp", StateValue.num(slice.getTimestamp())
);
return StateValue.obj(convertedMap);
}).toList();
state.append("memory_slices", StateValue.arr(convertedSliceList));
return state;
}
@Override
public boolean autoLoadOnRegister() {
return false;
}
}
@@ -1,13 +0,0 @@
package work.slhaf.partner.core.memory.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SliceRef {
private String unitId;
private String sliceId;
}
@@ -1,12 +0,0 @@
package work.slhaf.partner.core.perceive;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import java.time.Instant;
@Capability(value = "perceive")
public interface PerceiveCapability {
String refreshInteract();
Instant showLastInteract();
}
@@ -1,53 +0,0 @@
package work.slhaf.partner.core.perceive;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.state.State;
import work.slhaf.partner.framework.agent.state.StateSerializable;
import work.slhaf.partner.framework.agent.state.StateValue;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@CapabilityCore(value = "perceive")
public class PerceiveCore implements StateSerializable {
private Instant lastInteractTime = Instant.ofEpochMilli(0);
public PerceiveCore() {
register();
}
@CapabilityMethod
public String refreshInteract() {
String last = lastInteractTime.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
lastInteractTime = Instant.now();
return last;
}
@CapabilityMethod
public Instant showLastInteract() {
return lastInteractTime;
}
@Override
public @NotNull Path statePath() {
return Path.of("core", "perceive.json");
}
@Override
public void load(@NotNull JSONObject state) {
this.lastInteractTime = Instant.ofEpochMilli(state.getLong("last_interact_time"));
}
@Override
public @NotNull State convert() {
State state = new State();
state.append("last_interact_time", StateValue.num(lastInteractTime.toEpochMilli()));
return state;
}
}
@@ -1,14 +0,0 @@
package work.slhaf.partner.module.action.builtin;
import java.util.List;
public interface BuiltinActionProvider {
List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions();
String createActionKey(String actionName);
default String param(String requirement, String type, String detail) {
return requirement + "|" + type + "|" + detail;
}
}
@@ -1,130 +0,0 @@
package work.slhaf.partner.module.action.builtin;
import lombok.Getter;
import lombok.NonNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionLookupException;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
@Getter
private final Map<String, BuiltinActionDefinition> definitions = new LinkedHashMap<>();
@InjectCapability
private ActionCapability actionCapability;
@Init
public void init() {
actionCapability.runnerClient().setBuiltinActionRegistry(this);
}
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) {
BuiltinActionDefinition definition = definitions.get(actionKey);
if (definition == null) {
throw new ActionLookupException(
"Builtin action definition not found: " + actionKey,
actionKey,
"BUILTIN_DEFINITION"
);
}
String result = definition.invoker().apply(params);
if (result == null) {
return "null";
}
return result;
}
public record BuiltinActionDefinition(
String actionKey,
MetaActionInfo metaActionInfo,
Function<Map<String, Object>, String> invoker
) {
static String requireString(Map<String, Object> params, String key) {
Object value = params.get(key);
if (value == null) {
throw new IllegalArgumentException("缺少参数: " + key);
}
if (!(value instanceof String s)) {
throw new IllegalArgumentException("参数 " + key + " 必须为字符串");
}
return s;
}
static String optionalString(Map<String, Object> params, String key, String defaultValue) {
Object value = params.get(key);
if (value == null) {
return defaultValue;
}
if (!(value instanceof String s)) {
throw new IllegalArgumentException("参数 " + key + " 必须为字符串");
}
return s;
}
static Integer requireInt(Map<String, Object> params, String key) {
Object value = params.get(key);
if (value == null) {
throw new IllegalArgumentException("缺少参数: " + key);
}
if (value instanceof Number number) {
return number.intValue();
}
try {
if (value instanceof String string) {
return Integer.parseInt(string);
}
} catch (NumberFormatException ignored) {
}
throw new IllegalArgumentException("参数 " + key + " 必须为整数");
}
static Integer optionalInt(Map<String, Object> params, String key, Integer defaultValue) {
Object value = params.get(key);
if (value == null) {
return defaultValue;
}
if (value instanceof Number n) {
return n.intValue();
}
try {
if (value instanceof String string) {
return Integer.parseInt(string);
}
} catch (NumberFormatException ignored) {
}
throw new IllegalArgumentException("参数 " + key + " 必须为整数");
}
}
}
@@ -1,167 +0,0 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSONObject;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.memory.MemoryCapability;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.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.*;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
private static final String CAPABILITY_LOCATION = BUILTIN_LOCATION + "::" + "capability";
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Agent Capability");
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private MemoryCapability memoryCapability;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildInitiateTurnDefinition(),
buildMemoryRecallDefinition()
);
}
private BuiltinActionRegistry.BuiltinActionDefinition buildMemoryRecallDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Memory");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"unit_id", 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.")
),
"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(),
false,
JSONObject.of(
"ok", "boolean, whether the target memory slice is found and recalled successfully",
"message", "string, short execution result description"
)
);
Function<Map<String, Object>, String> invoker = params -> {
String unitId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "unit_id");
String sliceId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "slice_id");
Result<MemorySlice> sliceResult = memoryCapability.getMemorySlice(unitId, sliceId);
if (sliceResult.exceptionOrNull() != null) {
return JSONObject.of(
"ok", false,
"message", sliceResult.exceptionOrNull().getLocalizedMessage()
).toJSONString();
}
MemorySlice slice = sliceResult.getOrThrow();
MemoryUnit unit = memoryCapability.getMemoryUnit(unitId);
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildMemoryRecallFullBlock(unit, slice),
Set.of(ContextBlock.FocusedDomain.MEMORY),
60,
16,
28
));
return JSONObject.of(
"ok", true,
"message", "Memory slice found and recalled into context"
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("memory_recall"),
info,
invoker
);
}
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnit unit, MemorySlice slice) {
return new BlockContent("memory_recall", "memory_capability") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setAttribute("unit_id", unit.getId());
root.setAttribute("slice_id", slice.getId());
appendRepeatedElements(document, root, "message", unit.getConversationMessages().subList(slice.getStartIndex(), slice.getEndIndex()), (messageElement, message) -> {
messageElement.setAttribute("role", message.getRole().name().toLowerCase(Locale.ROOT));
messageElement.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
};
}
/**
* 用于发起自对话的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildInitiateTurnDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Agent Turn");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"input", param("required", "string", "Prompt content for the internal Agent Turn."),
"target", param("required", "string", "Target user expected to receive/respond to the turn.")
),
"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(),
false,
JSONObject.of(
"result", "turn initiate result"
)
);
Function<Map<String, Object>, String> invoker = params -> {
String input = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "input");
String target = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "target");
cognitionCapability.initiateTurn(input, target);
return "agent turn initiated";
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("initiate_turn"),
info,
invoker
);
}
@Override
public String createActionKey(String actionName) {
return CAPABILITY_LOCATION + "::" + actionName;
}
}
@@ -1,625 +0,0 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSONObject;
import lombok.AllArgsConstructor;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
import 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.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";
private static final String COMMAND_ARG_PREFIX = "arg";
private static final int DEFAULT_READ_LIMIT = 4096;
private static final int SUMMARY_MAX_LINES = 5;
private static final int SUMMARY_MAX_LENGTH = 2048;
private static final Duration COMMAND_SESSION_TTL = Duration.ofMinutes(10);
private static final int COMMAND_SESSION_TAIL_LIMIT = 64 * 1024;
private static final Duration OUTPUT_FLUSH_INTERVAL = Duration.ofMillis(100);
private static final Path COMMAND_SESSION_LOG_DIR = Path.of(System.getProperty("java.io.tmpdir"), "partner-command-sessions");
private final Set<String> basicTags = Set.of("Builtin MetaAction", "System Command Tool");
private final ConcurrentHashMap<String, CommandHandle> commandHandles = new ConcurrentHashMap<>();
private final CommandExecutionService commandExecutionService = CommandExecutionService.INSTANCE;
@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(
buildCommandExecuteDefinition(),
buildCommandStartDefinition(),
buildCommandInspectDefinition(),
buildCommandReadDefinition(),
buildCommandCancelDefinition(),
buildCommandOverviewDefinition()
);
}
/**
* 用于直接执行的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据,参数为常规命令列表,返回值为该命令的响应内容
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandExecuteDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Execution");
MetaActionInfo info = new MetaActionInfo(
false,
null,
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")),
false,
JSONObject.of("result", "Command execution result text.")
);
Function<Map<String, Object>, String> invoker = params -> {
List<String> commands = requireCommandArguments(params);
CommandExecutionService.Result result = commandExecutionService.exec(wrapCommands(commands));
return JSONObject.of("result", result.getTotal()).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("execute"),
info,
invoker
);
}
/**
* 用于启动后台命令的 Builtin MetaAction,后台将持续接收 stdout/stderr
*
* @return 内建 MetaAction 定义数据,参数为命令列表及进程描述,返回值为进程句柄 id
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandStartDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
MetaActionInfo info = new MetaActionInfo(
false,
null,
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")),
false,
JSONObject.of("executionId", "Command execution session id.")
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc");
List<String> commands = requireCommandArguments(params);
CommandExecutionService.CommandSession session = commandExecutionService.createSessionTask(wrapCommands(commands));
String executionId = UUID.randomUUID().toString();
Path stdoutLogPath = createSessionLogPath(executionId, "stdout.log");
Path stderrLogPath = createSessionLogPath(executionId, "stderr.log");
CommandHandle handle = new CommandHandle(
executionId,
desc,
commands,
Instant.now(),
session.getProcess(),
session.getStdoutBuffer(),
session.getStderrBuffer(),
new StringBuilder(),
new StringBuilder(),
stdoutLogPath,
stderrLogPath,
null,
null
);
commandHandles.put(executionId, handle);
startOutputFlusher(handle);
monitorProcess(handle);
return JSONObject.of("executionId", executionId).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("start"), info, invoker);
}
/**
* 用于返回指定后台 Builtin MetaAction 的摘要内容
*
* @return 内建 MetaAction 定义数据,参数为进程 id,返回值为摘要内容 JSON
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandInspectDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("id", 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(),
false,
JSONObject.of(
"executionId", "Command execution session id.",
"desc", "Command session description.",
"exitCode", "Process exit code. Null when the process is still running.",
"stdoutSize", "Current stdout buffer size in characters.",
"stderrSize", "Current stderr buffer size in characters.",
"stdoutSummary", "Summary text for stdout output.",
"stderrSummary", "Summary text for stderr output.",
"startAt", "Command session start time.",
"endAt", "Command session end time. Null when the process is still running."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
flushHandleBuffers(handle);
return JSONObject.of(
"executionId", handle.executionId,
"desc", handle.desc,
"exitCode", handle.exitCode,
"stdoutSize", streamLength(handle.stdoutLogPath, handle.stdoutSourceBuffer),
"stderrSize", streamLength(handle.stderrLogPath, handle.stderrSourceBuffer),
"stdoutSummary", summarizeBuffer(handle.stdoutBuffer),
"stderrSummary", summarizeBuffer(handle.stderrBuffer),
"startAt", handle.startAt,
"endAt", handle.exitAt
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("inspect"), info, invoker);
}
/**
* 用于读取指定后台 Builtin MetaAction 的输出内容
*
* @return 内建 MetaAction 定义数据,参数为进程 id 与读取流(stdout/stderr),返回值为读取内容 JSON
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandReadDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
tags.add("Command Read");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"id", 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.")
),
"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(),
false,
JSONObject.of(
"executionId", "Command execution session id.",
"desc", "Command session description.",
"stream", "The stream that was read, stdout or stderr.",
"content", "Content read from the selected stream in this call.",
"contentTruncated", "Whether the content was truncated because of the read limit.",
"offset", "Read start offset used in this call.",
"nextOffset", "Suggested next offset for the following read.",
"eof", "Whether the stream has reached end-of-file and the process has already exited."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
flushHandleBuffers(handle);
String stream = BuiltinActionRegistry.BuiltinActionDefinition.optionalString(params, "stream", "stdout");
if (!"stdout".equals(stream) && !"stderr".equals(stream)) {
throw new IllegalArgumentException("参数 stream 只能为 stdout 或 stderr");
}
int offset = BuiltinActionRegistry.BuiltinActionDefinition.optionalInt(params, "offset", 0);
int limit = BuiltinActionRegistry.BuiltinActionDefinition.optionalInt(params, "limit", DEFAULT_READ_LIMIT);
if (offset < 0) {
throw new IllegalArgumentException("参数 offset 必须大于等于 0");
}
if (limit <= 0) {
throw new IllegalArgumentException("参数 limit 必须大于 0");
}
Path logPath = "stderr".equals(stream) ? handle.stderrLogPath : handle.stdoutLogPath;
StreamChunk chunk = readChunk(logPath, offset, limit);
boolean eof = !handle.isRunning() && chunk.nextOffset >= chunk.totalLength;
return JSONObject.of(
"executionId", handle.executionId,
"desc", handle.desc,
"stream", stream,
"content", chunk.content,
"contentTruncated", chunk.truncated,
"offset", chunk.offset,
"nextOffset", chunk.nextOffset,
"eof", eof
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("read"), info, invoker);
}
/**
* 用于取消指定后台命令的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据,参数为进程 id,返回值为是否成功取消
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandCancelDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
tags.add("Command Cancel");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("id", 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")),
false,
JSONObject.of(
"ok", "Whether the command has been cancelled.",
"executionId", "Command execution session id."
)
);
Function<Map<String, Object>, String> invoker = params -> {
cleanupExpiredHandles();
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
flushHandleBuffers(handle);
if (handle.process.isAlive()) {
handle.process.destroy();
waitProcessExit(handle.process, 200);
if (handle.process.isAlive()) {
handle.process.destroyForcibly();
waitProcessExit(handle.process, 200);
}
}
if (!handle.process.isAlive()) {
try {
handle.exitCode = handle.process.exitValue();
} catch (IllegalThreadStateException ignored) {
}
if (handle.exitAt == null) {
handle.exitAt = Instant.now();
}
}
return JSONObject.of(
"ok", !handle.process.isAlive(),
"executionId", handle.executionId
).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("cancel"), info, invoker);
}
/**
* 用于列出全量后台进程的 Builtin MetaAction
*
* @return 内建 MetaAction 定义数据,无参数,返回值为后台进程集合 JSON
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCommandOverviewDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Command Session");
tags.add("Command Overview");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(),
"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(
"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 -> {
cleanupExpiredHandles();
commandHandles.values().forEach(this::flushHandleBuffers);
List<JSONObject> items = commandHandles.values().stream()
.sorted(Comparator.comparing(handle -> handle.startAt))
.map(handle -> JSONObject.of(
"executionId", handle.executionId,
"desc", handle.desc,
"exitCode", handle.exitCode
))
.toList();
return JSONObject.of("result", items).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("overview"), info, invoker);
}
@Override
public String createActionKey(String actionName) {
return COMMAND_LOCATION + "::" + actionName;
}
private void monitorProcess(CommandHandle handle) {
Thread.startVirtualThread(() -> {
try {
handle.exitCode = handle.process.waitFor();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} finally {
flushHandleBuffers(handle);
handle.exitAt = Instant.now();
}
});
}
private void startOutputFlusher(CommandHandle handle) {
Thread.startVirtualThread(() -> {
try {
while (handle.process.isAlive()) {
flushHandleBuffers(handle);
Thread.sleep(OUTPUT_FLUSH_INTERVAL.toMillis());
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} finally {
flushHandleBuffers(handle);
}
});
}
private void flushHandleBuffers(CommandHandle handle) {
flushStreamBuffer(handle.stdoutSourceBuffer, handle.stdoutBuffer, handle.stdoutLogPath);
flushStreamBuffer(handle.stderrSourceBuffer, handle.stderrBuffer, handle.stderrLogPath);
}
private void flushStreamBuffer(StringBuilder sourceBuffer, StringBuilder tailBuffer, Path logPath) {
String chunk;
synchronized (sourceBuffer) {
if (sourceBuffer.isEmpty()) {
return;
}
chunk = sourceBuffer.toString();
sourceBuffer.setLength(0);
}
try {
Files.writeString(logPath, chunk, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new IllegalStateException("写入命令输出日志失败: " + logPath, e);
}
synchronized (tailBuffer) {
tailBuffer.append(chunk);
trimTailBuffer(tailBuffer);
}
}
private void trimTailBuffer(StringBuilder buffer) {
if (buffer.length() <= COMMAND_SESSION_TAIL_LIMIT) {
return;
}
buffer.delete(0, buffer.length() - COMMAND_SESSION_TAIL_LIMIT);
}
private void cleanupExpiredHandles() {
Instant now = Instant.now();
Iterator<Map.Entry<String, CommandHandle>> iterator = commandHandles.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CommandHandle> entry = iterator.next();
CommandHandle handle = entry.getValue();
if (!isExpired(handle, now)) {
continue;
}
flushHandleBuffers(handle);
cleanupLogFiles(handle);
iterator.remove();
}
}
private boolean isExpired(CommandHandle handle, Instant now) {
Instant exitTime = handle.exitAt;
return exitTime != null && !exitTime.plus(COMMAND_SESSION_TTL).isAfter(now);
}
private Path createSessionLogPath(String executionId, String fileName) {
try {
Files.createDirectories(COMMAND_SESSION_LOG_DIR);
Path file = COMMAND_SESSION_LOG_DIR.resolve(executionId + "-" + fileName);
Files.deleteIfExists(file);
Files.createFile(file);
return file;
} catch (IOException e) {
throw new IllegalStateException("创建命令会话日志文件失败", e);
}
}
private long streamLength(Path logPath, StringBuilder sourceBuffer) {
long fileLength;
try {
fileLength = Files.exists(logPath) ? Files.size(logPath) : 0L;
} catch (IOException e) {
throw new IllegalStateException("读取命令会话日志长度失败", e);
}
synchronized (sourceBuffer) {
return fileLength + sourceBuffer.length();
}
}
private StreamChunk readChunk(Path logPath, int offset, int limit) {
if (!Files.exists(logPath)) {
return new StreamChunk("", 0, 0, false, 0);
}
try (RandomAccessFile raf = new RandomAccessFile(logPath.toFile(), "r")) {
int totalLength = (int) raf.length();
int safeOffset = Math.min(offset, totalLength);
int nextOffset = Math.min(safeOffset + limit, totalLength);
byte[] bytes = new byte[nextOffset - safeOffset];
raf.seek(safeOffset);
raf.readFully(bytes);
String content = new String(bytes, StandardCharsets.UTF_8);
return new StreamChunk(content, safeOffset, nextOffset, nextOffset < totalLength, totalLength);
} catch (IOException e) {
throw new IllegalStateException("读取命令会话日志失败", e);
}
}
private void cleanupLogFiles(CommandHandle handle) {
try {
Files.deleteIfExists(handle.stdoutLogPath);
Files.deleteIfExists(handle.stderrLogPath);
} catch (IOException ignored) {
}
}
private WrappedLaunchSpec wrapCommands(List<String> commands) {
return ExecutionPolicyRegistry.INSTANCE.prepare(commands);
}
private CommandHandle requireHandle(String id) {
CommandHandle handle = commandHandles.get(id);
if (handle == null) {
throw new IllegalArgumentException("未找到对应命令会话: " + id);
}
return handle;
}
private List<String> requireCommandArguments(Map<String, Object> params) {
List<Map.Entry<String, Object>> unexpectedArgs = params.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(COMMAND_ARG_PREFIX))
.filter(entry -> !COMMAND_ARG_PREFIX.equals(entry.getKey()) && !isNumberedCommandArg(entry.getKey()))
.toList();
if (!unexpectedArgs.isEmpty()) {
throw new IllegalArgumentException("非法命令参数: " + unexpectedArgs.getFirst().getKey());
}
List<String> commands = new ArrayList<>();
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 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 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) {
synchronized (buffer) {
return buffer.toString();
}
}
private String summarizeBuffer(StringBuilder buffer) {
String snapshot = bufferSnapshot(buffer);
if (snapshot.isBlank()) {
return "";
}
List<String> lines = snapshot.lines().toList();
if (lines.size() <= SUMMARY_MAX_LINES * 2) {
return trimSummary(snapshot);
}
List<String> head = lines.subList(0, SUMMARY_MAX_LINES);
List<String> tail = lines.subList(Math.max(lines.size() - SUMMARY_MAX_LINES, SUMMARY_MAX_LINES), lines.size());
String summary = String.join("\n", head)
+ "\n...\n"
+ String.join("\n", tail);
return trimSummary(summary);
}
private String trimSummary(String content) {
if (content.length() <= SUMMARY_MAX_LENGTH) {
return content;
}
return content.substring(0, SUMMARY_MAX_LENGTH);
}
private void waitProcessExit(Process process, long millis) {
try {
process.waitFor(millis, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
private record StreamChunk(String content, int offset, int nextOffset, boolean truncated, int totalLength) {
}
@AllArgsConstructor
private static class CommandHandle {
private String executionId;
private String desc;
private List<String> commands;
private Instant startAt;
private Process process;
private StringBuilder stdoutSourceBuffer;
private StringBuilder stderrSourceBuffer;
/**
* stdout/stderr 摘要 tail
*/
private StringBuilder stdoutBuffer;
private StringBuilder stderrBuffer;
private Path stdoutLogPath;
private Path stderrLogPath;
/**
* 退出码:进程未结束时可为 null
*/
private volatile Integer exitCode;
private volatile Instant exitAt;
boolean isRunning() {
return exitCode == null && process.isAlive();
}
}
}
@@ -1,261 +0,0 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import kotlin.Unit;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinDynamicActionProvider implements BuiltinActionProvider {
private static final String ORIGIN_LOCATION = "origin";
private static final long TEMP_ACTION_TTL_MILLIS = 30 * 60 * 1000L;
private static final String DYNAMIC_LOCATION = BUILTIN_LOCATION + "::" + "dynamic";
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Dynamic Generation");
private final ConcurrentHashMap<String, TempDynamicActionRecord> tempDynamicActions = new ConcurrentHashMap<>();
@InjectCapability
private ActionCapability actionCapability;
@InjectModule
private ActionScheduler actionScheduler;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildGenerateDynamicActionDefinition(),
buildPersistDynamicActionDefinition()
);
}
private BuiltinActionRegistry.BuiltinActionDefinition buildGenerateDynamicActionDefinition() {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"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.")
),
"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")),
false,
JSONObject.of(
"ok", "Whether the dynamic action was generated successfully.",
"actionKey", "Temporary ORIGIN actionKey."
)
);
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("generate"), info, params -> {
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc").trim();
if (desc.isEmpty()) {
throw new IllegalArgumentException("参数 desc 不能为空");
}
String code = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "code");
String codeType = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "codeType");
String launcher = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "launcher");
String metaJson = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "meta");
JSONObject meta = parseMeta(metaJson);
MetaActionInfo metaActionInfo = buildMetaActionInfo(meta, launcher, desc);
String tempName = "dyn-" + shortUuid();
String location = actionCapability.runnerClient().buildTmpPath(tempName, codeType);
MetaAction tempAction = new MetaAction(
tempName,
metaActionInfo.getIo(),
launcher,
MetaAction.Type.ORIGIN,
location
);
String actionKey = ORIGIN_LOCATION + "::" + location;
try {
actionCapability.runnerClient().tmpSerialize(tempAction, code, codeType);
} catch (java.io.IOException e) {
throw new IllegalStateException("临时动态行动序列化失败", e);
}
actionCapability.registerMetaActions(Map.of(actionKey, metaActionInfo));
ActionFileMetaData fileMetaData = buildActionFileMetaData(location, code, codeType);
StateAction cleanupAction = buildCleanupAction(actionKey);
tempDynamicActions.put(actionKey, new TempDynamicActionRecord(
actionKey,
location,
cleanupAction.getUuid(),
fileMetaData,
metaActionInfo
));
actionScheduler.schedule(cleanupAction);
return JSONObject.of(
"ok", true,
"actionKey", actionKey
).toJSONString();
});
}
private BuiltinActionRegistry.BuiltinActionDefinition buildPersistDynamicActionDefinition() {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of("actionKey", 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(),
false,
JSONObject.of(
"ok", "Whether the dynamic action was persisted successfully.",
"actionKey", "Temporary ORIGIN actionKey."
)
);
return new BuiltinActionRegistry.BuiltinActionDefinition(createActionKey("persist"), info, params -> {
String actionKey = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionKey");
TempDynamicActionRecord record = tempDynamicActions.get(actionKey);
if (record == null) {
throw new IllegalArgumentException("未找到对应临时动态行动: " + actionKey);
}
actionCapability.runnerClient().persistSerialize(record.metaActionInfo(), record.fileMetaData());
actionScheduler.cancel(record.cleanupActionId());
removeTempDynamicAction(actionKey);
return JSONObject.of(
"ok", true,
"actionKey", actionKey
).toJSONString();
});
}
@Override
public String createActionKey(String actionName) {
return DYNAMIC_LOCATION + "::" + actionName;
}
private JSONObject parseMeta(String metaJson) {
try {
return JSON.parseObject(metaJson);
} catch (Exception e) {
throw new IllegalArgumentException("参数 meta 必须为合法 JSON 字符串", e);
}
}
private MetaActionInfo buildMetaActionInfo(JSONObject meta, String launcher, String description) {
return new MetaActionInfo(
Boolean.TRUE.equals(meta.getBoolean("io")),
launcher,
copyStringMap(meta.getJSONObject("params")),
description,
toOrderedSet(meta.getJSONArray("tags")),
toOrderedSet(meta.getJSONArray("preActions")),
toOrderedSet(meta.getJSONArray("postActions")),
Boolean.TRUE.equals(meta.getBoolean("strictDependencies")),
copyJsonObject(meta.getJSONObject("responseSchema"))
);
}
private ActionFileMetaData buildActionFileMetaData(String location, String code, String codeType) {
ActionFileMetaData fileMetaData = new ActionFileMetaData();
fileMetaData.setContent(code);
fileMetaData.setExt(normalizeCodeType(codeType));
fileMetaData.setName(extractFileBaseName(location, fileMetaData.getExt()));
return fileMetaData;
}
private StateAction buildCleanupAction(String actionKey) {
return new StateAction(
"system",
"dynamic-action-cleanup:" + actionKey,
"清理临时动态行动",
Schedulable.ScheduleType.ONCE,
ZonedDateTime.now().plusSeconds(TEMP_ACTION_TTL_MILLIS / 1000).toString(),
new StateAction.Trigger.Call(() -> {
removeTempDynamicAction(actionKey);
return Unit.INSTANCE;
})
);
}
private void removeTempDynamicAction(String actionKey) {
TempDynamicActionRecord record = tempDynamicActions.remove(actionKey);
if (record == null) {
return;
}
actionCapability.listAvailableMetaActions().remove(actionKey);
deleteTempFileQuietly(record.location());
}
private void deleteTempFileQuietly(String location) {
try {
Files.deleteIfExists(Path.of(location));
} catch (Exception ignored) {
}
}
private Map<String, String> copyStringMap(JSONObject jsonObject) {
if (jsonObject == null) {
return Map.of();
}
Map<String, String> params = new LinkedHashMap<>();
jsonObject.forEach((key, value) -> params.put(key, value == null ? "" : String.valueOf(value)));
return params;
}
private Set<String> toOrderedSet(com.alibaba.fastjson2.JSONArray jsonArray) {
if (jsonArray == null) {
return Set.of();
}
return jsonArray.toJavaList(String.class).stream()
.filter(item -> item != null && !item.isBlank())
.collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
}
private JSONObject copyJsonObject(JSONObject jsonObject) {
return jsonObject == null ? JSONObject.of() : JSONObject.from(jsonObject);
}
private String normalizeCodeType(String codeType) {
return codeType.startsWith(".") ? codeType.substring(1) : codeType;
}
private String extractFileBaseName(String location, String ext) {
String fileName = Path.of(location).getFileName().toString();
String suffix = "." + ext;
if (fileName.endsWith(suffix)) {
return fileName.substring(0, fileName.length() - suffix.length());
}
return fileName;
}
private String shortUuid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 12);
}
private record TempDynamicActionRecord(
String actionKey,
String location,
String cleanupActionId,
ActionFileMetaData fileMetaData,
MetaActionInfo metaActionInfo
) {
}
}
@@ -1,416 +0,0 @@
package work.slhaf.partner.module.action.builtin;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.entity.Action;
import work.slhaf.partner.core.action.entity.ExecutableAction;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.cognition.*;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
import work.slhaf.partner.framework.agent.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.*;
import java.util.function.Function;
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
@AgentComponent
class BuiltinInterventionActionProvider implements BuiltinActionProvider {
private static final String INTERVENTION_LOCATION = BUILTIN_LOCATION + "::" + "intervention";
private final Set<String> basicTags = Set.of("Builtin MetaAction", "Action Intervention");
@InjectCapability
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private BuiltinActionRegistry builtinActionRegistry;
@Init
public void init() {
builtinActionRegistry.register(this);
}
@Override
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
return List.of(
buildCreateInterventionDefinition(),
buildShowAvailableMetaActionsDefinition(),
buildShowIntervenableActionsDefinition(),
buildAcquireInterventionDefinition(),
buildResumeInterruptedActionDefinition()
);
}
private BuiltinActionRegistry.BuiltinActionDefinition buildResumeInterruptedActionDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Agent Turn");
tags.add("Action Management");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"actionId", param("required", "string", "Uuid of an executable action currently in INTERRUPTED status.")
),
"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(),
false,
JSONObject.of(
"result", "Plain text resume result, describes whether it is succeed or fail reason."
)
);
Function<Map<String, Object>, String> invoker = params -> {
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId");
try {
ExecutableAction executableAction = getExecutableAction(actionId, Action.Status.INTERRUPTED);
executableAction.resume();
return "Resume succeed";
} catch (Exception e) {
return "Failed to resume action[" + actionId + "], reason: " + e.getLocalizedMessage();
}
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("resume_interrupted_action"),
info,
invoker
);
}
/**
* 尝试向指定用户请求干预,通过自对话通道
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildAcquireInterventionDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Agent Turn");
tags.add("Action Management");
MetaActionInfo info = new MetaActionInfo(
true,
null,
Map.of(
"actionId", 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).")
),
"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(),
false,
JSONObject.of(
"result", "Plain text intervention status. It describes whether the target answered before timeout, or returns an error reason when the turn/interruption flow fails."
)
);
Function<Map<String, Object>, String> invoker = params -> {
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId").trim();
String actionInfo = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionInfo").trim();
String demand = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "demand").trim();
String target = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "target").trim();
String input = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "input").trim();
int timeout = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "timeout");
ContextWorkspace contextWorkspace = cognitionCapability.contextWorkspace();
String blockName = "acquire_intervention-" + actionId;
String source = "action_executor";
contextWorkspace.register(new ContextBlock(
new CommunicationBlockContent(blockName, source, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "state", "Partner needs some help.");
appendTextElement(document, root, "action_id", actionId);
appendTextElement(document, root, "action_info", actionInfo);
appendTextElement(document, root, "demand", demand);
}
},
Set.of(ContextBlock.FocusedDomain.COMMUNICATION),
10,
10,
20
));
ExecutableAction executableAction = null;
try {
executableAction = getExecutableAction(actionId, 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";
} catch (Exception e) {
return "Error happened while calling turn: " + e.getLocalizedMessage();
} finally {
contextWorkspace.expire(blockName, source);
if (executableAction != null) {
executableAction.resume();
}
}
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("acquire_intervention"),
info,
invoker
);
}
private ExecutableAction getExecutableAction(String actionId, Action.Status status) {
return actionCapability.listActions(status, null)
.stream()
.filter(action -> action.getUuid().equals(actionId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未找到对应状态的 Action: " + actionId + ", status=" + status));
}
/**
* 用于展示当前已存在的可被干预的行动
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildShowIntervenableActionsDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Available Resource");
tags.add("Agent State");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(),
"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(),
false,
JSONObject.of(
"actions", "Info list of actions that could be intervened.",
"action.tendency", "Original tendency that the action is intended to resolve.",
"action.description", "Description of this action.",
"action.status", "Execution status of this action.",
"action.uuid", "Unique uuid of each action."
)
);
Function<Map<String, Object>, String> invoker = params -> {
Set<ExecutableAction> executableActions = actionCapability.listActions(null, null);
JSONArray interventions = new JSONArray();
for (ExecutableAction action : executableActions) {
JSONObject item = interventions.addObject();
item.put("tendency", action.getTendency());
item.put("description", action.getDescription());
item.put("status", action.getStatus().name().toLowerCase());
item.put("uuid", action.getUuid());
}
return interventions.toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("list_intervenable_actions"),
info,
invoker
);
}
/**
* 用于展示当前可用的 MetaAction
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildShowAvailableMetaActionsDefinition() {
Set<String> tags = new HashSet<>(basicTags);
tags.add("Available Resource");
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(),
"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(),
false,
JSONObject.of(
"meta_actions", "Available MetaAction info list.",
"meta_action.actionKey", "MetaAction actionKey, describing action source and its unique calling id.",
"meta_action.description", "MetaAction description.",
"meta_action.tags", "MetaAction tag list as string.",
"meta_action.params", "MetaAction parameter definition JSON string."
)
);
Function<Map<String, Object>, String> invoker = params -> {
Map<String, MetaActionInfo> availableMetaActions = actionCapability.listAvailableMetaActions();
JSONArray actions = new JSONArray();
for (Map.Entry<String, MetaActionInfo> entry : availableMetaActions.entrySet()) {
JSONObject item = actions.addObject();
item.put("location", entry.getKey());
MetaActionInfo action = entry.getValue();
item.put("description", action.getDescription());
item.put("tags", Arrays.toString(action.getTags().toArray()));
item.put("params", JSONObject.toJSONString(action.getParams()));
}
return actions.toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("list_available_meta_actions"),
info,
invoker
);
}
/**
* 用于创建一个 Action Intervention,并作用于指定的 Executable Action
*
* @return 内建 MetaAction 定义数据
*/
private BuiltinActionRegistry.BuiltinActionDefinition buildCreateInterventionDefinition() {
MetaActionInfo info = new MetaActionInfo(
false,
null,
Map.of(
"id", 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.")
),
"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"),
createActionKey("list_intervenable_actions")
),
Set.of(),
true,
JSONObject.of(
"ok", "Intervene status",
"result", "Intervene result or failed message."
)
);
Function<Map<String, Object>, String> invoker = params -> {
String targetId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id").trim();
if (targetId.isEmpty()) {
throw new IllegalArgumentException("参数 id 不能为空");
}
InterventionType type = requireInterventionType(params);
Integer order = BuiltinActionRegistry.BuiltinActionDefinition.requireInt(params, "order");
List<String> actions = requireActions(params, type);
ExecutableAction target = requireTargetAction(targetId);
Result<Void> validationResult = validateActionKeys(actions);
AgentRuntimeException validationFailure = validationResult.onFailure(ExceptionReporterHandler.INSTANCE::report).exceptionOrNull();
if (validationFailure != null) {
return JSONObject.of("ok", false, "result", validationFailure.getLocalizedMessage()).toJSONString();
}
MetaIntervention intervention = new MetaIntervention();
intervention.setType(type);
intervention.setOrder(order);
intervention.setActions(actions);
actionCapability.handleInterventions(List.of(intervention), target);
target.resume();
return JSONObject.of("ok", true).toJSONString();
};
return new BuiltinActionRegistry.BuiltinActionDefinition(
createActionKey("create_intervention"),
info,
invoker
);
}
@Override
public String createActionKey(String actionName) {
return INTERVENTION_LOCATION + "::" + actionName;
}
private InterventionType requireInterventionType(Map<String, Object> params) {
String typeValue = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "type").trim();
if (typeValue.isEmpty()) {
throw new IllegalArgumentException("参数 type 不能为空");
}
try {
return InterventionType.valueOf(typeValue.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("参数 type 非法: " + typeValue, e);
}
}
private List<String> requireActions(Map<String, Object> params, InterventionType type) {
Object value = params.get("actions");
if (value == null) {
if (type == InterventionType.CANCEL) {
return List.of();
}
throw new IllegalArgumentException("缺少参数: actions");
}
if (!(value instanceof String actionsValue)) {
throw new IllegalArgumentException("参数 actions 必须为字符串");
}
String trimmedActionsValue = actionsValue.trim();
if (trimmedActionsValue.isEmpty()) {
if (type == InterventionType.CANCEL) {
return List.of();
}
throw new IllegalArgumentException("参数 actions 不能为空");
}
List<String> actions = Arrays.stream(trimmedActionsValue.split(","))
.map(String::trim)
.filter(item -> !item.isEmpty())
.toList();
if (type != InterventionType.CANCEL && actions.isEmpty()) {
throw new IllegalArgumentException("参数 actions 不能为空");
}
return actions;
}
private ExecutableAction requireTargetAction(String targetId) {
return actionCapability.listActions(null, null)
.stream()
.filter(action -> targetId.equals(action.getUuid()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未找到对应的 Action: " + targetId));
}
private Result<Void> validateActionKeys(List<String> actions) {
for (String actionKey : actions) {
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
AgentRuntimeException failure = metaActionResult.exceptionOrNull();
if (failure != null) {
return Result.failure(failure);
}
}
return Result.success(null);
}
}
@@ -1,138 +0,0 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.action.executor.entity.CorrectorInput;
import work.slhaf.partner.module.action.executor.entity.RecognizerResult;
import java.util.List;
/**
* 负责在行动链执行过程中判断当前进度是否异常,是否需要引入 corrector 介入。
*/
public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<CorrectorInput, Result<RecognizerResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在行动链执行过程中识别当前执行是否出现异常,并判断是否需要引入 corrector 介入。
你会收到:
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
- 一条任务消息,其中包含:
- 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 与 result。
你的任务:
- 当 check_mode=PROCESS_CHECK 时,基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
- 当 check_mode=FINAL_CHECK 时,判断当前行动链即将结束时,original_tendency 是否已经被执行结果满足;
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移、目标未完成,或与最新语境不再一致的情况;
- 若需要引入 corrector,则返回 needCorrection=true,并给出简洁明确的 reason;
- 若当前仍可继续推进或已经满足目标,则返回 needCorrection=false。
识别原则:
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的判断必须围绕这条行动本身,而不是被无关历史带偏。
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构、已知阶段状态与执行结果。
- original_tendency 表示这条行动链最初要解决的问题;若当前链路明显偏离这一倾向,应优先视为异常信号。
- evaluation_passed_reason 与 description 表示该行动最初为何被判定为可推进;若当前状态已经与这些前提不再一致,应考虑触发纠正。
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常、整体失衡或最终目标未满足的主要依据。
- PROCESS_CHECK 关注执行过程是否仍然合理;FINAL_CHECK 关注链路结果是否满足 original_tendency。
- FINAL_CHECK 时应重点查看各 meta_action 的 result;如果 result 显示对象不存在、权限不足、参数错误、输出为空、命令失败、目标未产生或结果明显不符合 original_tendency,应触发纠正。
- communication 域用于判断最新交流语境是否已发生明显变化,导致当前行动继续推进不再合适。
- memory 域只在与当前行动明显相关时作为辅助参考使用。
应优先考虑需要纠正的情形:
- 当前行动长期没有形成有效推进,只在重复相近步骤、相近动作或相近结论;
- 当前执行显著偏离 original_tendency,开始围绕无关目标展开;
- 当前行动所依赖的前提已被新的交流内容或上下文推翻;
- 当前行动链反复遇到同类失败、阻塞或空转迹象,继续按原链推进意义不大;
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛;
- FINAL_CHECK 时,执行结果明确显示 original_tendency 尚未满足,且可能通过补救步骤继续推进。
不应轻易触发纠正的情形:
- PROCESS_CHECK 时,只是正常的多步推进;
- PROCESS_CHECK 时,存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
- 行动链整体仍与 original_tendency、evaluation_passed_reason 和当前语境保持一致;
- FINAL_CHECK 时,执行结果已经足以满足 original_tendency,只需要向用户报告结果;
- 仅凭旧对话、低相关记忆或轻微波动,不足以判定当前行动异常。
关于输出:
- needCorrection=true 表示当前应引入 corrector 介入。
- needCorrection=false 表示当前行动仍可继续按既有链路推进。
- reason 用于简洁说明判断依据;若 needCorrection=false,也应给出简短理由,说明为何当前仍可继续推进。
- 不要输出结构之外的解释、说明或额外文本。
输出要求:
- 严格按照 RecognizerResult 对应结构输出。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<RecognizerResult> doExecute(CorrectorInput input) {
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
return formattedChat(messages, RecognizerResult.class);
}
private Message resolveTaskMessage(CorrectorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
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());
appendTextElement(document, block, "evaluation_passed_reason", input.getReason());
appendTextElement(document, block, "description", input.getDescription());
appendTextElement(document, block, "from_who", input.getSource());
return Unit.INSTANCE;
});
appendListElement(document, root, "current_action_chain_overview", "action_chain_stage", input.getActionChainOverview().entrySet(), (stageElement, stageData) -> {
stageElement.setAttribute("stage_count", String.valueOf(stageData.getKey()));
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
appendTextElement(document, metaActionElement, "result", metaActionData.getResult());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_correction_recognizer";
}
}
@@ -1,171 +0,0 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.action.executor.entity.CorrectorInput;
import work.slhaf.partner.module.action.executor.entity.CorrectorResult;
import java.util.List;
/**
* 负责在单组行动执行后,根据行动意图与结果检查后续行动是否符合目的,必要时直接调整行动链,或发起自对话请求进行干预
*/
public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Result<CorrectorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在行动链执行过程中进行纠偏。你的任务不是重新评估是否要启动这条行动,而是在当前行动已经进入执行后,根据原始行动意图、当前链路结构与最新语境,判断是否需要调整后续行动链。
你会收到:
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
- 一条任务消息,其中包含:
- 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、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 用于简洁说明为何需要这些纠偏。
纠偏原则:
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的纠偏必须围绕这条行动本身,而不是转向新的无关目标。
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构与阶段状态。
- original_tendency 是这条行动链最初要解决的问题;后续行动的调整必须仍然围绕它。
- evaluation_passed_reason 与 description 表示该行动最初为何能够成立;若当前链路已经与这些前提不一致,可据此进行纠偏。
- current_action_chain_overview 是判断后续链路是否缺步骤、顺序失衡、重复冗余或整体方向错误的主要依据。
- available_meta_action 是你进行 APPEND、INSERT 或 REBUILD 时可选择的动作全集;不要使用其中不存在的 action_key。
- communication 域用于判断最新交流语境是否已经变化,导致当前链路需要调整。
- memory 域只在与当前行动明显相关时作为辅助参考使用。
何时应考虑干预:
- 当前链路缺少继续推进所必需的动作;
- 当前链路顺序明显不合理,继续执行会降低成功率或偏离目的;
- 某些动作已经失效、重复、无意义,或不再适合当前状态;
- 当前链路在某一阶段之后整体方向需要重建;
- 当前行动已不应继续推进,应直接取消整条链路。
何时不应轻易干预:
- 当前只是正常的多步推进;
- 某一步刚完成,尚不足以说明后续链路错误;
- 没有足够依据证明当前行动链存在明显问题;
- 仅凭旧对话、低相关记忆或轻微波动,不足以支持修改行动链。
关于干预类型:
- APPEND: 在指定 order 之后追加新的动作。
- INSERT: 在指定 order 执行过程中即时插入并执行新的动作。
- DELETE: 删除指定 order 上的指定动作。
- CANCEL: 取消当前行动链后续执行。
- REBUILD: 清空当前行动链与既有执行进度,并用新的规划内容整体重建行动链。
关于 intervention 列表:
- 系统会按照 metaInterventionList 的顺序逐条应用干预;前面的干预可能改变后续可理解的 order 位置,因此你在生成后续 intervention 时,必须考虑前面 intervention 已经生效后的链路形态。
- 不要把一组本应整体表达的修改拆成互相冲突、顺序不自洽的 intervention。
- actions 中只能填写当前系统中真实存在、可用的 action_key,不要编造不存在的动作。
- order 必须对应当前行动链中的合理阶段位置。
关于 REBUILD:
- REBUILD 表示放弃当前既有链路,并重新给出新的整体规划。
- 一旦本次纠偏结果中使用了 REBUILD,则 metaInterventionList 中所有 intervention 都必须是 REBUILD;不要将 REBUILD 与 APPEND、INSERT、DELETE、CANCEL 混用。
- 使用 REBUILD 时,应把新的链路规划完整表达为一组按 order 分布的 REBUILD intervention,而不是只局部补几步。
其他约束:
- 若无需干预,可返回空的 metaInterventionList,并给出简短的 correctionReason 说明当前为何可继续推进。
- 不要输出结构之外的解释、说明或额外文本。
输出要求:
- 严格按照 CorrectorResult 对应结构输出。
- metaInterventionList 中每一项都必须是明确、可执行、且与其他 intervention 顺序一致的干预单元。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private ActionCapability actionCapability;
@Override
protected @NotNull Result<CorrectorResult> doExecute(CorrectorInput input) {
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
return formattedChat(messages, CorrectorResult.class);
}
private Message resolveTaskMessage(CorrectorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
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());
appendTextElement(document, block, "evaluation_passed_reason", input.getReason());
appendTextElement(document, block, "description", input.getDescription());
appendTextElement(document, block, "from_who", input.getSource());
return Unit.INSTANCE;
});
appendListElement(document, root, "current_action_chain_overview", "action_chain_stage", input.getActionChainOverview().entrySet(), (stageElement, stageData) -> {
stageElement.setAttribute("stage_count", String.valueOf(stageData.getKey()));
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
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();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_corrector";
}
}
@@ -1,796 +0,0 @@
package work.slhaf.partner.module.action.executor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.factory.context.Shutdown;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.executor.entity.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@Slf4j
public class ActionExecutor extends AbstractAgentModule.Standalone {
private static final int MAX_EXTRACTOR_ATTEMPTS = 3;
private static final int MAX_FINAL_CORRECTION_ATTEMPTS = 1;
private final AssemblyHelper assemblyHelper = new AssemblyHelper();
@InjectCapability
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private ParamsExtractor paramsExtractor;
@InjectModule
private ActionCorrector actionCorrector;
@InjectModule
private ActionCorrectionRecognizer actionCorrectionRecognizer;
private ExecutingActionBlockManager blockManager;
private ExecutorService virtualExecutor;
private ExecutorService platformExecutor;
private RunnerClient runnerClient;
private final AtomicBoolean closed = new AtomicBoolean(false);
@Init
public void init() {
virtualExecutor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
platformExecutor = actionCapability.getExecutor(ActionCore.ExecutorType.PLATFORM);
runnerClient = actionCapability.runnerClient();
blockManager = new ExecutingActionBlockManager(cognitionCapability.contextWorkspace());
Set<ExecutableAction> recoveredActions = new HashSet<>();
recoveredActions.addAll(actionCapability.listActions(Action.Status.EXECUTING, null));
recoveredActions.addAll(actionCapability.listActions(Action.Status.INTERRUPTED, null).stream()
.peek(executableAction -> executableAction.setStatus(Action.Status.EXECUTING))
.collect(Collectors.toSet()));
if (recoveredActions.isEmpty()) {
return;
}
recoveredActions.forEach(this::execute);
blockManager.emitActionRecoveredBlock(recoveredActions);
}
@Shutdown
public void shutdown() {
closed.set(true);
}
public void execute(Action action) {
Future<?> future = virtualExecutor.submit(actionExecutionRouter(action));
virtualExecutor.execute(() -> {
try {
future.get(action.getTimeoutMills(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
action.setStatus(Action.Status.FAILED);
if (action instanceof ExecutableAction executableAction) {
ensureExecutableResult(executableAction, true, "行动执行超时");
}
log.warn("Action timeout, uuid: {}", action.getUuid());
} catch (Exception ignored) {
}
});
}
private Runnable actionExecutionRouter(Action action) {
return () -> {
try {
switch (action) {
case ExecutableAction executableAction -> handleExecutableAction(executableAction);
case StateAction stateAction -> handleStateAction(stateAction);
default -> handleUnknownAction(action);
}
if (action instanceof ExecutableAction executableAction) {
if (action.getStatus() == Action.Status.FAILED) {
ensureExecutableResult(executableAction, true, null);
emitExecutableActionFinished(executableAction);
return;
}
action.setStatus(Action.Status.SUCCESS);
ensureExecutableResult(executableAction, false, null);
emitExecutableActionFinished(executableAction);
return;
}
if (action.getStatus() == Action.Status.FAILED) {
return;
}
action.setStatus(Action.Status.SUCCESS);
} catch (Exception e) {
log.warn("Unexpected action execution failure, uuid: {}, description: {}, failure reason: {}", action.getUuid(), action.getDescription(), e.getLocalizedMessage());
action.setStatus(Action.Status.FAILED);
if (action instanceof ExecutableAction executableAction) {
ensureExecutableResult(executableAction, true, e.getLocalizedMessage());
emitExecutableActionFinished(executableAction);
}
}
};
}
private void handleUnknownAction(Action action) {
log.warn("unknown Action type: {}", action.getClass().getSimpleName());
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;
}
blockManager.emitStateActionTriggeredBlock(stateAction);
stateAction.getTrigger().onTrigger();
}
private void handleExecutableAction(ExecutableAction executableAction) {
actionCapability.putAction(executableAction);
val actionChain = executableAction.getActionChain();
if (!prepareExecutableAction(executableAction, actionChain)) {
return;
}
blockManager.emitActionLaunchedBlock(executableAction);
val stageCursor = initStageCursor(executableAction, actionChain);
int finalCorrectionAttempts = 0;
while (true) {
val stageSelection = selectCurrentStage(executableAction, actionChain);
if (stageSelection.shouldReturn()) {
return;
}
if (stageSelection.shouldStop()) {
break;
}
val stageExecution = runCurrentStage(executableAction, stageCursor, stageSelection.metaActions());
if (stageExecution.closed()) {
return;
}
if (!applyStageCorrectionAndAdvance(executableAction, stageCursor, stageExecution)) {
if (finalCorrectionAttempts >= MAX_FINAL_CORRECTION_ATTEMPTS || !applyFinalCorrection(executableAction, stageCursor)) {
break;
}
finalCorrectionAttempts++;
}
}
finishExecutableAction(executableAction);
}
private boolean prepareExecutableAction(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
synchronized (executableAction.getExecutionLock()) {
val status = executableAction.getStatus();
if (status != Action.Status.PREPARE && status != Action.Status.EXECUTING) {
return false;
}
if (actionChain.isEmpty()) {
executableAction.setStatus(Action.Status.FAILED);
executableAction.setResult("行动链为空");
return false;
}
normalizeExecutingStage(executableAction, actionChain);
executableAction.setStatus(Action.Status.EXECUTING);
return true;
}
}
private StageCursor initStageCursor(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
StageCursor stageCursor = new StageCursor(executableAction, actionChain);
synchronized (executableAction.getExecutionLock()) {
stageCursor.init();
}
return stageCursor;
}
private StageSelection selectCurrentStage(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
synchronized (executableAction.getExecutionLock()) {
if (closed.get()) {
return StageSelection.returnNow();
}
if (executableAction.getStatus() == Action.Status.FAILED) {
return StageSelection.stop();
}
return StageSelection.continueWith(actionChain.get(executableAction.getExecutingStage()));
}
}
private StageExecution runCurrentStage(
ExecutableAction executableAction,
StageCursor stageCursor,
List<MetaAction> metaActions
) {
Phaser phaser = new Phaser();
val recognizerRecord = startRecognizerIfNeeded(executableAction, phaser);
val listeningRecord = executeAndListening(metaActions, phaser, executableAction);
phaser.awaitAdvance(listeningRecord.phase());
// synchronized 同步防止 accepting 循环间、phase guard 判定后发生 stage 推进
// 导致新行动的 phaser 投放阶段错乱无法阻塞的场景
// 该 synchronized 将阶段推进与 accepting 监听 loop 捆绑为互斥的原子事件,避免了细粒度的 phaser 阶段竞态问题
if (closed.get()) {
return StageExecution.closed(recognizerRecord, metaActions);
}
synchronized (executableAction.getExecutionLock()) {
synchronized (listeningRecord.accepting()) {
listeningRecord.accepting().set(false);
// 立即尝试推进,本次推进中,如果前方仍有未执行 stage,将执行一次阶段推进
stageCursor.requestAdvance();
}
}
blockManager.emitActionStageSettledBlock(executableAction);
return StageExecution.completed(recognizerRecord, metaActions);
}
private boolean applyStageCorrectionAndAdvance(
ExecutableAction executableAction,
StageCursor stageCursor,
StageExecution stageExecution
) {
boolean hasFailedMetaAction = hasFailedMetaAction(stageExecution.metaActions());
boolean shouldRunCorrector = hasFailedMetaAction;
if (!shouldRunCorrector) {
val recognizerResult = resolveRecognizerResult(stageExecution.recognizerRecord());
shouldRunCorrector = recognizerResult != null && recognizerResult.isNeedCorrection();
}
if (shouldRunCorrector) {
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.PROCESS_CHECK);
actionCorrector.execute(correctorInput)
.onSuccess(correctorResult -> {
actionCapability.handleInterventions(correctorResult.getMetaInterventionList(), executableAction);
blockManager.emitActionCorrectionBlock(
executableAction,
hasFailedMetaAction ? "has_failed_meta_action" : correctorResult.getCorrectionReason(),
correctorResult.getMetaInterventionList()
);
});
}
// 第二次尝试进行阶段推进,本次负责补充上一次在不存在 stage时,但 corrector 执行期间发生了 actionChain 的插入事件
// 如果第一次已经推进完毕,本次将会跳过
synchronized (executableAction.getExecutionLock()) {
stageCursor.requestAdvance();
return stageCursor.next();
}
}
private 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) {
scheduledActionData.recordAndReset();
}
}
private MetaActionsListeningRecord executeAndListening(List<MetaAction> metaActions, Phaser phaser, ExecutableAction executableAction) {
AtomicBoolean accepting = new AtomicBoolean(true);
AtomicInteger cursor = new AtomicInteger();
CountDownLatch latch = new CountDownLatch(1);
val phase = phaser.register();
platformExecutor.execute(() -> {
boolean first = true;
while (accepting.get()) {
synchronized (accepting) {
MetaAction next = null;
synchronized (metaActions) {
if (cursor.get() < metaActions.size()) {
next = metaActions.get(cursor.getAndIncrement());
}
}
if (next == null) {
Thread.onSpinWait();
continue;
}
if (phaser.getPhase() != phase) {
metaActions.remove(next);
log.warn("行动阶段已推进,丢弃该行动: {}", next);
continue;
}
ExecutorService executor = next.getIo() ? virtualExecutor : platformExecutor;
executor.execute(buildMataActionTask(next, phaser, executableAction));
if (first) {
phaser.arriveAndDeregister();
latch.countDown();
first = false;
}
}
}
});
try {
// 确保执行一次,防止没来得及注册任务就已经结束
latch.await();
} catch (InterruptedException ignored) {
}
return new MetaActionsListeningRecord(accepting, phase);
}
private Runnable buildMataActionTask(MetaAction metaAction, Phaser phaser, ExecutableAction executableAction) {
phaser.register();
return () -> {
val actionKey = metaAction.getKey();
try {
executeMetaActionWithRetry(metaAction, executableAction);
} catch (Exception e) {
log.error("Action executing failed: {}", actionKey, e);
} finally {
phaser.arriveAndDeregister();
}
};
}
private void executeMetaActionWithRetry(MetaAction metaAction, ExecutableAction actionData) {
AtomicReference<String> failureReason = new AtomicReference<>("参数提取失败");
int executingStage = actionData.getExecutingStage();
boolean succeeded = false;
for (int attempt = 1; attempt <= MAX_EXTRACTOR_ATTEMPTS; attempt++) {
val result = metaAction.getResult();
result.reset();
metaAction.getParams().clear();
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(
metaAction.getKey(),
actionData.getUuid(),
actionData.getDescription(),
resolveCurrentStageDescription(actionData, executingStage)
);
AgentRuntimeException exception = extractorInputResult.exceptionOrNull();
if (exception != null) {
failureReason.set(exception.getMessage());
break;
}
ExtractorInput extractorInput = extractorInputResult.getOrThrow();
Result<ExtractorResult> extractorResultWrapped = paramsExtractor.execute(extractorInput).onFailure(exp -> failureReason.set(exp.getLocalizedMessage()));
if (extractorResultWrapped.exceptionOrNull() != null) {
continue;
}
ExtractorResult extractorResult = extractorResultWrapped.getOrThrow();
if (!extractorResult.isOk()) {
failureReason.set(buildAttemptFailureReason("参数提取失败", null));
continue;
}
metaAction.getParams().putAll(toMetaActionParams(extractorResult.getParams()));
try {
runnerClient.submit(metaAction);
} catch (Exception e) {
failureReason.set(buildAttemptFailureReason("行动执行异常", e.getLocalizedMessage()));
continue;
}
if (result.getStatus() == MetaAction.Result.Status.SUCCESS) {
succeeded = true;
break;
}
failureReason.set(buildAttemptFailureReason("行动执行失败", result.getData()));
}
if (!succeeded) {
metaAction.getResult().setStatus(MetaAction.Result.Status.FAILED);
metaAction.getResult().setData(failureReason.get());
}
appendHistoryAction(actionData, executingStage, metaAction);
}
private Map<String, Object> toMetaActionParams(List<ExtractorResult.ParamEntry> params) {
if (params == null || params.isEmpty()) {
return Map.of();
}
Map<String, Object> converted = new LinkedHashMap<>();
for (ExtractorResult.ParamEntry entry : params) {
if (entry == null) {
continue;
}
String name = entry.getName();
if (name == null || name.isBlank()) {
continue;
}
converted.put(name, entry.getValue());
}
return converted;
}
private void appendHistoryAction(ExecutableAction actionData, int executingStage, MetaAction metaAction) {
HistoryAction historyAction = new HistoryAction(
metaAction.getKey(),
resolveHistoryDescription(metaAction.getKey()),
metaAction.getResult().getData()
);
actionData.getHistory()
.computeIfAbsent(executingStage, integer -> new ArrayList<>())
.add(historyAction);
}
private RecognizerTaskRecord startRecognizerIfNeeded(ExecutableAction executableAction, Phaser phaser) {
if (!shouldRunCorrectionRecognizer(executableAction)) {
return RecognizerTaskRecord.disabled();
}
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.PROCESS_CHECK);
val task = buildRecognizerTask(recognizerInput, phaser);
Future<RecognizerResult> future = virtualExecutor.submit(task);
return new RecognizerTaskRecord(true, future);
}
private Callable<RecognizerResult> buildRecognizerTask(CorrectorInput input, Phaser phaser) {
phaser.register();
return () -> {
try {
return actionCorrectionRecognizer.execute(input)
.getOrDefault(new RecognizerResult());
} finally {
phaser.arriveAndDeregister();
}
};
}
private RecognizerResult resolveRecognizerResult(RecognizerTaskRecord record) {
if (record == null || !record.enabled() || record.future() == null) {
return null;
}
try {
if (!record.future().isDone()) {
return null;
}
return record.future().get();
} catch (Exception e) {
return null;
}
}
private String resolveCurrentStageDescription(ExecutableAction executableAction, int executingStage) {
return executableAction.getStageDescriptions().get(executingStage);
}
private String buildAttemptFailureReason(String prefix, String detail) {
if (detail == null || detail.isBlank()) {
return prefix;
}
return prefix + ": " + detail;
}
private boolean hasFailedMetaAction(List<MetaAction> metaActions) {
return metaActions.stream().anyMatch(metaAction -> metaAction.getResult().getStatus() == MetaAction.Result.Status.FAILED);
}
private boolean shouldRunCorrectionRecognizer(ExecutableAction executableAction) {
val orderedStages = new ArrayList<>(executableAction.getActionChain().keySet());
orderedStages.sort(Integer::compareTo);
int totalStages = orderedStages.size();
if (totalStages < 3) {
return false;
}
int stageIndex = orderedStages.indexOf(executableAction.getExecutingStage());
if (stageIndex < 0) {
return false;
}
if (stageIndex == totalStages - 1) {
return true;
}
return stageIndex >= 2 && (stageIndex - 2) % 2 == 0;
}
private void normalizeExecutingStage(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
Integer firstStage = actionChain.keySet().stream()
.min(Integer::compareTo)
.orElse(null);
if (firstStage == null) {
return;
}
if (actionChain.containsKey(executableAction.getExecutingStage())) {
return;
}
if (executableAction.getStatus() == Action.Status.EXECUTING) {
resetExecutableActionForReplay(executableAction);
}
executableAction.setExecutingStage(firstStage);
}
private void resetExecutableActionForReplay(ExecutableAction executableAction) {
executableAction.getHistory().clear();
executableAction.getActionChain().values().forEach(metaActions -> metaActions.forEach(metaAction -> {
metaAction.getParams().clear();
metaAction.getResult().reset();
}));
if (hasExecutableResult(executableAction)) {
executableAction.setResult("");
}
}
private void ensureExecutableResult(ExecutableAction executableAction, boolean failed, String failureReason) {
if (hasExecutableResult(executableAction)) {
return;
}
executableAction.setResult(resolveExecutableResult(executableAction, failed, failureReason));
}
private String resolveExecutableResult(ExecutableAction executableAction, boolean failed, String failureReason) {
String extracted = extractLastMetaActionResult(executableAction);
if (extracted != null && !extracted.isBlank()) {
return extracted;
}
if (!failed) {
return "行动执行成功";
}
if (failureReason != null && !failureReason.isBlank()) {
return "行动执行失败: " + failureReason;
}
return "行动执行失败";
}
private String extractLastMetaActionResult(ExecutableAction executableAction) {
if (!executableAction.getHistory().isEmpty()) {
Integer lastStage = executableAction.getHistory().keySet().stream()
.max(Integer::compareTo)
.orElse(null);
if (lastStage != null) {
List<HistoryAction> historyActions = executableAction.getHistory().get(lastStage);
if (historyActions != null && !historyActions.isEmpty()) {
String result = historyActions.getLast().result();
if (result != null && !result.isBlank()) {
return result;
}
}
}
}
if (!executableAction.getActionChain().isEmpty()) {
Integer lastStage = executableAction.getActionChain().keySet().stream()
.max(Integer::compareTo)
.orElse(null);
if (lastStage != null) {
List<MetaAction> metaActions = executableAction.getActionChain().get(lastStage);
if (metaActions != null && !metaActions.isEmpty()) {
String result = metaActions.getLast().getResult().getData();
if (result != null && !result.isBlank()) {
return result;
}
}
}
}
return null;
}
private boolean hasExecutableResult(ExecutableAction executableAction) {
try {
String result = executableAction.getResult();
return result != null && !result.isBlank();
} catch (Exception e) {
return false;
}
}
private String resolveHistoryDescription(String actionKey) {
return actionCapability.loadMetaActionInfo(actionKey)
.fold(
metaActionInfo -> metaActionInfo.getDescription().isBlank() ? actionKey : metaActionInfo.getDescription(),
exception -> actionKey
);
}
private enum StageSelectionType {
CONTINUE,
STOP,
RETURN
}
private record StageSelection(StageSelectionType type, List<MetaAction> metaActions) {
private static StageSelection continueWith(List<MetaAction> metaActions) {
return new StageSelection(StageSelectionType.CONTINUE, metaActions);
}
private static StageSelection stop() {
return new StageSelection(StageSelectionType.STOP, null);
}
private static StageSelection returnNow() {
return new StageSelection(StageSelectionType.RETURN, null);
}
private boolean shouldStop() {
return type == StageSelectionType.STOP;
}
private boolean shouldReturn() {
return type == StageSelectionType.RETURN;
}
}
private record StageExecution(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions, boolean closed) {
private static StageExecution completed(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions) {
return new StageExecution(recognizerRecord, metaActions, false);
}
private static StageExecution closed(RecognizerTaskRecord recognizerRecord, List<MetaAction> metaActions) {
return new StageExecution(recognizerRecord, metaActions, true);
}
}
private record MetaActionsListeningRecord(AtomicBoolean accepting, int phase) {
}
private record RecognizerTaskRecord(boolean enabled, Future<RecognizerResult> future) {
private static RecognizerTaskRecord disabled() {
return new RecognizerTaskRecord(false, null);
}
}
private static final class StageCursor {
private final ExecutableAction executableAction;
private final Map<Integer, List<MetaAction>> actionChain;
private int stageCount;
private boolean executingStageUpdated;
private boolean stageCountUpdated;
private StageCursor(ExecutableAction executableAction, Map<Integer, List<MetaAction>> actionChain) {
this.executableAction = executableAction;
this.actionChain = actionChain;
}
private void init() {
val orderList = new ArrayList<>(actionChain.keySet());
orderList.sort(Integer::compareTo);
stageCount = orderList.indexOf(executableAction.getExecutingStage());
update();
}
private void requestAdvance() {
if (!stageCountUpdated) {
stageCount++;
stageCountUpdated = true;
}
if (stageCount < actionChain.size() && !executingStageUpdated) {
update();
executingStageUpdated = true;
}
}
private boolean next() {
executingStageUpdated = false;
stageCountUpdated = false;
return stageCount < actionChain.size();
}
private void 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);
executableAction.setExecutingStage(orderList.get(stageCount));
}
}
@SuppressWarnings("InnerClassMayBeStatic")
private class AssemblyHelper {
private AssemblyHelper() {
}
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, 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()),
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())
.description(executableAction.getDescription())
.actionId(executableAction.getUuid())
.actionChainOverview(overview)
.build();
}
}
}
@@ -1,452 +0,0 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.ContextWorkspace;
import work.slhaf.partner.module.StateHintContent;
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
class ExecutingActionBlockManager {
private static final String SOURCE = "action_executor";
private final ContextWorkspace contextWorkspace;
ExecutingActionBlockManager(ContextWorkspace contextWorkspace) {
this.contextWorkspace = contextWorkspace;
}
void emitActionRecoveredBlock(Set<ExecutableAction> recoveredActions) {
Set<ExecutableActionSnapshot> snapshots = recoveredActions.stream().map(ExecutableAction::snapshot).collect(Collectors.toSet());
String blockName = "actions_recovered";
String emittedAt = emittedAt();
String event = "actions_recovered";
contextWorkspace.register(new ContextBlock(
buildExecutingActionRecoveredFullBlock(snapshots, blockName, emittedAt, event),
buildExecutingActionRecoveredCompactBlock(snapshots, blockName, emittedAt, event),
buildExecutingActionRecoveredAbstractBlock(snapshots, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
100,
12,
1
));
}
private @NotNull BlockContent buildExecutingActionRecoveredAbstractBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "abstract", recoveredExecutingActions.size() + " executing actions recovered.");
}
};
}
private @NotNull BlockContent buildExecutingActionRecoveredCompactBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendListElement(document, root, "recovered_actions", "action", recoveredExecutingActions, (actionElement, action) -> {
appendTextElement(document, actionElement, "description", action.getDescription());
return Unit.INSTANCE;
});
}
};
}
private @NotNull BlockContent buildExecutingActionRecoveredFullBlock(Set<ExecutableActionSnapshot> recoveredExecutingActions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendListElement(document, root, "recovered_actions", "action", recoveredExecutingActions, (actionElement, action) -> {
appendTextElement(document, actionElement, "description", action.getDescription());
appendTextElement(document, actionElement, "source", action.getSource());
appendTextElement(document, actionElement, "executing_stage", action.getExecutingStage());
return Unit.INSTANCE;
});
}
};
}
void emitStateActionTriggeredBlock(StateAction stateAction) {
StateActionSnapshot snapshot = stateAction.snapshot();
String blockName = buildBlockName(stateAction.getUuid());
String emittedAt = emittedAt();
String event = "state_action_triggered";
contextWorkspace.register(new ContextBlock(
buildStateActionFullBlock(snapshot, blockName, emittedAt, event),
buildStateActionCompactBlock(snapshot, blockName, emittedAt, event),
buildStateActionAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
90,
30,
10
));
}
private @NotNull BlockContent buildStateActionAbstractBlock(StateActionSnapshot snapshot, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "description", snapshot.getDescription());
}
};
}
private @NotNull BlockContent buildStateActionCompactBlock(StateActionSnapshot snapshot, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
}
};
}
private @NotNull BlockContent buildStateActionFullBlock(StateActionSnapshot snapshot, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
appendTextElement(document, root, "source", snapshot.getSource());
appendTextElement(document, root, "schedule_type", snapshot.getScheduleType().name().toLowerCase(Locale.ROOT));
}
};
}
void emitActionLaunchedBlock(ExecutableAction action) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_launched";
contextWorkspace.register(new ContextBlock(
buildActionLaunchedFullBlock(snapshot, blockName, event, emittedAt),
buildActionCompactBlock(snapshot, blockName, event, emittedAt),
buildActionAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
28,
6,
18
));
}
private @NotNull BlockContent buildActionAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "description", snapshot.getDescription());
}
};
}
private @NotNull BlockContent buildActionCompactBlock(ExecutableActionSnapshot snapshot, String blockName, String event, String emittedAt) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "primary_action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
Schedulable.ScheduleType scheduleType = snapshot.getScheduleType();
String scheduleContent = snapshot.getScheduleContent();
if (scheduleType != null && scheduleContent != null) {
appendChildElement(document, root, "schedule_info", (element) -> {
appendTextElement(document, element, "schedule_type", scheduleType.name().toLowerCase(Locale.ROOT));
appendTextElement(document, element, "schedule_content", scheduleContent);
return Unit.INSTANCE;
});
}
}
};
}
private @NotNull BlockContent buildActionLaunchedFullBlock(ExecutableActionSnapshot snapshot, String blockName, String event, String emittedAt) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "primary_action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "reason", snapshot.getReason());
appendTextElement(document, root, "description", snapshot.getDescription());
appendTextElement(document, root, "source", snapshot.getSource());
appendTextElement(document, root, "tendency", snapshot.getTendency());
Schedulable.ScheduleType scheduleType = snapshot.getScheduleType();
String scheduleContent = snapshot.getScheduleContent();
if (scheduleType != null && scheduleContent != null) {
appendChildElement(document, root, "schedule_info", (element) -> {
appendTextElement(document, element, "schedule_type", scheduleType.name().toLowerCase(Locale.ROOT));
appendTextElement(document, element, "schedule_content", scheduleContent);
return Unit.INSTANCE;
});
}
}
};
}
void emitActionStageSettledBlock(ExecutableAction action) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_stage_settled";
contextWorkspace.register(new ContextBlock(
buildActionStageFullBlock(snapshot, blockName, emittedAt, event),
buildActionStageCompactBlock(snapshot, blockName, emittedAt, event),
buildActionStageAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
55,
10,
12
));
}
private @NotNull BlockContent buildActionStageAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
int settledStage = snapshot.getExecutingStage();
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "abstract", history.size() + " meta actions are resolved in stage " + settledStage);
}
};
}
private @NotNull BlockContent buildActionStageCompactBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
int settledStage = snapshot.getExecutingStage();
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "abstract", history.size() + " meta actions are resolved in stage " + settledStage);
}
};
}
private @NotNull BlockContent buildActionStageFullBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
int settledStage = snapshot.getExecutingStage();
List<HistoryAction> history = resolveStageHistory(snapshot, settledStage);
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "settled_action_chain_stage", settledStage);
appendListElement(document,
root,
"settled_meta_actions",
"meta_action",
history.subList(0, Math.min(3, history.size())),
(item, action) -> {
String primaryResult = action.result();
String result = primaryResult.length() > 160 ? primaryResult.substring(0, 160) : primaryResult;
appendTextElement(document, item, "action_key", action.actionKey());
appendTextElement(document, item, "result", result);
return Unit.INSTANCE;
}
);
}
};
}
void emitActionCorrectionBlock(ExecutableAction action, String reason, List<MetaIntervention> interventions) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_correction_triggered";
contextWorkspace.register(new ContextBlock(
buildActionCorrectionFullBlock(snapshot, reason, interventions, blockName, emittedAt, event),
buildActionCorrectionCompactBlock(snapshot, reason, interventions, blockName, emittedAt, event),
buildActionCorrectionAbstractBlock(snapshot, interventions, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
22,
5,
22
));
}
private List<HistoryAction> resolveStageHistory(ExecutableActionSnapshot snapshot, int settledStage) {
List<HistoryAction> history = snapshot.getHistory().get(settledStage);
return history == null ? List.of() : history;
}
private @NotNull BlockContent buildActionCorrectionAbstractBlock(ExecutableActionSnapshot snapshot, List<MetaIntervention> interventions, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "abstract", interventions.size() + " interventions occurred after stage " + snapshot.getExecutingStage());
}
};
}
private @NotNull BlockContent buildActionCorrectionCompactBlock(ExecutableActionSnapshot snapshot, String reason, List<MetaIntervention> interventions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
Set<Integer> affectedStage = interventions.stream().map(MetaIntervention::getOrder).collect(Collectors.toSet());
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "abstract", interventions.size() + " interventions occurred after stage " + snapshot.getExecutingStage() + ", stage: " + affectedStage + " are affected");
appendTextElement(document, root, "correction_reason", reason);
}
};
}
private @NotNull BlockContent buildActionCorrectionFullBlock(ExecutableActionSnapshot snapshot, String reason, List<MetaIntervention> interventions, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "action_chain_size", snapshot.getActionChainSize());
appendTextElement(document, root, "correction_reason", reason);
appendListElement(document,
root,
"applied_interventions",
"intervention",
interventions,
(item, intervention) -> {
appendTextElement(document, item, "type", intervention.getType().name().toLowerCase(Locale.ROOT));
appendTextElement(document, item, "affected_stage", intervention.getOrder());
appendTextElement(document, item, "applied_action_key_set", intervention.getActions());
return Unit.INSTANCE;
}
);
}
};
}
void emitActionFinishedBlock(ExecutableAction action) {
ExecutableActionSnapshot snapshot = action.snapshot();
String blockName = buildBlockName(action.getUuid());
String emittedAt = emittedAt();
String event = "executable_action_finished";
contextWorkspace.register(new ContextBlock(
buildActionFinishedFullBlock(snapshot, blockName, emittedAt, event),
buildActionFinishedFullBlock(snapshot, blockName, emittedAt, event),
buildActionFinishedAbstractBlock(snapshot, blockName, event),
Set.of(ContextBlock.FocusedDomain.ACTION),
35,
14,
24
));
contextWorkspace.register(buildActionFinishedStateHintBlock(snapshot, emittedAt, event));
}
private @NotNull BlockContent buildActionFinishedAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
return new ActionBlockContent(blockName, SOURCE) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
}
};
}
private @NotNull BlockContent buildActionFinishedFullBlock(ExecutableActionSnapshot snapshot, String blockName, String emittedAt, String event) {
return new ActionBlockContent(blockName, SOURCE, BlockContent.Urgency.HIGH) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendEventElement(document, root, event);
appendTextElement(document, root, "emitted_at", emittedAt);
appendTextElement(document, root, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
appendTextElement(document, root, "result", snapshot.getResult());
}
};
}
private @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"));
}
private String buildBlockName(String actionId) {
return "executing_action-" + actionId;
}
private static abstract class ActionBlockContent extends BlockContent {
private ActionBlockContent(@NotNull String blockName, @NotNull String source, @NotNull Urgency urgency) {
super(blockName, source, urgency);
}
private ActionBlockContent(@NotNull String blockName, @NotNull String source) {
super(blockName, source);
}
protected void appendEventElement(@NotNull Document document, @NotNull Element root, String event) {
appendTextElement(document, root, "event", event);
}
}
}
@@ -1,123 +0,0 @@
package work.slhaf.partner.module.action.executor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.action.executor.entity.ExtractorInput;
import work.slhaf.partner.module.action.executor.entity.ExtractorResult;
import java.util.List;
/**
* 负责依据输入内容进行行动单元的参数信息提取
*/
public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Result<ExtractorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责为指定行动提取参数信息。
你会收到:
- 一条结构化上下文消息,其中可能包含当前行动相关状态、近期交流轨迹、以及活跃记忆切片;
- 一条任务消息,其中包含:
- target_action:本次参数提取所面向的目标行动,用于帮助你判断上下文中哪些内容与当前提取直接相关;其中 current_stage_description 表示本次 MetaAction 所属阶段的具体执行目标,应优先用于约束参数提取;
- meta_action_info:该行动对应的说明,以及允许提取的参数列表。每个 <param name="..."> 节点的文本内容表示该参数的含义或期望内容。
你的任务:
- 根据当前上下文与目标行动信息,提取本次行动所需的参数;
- 只提取能够从当前输入、近期会话、相关行动历史或明确上下文中得到支持的参数;
- 若当前信息不足以支持可靠提取,则返回 ok=false,而不是猜测或编造参数。
提取原则:
- target_action 用于帮助你在上下文中定位当前面对的是哪一个行动,不要把无关行动历史混入当前参数提取。
- action 域中的执行中行动块及其阶段历史,可作为理解当前行动推进位置、已知条件与已出现参数的参考。
- communication 域主要用于理解用户最近表达的条件、补充、修正、确认或否定信息。
- memory 域只在与当前目标行动明显相关时作为辅助参考使用。
- params 中只能填写 meta_action_info.params 中声明过的参数名,不要编造不存在的参数。
- 每个参数值都应尽量保持为简洁、明确、可直接使用的文本,不要输出冗长解释。
- 若某个参数无法从现有信息中可靠确定,就不要填写它。
关于输出:
- ok=true 表示你已经提取出了一组可用参数;不要求必须填满全部参数,但结果应足以支持后续执行继续推进。
- ok=false 表示当前信息不足以形成可靠参数结果。
- params 为参数项列表;每一项都必须包含:
- name: 参数名
- value: 参数值文本
- 不要把 params 输出为对象、Map 或其他结构,只能输出 ParamEntry 列表。
- 不要输出结构之外的解释、说明或额外文本。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<ExtractorResult> doExecute(ExtractorInput input) {
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
return formattedChat(messages, ExtractorResult.class);
}
private Message resolveTaskMessage(ExtractorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "target_action", block -> {
appendTextElement(document, block, "uuid", input.getTargetActionId());
appendTextElement(document, block, "description", input.getTargetActionDesc());
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 -> {
MetaActionInfo info = input.getMetaActionInfo();
appendTextElement(document, element, "description", info.getDescription());
appendListElement(document, element, "params", "param", info.getParams().entrySet(), (item, param) -> {
item.setAttribute("name", param.getKey());
item.setTextContent(param.getValue());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace()
.resolve(List.of(
ContextBlock.FocusedDomain.ACTION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.MEMORY
))
.encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "params_extractor";
}
}
@@ -1,36 +0,0 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class CorrectorInput {
private CheckMode checkMode;
private String tendency;
private String source;
private String reason;
private String description;
private String actionId;
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;
}
}
@@ -1,12 +0,0 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.Data;
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
import java.util.List;
@Data
public class CorrectorResult {
private List<MetaIntervention> metaInterventionList;
private String correctionReason;
}
@@ -1,28 +0,0 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.Data;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
@Data
public class ExtractorInput {
/**
* 目标行动数据的 uuid
*/
private String targetActionId;
/**
* 目标行动的 description
*/
private String targetActionDesc;
/**
* 当前执行阶段的 description
*/
private String currentStageDesc;
/**
* 目标 MetaActionInfo
*/
private MetaActionInfo metaActionInfo;
}
@@ -1,21 +0,0 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
public class ExtractorResult {
private boolean ok;
private List<ParamEntry> params;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ParamEntry {
private String name;
private String value;
}
}
@@ -1,4 +0,0 @@
package work.slhaf.partner.module.action.executor.entity;
public record HistoryAction(String actionKey, String description, String result) {
}
@@ -1,9 +0,0 @@
package work.slhaf.partner.module.action.executor.entity;
import lombok.Data;
@Data
public class RecognizerResult {
private boolean needCorrection = false;
private String reason;
}
@@ -1,423 +0,0 @@
package work.slhaf.partner.module.action.planner;
import kotlin.Unit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.action.entity.*;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.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;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorResult;
import work.slhaf.partner.module.action.planner.extractor.ActionExtractor;
import work.slhaf.partner.module.action.planner.extractor.entity.ExtractorResult;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import work.slhaf.partner.runtime.exception.ContextExceptionReporter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 负责针对本次输入生成基础的行动计划
*/
@Slf4j
public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
private static final String IMMEDIATE_WATCHER_CRON = "0/5 * * * * ?";
private static final String TENDENCIES_EVALUATING_BLOCK_NAME = "pending_action_intentions";
private final ActionAssemblyHelper assemblyHelper = new ActionAssemblyHelper();
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private ActionCapability actionCapability;
@InjectModule
private ActionEvaluator actionEvaluator;
@InjectModule
private ActionExtractor actionExtractor;
@InjectModule
private ActionScheduler actionScheduler;
@InjectModule
private ActionExecutor actionExecutor;
private ExecutorService executor;
@Init
public void init() {
this.setModuleName("action_planner");
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
String input = context.encodeInputsBlock().encodeToXmlString();
Result<ExtractorResult> result = actionExtractor.execute(input)
.onFailure(exp -> ExceptionReporterHandler.INSTANCE.report(exp, ContextExceptionReporter.REPORTER_NAME));
if (result.exceptionOrNull() != null) {
return;
}
ExtractorResult extractorResult = result.getOrThrow();
List<String> tendencies = extractorResult.getTendencies();
if (tendencies.isEmpty()) {
return;
}
appendTendencyBlock(tendencies);
evaluateTendency(context.getSource(), 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(
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
buildTendenciesEvaluatingAbstractBlock(tendencies, datetime),
Set.of(ContextBlock.FocusedDomain.ACTION),
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()) {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "datetime", datetime);
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()) {
@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, "tendencies_count", size);
appendListElement(document, root, num ? "tendencies_truncated" : "tendencies", "tendency", num ? tendencies.subList(0, 3) : tendencies);
}
};
}
private void evaluateTendency(String source, ExtractorResult extractorResult) {
executor.execute(() -> {
EvaluatorInput evaluatorInput = assemblyHelper.buildEvaluatorInput(extractorResult);
List<EvaluatorResult> evaluatorResults = actionEvaluator.execute(evaluatorInput); // 并发操作均为访问
handleEvaluatorResults(evaluatorResults, source);
cognitionCapability.contextWorkspace().expire(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName());
});
}
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;
}
if (evaluatorResult.isNeedConfirm()) {
hasPendingConfirmation = true;
registerPendingContextBlock(executableAction, evaluatorResult);
continue;
}
executeOrSchedule(executableAction);
}
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());
}
}
private void expireResolvedPending(EvaluatorResult evaluatorResult) {
EvaluatorResult.ResolvedPending resolvedPending = evaluatorResult.getResolvedPending();
if (resolvedPending == null) {
return;
}
if (resolvedPending.getBlockName() == null || resolvedPending.getSource() == null) {
return;
}
cognitionCapability.contextWorkspace().expire(
resolvedPending.getBlockName(),
resolvedPending.getSource()
);
}
private void registerPendingContextBlock(ExecutableAction executableAction, EvaluatorResult evaluatorResult) {
cognitionCapability.contextWorkspace().register(StateHintContent.createBlock(new StateHintContent(
"actions_need_confirmation",
"Partner is waiting for user confirmation before executing a pending action."
) {
@Override
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, 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");
stateElement.appendChild(chainRoot);
List<Integer> orders = new ArrayList<>(primaryActionChain.keySet());
orders.sort(Integer::compareTo);
for (Integer order : orders) {
Element orderElement = document.createElement("step");
orderElement.setAttribute("order", String.valueOf(order));
chainRoot.appendChild(orderElement);
appendRepeatedElements(
document,
orderElement,
"action_key",
primaryActionChain.getOrDefault(order, List.of())
);
}
}
}));
}
private void executeOrSchedule(ExecutableAction executableAction) {
switch (executableAction) {
case ImmediateExecutableAction action -> executeImmediateWithWatcher(action);
case SchedulableExecutableAction action -> actionScheduler.schedule(action);
default -> log.error("unknown executable action type: {}", executableAction.getClass().getSimpleName());
}
}
private void executeImmediateWithWatcher(ImmediateExecutableAction action) {
actionExecutor.execute(action);
AtomicBoolean notified = new AtomicBoolean(false);
final StateAction[] watcherRef = new StateAction[1];
StateAction watcher = new StateAction(
action.getSource(),
"immediate-action-watcher:" + action.getUuid(),
"轮询即时行动执行结果",
Schedulable.ScheduleType.CYCLE,
IMMEDIATE_WATCHER_CRON,
new StateAction.Trigger.Call(() -> {
Action.Status status = action.getStatus();
if (status != Action.Status.SUCCESS && status != Action.Status.FAILED) {
return Unit.INSTANCE;
}
if (watcherRef[0] != null) {
watcherRef[0].setEnabled(false);
}
if (!notified.compareAndSet(false, true)) {
return Unit.INSTANCE;
}
return Unit.INSTANCE;
})
);
watcherRef[0] = watcher;
actionScheduler.schedule(watcher);
}
@Override
public int order() {
return 2;
}
private final class ActionAssemblyHelper {
private ActionAssemblyHelper() {
}
private EvaluatorInput buildEvaluatorInput(ExtractorResult extractorResult) {
EvaluatorInput input = new EvaluatorInput();
input.setTendencies(extractorResult.getTendencies());
return input;
}
private ExecutableAction buildActionData(EvaluatorResult evaluatorResult, String userId) {
Map<Integer, List<MetaAction>> actionChain = getActionChain(evaluatorResult);
if (actionChain == null) {
return null;
}
ExecutableAction executableAction = switch (evaluatorResult.getType()) {
case PLANNING -> new SchedulableExecutableAction(
evaluatorResult.getTendency(),
actionChain,
evaluatorResult.getReason(),
evaluatorResult.getDescription(),
userId,
evaluatorResult.getScheduleData().getType(),
evaluatorResult.getScheduleData().getContent()
);
case IMMEDIATE -> new ImmediateExecutableAction(
evaluatorResult.getTendency(),
actionChain,
evaluatorResult.getReason(),
evaluatorResult.getDescription(),
userId
);
};
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) {
Map<Integer, List<MetaAction>> actionChain = new HashMap<>();
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
if (!fixDependencies(primaryActionChain)) {
return null;
}
for (Map.Entry<Integer, List<String>> entry : primaryActionChain.entrySet()) {
List<MetaAction> metaActions = new ArrayList<>();
for (String actionKey : entry.getValue()) {
Result<MetaAction> metaActionResult = actionCapability.loadMetaAction(actionKey);
AgentRuntimeException failure = metaActionResult.onSuccess(metaActions::add)
.exceptionOrNull();
if (failure != null) {
return null;
}
}
actionChain.put(entry.getKey(), metaActions);
}
return actionChain;
}
private boolean fixDependencies(Map<Integer, List<String>> primaryActionChain) {
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return false;
}
// 先将 primaryActionChain 的节点序号修正为从1开始依次增大
fixOrder(primaryActionChain);
List<Integer> fixedOrders = new ArrayList<>(primaryActionChain.keySet().stream().toList());
AtomicBoolean fixed = new AtomicBoolean(false);
do {
Set<Integer> tempOrders = new HashSet<>();
fixedOrders.sort(Integer::compareTo);
for (Integer fixedOrder : fixedOrders) {
int lastOrder = fixedOrder - 1;
List<String> actionKeys = primaryActionChain.get(fixedOrder);
if (actionKeys == null || actionKeys.isEmpty()) {
continue;
}
for (String actionKey : actionKeys) {
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
Result<MetaActionInfo> infoResult = actionCapability.loadMetaActionInfo(actionKey);
if (infoResult.exceptionOrNull() != null) {
return false;
}
MetaActionInfo metaActionInfo = infoResult.getOrThrow();
Set<String> preActions = metaActionInfo.getPreActions();
boolean preActionsExist = preActions.isEmpty();
if (!preActionsExist) {
continue;
}
if (!metaActionInfo.getStrictDependencies()) {
continue;
}
if (checkDependenciesExist(lastOrder, preActions, primaryActionChain)) {
continue;
}
// 如果存在前置依赖,则将其放置在当前order之前的位置,
// 放置位置优先选择已存在的上一节点,如果不存在(行动链的头节点时)则需要向行动链新增order
// 不需要检查行动链的当前节点的已存在 Action 是否为新 Action 的依赖项,因为这些 Action 实际来自 LLM
// 的评估结果,并非作为依赖项存在
fixed.set(true);
List<String> actionsInChain = primaryActionChain.computeIfAbsent(lastOrder,
list -> new ArrayList<>());
preActions = new HashSet<>(preActions);
actionsInChain.forEach(preActions::remove);
actionsInChain.addAll(preActions);
tempOrders.add(lastOrder);
}
}
fixedOrders.clear();
fixedOrders.addAll(tempOrders);
} while (fixed.getAndSet(false));
return true;
}
private void fixOrder(Map<Integer, List<String>> primaryActionChain) {
Map<Integer, List<String>> tempChain = new HashMap<>(primaryActionChain);
primaryActionChain.clear();
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));
}
}
private boolean checkDependenciesExist(int lastOrder, Set<String> preActions,
Map<Integer, List<String>> primaryActionChain) {
if (!primaryActionChain.containsKey(lastOrder)) {
return false;
}
List<String> existActions = primaryActionChain.get(lastOrder);
//noinspection SlowListContainsAll
return existActions.containsAll(preActions);
}
}
}
@@ -1,292 +0,0 @@
package work.slhaf.partner.module.action.planner.evaluator;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.cognition.ResolvedContext;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput;
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorResult;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责评估单条行动倾向 tendency,并判断它在当前系统状态下是否可以进入后续行动链。
你会收到:
- 一条结构化上下文消息,其中可能包含 action、communication、perceive、memory、cognition 等上下文;
- 一组 <available_meta_actions>,表示当前系统真实可用的 MetaAction 候选;
- 一条 user message,其内容是上游 ActionExtractor 提取出的单条 tendency。
重要前提:
- 你的输入 tendency 已经由 ActionExtractor 根据当前输入和上下文提取完成。
- 你不负责重新判断 tendency 是否来自最新用户输入。
- 你不负责重新做意图抽取、指代解析或上下文对齐。
- recent_chatmessage 中的最后一条不一定是最新输入,可能只是上一轮对话记录;不要依赖它来否定 tendency。
- 除非上下文中存在明确证据表明 tendency 已过期、已被取消、已被覆盖或与当前行动状态冲突,否则应把 tendency 当作当前待评估的候选行动。
你的任务:
- 判断该 tendency 在当前系统状态下是否可推进;
- 判断完成该 tendency 是否需要行动链;
- 判断 available_meta_actions 是否能承接该 tendency;
- 判断是否需要用户确认;
- 判断该 tendency 更适合立即执行还是进入规划;
- 输出严格符合 EvaluatorResult 的评估结果。
核心原则:
- 不要把“需要确认”误判成“不可执行”。
- 不要把“有风险”误判成“用户没有要求”。
- 不要把“能力不足”误判成“用户没有要求”。
- 不要因为 recent_chatmessage 没有出现对应内容就否定 tendency。
- 不要根据上一轮聊天内容推翻当前 tendency。
- 只有在可行性、能力、风险、状态覆盖、信息完整性等评估层面拒绝 tendency。
决策流程:
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
private ActionCapability actionCapability;
@InjectCapability
private CognitionCapability cognitionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
/**
* 对输入的行为倾向进行评估,并根据评估结果,对缓存做出调整
*
* @param data 评估输入内容,包含提取/命中缓存的行动倾向、近几条聊天记录,正在生效的记忆切片内容
* @return 评估结果集合
*/
@Override
protected List<EvaluatorResult> doExecute(EvaluatorInput data) {
List<String> tendencies = data.getTendencies();
CountDownLatch latch = new CountDownLatch(tendencies.size());
List<EvaluatorResult> evaluatorResults = new ArrayList<>();
for (String tendency : tendencies) {
executor.execute(() -> {
try {
List<Message> messages = List.of(
cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.ACTION,
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.MEMORY,
ContextBlock.FocusedDomain.COGNITION
)).encodeToMessage(),
availableMetaActionContext(),
new Message(Message.Character.USER, tendency)
);
Result<EvaluatorResult> result = formattedChat(
messages,
EvaluatorResult.class
);
result.onSuccess(evaluatorResult -> {
evaluatorResult.setTendency(tendency);
synchronized (evaluatorResults) {
evaluatorResults.add(evaluatorResult);
}
});
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException ignored) {
}
return evaluatorResults;
}
private Message availableMetaActionContext() {
// TODO select and filter available MetaActions by tags and embedding
BlockContent content = new BlockContent("available_meta_actions", "action_planner") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendRepeatedElements(
document,
root,
"available_meta_action",
actionCapability.listAvailableMetaActions().entrySet(),
(block, value) -> {
appendTextElement(document, block, "meta_action_key", value.getKey());
appendTextElement(document, block, "meta_action_description", value.getValue().getDescription());
return Unit.INSTANCE;
}
);
}
};
return new ResolvedContext(List.of(content)).encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_evaluator";
}
}
@@ -1,16 +0,0 @@
package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
import java.util.List;
import java.util.Map;
@Data
public class EvaluatorBatchInput {
private List<Message> recentMessages;
private List<ActivatedMemorySlice> activatedSlices;
private Map<String, String> availableActions;
private String tendency;
}
@@ -1,10 +0,0 @@
package work.slhaf.partner.module.action.planner.evaluator.entity;
import lombok.Data;
import java.util.List;
@Data
public class EvaluatorInput {
private List<String> tendencies;
}
@@ -1,61 +0,0 @@
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;
import java.util.stream.Collectors;
@Data
public class EvaluatorResult {
private boolean ok;
private boolean needConfirm;
private ResolvedPending resolvedPending;
private ActionType type;
private ScheduleData scheduleData;
private List<ChainElement> primaryActionChain;
private String tendency;
private String reason;
private String description;
public enum ActionType {
IMMEDIATE, PLANNING
}
@Data
public static class ResolvedPending {
private String blockName;
private String source;
}
public Map<Integer, List<String>> getPrimaryActionChainAsMap() {
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
return new LinkedHashMap<>();
}
return primaryActionChain.stream().collect(Collectors.toMap(
ChainElement::getOrder,
chainElement -> {
List<String> actionKeys = chainElement.getActionKeys();
return actionKeys == null ? new ArrayList<>() : new ArrayList<>(actionKeys);
},
(oldValue, newValue) -> newValue,
LinkedHashMap::new
));
}
@Data
public static class ScheduleData {
private String content;
private Schedulable.ScheduleType type;
}
@Data
public static class ChainElement {
private Integer order;
private String description;
private List<String> actionKeys;
}
}
@@ -1,107 +0,0 @@
package work.slhaf.partner.module.action.planner.extractor;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ModuleExecutionException;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.planner.extractor.entity.ExtractorResult;
import java.util.List;
public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责从当前输入中提取“可能值得进入后续行动评估”的行动倾向。
你会收到:
- 一条结构化上下文消息,主要包含 communication 域、perceive 域与 action 域内容;
- 一条最新输入,作为本轮重点分析对象。
你的任务:
- 基于当前输入与上下文,提取少量、明确、值得继续进入行动评估的行动倾向;
- 这里只提取“可能需要通过行动推进”的倾向,而不是所有可回应的意图;
- 若当前输入更适合直接交流回应,而不需要额外行动推进,则返回空结果。
基本原则:
- communication 域主要用于理解当前会话语境、话题延续、指代对象与用户真实意图。
- perceive 域可用于理解当前系统状态、环境信息、时间信息等;若当前输入仅是在询问这些已可直接回答的信息,不应因此提取行动倾向。
- action 域主要用于判断相关行动是否已经在执行、等待确认、或已被覆盖。
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤、工具细节或命令内容。
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息、确认信息或条件变化,应避免重复提出。
- 若 action 域中存在等待确认的 block,且当前输入与其相关,则必须提取出对应的行动倾向;不要因为其已存在于上下文中而省略。
复合任务合并规则:
- 若当前输入包含“先...再...最后...”“并且/然后/同时”等多个动作描述,但这些动作共同服务于同一个最终目标,应提取为一个整体行动倾向,而不是拆成多个并列倾向。
- 中间的信息获取、检查、读取、查询步骤如果只是为了支撑最终判断、汇报、生成结果,不应作为独立 tendency 输出。
- 最终的“总结、判断、汇报、说明结果”如果依赖前置行动结果,应并入同一个整体 tendency,而不是作为单独的 communication 类 tendency 输出。
- 只有当多个动作目标彼此独立、可以分别完成且不共享同一个最终目的时,才输出多个 tendency。
什么情况下应提取行动倾向:
- 用户明确要求系统执行某个动作、调用某项能力、访问某类外部信息、操作某个对象或推进某个待办;
- 用户要求修改、继续、取消、确认或补充某个已有待处理行动;
- 用户明确要求系统代其完成某件事,而不是仅作解释、说明或回答;
- 若不进入行动链,则该请求无法被真正推进或完成。
什么情况下不应提取行动倾向:
- 当前输入本质上是一个可直接回答的提问;
- 当前输入只是要求解释、分析、总结、说明、翻译、评价、闲聊或感叹;
- 当前输入只是询问当前上下文、系统状态、已知信息、记忆内容或当前可见内容,而这些内容可直接通过交流给出;
- 当前输入只是一般性的信息查询,但没有明确要求系统通过外部行动补充信息;
- 当前输入只是对已有回复的轻量追问、澄清或自然延续,仍然更适合直接回应;
- 当前输入语义模糊,仅凭“可能有东西可做”不足以进入行动评估;
- 当前输入虽然提到某个对象或任务,但并未形成明确的行动推进意图。
重要限制:
- 不要把“可以通过行动获得更多信息”当成提取理由。
- 不要把“询问信息”自动等同于“要求去执行查询动作”。
- 不要为了提高召回而泛化提取。
- 如果更适合 communication 直接回答,就返回空结果。
- 倾向应简洁明确,表达“要推进什么”,不要写成自然语言执行方案。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
- 不要输出结构之外的解释或额外文本。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<ExtractorResult> doExecute(String input) {
List<Message> messages = List.of(
cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION,
ContextBlock.FocusedDomain.PERCEIVE,
ContextBlock.FocusedDomain.ACTION
)).encodeToMessage(),
new Message(Message.Character.USER, input)
);
try {
return Result.success(formattedChat(messages, ExtractorResult.class).getOrThrow());
} catch (AgentRuntimeException e) {
return Result.failure(new ModuleExecutionException(
"collecting action tendencies failed",
this.getClass(),
getModuleName()
));
}
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "action_extractor";
}
}
@@ -1,12 +0,0 @@
package work.slhaf.partner.module.action.planner.extractor.entity;
import lombok.Data;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
@Data
public class ExtractorInput {
private String input;
private List<Message> recentMessages;
}
@@ -1,11 +0,0 @@
package work.slhaf.partner.module.action.planner.extractor.entity;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ExtractorResult {
private List<String> tendencies = new ArrayList<>();
}
@@ -1,490 +0,0 @@
package work.slhaf.partner.module.action.scheduler
import com.cronutils.model.CronType
import com.cronutils.model.definition.CronDefinition
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.slf4j.LoggerFactory
import work.slhaf.partner.core.action.ActionCapability
import work.slhaf.partner.core.action.entity.Action
import work.slhaf.partner.core.action.entity.Schedulable
import work.slhaf.partner.core.action.entity.SchedulableExecutableAction
import work.slhaf.partner.framework.agent.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
import java.util.*
import java.util.stream.Collectors
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
class ActionScheduler : AbstractAgentModule.Standalone() {
@InjectCapability
private lateinit var actionCapability: ActionCapability
@InjectModule
private lateinit var actionExecutor: ActionExecutor
private lateinit var timeWheel: TimeWheel
private val runtimeSchedulables: MutableSet<Schedulable> =
Collections.synchronizedSet(mutableSetOf())
private val schedulerScope =
CoroutineScope(Dispatchers.Default + SupervisorJob() + CoroutineName("ActionScheduler"))
companion object {
private val log = LoggerFactory.getLogger(ActionScheduler::class.java)
}
@Init
fun init() {
fun loadScheduledActions() {
val listScheduledActions: () -> Set<Schedulable> = {
val persistedExecutable = actionCapability.listActions(null, null)
.stream()
.filter { it is SchedulableExecutableAction }
.map { it as SchedulableExecutableAction }
.collect(Collectors.toSet())
val persisted: MutableSet<Schedulable> = mutableSetOf()
persisted.addAll(persistedExecutable)
synchronized(runtimeSchedulables) {
persisted.addAll(runtimeSchedulables.filter { it.enabled })
}
persisted
}
val onTrigger: (Set<Schedulable>) -> Unit = { schedulableSet ->
schedulableSet.filterIsInstance<Action>()
.forEach { actionExecutor.execute(it) }
}
val doneCondition: (Schedulable) -> Boolean = { schedulable ->
if (schedulable is Action) {
schedulable.status == Action.Status.FAILED || schedulable.status == Action.Status.SUCCESS
} else {
true
}
}
timeWheel = TimeWheel(listScheduledActions, onTrigger, doneCondition)
}
loadScheduledActions()
setupShutdownHook()
}
private fun setupShutdownHook() {
Runtime.getRuntime().addShutdownHook(Thread {
timeWheel.close()
schedulerScope.cancel()
})
}
fun <T> schedule(schedulableAction: T) where T : Action, T : Schedulable = schedulerScope.launch {
if (!schedulableAction.enabled) {
return@launch
}
runtimeSchedulables.add(schedulableAction)
log.debug("New data to schedule: {}", schedulableAction)
timeWheel.schedule(schedulableAction)
if (schedulableAction is SchedulableExecutableAction) {
actionCapability.putAction(schedulableAction)
}
}
fun cancel(actionId: String): Boolean {
val runtimeMatches = synchronized(runtimeSchedulables) {
runtimeSchedulables.filter { it.uuid == actionId }.toSet()
}
val persistedMatches = actionCapability.listActions(null, null)
.asSequence()
.filterIsInstance<SchedulableExecutableAction>()
.filter { it.uuid == actionId }
.toSet()
val matches = LinkedHashSet<Schedulable>()
matches.addAll(runtimeMatches)
matches.addAll(persistedMatches)
matches.forEach { it.enabled = false }
val removedFromWheel = if (::timeWheel.isInitialized) {
runBlocking { timeWheel.cancel(actionId) }
} else {
false
}
return matches.isNotEmpty() || removedFromWheel
}
private class TimeWheel(
val listSource: () -> Set<Schedulable>,
val onTrigger: (toTrigger: Set<Schedulable>) -> Unit,
val doneCondition: (schedulable: Schedulable) -> Boolean
) : Closeable {
private val 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
private var recordDay: Int = -1
private val state = MutableStateFlow(WheelState.SLEEPING)
private val wheelActionsLock = Mutex()
private val timeWheelScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("TimeWheel"))
private val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
private val cronParser: CronParser = CronParser(cronDefinition)
init {
// 启动时间轮
wheel()
}
suspend fun schedule(schedulableData: Schedulable) {
checkThenExecute {
val parseToZonedDateTime = parseToZonedDateTime(
schedulableData.scheduleType,
schedulableData.scheduleContent,
it
) ?: run {
logFailedStatus(schedulableData)
return@checkThenExecute
}
log.debug("${schedulableData.uuid} next execution time: {}", parseToZonedDateTime)
val hour = parseToZonedDateTime.hour
schedulableGroupByHour[hour].add(schedulableData)
if (it.hour == hour) {
val wheelOffset = parseToZonedDateTime.minute * 60 + parseToZonedDateTime.second
wheel[wheelOffset].add(schedulableData)
state.value = WheelState.ACTIVE
log.debug("${schedulableData.uuid} scheduled at wheel offset {}", wheelOffset)
}
}
}
suspend fun cancel(actionId: String): Boolean = wheelActionsLock.withLock {
var found = false
for (bucket in schedulableGroupByHour) {
var bucketFound = false
bucket.removeIf {
if (it.uuid == actionId) {
it.enabled = false
bucketFound = true
true
} else {
false
}
}
found = found || bucketFound
}
for (bucket in wheel) {
var bucketFound = false
bucket.removeIf {
if (it.uuid == actionId) {
it.enabled = false
bucketFound = true
true
} else {
false
}
}
found = found || bucketFound
}
found
}
private fun wheel() {
data class WheelStepResult(
val toTrigger: Set<Schedulable>?,
val shouldBreak: Boolean
)
fun collectToTrigger(
tick: Int,
previousTick: Int,
triggerHour: Int,
includeRemainingHour: Boolean = false
): Set<Schedulable>? {
if (tick >= previousTick) {
val toTrigger = mutableSetOf<Schedulable>()
for (i in previousTick..tick) {
val bucket = wheel[i]
if (bucket.isNotEmpty()) {
toTrigger.addAll(bucket.filter { it.enabled })
val bucketUuids = bucket.asSequence().map { it.uuid }.toHashSet()
schedulableGroupByHour[triggerHour].removeIf { it.uuid in bucketUuids }
bucket.clear() // 避免重复触发
}
}
if (includeRemainingHour) {
val remainingHourBucket = schedulableGroupByHour[triggerHour]
if (remainingHourBucket.isNotEmpty()) {
toTrigger.addAll(remainingHourBucket.filter { it.enabled })
remainingHourBucket.clear()
}
}
return toTrigger
}
return null
}
fun handleToTrigger(toTrigger: Set<Schedulable>) {
timeWheelScope.launch {
onTrigger(toTrigger)
}
for (schedulable in toTrigger) timeWheelScope.launch {
if (schedulable.scheduleType == Schedulable.ScheduleType.ONCE) {
return@launch
}
withTimeoutOrNull(schedulable.timeout) {
while (!doneCondition(schedulable)) {
delay(50.milliseconds)
}
}
if (!schedulable.enabled) {
return@launch
}
this@TimeWheel.schedule(schedulable)
}
}
suspend fun CoroutineScope.wheel(launchingTime: ZonedDateTime, primaryTickAdvanceTime: Long) {
val launchingHour = launchingTime.hour
var tick = launchingTime.minute * 60 + launchingTime.second
// 让节拍器从“启动时刻的下一秒”开始(避免立即 step=0)
var nextTickNanos = primaryTickAdvanceTime + 1_000_000_000L
while (isActive) {
// 1) 计算落后多少秒:至少 1(正常推进),也可能 >1(追赶)
val now0 = System.nanoTime()
val lagNanos = now0 - nextTickNanos
val step = if (lagNanos < 0) 1 else (lagNanos / 1_000_000_000L).toInt() + 1
val previousTick = tick
tick = (tick + step).coerceAtMost(wheel.lastIndex)
// 2) 推进节拍器:按“理论秒”前进 step 次
nextTickNanos += step.toLong() * 1_000_000_000L
val stepResult = run {
var shouldBreak = false
var toTrigger: Set<Schedulable>? = null
checkThenExecute(false) {
if (it.hour != launchingHour) {
shouldBreak = true
toTrigger = collectToTrigger(
wheel.lastIndex,
previousTick,
launchingHour,
includeRemainingHour = true
)
log.debug(
"Hour changed, previousTick: {}, tick: {}, toTriggerSize: {}",
previousTick,
tick,
toTrigger?.size
)
return@checkThenExecute
}
toTrigger = collectToTrigger(
tick,
previousTick,
launchingHour,
includeRemainingHour = tick >= wheel.lastIndex
)
if (tick >= wheel.lastIndex) {
state.value = WheelState.SLEEPING
shouldBreak = true
}
}
WheelStepResult(toTrigger, shouldBreak)
}
stepResult.toTrigger?.let { handleToTrigger(it) }
if (stepResult.shouldBreak) {
log.debug("Wheel stopped at tick {}", tick)
break
}
// 3) 精确睡到下一次理论 tick(用最新 nanoTime)
val now1 = System.nanoTime()
val sleepNanos = nextTickNanos - now1
if (sleepNanos > 0) {
delay((sleepNanos / 1_000_000L).milliseconds) // 毫秒级 delay 足够;剩余 nanos 不必忙等
}
}
}
suspend fun wait(currentTime: ZonedDateTime) {
val nextHour = currentTime.truncatedTo(ChronoUnit.HOURS).plusHours(1)
val seconds = Duration.between(
currentTime, nextHour
).toMillis()
// withTimeoutOrNull 内部已处理 seconds 小于 0 的情况
log.debug("Start waiting {} ms at {}, target time: {}", seconds, currentTime, nextHour)
withTimeoutOrNull(seconds.milliseconds) {
state.first { it == WheelState.ACTIVE }
}
log.debug("Waiting ended at {}", ZonedDateTime.now())
}
timeWheelScope.launch {
while (isActive) {
// 判断是否该步入下一小时
var shouldWait: Boolean? = null
var currentTime: ZonedDateTime? = null
var primaryTickAdvanceTime: Long? = null
checkThenExecute {
currentTime = it
shouldWait = schedulableGroupByHour[it.hour].isEmpty()
// 由于 wheel 的启动时间可能存在延迟,而时内推进由 nanoTime 保证不会漏发,
// 正常的时序结束又由 tick 是否触顶、当前时是否存在额外任务触发,
// 而启动时无触发保障,此时一并初始化 tick 推进时间,足以应对 check 与 wheel 间的这段时间间隔
primaryTickAdvanceTime = System.nanoTime()
}
// 如果该时无任务则等待,插入事件可提前唤醒
if (shouldWait!!) {
// 计算距离下一小时的时间,等待
currentTime?.let { wait(it) }
continue
}
// 唤醒进行时间轮循环
wheel(currentTime!!, primaryTickAdvanceTime!!)
}
}
}
suspend fun checkThenExecute(finallyToExecute: Boolean = true, then: (currentTime: ZonedDateTime) -> Unit) =
wheelActionsLock.withLock {
fun loadActions(
source: Set<Schedulable>,
now: ZonedDateTime,
load: (latestExecutingTime: ZonedDateTime, schedulableData: Schedulable) -> Unit,
repair: () -> Unit
) {
val runLoading = {
for (schedulableData in source) {
val nextExecutingTime =
parseToZonedDateTime(
schedulableData.scheduleType,
schedulableData.scheduleContent,
now
) ?: run {
logFailedStatus(schedulableData)
continue
}
load(nextExecutingTime, schedulableData)
}
}
repair()
runLoading()
}
fun loadHourActions(currentTime: ZonedDateTime) {
val load: (ZonedDateTime, Schedulable) -> Unit =
{ latestExecutionTime, schedulableData ->
val secondsTime = latestExecutionTime.minute * 60 + latestExecutionTime.second
wheel[secondsTime].add(schedulableData)
log.debug("Action loaded to hour: {}", schedulableData)
}
val repair: () -> Unit = {
for (set in wheel) {
set.clear()
}
}
loadActions(schedulableGroupByHour[currentTime.hour], currentTime, load, repair)
}
fun loadDayActions(currentTime: ZonedDateTime) {
val load: (ZonedDateTime, Schedulable) -> Unit =
{ latestExecutingTime, schedulableData ->
schedulableGroupByHour[latestExecutingTime.hour].add(schedulableData)
log.debug("Action loaded to day: {}", schedulableData)
}
val repair: () -> Unit = {
for (set in schedulableGroupByHour) {
set.clear()
}
}
loadActions(listSource(), currentTime, load, repair)
}
fun refreshIfNeeded(now: ZonedDateTime) {
val d = now.dayOfMonth
val h = now.hour
if (d != recordDay) {
recordDay = d
recordHour = h
loadDayActions(now)
loadHourActions(now)
} else if (h != recordHour) {
recordHour = h
loadHourActions(now)
}
}
val now = ZonedDateTime.now()
if (finallyToExecute) {
refreshIfNeeded(now)
then(now)
} else {
then(now)
refreshIfNeeded(now)
}
}
private fun parseToZonedDateTime(
scheduleType: Schedulable.ScheduleType,
scheduleContent: String,
now: ZonedDateTime
): ZonedDateTime? {
return when (scheduleType) {
Schedulable.ScheduleType.CYCLE
-> {
val cron = try {
cronParser.parse(scheduleContent).validate()
} catch (_: Exception) {
return null
}
val executionTime = ExecutionTime.forCron(cron)
executionTime.nextExecution(now).getOrNull()
}
Schedulable.ScheduleType.ONCE -> {
val executionTime = try {
ZonedDateTime.parse(scheduleContent)
} catch (_: Exception) {
return null
}
if (executionTime.plusSeconds(1).isBefore(now) || executionTime.dayOfMonth != now.dayOfMonth)
null
else
executionTime
}
}
}
private fun logFailedStatus(scheduleData: Schedulable) {
log.warn(
"行动未加载,scheduleType: {}, scheduleContent: {}",
scheduleData.scheduleType,
scheduleData.scheduleContent,
)
}
override fun close() {
timeWheelScope.cancel()
}
private enum class WheelState {
ACTIVE,
SLEEPING,
}
}
}
@@ -1,50 +0,0 @@
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.CommunicationBlockContent
import work.slhaf.partner.core.cognition.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()
}
}
}
@@ -1,5 +0,0 @@
package work.slhaf.partner.module.communication;
public interface AfterRolling {
void consume(RollingResult result);
}
@@ -1,52 +0,0 @@
package work.slhaf.partner.module.communication;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class AfterRollingRegistry extends AbstractAgentModule.Standalone {
private final CopyOnWriteArrayList<AfterRolling> consumers = new CopyOnWriteArrayList<>();
@InjectCapability
private ActionCapability actionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
public void register(AfterRolling consumer) {
if (consumers.contains(consumer)) {
return;
}
consumers.add(consumer);
}
public void trigger(RollingResult result) {
if (consumers.isEmpty()) {
return;
}
executor.execute(() -> {
for (AfterRolling consumer : List.copyOf(consumers)) {
try {
consumer.consume(result);
} catch (Exception e) {
ExceptionReporterHandler.INSTANCE.report(new AgentRuntimeException("after-rolling consumer occurred exception: " + consumer.getClass().getSimpleName(), e));
}
}
});
}
}
@@ -1,295 +0,0 @@
package work.slhaf.partner.module.communication;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import work.slhaf.partner.core.cognition.*;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRunningFlowContext> implements ActivateModel {
private static final String NO_REPLY_MARKER = "NO_REPLY";
private static final String AGENT_MARKER = "[[AGENT]: self]";
private static final String NOT_REPLIED_PREFIX = "[NOT_REPLIED]";
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
private static final String MODULE_PROMPT = """
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
- 不要承诺、暗示或宣称自己即将执行某个动作;例如不要说“稍等”“我去查看”“正在为你获取”“马上执行”。
- 只有当 <context> 的 action 域明确存在正在执行、等待确认、刚完成、或正在评估的行动状态时,才可以提及行动链进展。
- 若最新输入看起来像行动请求,但 <context> 中没有对应的 action 状态信号,不要自行补出“将会执行”的过渡话术;应直接给出可交流的回应,或说明当前没有可确认的执行状态。
- 若收到“action finished”一类内部触发输入,只能依据 <context> 中真实存在的行动完成状态回复;若没有可验证的完成结果,应使用 NO_REPLY 或说明没有可确认结果,不得编造结果。
你接下来收到的消息,将按照出现顺序,固定分为三个区段:
1. system message 是 Head,用于说明整个输入结构与输出要求,即本条消息。
2. <context> 区段承载系统中所有模块产生的上下文块。它表示 Partner 在当前时刻的系统状态投影,不应被理解为普通聊天历史;其中每个子块都带有独立来源,可作为理解当前状态和辅助决策的依据。
3. <conversation> 区段是系统此刻保留的对话轨迹,用于帮助你理解当前交流延续、最近问答关系与最新输入所处的位置;最新的一条 user message 会使用 <input> 结构,其中 <inputs> 承载本轮按时间顺序排列的输入序列,每个 <input> 节点会带有相对首条输入的时间间隔属性;其他子标签是输入元信息与 type=SUPPLY 的补充块,补充块会按 blockName 分区。
你的任务:
- 最新输入是当前交流的直接触发点。
- <conversation> 主要用于理解对话延续关系。
- <context> 主要用于理解 Partner 此刻的系统状态;其中明显相关的状态信号不应被当作普通历史材料忽略。
- 若最新输入与已有上下文存在张力,应以最新输入为当前交流的直接依据,再结合 <conversation> 与 <context> 判断如何回应。
- 你当前负责的是对外交流,不负责直接规划行动、修改系统状态,或伪造并不存在的执行结果。
输出契约:
- 默认情况下,直接输出要发送给用户的最终回复正文,不要添加额外标签、解释或前后缀。
- 对话轨迹中的身份行、状态行、调试行和传输边界行都属于系统内部序列化标记,只用于理解上下文,不属于可复用正文。
- 不得在输出中复现、仿写或携带任何历史序列化标记、调试前缀或传输边界标记;即使用户正在讨论这些前缀,也只能用自然语言确认或说明。
- 若当前情境下不应直接向用户发出回复,但仍需要留下本轮交流结果供系统后续保留在交流轨迹中,则输出以 NO_REPLY 开头。
- 使用 NO_REPLY 时,格式为:
NO_REPLY
这里写本轮交流结果正文
- 以 NO_REPLY 开头的输出不会直接展示给用户;系统在写入交流轨迹时,会以单独的历史标记形式保留该结果。
- 不要输出空字符串;若选择不直接回复用户,应使用 NO_REPLY 契约明确表达。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Init
public void init() {
log.info("CommunicationProducer 注册完毕...");
}
@Override
public @NotNull String modelKey() {
return "communication_producer";
}
@Override
public @NotNull List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@Override
protected void doExecute(PartnerRunningFlowContext runningFlowContext) {
log.debug("Communicating with: {}", runningFlowContext.getSource());
executeChat(runningFlowContext);
}
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget(), runningFlowContext.getResopnseChannel());
this.streamChat(buildChatMessages(runningFlowContext), consumer);
updateChatMessages(runningFlowContext, consumer.collectResponse());
cognitionCapability.refreshRecentChatMessagesContext();
}
private List<Message> buildChatMessages(PartnerRunningFlowContext runningFlowContext) {
ResolvedContext resolvedContext = cognitionCapability.contextWorkspace()
.resolve(List.of(ContextBlock.FocusedDomain.COGNITION, ContextBlock.FocusedDomain.ACTION, ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.PERCEIVE));
List<BlockContent> communicationBlocks = resolvedContext.getBlocks();
List<Message> historyMessages = snapshotConversationMessages();
List<Message> temp = new ArrayList<>(historyMessages.size() + 2);
temp.add(buildContextMessage(communicationBlocks));
temp.addAll(historyMessages);
temp.add(buildInputMessage(runningFlowContext, communicationBlocks));
return temp;
}
private void updateChatMessages(PartnerRunningFlowContext runningFlowContext, String response) {
cognitionCapability.getMessageLock().lock();
try {
List<Message> chatMessages = cognitionCapability.getChatMessages();
chatMessages.removeIf(this::isStructuredUserMessage);
Message primaryUserMessage = new Message(
Message.Character.USER,
formatConversationUserMessage(runningFlowContext)
);
chatMessages.add(primaryUserMessage);
Message assistantMessage = new Message(
Message.Character.ASSISTANT,
normalizeAssistantHistoryMessage(response)
);
chatMessages.add(assistantMessage);
} finally {
cognitionCapability.getMessageLock().unlock();
}
}
private String normalizeAssistantHistoryMessage(String response) {
String trimmed = response == null ? "" : response.trim();
if (trimmed.equals(NO_REPLY_MARKER)) {
return formatMarkedHistoryMessage(AGENT_MARKER, NOT_REPLIED_PREFIX, "");
}
if (trimmed.startsWith(NO_REPLY_MARKER + "\n")) {
return formatMarkedHistoryMessage(
AGENT_MARKER,
NOT_REPLIED_PREFIX,
trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim()
);
}
if (trimmed.startsWith(NO_REPLY_MARKER + "\r\n")) {
return formatMarkedHistoryMessage(
AGENT_MARKER,
NOT_REPLIED_PREFIX,
trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim()
);
}
return formatMarkedHistoryMessage(AGENT_MARKER, "", trimmed);
}
private List<Message> snapshotConversationMessages() {
List<Message> snapshot = new ArrayList<>(cognitionCapability.snapshotChatMessages());
snapshot.removeIf(this::isStructuredUserMessage);
return snapshot;
}
private Message buildContextMessage(List<BlockContent> communicationBlocks) {
List<BlockContent> contextBlocks = communicationBlocks.stream()
.filter(this::belongsToContextSection)
.toList();
return new ResolvedContext(contextBlocks).encodeToMessage();
}
private Message buildInputMessage(PartnerRunningFlowContext runningFlowContext, List<BlockContent> communicationBlocks) {
return new Message(Message.Character.USER, buildInputXml(runningFlowContext, communicationBlocks));
}
private String buildInputXml(PartnerRunningFlowContext runningFlowContext, List<BlockContent> communicationBlocks) {
try {
Document document = newDocument();
Element root = document.createElement("input");
document.appendChild(root);
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());
}
appendSupplyBlocks(document, root, communicationBlocks);
return toXmlString(document);
} catch (Exception e) {
throw new IllegalStateException("构建 input 区段失败", e);
}
}
private boolean isStructuredUserMessage(Message message) {
if (message.getRole() == Message.Character.ASSISTANT) {
return false;
}
String content = message.getContent();
String trimmed = content.trim();
return trimmed.startsWith("<input>") || trimmed.startsWith("<context>") || trimmed.startsWith("<?xml");
}
private boolean belongsToContextSection(BlockContent blockContent) {
if (!(blockContent instanceof CommunicationBlockContent communicationBlockContent)) {
return true;
}
return communicationBlockContent.getType() == CommunicationBlockContent.Projection.CONTEXT;
}
private String formatConversationUserMessage(PartnerRunningFlowContext runningFlowContext) {
return formatMarkedHistoryMessage("[" + runningFlowContext.getSource() + "]", "", runningFlowContext.formatInputsForHistory());
}
private String formatMarkedHistoryMessage(String identityMarker, String statusMarkers, String body) {
String markerLine = statusMarkers == null || statusMarkers.isBlank()
? identityMarker
: identityMarker + ": " + statusMarkers;
if (body == null || body.isBlank()) {
return markerLine + ":";
}
return markerLine + MARKER_BODY_SEPARATOR + body.trim();
}
private Document newDocument() throws Exception {
return DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument();
}
private void appendTextElement(Document document, Element parent, String tagName, String value) {
Element element = document.createElement(tagName);
element.setTextContent(value == null ? "" : value);
parent.appendChild(element);
}
private void appendSupplyBlocks(Document document, Element inputRoot, List<BlockContent> contextBlocks) {
Map<String, List<CommunicationBlockContent>> groupedBlocks = contextBlocks.stream()
.filter(CommunicationBlockContent.class::isInstance)
.map(CommunicationBlockContent.class::cast)
.filter(block -> block.getType() == CommunicationBlockContent.Projection.SUPPLY)
.collect(Collectors.groupingBy(
block -> sanitizeTagName(block.getBlockName()),
LinkedHashMap::new,
Collectors.toList()
));
for (Map.Entry<String, List<CommunicationBlockContent>> entry : groupedBlocks.entrySet()) {
Element groupElement = document.createElement(entry.getKey());
inputRoot.appendChild(groupElement);
for (CommunicationBlockContent block : entry.getValue()) {
Element blockElement = block.encodeToXml();
groupElement.appendChild(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";
}
String sanitized = rawTagName.replaceAll("[^A-Za-z0-9_.-]", "_");
if (!Character.isLetter(sanitized.charAt(0)) && sanitized.charAt(0) != '_') {
sanitized = "_" + sanitized;
}
return sanitized;
}
private String toXmlString(Document document) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(document), new StreamResult(writer));
return writer.toString();
}
@Override
public int order() {
return 5;
}
}
@@ -1,202 +0,0 @@
package work.slhaf.partner.module.communication;
import kotlin.Unit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
import work.slhaf.partner.core.action.entity.Schedulable;
import work.slhaf.partner.core.action.entity.StateAction;
import work.slhaf.partner.core.cognition.BlockContent;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.core.memory.MemoryCapability;
import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.core.perceive.PerceiveCapability;
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
private static final String AUTO_UPDATE_CRON = "0/10 * * * * ?";
private static final long UPDATE_TRIGGER_INTERVAL = 60 * 60 * 1000;
private static final int CONTEXT_RETAIN_DIVISOR = 6;
private static final int FULL_ROLLING_RETAIN_DIVISOR = Integer.MAX_VALUE;
private static final int DIALOG_ROLLING_TRIGGER_LIMIT = 72;
private final AtomicBoolean rolling = new AtomicBoolean(false);
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectCapability
private MemoryCapability memoryCapability;
@InjectCapability
private PerceiveCapability perceiveCapability;
@InjectCapability
private ActionCapability actionCapability;
@InjectModule
private MessageSummarizer messageSummarizer;
@InjectModule
private MessageCompressor messageCompressor;
@InjectModule
private ActionScheduler actionScheduler;
@InjectModule
private AfterRollingRegistry afterRollingRegistry;
@Init
public void init() {
registerScheduledUpdater();
}
private void registerScheduledUpdater() {
StateAction stateAction = new StateAction(
"system",
"dialog-rolling-auto-update",
"定时检查并触发对话滚动",
Schedulable.ScheduleType.CYCLE,
AUTO_UPDATE_CRON,
new StateAction.Trigger.Call(() -> {
tryAutoRolling();
return Unit.INSTANCE;
})
);
actionScheduler.schedule(stateAction);
log.info("Dialog rolling has been registered into ActionScheduler, cron={}", AUTO_UPDATE_CRON);
}
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext context) {
if (cognitionCapability.getChatMessages().size() < DIALOG_ROLLING_TRIGGER_LIMIT) {
return;
}
actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL).execute(() -> triggerRolling(false));
}
private void tryAutoRolling() {
long currentTime = System.currentTimeMillis();
int chatCount = cognitionCapability.snapshotChatMessages().size();
if (currentTime - perceiveCapability.showLastInteract().toEpochMilli() > UPDATE_TRIGGER_INTERVAL && chatCount > 1) {
triggerRolling(true);
log.debug("Dialog rolling: auto triggered");
}
}
private void triggerRolling(boolean refreshMemoryId) {
if (!rolling.compareAndSet(false, true)) {
log.debug("Dialog rolling: rolling is already executing");
return;
}
try {
List<Message> fullChatSnapshot = cognitionCapability.snapshotChatMessages();
if (fullChatSnapshot.size() <= 1) {
return;
}
List<Message> chatIncrement = resolveChatIncrement(fullChatSnapshot);
if (chatIncrement.isEmpty()) {
if (refreshMemoryId) {
memoryCapability.refreshMemorySession();
}
return;
}
int retainDivisor = refreshMemoryId ? FULL_ROLLING_RETAIN_DIVISOR : CONTEXT_RETAIN_DIVISOR;
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), retainDivisor);
applyRolling(result);
afterRollingRegistry.trigger(result);
if (refreshMemoryId) {
memoryCapability.refreshMemorySession();
}
} catch (Exception e) {
ExceptionReporterHandler.INSTANCE.report(new AgentRuntimeException("Dialog rolling failed", e));
} finally {
rolling.set(false);
}
}
List<Message> resolveChatIncrement(List<Message> fullChatSnapshot) {
String memoryId = memoryCapability.getMemorySessionId();
if (memoryId.isBlank()) {
return fullChatSnapshot;
}
MemoryUnit existingUnit = memoryCapability.getMemoryUnit(memoryId);
if (existingUnit.getConversationMessages().isEmpty()) {
return fullChatSnapshot;
}
List<Message> existingMessages = existingUnit.getConversationMessages();
int maxOverlap = Math.min(existingMessages.size(), fullChatSnapshot.size());
for (int overlap = maxOverlap; overlap > 0; overlap--) {
List<Message> existingSuffix = existingMessages.subList(existingMessages.size() - overlap, existingMessages.size());
List<Message> snapshotPrefix = fullChatSnapshot.subList(0, overlap);
if (existingSuffix.equals(snapshotPrefix)) {
return fullChatSnapshot.subList(overlap, fullChatSnapshot.size());
}
}
return fullChatSnapshot;
}
@NotNull
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
messageCompressor.execute(chatSnapshot);
Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
String summary = summaryResult.fold(
value -> value,
exp -> "no summary, due to exception"
);
if (summary.isBlank()) {
summary = "no summary, due to empty summarize result";
}
MemoryUnit memoryUnit = memoryCapability.updateMemoryUnit(chatSnapshot, summary);
MemorySlice newSlice = memoryUnit.getSlices().getLast();
return new RollingResult(memoryUnit, newSlice, List.copyOf(chatSnapshot), newSlice.getSummary(), rollingSize, retainDivisor);
}
private void applyRolling(RollingResult result) {
cognitionCapability.contextWorkspace().register(new ContextBlock(
buildDialogAbstractBlock(result.summary(), result.memoryUnit().getId(), result.memorySlice().getId()),
Set.of(ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.COMMUNICATION),
20,
5,
10
));
cognitionCapability.rollChatMessagesWithSnapshot(result.rollingSize(), result.retainDivisor());
}
private @NotNull BlockContent buildDialogAbstractBlock(String summary, String unitId, String sliceId) {
return new BlockContent("dialog_history", "dialog_rolling") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setAttribute("related_memory_unit_id", unitId);
root.setAttribute("related_memory_slice_id", sliceId);
root.setAttribute("datetime", ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
appendTextElement(document, root, "summary", summary);
}
};
}
@Override
public int order() {
return 7;
}
}
@@ -1,203 +0,0 @@
package work.slhaf.partner.module.communication
import kotlinx.coroutines.*
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.model.StreamChatMessageConsumer
import kotlin.time.Duration.Companion.milliseconds
object ReplyDispatcher {
fun createConsumer(target: String, responseChannel: String?): StreamChatMessageConsumer = ReplyConsumer(
target = target,
responseChannel = responseChannel
)
}
private class ReplyConsumer(
private val target: String,
private val responseChannel: String?
) : StreamChatMessageConsumer {
private enum class VisibilityState {
UNDECIDED,
NO_REPLY,
VISIBLE
}
private val rawResponse = StringBuilder()
private val undecidedBuffer = StringBuilder()
private val visibleBuffer = StringBuilder()
private val lock = Any()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
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 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 -> 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)
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 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
)
)
}
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
) {
}

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