@bolloon/bolloon-agent 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +495 -467
- package/bin/bolloon-cli.cjs +183 -183
- package/bin/bolloon-daemon.sh +207 -0
- package/bin/bolloon.cjs +0 -0
- package/bin/ipfs +0 -0
- package/dist/agents/agent-manifest-protocol.js +52 -0
- package/dist/agents/chat-segmenter.js +298 -0
- package/dist/agents/constraint-layer.js +19 -19
- package/dist/agents/judgment-protocol.js +479 -0
- package/dist/agents/parse-tool-call.js +304 -0
- package/dist/agents/peer-manifest-loader.js +210 -0
- package/dist/agents/pi-sdk-session-factory.js +110 -0
- package/dist/agents/pi-sdk-session-manager.js +297 -0
- package/dist/agents/pi-sdk-tools.js +1288 -0
- package/dist/agents/pi-sdk-types.js +13 -0
- package/dist/agents/pi-sdk.js +553 -1306
- package/dist/agents/react-loop.js +120 -0
- package/dist/agents/session-store.js +171 -0
- package/dist/agents/shell-guard.js +2 -2
- package/dist/agents/tool-registry.js +136 -0
- package/dist/agents/workflow-pivot-loop.js +200 -22
- package/dist/bollharness/src/scripts/context_router.js +7 -0
- package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
- package/dist/bollharness-integration/context-router-judgment.js +4 -4
- package/dist/bollharness-integration/context-router.js +292 -292
- package/dist/bollharness-integration/gate-state-machine.js +26 -13
- package/dist/bollharness-integration/integration.js +71 -0
- package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
- package/dist/bollharness-integration/skill-adapter.js +57 -127
- package/dist/bootstrap/chat-archiver.js +276 -0
- package/dist/bootstrap/context-collector.js +6 -3
- package/dist/bootstrap/event-log.js +160 -0
- package/dist/bootstrap/lifecycle-hooks.js +15 -1
- package/dist/bootstrap/memory-compressor.js +170 -0
- package/dist/bootstrap/persona-loader.js +94 -0
- package/dist/bootstrap/project-state.js +159 -0
- package/dist/bootstrap/remote-mirror.js +97 -0
- package/dist/bootstrap/session-window.js +121 -0
- package/dist/bootstrap/vector-index.js +248 -0
- package/dist/cli/loading-tui.js +40 -0
- package/dist/cli-entry.js +41 -37
- package/dist/context-compaction/auto-compact.js +7 -7
- package/dist/electron/config.js +21 -0
- package/dist/electron/config.js.map +1 -0
- package/dist/electron/dialogs.js +108 -0
- package/dist/electron/dialogs.js.map +1 -0
- package/dist/electron/first-run.js +170 -0
- package/dist/electron/first-run.js.map +1 -0
- package/dist/electron/ipc.js +20 -0
- package/dist/electron/ipc.js.map +1 -0
- package/dist/electron/logger.js +114 -0
- package/dist/electron/logger.js.map +1 -0
- package/dist/electron/main.js +63 -0
- package/dist/electron/main.js.map +1 -0
- package/dist/electron/menu.js +145 -0
- package/dist/electron/menu.js.map +1 -0
- package/dist/electron/paths.js +75 -0
- package/dist/electron/paths.js.map +1 -0
- package/dist/electron/server.js +119 -0
- package/dist/electron/server.js.map +1 -0
- package/dist/electron/tray.js +111 -0
- package/dist/electron/tray.js.map +1 -0
- package/dist/electron/window.js +111 -0
- package/dist/electron/window.js.map +1 -0
- package/dist/electron-preload.js +26 -6
- package/dist/electron-preload.js.map +1 -1
- package/dist/electron.js +6 -168
- package/dist/electron.js.map +1 -1
- package/dist/index.js +323 -266
- package/dist/llm/config-store.js +24 -8
- package/dist/llm/llm-judgment-client.js +102 -102
- package/dist/llm/pi-ai.js +137 -85
- package/dist/llm/system-prompt/layers/channel/local.md +14 -14
- package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
- package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
- package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
- package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
- package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
- package/dist/llm/system-prompt/layers/core/identity.md +37 -37
- package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
- package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
- package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
- package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
- package/dist/llm/system-prompt/layers/core/tone.md +31 -31
- package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
- package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
- package/dist/llm/system-prompt/layers/role/architect.md +20 -20
- package/dist/llm/system-prompt/layers/role/expert.md +19 -19
- package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
- package/dist/llm/system-prompt/layers/role/security.md +15 -15
- package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
- package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
- package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
- package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
- package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
- package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
- package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
- package/dist/llm/tool-manifest/ask_user_input.js +1 -1
- package/dist/llm/tool-manifest/bash.js +3 -3
- package/dist/llm/tool-manifest/create_file.js +4 -4
- package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
- package/dist/llm/tool-manifest/image_search.js +2 -2
- package/dist/llm/tool-manifest/mcp.js +2 -2
- package/dist/llm/tool-manifest/message_compose.js +3 -3
- package/dist/llm/tool-manifest/places.js +2 -2
- package/dist/llm/tool-manifest/present_files.js +1 -1
- package/dist/llm/tool-manifest/recipe.js +2 -2
- package/dist/llm/tool-manifest/str_replace.js +5 -5
- package/dist/llm/tool-manifest/view.js +3 -3
- package/dist/llm/tool-manifest/weather.js +4 -4
- package/dist/llm/tool-manifest/web.js +4 -4
- package/dist/network/local-inbox-bus.js +73 -0
- package/dist/network/p2p-outbox.js +161 -0
- package/dist/network/peer-fs.js +420 -0
- package/dist/network/peer-resource-bridge.js +216 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
- package/dist/pi-ecosystem-judgment/cleanup.js +126 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
- package/dist/pi-ecosystem-judgment/distillation.js +14 -14
- package/dist/pi-ecosystem-judgment/human-value-store.js +45 -11
- package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
- package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
- package/dist/security/context-router-tool.js +15 -15
- package/dist/security/tool-gate.js +11 -0
- package/dist/web/api-config.html +520 -520
- package/dist/web/client-loop-status.js +220 -0
- package/dist/web/client.js +22 -5
- package/dist/web/client.js.map +7 -0
- package/dist/web/components/wallet-viem.mjs +118 -118
- package/dist/web/index.html +375 -375
- package/dist/web/input-validator.js +103 -0
- package/dist/web/manifest.json +20 -20
- package/dist/web/routes-judgments.js +738 -0
- package/dist/web/routes-llm-config.js +279 -0
- package/dist/web/routes-tasks.js +222 -0
- package/dist/web/server-sse.js +121 -0
- package/dist/web/server-storage.js +154 -0
- package/dist/web/server-types.js +15 -0
- package/dist/web/server-v3-p2p.js +224 -0
- package/dist/web/server.js +1070 -1307
- package/dist/web/style.css +4742 -4742
- package/dist/web/util/safe-name.js +32 -0
- package/package.json +162 -160
- package/scripts/build-cli.js +215 -215
- package/scripts/build-web.ts +130 -130
- package/scripts/postinstall.js +152 -152
- package/bin/bolloon.js +0 -157
- package/dist/constraint-runtime/src/_archive_helper.js +0 -9
- package/dist/constraint-runtime/src/agent/coordinator.js +0 -48
- package/dist/constraint-runtime/src/agent/index.js +0 -1
- package/dist/constraint-runtime/src/assistant/index.js +0 -12
- package/dist/constraint-runtime/src/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/src/bootstrap_graph.js +0 -13
- package/dist/constraint-runtime/src/bridge/index.js +0 -12
- package/dist/constraint-runtime/src/buddy/index.js +0 -12
- package/dist/constraint-runtime/src/cli/index.js +0 -12
- package/dist/constraint-runtime/src/command_graph.js +0 -10
- package/dist/constraint-runtime/src/commands.js +0 -60
- package/dist/constraint-runtime/src/components/index.js +0 -12
- package/dist/constraint-runtime/src/constants/index.js +0 -12
- package/dist/constraint-runtime/src/constraint/budget.js +0 -22
- package/dist/constraint-runtime/src/constraint/index.js +0 -2
- package/dist/constraint-runtime/src/constraint/permission.js +0 -20
- package/dist/constraint-runtime/src/context.js +0 -30
- package/dist/constraint-runtime/src/coordinator/index.js +0 -12
- package/dist/constraint-runtime/src/cost_hook.js +0 -4
- package/dist/constraint-runtime/src/cost_tracker.js +0 -8
- package/dist/constraint-runtime/src/deferred_init.js +0 -10
- package/dist/constraint-runtime/src/direct_modes.js +0 -6
- package/dist/constraint-runtime/src/dynamic-tool-loader.js +0 -85
- package/dist/constraint-runtime/src/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/src/execution_registry.js +0 -44
- package/dist/constraint-runtime/src/history.js +0 -9
- package/dist/constraint-runtime/src/hooks/index.js +0 -12
- package/dist/constraint-runtime/src/index.js +0 -26
- package/dist/constraint-runtime/src/ink.js +0 -4
- package/dist/constraint-runtime/src/keybindings/index.js +0 -12
- package/dist/constraint-runtime/src/memdir/index.js +0 -12
- package/dist/constraint-runtime/src/migrations/index.js +0 -12
- package/dist/constraint-runtime/src/models.js +0 -1
- package/dist/constraint-runtime/src/moreright/index.js +0 -12
- package/dist/constraint-runtime/src/native_ts/index.js +0 -12
- package/dist/constraint-runtime/src/output_styles/index.js +0 -12
- package/dist/constraint-runtime/src/parity_audit.js +0 -12
- package/dist/constraint-runtime/src/plugins/index.js +0 -12
- package/dist/constraint-runtime/src/port_manifest.js +0 -11
- package/dist/constraint-runtime/src/prefetch.js +0 -9
- package/dist/constraint-runtime/src/query.js +0 -1
- package/dist/constraint-runtime/src/remote/index.js +0 -12
- package/dist/constraint-runtime/src/remote_runtime.js +0 -9
- package/dist/constraint-runtime/src/runtime/index.js +0 -1
- package/dist/constraint-runtime/src/runtime/session.js +0 -35
- package/dist/constraint-runtime/src/schemas/index.js +0 -12
- package/dist/constraint-runtime/src/screens/index.js +0 -12
- package/dist/constraint-runtime/src/server/index.js +0 -12
- package/dist/constraint-runtime/src/services/index.js +0 -12
- package/dist/constraint-runtime/src/session_store.js +0 -22
- package/dist/constraint-runtime/src/setup.js +0 -30
- package/dist/constraint-runtime/src/skills/index.js +0 -1
- package/dist/constraint-runtime/src/skills/skill-registry.js +0 -28
- package/dist/constraint-runtime/src/state/index.js +0 -12
- package/dist/constraint-runtime/src/system_init.js +0 -20
- package/dist/constraint-runtime/src/thinking/engine.js +0 -42
- package/dist/constraint-runtime/src/thinking/index.js +0 -1
- package/dist/constraint-runtime/src/tool_pool.js +0 -8
- package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +0 -7
- package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +0 -7
- package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +0 -7
- package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +0 -8
- package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +0 -6
- package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +0 -6
- package/dist/constraint-runtime/src/tools.js +0 -80
- package/dist/constraint-runtime/src/transcript.js +0 -19
- package/dist/constraint-runtime/src/types/index.js +0 -12
- package/dist/constraint-runtime/src/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/src/utils/index.js +0 -12
- package/dist/constraint-runtime/src/vim/index.js +0 -12
- package/dist/constraint-runtime/src/voice/index.js +0 -12
- package/dist/constraint-runtime/tests/agent.test.js +0 -16
- package/dist/constraint-runtime/tests/constraint.test.js +0 -41
- package/dist/constraint-runtime/tests/skill.test.js +0 -19
- package/dist/constraint-runtime/tests/thinking.test.js +0 -22
- package/dist/pi-ecosystem-colony/index.js +0 -365
- package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
- package/dist/social/ant-colony/PheromoneEngine.js +0 -227
- package/dist/social/ant-colony/index.js +0 -6
- package/dist/social/ant-colony/types.js +0 -24
- package/dist/test/ai-judgment-test.js +0 -80
- package/dist/test/bollharness-integration.test.js +0 -318
- package/dist/test/channel-agent-multi-dialogue.js +0 -205
- package/dist/test/channel-heartbeat-agent-test.js +0 -201
- package/dist/test/constraint-layer.test.js +0 -164
- package/dist/test/diap-identity-test.js +0 -172
- package/dist/test/diap-quick-test.js +0 -62
- package/dist/test/global-shared-context.test.js +0 -315
- package/dist/test/harness-judgment-injection.test.js +0 -246
- package/dist/test/harness-workflow-integrator-test.js +0 -228
- package/dist/test/human-value-store.test.js +0 -243
- package/dist/test/hybrid-integration-test.js +0 -118
- package/dist/test/hybrid-messenger-verify.js +0 -55
- package/dist/test/iroh-bistream-debug.js +0 -38
- package/dist/test/iroh-communication.test.js +0 -66
- package/dist/test/iroh-debug-test.js +0 -57
- package/dist/test/iroh-diap-test.js +0 -71
- package/dist/test/iroh-direct-connect.js +0 -55
- package/dist/test/iroh-e2e-fixed.js +0 -89
- package/dist/test/iroh-e2e-same-process.js +0 -63
- package/dist/test/iroh-e2e.js +0 -66
- package/dist/test/iroh-final-e2e.js +0 -72
- package/dist/test/iroh-relay-test.js +0 -37
- package/dist/test/iroh-simple-test.js +0 -41
- package/dist/test/iroh-transport-verify.js +0 -54
- package/dist/test/iroh-transport.test.js +0 -37
- package/dist/test/iroh-two-nodes.js +0 -70
- package/dist/test/iroh-verify.js +0 -44
- package/dist/test/judgment-decision.test.js +0 -219
- package/dist/test/llm-judgment-integration.test.js +0 -220
- package/dist/test/p2p-agent-complex-dialogue.js +0 -385
- package/dist/test/p2p-agent-dialogue.js +0 -341
- package/dist/test/p2p-agent-full-bidirectional.js +0 -510
- package/dist/test/p2p-agent-harness-flow.js +0 -437
- package/dist/test/p2p-agent-harness-single.js +0 -143
- package/dist/test/p2p-ai-dialogue-test.js +0 -318
- package/dist/test/p2p-cid-connect-test.js +0 -195
- package/dist/test/p2p-connect-receiver.js +0 -69
- package/dist/test/p2p-doc-transfer.js +0 -110
- package/dist/test/p2p-identity-page-test.js +0 -77
- package/dist/test/p2p-iroh-test.js +0 -171
- package/dist/test/p2p-minimal-test.js +0 -241
- package/dist/test/p2p-node-1.js +0 -148
- package/dist/test/p2p-node-2.js +0 -148
- package/dist/test/p2p-server.js +0 -281
- package/dist/test/p2p-two-nodes-test.js +0 -438
- package/dist/test/pi-sdk.test.js +0 -44
- package/dist/test/set-persona.js +0 -40
- package/dist/test/simple.test.js +0 -9
- package/dist/test/storage-integration.test.js +0 -150
- package/dist/test/subagent-manager.test.js +0 -276
- package/dist/test/test-gate-flow.test.js +0 -81
- package/dist/test/workflow-engine.test.js +0 -87
- package/dist/test/workflow-pivot-loop.test.js +0 -246
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pi-SDK - Agent Session for Document Processing
|
|
3
3
|
* Part of OpenClaw dual-layer architecture
|
|
4
|
+
*
|
|
5
|
+
* 模块拆分 (2026-07-06):
|
|
6
|
+
* - types → ./pi-sdk-types.ts (interface / type)
|
|
7
|
+
* - session mgr → ./pi-sdk-session-manager.ts (PiSessionManager 类)
|
|
8
|
+
* - tools → ./pi-sdk-tools.ts (registerBuiltinTools / Wallet / IdempotencyCache)
|
|
9
|
+
* - factory → ./pi-sdk-session-factory.ts (createAgentSession / getAgentSession / resetAgentSession / runSelfImproveLoop)
|
|
10
|
+
* - 本文件 (PiAgentSession 类: LLM 循环 / 系统提示 / 工具调用分发 / 压缩 / persistence)
|
|
4
11
|
*/
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
12
|
import * as fsSync from 'fs';
|
|
7
13
|
import * as os from 'os';
|
|
8
14
|
import * as path from 'path';
|
|
@@ -13,13 +19,16 @@ import { ConstraintLayer } from './constraint-layer.js';
|
|
|
13
19
|
import { WorkflowEngine } from './workflow-engine.js';
|
|
14
20
|
import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtime';
|
|
15
21
|
import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
|
|
16
|
-
import {
|
|
17
|
-
import { shellExec } from './shell-tool.js';
|
|
18
|
-
import { getBranchPrefix, getCooldownMs, checkWritePath } from './shell-guard.js';
|
|
22
|
+
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
19
23
|
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
24
|
+
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
25
|
+
import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
|
|
26
|
+
// 拆分后的子模块 — 重新导出保 backward compat
|
|
27
|
+
export { TOOL_DEFINITIONS, } from './pi-sdk-types.js';
|
|
28
|
+
export { PiSessionManager } from './pi-sdk-session-manager.js';
|
|
29
|
+
import { PiSessionManager } from './pi-sdk-session-manager.js';
|
|
30
|
+
import { registerBuiltinTools, registerWalletTools, setupInboxListener, IdempotencyCache, } from './pi-sdk-tools.js';
|
|
31
|
+
export { createAgentSession, getAgentSession, resetAgentSession, runSelfImproveLoop, } from './pi-sdk-session-factory.js';
|
|
23
32
|
// Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
|
|
24
33
|
// 失败静默, 不阻塞主对话
|
|
25
34
|
import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
|
|
@@ -31,306 +40,14 @@ import { onPostToolUse } from '../bootstrap/lifecycle-hooks.js';
|
|
|
31
40
|
import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
|
|
32
41
|
// React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
|
|
33
42
|
import { ReactHarness } from '../security/react-harness.js';
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
channelsPath;
|
|
43
|
-
initialized = false;
|
|
44
|
-
sessionDir;
|
|
45
|
-
cwd;
|
|
46
|
-
sharedContext;
|
|
47
|
-
agentId;
|
|
48
|
-
constructor(agentId, cwd) {
|
|
49
|
-
this.cwd = cwd;
|
|
50
|
-
this.sessionDir = path.join(cwd, '.port_sessions');
|
|
51
|
-
this.agentId = agentId;
|
|
52
|
-
const sessionId = `pi-session-${Date.now()}`;
|
|
53
|
-
this.session = new Session(sessionId);
|
|
54
|
-
this.state = {
|
|
55
|
-
id: sessionId,
|
|
56
|
-
agentId,
|
|
57
|
-
cwd,
|
|
58
|
-
startedAt: new Date().toISOString(),
|
|
59
|
-
lastActive: new Date().toISOString()
|
|
60
|
-
};
|
|
61
|
-
this.memory = {
|
|
62
|
-
workingMemory: [],
|
|
63
|
-
summarizedMemory: [],
|
|
64
|
-
fileContext: new Map()
|
|
65
|
-
};
|
|
66
|
-
this.channelsPath = path.join(SHARED_SESSION_PATH, 'pi-channels.json');
|
|
67
|
-
this.sharedContext = getGlobalSharedContext();
|
|
68
|
-
}
|
|
69
|
-
get sessionId() {
|
|
70
|
-
return this.session.sessionId;
|
|
71
|
-
}
|
|
72
|
-
get turnCount() {
|
|
73
|
-
return this.session.turnCount;
|
|
74
|
-
}
|
|
75
|
-
addSessionMessage(msg) {
|
|
76
|
-
this.session.addMessage(msg);
|
|
77
|
-
this.persistSession();
|
|
78
|
-
}
|
|
79
|
-
getSessionHistory() {
|
|
80
|
-
return this.session.history;
|
|
81
|
-
}
|
|
82
|
-
setSessionContext(key, value) {
|
|
83
|
-
this.session.setContext(key, value);
|
|
84
|
-
this.persistSession();
|
|
85
|
-
}
|
|
86
|
-
getSessionContext(key) {
|
|
87
|
-
return this.session.getContext(key);
|
|
88
|
-
}
|
|
89
|
-
persistSession() {
|
|
90
|
-
try {
|
|
91
|
-
const stored = {
|
|
92
|
-
sessionId: this.session.sessionId,
|
|
93
|
-
messages: this.session.history,
|
|
94
|
-
inputTokens: 0,
|
|
95
|
-
outputTokens: 0
|
|
96
|
-
};
|
|
97
|
-
saveSession(stored);
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
console.warn('Failed to persist session:', e);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
loadPersistedSession() {
|
|
104
|
-
try {
|
|
105
|
-
const sessionId = this.state.id;
|
|
106
|
-
const stored = loadSession(sessionId);
|
|
107
|
-
for (const msg of stored.messages) {
|
|
108
|
-
this.session.addMessage(msg);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// No persisted session found, start fresh
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async initialize() {
|
|
116
|
-
if (this.initialized)
|
|
117
|
-
return;
|
|
118
|
-
await fs.mkdir(SHARED_SESSION_PATH, { recursive: true });
|
|
119
|
-
await fs.mkdir(this.sessionDir, { recursive: true });
|
|
120
|
-
this.persona = await this.loadPersona();
|
|
121
|
-
await this.loadChannels();
|
|
122
|
-
this.loadPersistedSession();
|
|
123
|
-
await this.sharedContext.initialize();
|
|
124
|
-
await this.sharedContext.registerAgent({
|
|
125
|
-
agentId: this.agentId,
|
|
126
|
-
sessionId: this.sessionId,
|
|
127
|
-
channelId: 'system',
|
|
128
|
-
capabilities: this.persona?.capabilities || [],
|
|
129
|
-
status: 'active',
|
|
130
|
-
name: this.persona?.name,
|
|
131
|
-
persona: this.persona ? {
|
|
132
|
-
name: this.persona.name,
|
|
133
|
-
description: this.persona.description,
|
|
134
|
-
capabilities: this.persona.capabilities
|
|
135
|
-
} : undefined
|
|
136
|
-
});
|
|
137
|
-
this.initialized = true;
|
|
138
|
-
}
|
|
139
|
-
async loadPersona() {
|
|
140
|
-
try {
|
|
141
|
-
const data = await fs.readFile(PERSONA_PATH, 'utf-8');
|
|
142
|
-
return JSON.parse(data);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async loadChannels() {
|
|
149
|
-
try {
|
|
150
|
-
const data = await fs.readFile(this.channelsPath, 'utf-8');
|
|
151
|
-
const channelsArray = JSON.parse(data);
|
|
152
|
-
this.channels.clear();
|
|
153
|
-
for (const channel of channelsArray) {
|
|
154
|
-
this.channels.set(channel.id, channel);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
this.channels.clear();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
async saveChannels() {
|
|
162
|
-
const channelsArray = Array.from(this.channels.values());
|
|
163
|
-
await fs.writeFile(this.channelsPath, JSON.stringify(channelsArray, null, 2));
|
|
164
|
-
}
|
|
165
|
-
async savePersona(persona) {
|
|
166
|
-
await fs.writeFile(PERSONA_PATH, JSON.stringify(persona, null, 2));
|
|
167
|
-
this.persona = persona;
|
|
168
|
-
}
|
|
169
|
-
getPersona() {
|
|
170
|
-
return this.persona;
|
|
171
|
-
}
|
|
172
|
-
getState() {
|
|
173
|
-
return { ...this.state, lastActive: new Date().toISOString() };
|
|
174
|
-
}
|
|
175
|
-
getMemory() {
|
|
176
|
-
return this.memory;
|
|
177
|
-
}
|
|
178
|
-
addToWorkingMemory(content) {
|
|
179
|
-
this.memory.workingMemory.push(content);
|
|
180
|
-
if (this.memory.workingMemory.length > 100) {
|
|
181
|
-
this.memory.workingMemory = this.memory.workingMemory.slice(-100);
|
|
182
|
-
}
|
|
183
|
-
this.state.lastActive = new Date().toISOString();
|
|
184
|
-
}
|
|
185
|
-
addSummarizedMemory(content) {
|
|
186
|
-
this.memory.summarizedMemory.push(content);
|
|
187
|
-
if (this.memory.summarizedMemory.length > 50) {
|
|
188
|
-
this.memory.summarizedMemory = this.memory.summarizedMemory.slice(-50);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
addFileContext(filePath, content) {
|
|
192
|
-
this.memory.fileContext.set(filePath, content);
|
|
193
|
-
if (this.memory.fileContext.size > 20) {
|
|
194
|
-
const entries = Array.from(this.memory.fileContext.entries());
|
|
195
|
-
this.memory.fileContext = new Map(entries.slice(-20));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
updateTokenUsage(promptTokens, completionTokens) {
|
|
199
|
-
this.state.tokenUsage = {
|
|
200
|
-
promptTokens: (this.state.tokenUsage?.promptTokens || 0) + promptTokens,
|
|
201
|
-
completionTokens: (this.state.tokenUsage?.completionTokens || 0) + completionTokens,
|
|
202
|
-
totalTokens: (this.state.tokenUsage?.totalTokens || 0) + promptTokens + completionTokens
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
async addMessage(channelId, message) {
|
|
206
|
-
await this.initialize();
|
|
207
|
-
if (!this.channels.has(channelId)) {
|
|
208
|
-
this.channels.set(channelId, {
|
|
209
|
-
id: channelId,
|
|
210
|
-
name: channelId,
|
|
211
|
-
messages: [],
|
|
212
|
-
createdAt: new Date().toISOString(),
|
|
213
|
-
updatedAt: new Date().toISOString()
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
const channel = this.channels.get(channelId);
|
|
217
|
-
channel.messages.push(message);
|
|
218
|
-
channel.updatedAt = new Date().toISOString();
|
|
219
|
-
await this.saveChannels();
|
|
220
|
-
}
|
|
221
|
-
async getChannelMessages(channelId) {
|
|
222
|
-
await this.initialize();
|
|
223
|
-
return this.channels.get(channelId)?.messages || [];
|
|
224
|
-
}
|
|
225
|
-
async createChannel(name, peerInfo, persona) {
|
|
226
|
-
await this.initialize();
|
|
227
|
-
const channelId = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
228
|
-
const channel = {
|
|
229
|
-
id: channelId,
|
|
230
|
-
name,
|
|
231
|
-
messages: [],
|
|
232
|
-
createdAt: new Date().toISOString(),
|
|
233
|
-
updatedAt: new Date().toISOString(),
|
|
234
|
-
...peerInfo,
|
|
235
|
-
persona: persona || undefined
|
|
236
|
-
};
|
|
237
|
-
this.channels.set(channelId, channel);
|
|
238
|
-
await this.saveChannels();
|
|
239
|
-
return channel;
|
|
240
|
-
}
|
|
241
|
-
async getOrCreatePeerChannel(peerDid, peerName, persona) {
|
|
242
|
-
await this.initialize();
|
|
243
|
-
for (const channel of this.channels.values()) {
|
|
244
|
-
if (channel.peerDid === peerDid) {
|
|
245
|
-
return channel;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return this.createChannel(`与 ${peerName} 的对话`, {
|
|
249
|
-
peerDid,
|
|
250
|
-
peerName
|
|
251
|
-
}, persona);
|
|
252
|
-
}
|
|
253
|
-
async setChannelInfo(channelId, info) {
|
|
254
|
-
await this.initialize();
|
|
255
|
-
const channel = this.channels.get(channelId);
|
|
256
|
-
if (channel) {
|
|
257
|
-
Object.assign(channel, info, { updatedAt: new Date().toISOString() });
|
|
258
|
-
await this.saveChannels();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
getAllChannels() {
|
|
262
|
-
return Array.from(this.channels.values());
|
|
263
|
-
}
|
|
264
|
-
getChannelPersona(channelId) {
|
|
265
|
-
return this.channels.get(channelId)?.persona;
|
|
266
|
-
}
|
|
267
|
-
async setChannelPersona(channelId, persona) {
|
|
268
|
-
await this.initialize();
|
|
269
|
-
const channel = this.channels.get(channelId);
|
|
270
|
-
if (channel) {
|
|
271
|
-
channel.persona = persona;
|
|
272
|
-
channel.updatedAt = new Date().toISOString();
|
|
273
|
-
await this.saveChannels();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
async addUserActionToSharedContext(content, importance) {
|
|
277
|
-
await this.initialize();
|
|
278
|
-
await this.sharedContext.addUserAction(content, this.agentId, undefined, importance);
|
|
279
|
-
await this.sharedContext.updateAgentStatus(this.agentId, 'active');
|
|
280
|
-
}
|
|
281
|
-
async addSharedKnowledge(knowledge) {
|
|
282
|
-
await this.initialize();
|
|
283
|
-
await this.sharedContext.addSharedKnowledge(knowledge);
|
|
284
|
-
}
|
|
285
|
-
async getRecentActionsSummary(count) {
|
|
286
|
-
return this.sharedContext.getRecentActionsSummary(count);
|
|
287
|
-
}
|
|
288
|
-
async getSharedKnowledge() {
|
|
289
|
-
return this.sharedContext.getSharedKnowledge();
|
|
290
|
-
}
|
|
291
|
-
async getGlobalContext() {
|
|
292
|
-
return this.sharedContext.getFullContext();
|
|
293
|
-
}
|
|
294
|
-
async getGlobalContextSummary() {
|
|
295
|
-
return this.sharedContext.getContextSummary();
|
|
296
|
-
}
|
|
297
|
-
async createCooperation(type, task, toAgentId, context) {
|
|
298
|
-
await this.initialize();
|
|
299
|
-
return this.sharedContext.createCooperation(type, this.agentId, task, toAgentId, context);
|
|
300
|
-
}
|
|
301
|
-
async getPendingCooperations() {
|
|
302
|
-
return this.sharedContext.getPendingCooperations(this.agentId);
|
|
303
|
-
}
|
|
304
|
-
async updateCooperationStatus(cooperationId, status, result) {
|
|
305
|
-
await this.sharedContext.updateCooperationStatus(cooperationId, status, result);
|
|
306
|
-
}
|
|
307
|
-
async getAllRegisteredAgents() {
|
|
308
|
-
return this.sharedContext.getAllAgents();
|
|
309
|
-
}
|
|
310
|
-
async findAgentByCapability(capability) {
|
|
311
|
-
return this.sharedContext.findAgentByCapability(capability);
|
|
312
|
-
}
|
|
313
|
-
async getCooperation(cooperationId) {
|
|
314
|
-
return this.sharedContext.getCooperation(cooperationId);
|
|
315
|
-
}
|
|
316
|
-
async updateAgentStatusInRegistry(status) {
|
|
317
|
-
await this.sharedContext.updateAgentStatus(this.agentId, status);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const TOOL_DEFINITIONS = `
|
|
321
|
-
可用工具:
|
|
322
|
-
1. read_document(path) - 读取文档内容,支持 .txt, .md, .pdf, .docx
|
|
323
|
-
2. summarize_document(path, context?) - 总结文档内容,可选提供上下文
|
|
324
|
-
3. improve_document(path, requirements) - 改进文档,需提供文件路径和改进要求
|
|
325
|
-
4. list_peers() - 列出已连接的对等节点
|
|
326
|
-
5. send_message(peer_id, message) - 向指定对等节点发送消息
|
|
327
|
-
6. broadcast_message(message) - 向所有对等节点广播消息
|
|
328
|
-
7. get_identity() - 获取当前智能体身份信息
|
|
329
|
-
8. set_persona(persona_json) - 更新智能体 persona,包含 name、description、personality、greeting 等
|
|
330
|
-
9. run_workflow(steps) - 执行预定义工作流
|
|
331
|
-
10. get_operation_logs() - 获取操作日志
|
|
332
|
-
`;
|
|
333
|
-
class PiAgentSession {
|
|
43
|
+
import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
|
|
44
|
+
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
45
|
+
import { ToolRegistry } from './tool-registry.js';
|
|
46
|
+
import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
|
|
47
|
+
// PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
|
|
48
|
+
// Tool / ToolResult / Message / StreamCallback / StreamEvent / HeartbeatConfig / AgentSession / TOOL_DEFINITIONS
|
|
49
|
+
// 已抽到 ./pi-sdk-types.ts (2026-07-06)
|
|
50
|
+
export class PiAgentSession {
|
|
334
51
|
cwd;
|
|
335
52
|
peerId;
|
|
336
53
|
identity;
|
|
@@ -344,11 +61,17 @@ class PiAgentSession {
|
|
|
344
61
|
socialHeartbeat = null;
|
|
345
62
|
messageHistory = [];
|
|
346
63
|
tools = new Map();
|
|
64
|
+
/** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
|
|
65
|
+
_toolRegistry = new ToolRegistry();
|
|
347
66
|
skillRegistry = new SkillRegistry();
|
|
348
67
|
/** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
|
|
349
68
|
cachedToolDefinitions = '';
|
|
350
69
|
/** M2.4: 缓存 persona section */
|
|
351
70
|
cachedPersonaSection = '';
|
|
71
|
+
/** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
|
|
72
|
+
_sessionStore;
|
|
73
|
+
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
74
|
+
_readyPromise = null;
|
|
352
75
|
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
353
76
|
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
354
77
|
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
@@ -360,6 +83,10 @@ class PiAgentSession {
|
|
|
360
83
|
/** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
|
|
361
84
|
* 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
|
|
362
85
|
MAX_TOTAL_ERRORS = 20;
|
|
86
|
+
/** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
|
|
87
|
+
successfulToolResults = [];
|
|
88
|
+
/** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
|
|
89
|
+
_inboxMessages = [];
|
|
363
90
|
/** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
|
|
364
91
|
LOOP_COMPACT_RATIO = 0.8;
|
|
365
92
|
/** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
|
|
@@ -382,6 +109,9 @@ class PiAgentSession {
|
|
|
382
109
|
*/
|
|
383
110
|
judgmentGateAddition = '';
|
|
384
111
|
judgmentGateUsedIds = [];
|
|
112
|
+
// 2026-06-18: 来自 web server markedPrompt 外的 contextHint (channel/judgment/distill/remote channels),
|
|
113
|
+
// 拼到 systemPrompt 末尾, 别再混进 user message
|
|
114
|
+
contextHintAddition = '';
|
|
385
115
|
/**
|
|
386
116
|
* 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
|
|
387
117
|
* 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
|
|
@@ -394,6 +124,8 @@ class PiAgentSession {
|
|
|
394
124
|
promptStartTime = 0;
|
|
395
125
|
/** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
|
|
396
126
|
currentChannelId = '';
|
|
127
|
+
/** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
|
|
128
|
+
currentAgentId = '';
|
|
397
129
|
/** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
|
|
398
130
|
currentIntent = 'chitchat';
|
|
399
131
|
currentIntentHint = '';
|
|
@@ -437,6 +169,10 @@ class PiAgentSession {
|
|
|
437
169
|
this.peerId = config.peerId || 'local';
|
|
438
170
|
this.identity = config.identityDoc || this.createDefaultIdentity();
|
|
439
171
|
this.minimaxAvailable = this.checkMinimax();
|
|
172
|
+
// 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
|
|
173
|
+
this.currentAgentId = config.agentId || '';
|
|
174
|
+
// 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
|
|
175
|
+
this._sessionStore = config.sessionStore ?? defaultSessionStore;
|
|
440
176
|
this.constraintLayer = new ConstraintLayer();
|
|
441
177
|
this.workflowEngine = new WorkflowEngine(this.constraintLayer);
|
|
442
178
|
this.sessionManager = new PiSessionManager(this.identity.did, this.cwd);
|
|
@@ -452,38 +188,37 @@ class PiAgentSession {
|
|
|
452
188
|
// 之前 messageHistory 是空的, 服务重启后 LLM 看到的是新对话
|
|
453
189
|
// 现在 loadSessionKey 形如 "channel-xxx:default" 走 ~/.bolloon/sessions/cache/<key>.json
|
|
454
190
|
if (config.loadSessionKey) {
|
|
455
|
-
this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30)
|
|
191
|
+
this._readyPromise = this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30).catch((err) => {
|
|
192
|
+
// 失败静默, 但不让 whenReady 永久 hang
|
|
193
|
+
console.warn(`[PiAgent] hydrateMessageHistory failed: ${err.message?.slice(0, 100)}`);
|
|
194
|
+
});
|
|
456
195
|
}
|
|
457
196
|
}
|
|
458
197
|
/**
|
|
459
|
-
*
|
|
198
|
+
* 2026-06-30: 让外部 await 构造期间的 hydrate 完成.
|
|
199
|
+
* 解决 fire-and-forget 让 messageHistory 不可预测的问题.
|
|
200
|
+
* 不传 loadSessionKey 时立即返回.
|
|
201
|
+
*/
|
|
202
|
+
whenReady() {
|
|
203
|
+
return this._readyPromise ?? Promise.resolve();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* M2.3 (2026-06-30 重构): 从 SessionStore 加载历史, 转成 messageHistory 格式
|
|
460
207
|
* - 失败静默 (历史加载失败不应该阻塞 agent 启动)
|
|
461
208
|
* - 限制 max 条数, 防止 context 爆
|
|
462
|
-
* -
|
|
463
|
-
* -
|
|
209
|
+
* - 跳过错误消息 ([AI 服务调用失败] / [错误:...]) 不污染 LLM
|
|
210
|
+
* - 委托 SessionStore 完成 IO, 保证 save/load 路径对称
|
|
211
|
+
*
|
|
212
|
+
* 历史格式兼容旧 schema ({type, content}) 和新 schema (PersistedMessage[])
|
|
464
213
|
*/
|
|
465
214
|
async hydrateMessageHistory(sessionKey, maxMessages) {
|
|
466
215
|
try {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// 保留最后 N 条, 转换 role 字段
|
|
472
|
-
const tail = messages.slice(-maxMessages);
|
|
473
|
-
const hydrated = [];
|
|
474
|
-
for (const m of tail) {
|
|
475
|
-
// 跳过错误的 AI 消息 (M1.2 之后, AI 错误时 reply 是 [AI 服务调用失败] 字符串, 不该进 history)
|
|
476
|
-
if (m?.type === 'ai' && typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
|
|
477
|
-
continue;
|
|
478
|
-
if (m?.type === 'ai' && typeof m.content === 'string' && m.content.startsWith('[错误:'))
|
|
479
|
-
continue;
|
|
480
|
-
if (!m?.content)
|
|
481
|
-
continue;
|
|
482
|
-
const role = m.type === 'user' ? 'user' : m.type === 'ai' ? 'assistant' : null;
|
|
483
|
-
if (!role)
|
|
484
|
-
continue;
|
|
485
|
-
hydrated.push({ role, content: String(m.content) });
|
|
216
|
+
const loaded = await this._sessionStore.loadMessages(sessionKey);
|
|
217
|
+
if (!loaded) {
|
|
218
|
+
console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
|
|
219
|
+
return;
|
|
486
220
|
}
|
|
221
|
+
const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
|
|
487
222
|
if (hydrated.length > 0) {
|
|
488
223
|
this.messageHistory = hydrated;
|
|
489
224
|
console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
|
|
@@ -493,6 +228,84 @@ class PiAgentSession {
|
|
|
493
228
|
console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${err.message?.slice(0, 100)}`);
|
|
494
229
|
}
|
|
495
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* 2026-06-30: 把当前 messageHistory 持久化到 SessionStore.
|
|
233
|
+
* 公开方法 — claude code / 外部 harness 在每次 prompt 完成后调一下,
|
|
234
|
+
* 即可获得"重启 / 跨进程接续"的语义.
|
|
235
|
+
*/
|
|
236
|
+
async saveCurrentSession(key) {
|
|
237
|
+
const persisted = this.messageHistory.map((m) => ({
|
|
238
|
+
role: m.role,
|
|
239
|
+
content: m.content,
|
|
240
|
+
toolCall: m.toolCall,
|
|
241
|
+
toolResult: m.toolResult,
|
|
242
|
+
toolCallId: m.toolCallId,
|
|
243
|
+
timestamp: Date.now(),
|
|
244
|
+
source: 'pi-session',
|
|
245
|
+
}));
|
|
246
|
+
await this._sessionStore.saveMessages(key, persisted);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 2026-06-30: 从 disk 拉历史覆盖当前 messageHistory.
|
|
250
|
+
* 返回加载条数 — 失败或空则返回 0.
|
|
251
|
+
* 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
|
|
252
|
+
*/
|
|
253
|
+
async resumeSession(key, maxMessages = 30) {
|
|
254
|
+
const before = this.messageHistory.length;
|
|
255
|
+
await this.hydrateMessageHistory(key, maxMessages);
|
|
256
|
+
return this.messageHistory.length - before;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 2026-06-30: 读历史不修改 messageHistory.
|
|
260
|
+
* 给 claude code / 测试做"先看一下历史"用 — 不破坏当前会话.
|
|
261
|
+
* 返回 Message[] 数组 (空数组表示无历史).
|
|
262
|
+
*/
|
|
263
|
+
async peekSessionHistory(key, maxMessages = 30) {
|
|
264
|
+
try {
|
|
265
|
+
const loaded = await this._sessionStore.loadMessages(key);
|
|
266
|
+
if (!loaded)
|
|
267
|
+
return [];
|
|
268
|
+
return this._filterToMessage(loaded).slice(-maxMessages);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** hydrateMessageHistory 用的过滤逻辑 — 提到外面复用 */
|
|
275
|
+
_filterToMessage(loaded) {
|
|
276
|
+
const hydrated = [];
|
|
277
|
+
const VALID_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
|
|
278
|
+
for (const m of loaded) {
|
|
279
|
+
// role 必须合法 (拒绝旧 schema {type:'user'} 没 role 字段的)
|
|
280
|
+
if (!VALID_ROLES.has(m.role))
|
|
281
|
+
continue;
|
|
282
|
+
// 跳过污染消息
|
|
283
|
+
if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
|
|
284
|
+
continue;
|
|
285
|
+
if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
|
|
286
|
+
continue;
|
|
287
|
+
// 注意: '!m.content' 会跳过 content='' 的 tool call 消息 (assistant role + toolCall 字段),
|
|
288
|
+
// 这种是合法的 (LLM 输出只有 tool call, 没有正文) — 必须保留.
|
|
289
|
+
// 这里只跳过"无内容 + 也没 tool call/tool result"的废消息.
|
|
290
|
+
if (!m.content && !m.toolCall && !m.toolResult)
|
|
291
|
+
continue;
|
|
292
|
+
// 跳过空 tool role (tool result 占位但没有任何内容)
|
|
293
|
+
if (m.role === 'tool' && !m.toolResult)
|
|
294
|
+
continue;
|
|
295
|
+
hydrated.push({
|
|
296
|
+
role: m.role,
|
|
297
|
+
content: m.content ?? '',
|
|
298
|
+
toolCall: m.toolCall,
|
|
299
|
+
toolResult: m.toolResult,
|
|
300
|
+
toolCallId: m.toolCallId,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return hydrated;
|
|
304
|
+
}
|
|
305
|
+
/** 暴露 store 给测试 / 高级集成用. */
|
|
306
|
+
get sessionStoreInstance() {
|
|
307
|
+
return this._sessionStore;
|
|
308
|
+
}
|
|
496
309
|
/**
|
|
497
310
|
* 从 SKILL.md 目录加载 skills 进 skillRegistry.
|
|
498
311
|
*
|
|
@@ -501,23 +314,17 @@ class PiAgentSession {
|
|
|
501
314
|
* 2. ~/.bolloon/skills/ 全局用户级
|
|
502
315
|
* 3. <cwd>/.bolloon/skills/ 项目级
|
|
503
316
|
* 4. ~/.boll/skills/ 全局 (兼容 bollharness 旧用户)
|
|
504
|
-
*
|
|
505
|
-
*
|
|
317
|
+
*
|
|
318
|
+
* 2026-07-04: 移除 18 个 bollharness builtin skill (findBolloonBuiltinSkillsPath).
|
|
319
|
+
* 历史遗留: 写 pi-sdk 时为方便演示, 把 bolloon 项目里的 19 个 skill 强制注入到 system prompt.
|
|
320
|
+
* 问题: system prompt 涨到 22K chars, LLM (minimax M3) 在 pivot loop 里反复 think 不输出
|
|
321
|
+
* `<final gen>`, session 落盘拿不到最终回答.
|
|
322
|
+
* 现在: 只让用户放 .bolloon/skills/SKILL.md 才生效, 干净且 project-owned.
|
|
506
323
|
*
|
|
507
324
|
* 静默忽略不存在的目录.
|
|
508
325
|
*/
|
|
509
326
|
loadSkills(paths) {
|
|
510
|
-
|
|
511
|
-
if (paths && paths.length > 0) {
|
|
512
|
-
resolved = paths;
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
resolved = [
|
|
516
|
-
...defaultSkillPaths(os.homedir(), this.cwd),
|
|
517
|
-
// bolloon 仓库内置 skill (相对本 npm 包的位置)
|
|
518
|
-
this.findBolloonBuiltinSkillsPath(),
|
|
519
|
-
].filter((p) => Boolean(p));
|
|
520
|
-
}
|
|
327
|
+
const resolved = (paths && paths.length > 0) ? paths : defaultSkillPaths(os.homedir(), this.cwd);
|
|
521
328
|
loadSkillsFromPaths(resolved)
|
|
522
329
|
.then((skills) => {
|
|
523
330
|
for (const s of skills) {
|
|
@@ -526,36 +333,17 @@ class PiAgentSession {
|
|
|
526
333
|
}
|
|
527
334
|
this.skillRegistry.register(s);
|
|
528
335
|
}
|
|
529
|
-
console.log(`[loadSkills] 已加载 ${skills.length} 个 skill
|
|
336
|
+
console.log(`[loadSkills] 已加载 ${skills.length} 个 skill from ${resolved.join(', ')}`);
|
|
337
|
+
if (skills.length > 0) {
|
|
338
|
+
for (const s of skills) {
|
|
339
|
+
console.log(` - ${s.name}: ${s.description.substring(0, 100)}${s.description.length > 100 ? '...' : ''}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
530
342
|
})
|
|
531
343
|
.catch((err) => {
|
|
532
344
|
console.error('[loadSkills] 加载失败:', err);
|
|
533
345
|
});
|
|
534
346
|
}
|
|
535
|
-
/**
|
|
536
|
-
* 定位 bolloon 仓库内置的 bollharness skill 目录.
|
|
537
|
-
* 向上回溯 cwd, 找第一个包含 src/bollharness/.boll/skills 的祖先.
|
|
538
|
-
* 找不到时返回 null (例如把 bolloon-agent 作为外部依赖安装时).
|
|
539
|
-
*/
|
|
540
|
-
findBolloonBuiltinSkillsPath() {
|
|
541
|
-
let dir = this.cwd;
|
|
542
|
-
for (let i = 0; i < 6; i++) {
|
|
543
|
-
const candidate = path.join(dir, 'src', 'bollharness', '.boll', 'skills');
|
|
544
|
-
try {
|
|
545
|
-
if (fsSync.existsSync(candidate) && fsSync.statSync(candidate).isDirectory()) {
|
|
546
|
-
return candidate;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
// 忽略 stat 异常, 继续向上
|
|
551
|
-
}
|
|
552
|
-
const parent = path.dirname(dir);
|
|
553
|
-
if (parent === dir)
|
|
554
|
-
break;
|
|
555
|
-
dir = parent;
|
|
556
|
-
}
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
347
|
async initHarness() {
|
|
560
348
|
try {
|
|
561
349
|
const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
|
|
@@ -572,634 +360,35 @@ class PiAgentSession {
|
|
|
572
360
|
}
|
|
573
361
|
}
|
|
574
362
|
registerTools() {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
this.tools.set('summarize_document', {
|
|
593
|
-
name: 'summarize_document',
|
|
594
|
-
description: '总结文档内容,分析并生成摘要',
|
|
595
|
-
parameters: { path: 'string', context: 'string' },
|
|
596
|
-
execute: async (args) => {
|
|
597
|
-
try {
|
|
598
|
-
if (!this.minimaxAvailable) {
|
|
599
|
-
return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
|
|
600
|
-
}
|
|
601
|
-
const result = await this.summarizeDocument(args.path, args.context);
|
|
602
|
-
return {
|
|
603
|
-
success: true,
|
|
604
|
-
output: `📝 摘要:\n${result.summary}\n\n质量评分: ${(result.qualityScore * 10).toFixed(1)}/10`
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
catch (e) {
|
|
608
|
-
return { success: false, error: String(e) };
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
this.tools.set('improve_document', {
|
|
613
|
-
name: 'improve_document',
|
|
614
|
-
description: '根据要求改进文档内容',
|
|
615
|
-
parameters: { path: 'string', requirements: 'string' },
|
|
616
|
-
execute: async (args) => {
|
|
617
|
-
try {
|
|
618
|
-
if (!this.minimaxAvailable) {
|
|
619
|
-
return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
|
|
620
|
-
}
|
|
621
|
-
const result = await this.improveDocument({
|
|
622
|
-
originalPath: args.path,
|
|
623
|
-
requirements: args.requirements
|
|
624
|
-
});
|
|
625
|
-
return {
|
|
626
|
-
success: true,
|
|
627
|
-
output: `✅ 改进${result.improved ? '成功' : '失败'}\n质量评分: ${(result.qualityScore * 10).toFixed(1)}/10\n${result.newContent ? '\n改进内容:\n' + result.newContent.substring(0, 500) + '...' : ''}`
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
catch (e) {
|
|
631
|
-
return { success: false, error: String(e) };
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
this.tools.set('list_peers', {
|
|
636
|
-
name: 'list_peers',
|
|
637
|
-
description: '列出已连接的对等节点',
|
|
638
|
-
parameters: {},
|
|
639
|
-
execute: async () => {
|
|
640
|
-
const peers = p2pNetwork.getPeers();
|
|
641
|
-
if (peers.length === 0) {
|
|
642
|
-
return { success: true, output: '当前无连接的对等节点' };
|
|
643
|
-
}
|
|
644
|
-
return { success: true, output: `已连接节点 (${peers.length}):\n${peers.map(p => ` - ${p}`).join('\n')}` };
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
this.tools.set('send_message', {
|
|
648
|
-
name: 'send_message',
|
|
649
|
-
description: '向指定对等节点发送消息',
|
|
650
|
-
parameters: { peer_id: '对等节点ID', message: '消息内容' },
|
|
651
|
-
execute: async (args) => {
|
|
652
|
-
try {
|
|
653
|
-
await p2pNetwork.sendMessage(args.peer_id, 'message', args.message);
|
|
654
|
-
return { success: true, output: `消息已发送到 ${args.peer_id}` };
|
|
655
|
-
}
|
|
656
|
-
catch (e) {
|
|
657
|
-
return { success: false, error: String(e) };
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
this.tools.set('broadcast_message', {
|
|
662
|
-
name: 'broadcast_message',
|
|
663
|
-
description: '向所有对等节点广播消息',
|
|
664
|
-
parameters: { message: '消息内容' },
|
|
665
|
-
execute: async (args) => {
|
|
666
|
-
try {
|
|
667
|
-
await p2pNetwork.broadcast('message', args.message);
|
|
668
|
-
return { success: true, output: '消息已广播到所有节点' };
|
|
669
|
-
}
|
|
670
|
-
catch (e) {
|
|
671
|
-
return { success: false, error: String(e) };
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
});
|
|
675
|
-
this.tools.set('get_identity', {
|
|
676
|
-
name: 'get_identity',
|
|
677
|
-
description: '获取当前智能体身份信息',
|
|
678
|
-
parameters: {},
|
|
679
|
-
execute: async () => {
|
|
680
|
-
const id = this.getIdentity();
|
|
681
|
-
const extraInfo = id.cid ? `\nCID: ${id.cid}` : '';
|
|
682
|
-
const ipnsInfo = id.ipnsName ? `\nIPNS: ${id.ipnsName}` : '';
|
|
683
|
-
return {
|
|
684
|
-
success: true,
|
|
685
|
-
output: `DID: ${id.did}\n名称: ${id.name}\n公钥: ${id.publicKey}${extraInfo}${ipnsInfo}\n创建时间: ${new Date(id.createdAt).toISOString()}`
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
this.tools.set('set_persona', {
|
|
690
|
-
name: 'set_persona',
|
|
691
|
-
description: '更新智能体的 persona 信息,包括名字、描述、性格等',
|
|
692
|
-
parameters: { persona_json: 'Persona JSON 对象,包含 name、description、personality、greeting 等字段' },
|
|
693
|
-
execute: async (args) => {
|
|
694
|
-
try {
|
|
695
|
-
const personaData = typeof args.persona_json === 'string' ? JSON.parse(args.persona_json) : args.persona_json;
|
|
696
|
-
const now = new Date().toISOString();
|
|
697
|
-
const newPersona = {
|
|
698
|
-
name: personaData.name || this.identity.name,
|
|
699
|
-
description: personaData.description || '',
|
|
700
|
-
capabilities: personaData.capabilities || [],
|
|
701
|
-
personality: personaData.personality || '',
|
|
702
|
-
greeting: personaData.greeting || '',
|
|
703
|
-
interests: personaData.interests || [],
|
|
704
|
-
createdAt: this.persona?.createdAt || now,
|
|
705
|
-
updatedAt: now
|
|
706
|
-
};
|
|
707
|
-
await this.setPersona(newPersona);
|
|
708
|
-
this.persona = newPersona;
|
|
709
|
-
if (newPersona.name) {
|
|
710
|
-
this.identity.name = newPersona.name;
|
|
711
|
-
}
|
|
712
|
-
return { success: true, output: `Persona 已更新:\n名称: ${newPersona.name}\n描述: ${newPersona.description}\n性格: ${newPersona.personality}` };
|
|
713
|
-
}
|
|
714
|
-
catch (e) {
|
|
715
|
-
return { success: false, error: `更新 persona 失败: ${String(e)}` };
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
this.tools.set('get_operation_logs', {
|
|
720
|
-
name: 'get_operation_logs',
|
|
721
|
-
description: '获取约束层的操作日志',
|
|
722
|
-
parameters: {},
|
|
723
|
-
execute: async () => {
|
|
724
|
-
const logs = this.constraintLayer.getLogs();
|
|
725
|
-
if (logs.length === 0) {
|
|
726
|
-
return { success: true, output: '暂无操作日志' };
|
|
727
|
-
}
|
|
728
|
-
return {
|
|
729
|
-
success: true,
|
|
730
|
-
output: `操作日志 (${logs.length} 条):\n${logs.slice(-10).map(l => `[${new Date(l.timestamp).toISOString()}] ${l.action} - ${l.status}`).join('\n')}`
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
// 添加文件列表工具
|
|
735
|
-
this.tools.set('list_files', {
|
|
736
|
-
name: 'list_files',
|
|
737
|
-
description: '列出目录中的文件',
|
|
738
|
-
parameters: { path: '目录路径(可选,默认为当前目录)' },
|
|
739
|
-
execute: async (args) => {
|
|
740
|
-
try {
|
|
741
|
-
const fs = await import('fs');
|
|
742
|
-
const path = args.path || this.cwd;
|
|
743
|
-
const files = fs.readdirSync(path);
|
|
744
|
-
return {
|
|
745
|
-
success: true,
|
|
746
|
-
output: `📁 目录 ${path} 中的文件 (${files.length} 个):\n${files.slice(0, 20).map(f => ` - ${f}`).join('\n')}${files.length > 20 ? '\n ...' : ''}`
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
catch (e) {
|
|
750
|
-
return { success: false, error: String(e) };
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
// 添加目录读取工具(更完整的实现)
|
|
755
|
-
this.tools.set('read_directory', {
|
|
756
|
-
name: 'read_directory',
|
|
757
|
-
description: '读取目录内容,返回文件列表和目录结构',
|
|
758
|
-
parameters: { path: '目录路径(可选,默认为当前目录)' },
|
|
759
|
-
execute: async (args) => {
|
|
760
|
-
try {
|
|
761
|
-
const fs = await import('fs');
|
|
762
|
-
const pathModule = await import('path');
|
|
763
|
-
const targetPath = args.path || this.cwd;
|
|
764
|
-
const items = fs.readdirSync(targetPath);
|
|
765
|
-
const result = [];
|
|
766
|
-
for (const item of items.slice(0, 30)) {
|
|
767
|
-
const fullPath = pathModule.join(targetPath, item);
|
|
768
|
-
try {
|
|
769
|
-
const stat = fs.statSync(fullPath);
|
|
770
|
-
const type = stat.isDirectory() ? '📁' : '📄';
|
|
771
|
-
result.push(`${type} ${item}${stat.isDirectory() ? '/' : ''}`);
|
|
772
|
-
}
|
|
773
|
-
catch {
|
|
774
|
-
result.push(`📄 ${item}`);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
return {
|
|
778
|
-
success: true,
|
|
779
|
-
output: `📂 ${targetPath} (${items.length} 项):\n${result.join('\n')}${items.length > 30 ? '\n... 还有更多文件' : ''}`
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
catch (e) {
|
|
783
|
-
return { success: false, error: `无法读取目录: ${String(e)}` };
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
// P2P Document Tools
|
|
788
|
-
for (const tool of p2pDocumentTools) {
|
|
789
|
-
this.tools.set(tool.name, tool);
|
|
790
|
-
}
|
|
791
|
-
// Shell Exec 工具: 给 AI 跑受限的 shell 命令
|
|
792
|
-
// **只能** 跑白名单内的命令 (git/npm/tsc/vitest/cat/...)
|
|
793
|
-
// **不能** 改禁区路径 (见 shell-guard.ts 的 FORBIDDEN_PATH_PATTERNS)
|
|
794
|
-
// 沙箱 cwd: .bolloon-shell-sandbox/
|
|
795
|
-
this.tools.set('shell_exec', {
|
|
796
|
-
name: 'shell_exec',
|
|
797
|
-
description: '在 cwd (process.cwd(), 即 bolloon 当前工作目录) 跑 shell 命令. 仅支持白名单内命令: git, npm, npx, tsx, tsc, vitest, cat, head, tail, ls, wc, echo, pwd, date, mkdir, touch. 禁止管道/重定向/rm -rf/sudo. 命中护栏黑名单会被拒. 注意: cwd 是真正的 git repo 工作目录, git add/commit/push 等命令会作用于本仓库, 不是 sandbox.',
|
|
798
|
-
parameters: { command: '可执行文件 (必填, 必须在白名单)', args: '参数数组, 逗号分隔', timeoutMs: '超时毫秒, 默认 30000' },
|
|
799
|
-
execute: async (args) => {
|
|
800
|
-
const cmd = String(args.command || '').trim();
|
|
801
|
-
if (!cmd)
|
|
802
|
-
return { success: false, error: 'command 必填' };
|
|
803
|
-
const argList = String(args.args || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
804
|
-
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
805
|
-
const result = await shellExec(cmd, argList, { timeoutMs });
|
|
806
|
-
if (result.deniedByGuard) {
|
|
807
|
-
return { success: false, error: result.error };
|
|
808
|
-
}
|
|
809
|
-
if (!result.success) {
|
|
810
|
-
return { success: false, error: result.error, output: result.output };
|
|
811
|
-
}
|
|
812
|
-
return { success: true, output: result.output };
|
|
813
|
-
}
|
|
814
|
-
});
|
|
815
|
-
// self_improve 工具: AI 触发自我改进循环
|
|
816
|
-
// **必须** 在 branchPrefix 命名的分支上工作
|
|
817
|
-
// 心跳事件会自动调用; 用户对话里也能手动调
|
|
818
|
-
this.tools.set('self_improve', {
|
|
819
|
-
name: 'self_improve',
|
|
820
|
-
description: `触发自我改进循环. AI 会在分支 ${getBranchPrefix()}<timestamp> 上工作, 跑 tsc + vitest 验证, 通过后输出分支名给用户审. 冷却期由策略文件决定. 命中护栏禁区的改动会被拒.`,
|
|
821
|
-
parameters: { goal: '本轮改进目标 (1 句话)' },
|
|
822
|
-
execute: async (args) => {
|
|
823
|
-
const goal = String(args.goal || '').trim();
|
|
824
|
-
if (!goal)
|
|
825
|
-
return { success: false, error: 'goal 必填' };
|
|
826
|
-
return await runSelfImproveLoop(goal);
|
|
827
|
-
}
|
|
828
|
-
});
|
|
829
|
-
// list_skills 工具: 列出当前 session 已加载的 skills
|
|
830
|
-
// 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
|
|
831
|
-
this.tools.set('list_skills', {
|
|
832
|
-
name: 'list_skills',
|
|
833
|
-
description: '列出当前 session 已加载的 skills 及其描述. Skills 是从 SKILL.md 文件加载的, 兼容 Anthropic Agent Skills 标准 frontmatter 和 bollharness 现有 frontmatter.',
|
|
834
|
-
parameters: {},
|
|
835
|
-
execute: async () => {
|
|
836
|
-
const skills = this.skillRegistry.list();
|
|
837
|
-
if (skills.length === 0) {
|
|
838
|
-
return {
|
|
839
|
-
success: true,
|
|
840
|
-
output: '当前 session 没有加载任何 skill. 检查 ~/.bolloon/skills/ 或项目 .bolloon/skills/ 目录.',
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
const lines = skills.map((s, i) => `${i + 1}. ${s.name} — ${s.description}`);
|
|
844
|
-
return { success: true, output: `已加载 ${skills.length} 个 skill:\n${lines.join('\n')}` };
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
// use_skill 工具: 加载指定 skill 的 body 进 LLM context
|
|
848
|
-
// Skills 协议核心: 把 SKILL.md body 作为 Markdown 指南返回, LLM 下一轮按它执行
|
|
849
|
-
this.tools.set('use_skill', {
|
|
850
|
-
name: 'use_skill',
|
|
851
|
-
description: '按名称加载一个 skill, 把它的 SKILL.md body 作为 Markdown 文档返回. 调用后 LLM 会在下一轮按 skill 指南执行. 与 shell_exec / read_document 这些 "能力工具" 不同, use_skill 是 "知识注入".',
|
|
852
|
-
parameters: { name: 'skill 名称 (用 list_skills 查可用的)' },
|
|
853
|
-
execute: async (args) => {
|
|
854
|
-
const name = String(args.name || '').trim();
|
|
855
|
-
if (!name)
|
|
856
|
-
return { success: false, error: 'name 必填' };
|
|
857
|
-
if (!this.skillRegistry.has(name)) {
|
|
858
|
-
const available = this.skillRegistry.list().map((s) => s.name).join(', ');
|
|
859
|
-
return { success: false, error: `skill "${name}" 未找到. 已加载: ${available || '(无)'}` };
|
|
860
|
-
}
|
|
861
|
-
try {
|
|
862
|
-
const body = await this.skillRegistry.execute(name, {});
|
|
863
|
-
return { success: true, output: body };
|
|
864
|
-
}
|
|
865
|
-
catch (e) {
|
|
866
|
-
return { success: false, error: `执行 skill 失败: ${String(e)}` };
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
// M2.1 (2026-06-17): 注册 4 个长期缺失的工具 — 让 agent 真正能"修改 + 提交"代码
|
|
871
|
-
// 路径限制走 shell-guard 同款 allowlist (复用 checkWritePath / checkCommand)
|
|
872
|
-
// 这些工具之前在 tool-gate 白名单 + tool-manifest 中存在, 但 pi-sdk 未注册 — 修复孤儿
|
|
873
|
-
this.tools.set('write_file', {
|
|
874
|
-
name: 'write_file',
|
|
875
|
-
description: '写入一个文件. 路径必须在白名单 (src/web/*, src/agents/workflow-*, *.md, docs/**, src/test/**, src/agents/pi-sdk.ts 等). 大文件 (> 100KB) 会被拒. 命中护栏黑名单 (shell-guard.ts, package.json, .env, .git, .bolloon, dist) 会拒.',
|
|
876
|
-
parameters: { path: '相对路径 (必填, 相对 cwd)', content: '文件内容 (必填)' },
|
|
877
|
-
execute: async (args) => {
|
|
878
|
-
const relPath = String(args.path || '').trim();
|
|
879
|
-
const content = String(args.content ?? '');
|
|
880
|
-
if (!relPath)
|
|
881
|
-
return { success: false, error: 'path 必填' };
|
|
882
|
-
if (content.length > 100_000)
|
|
883
|
-
return { success: false, error: `内容过大 (${content.length} > 100000 字节), 请分块写` };
|
|
884
|
-
// 路径检查: 复用 shell-guard 的 checkWritePath
|
|
885
|
-
const pathResult = checkWritePath(relPath);
|
|
886
|
-
if (!pathResult.allowed) {
|
|
887
|
-
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
888
|
-
}
|
|
889
|
-
try {
|
|
890
|
-
const absPath = path.resolve(this.cwd, relPath);
|
|
891
|
-
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
892
|
-
await fs.writeFile(absPath, content, 'utf-8');
|
|
893
|
-
return { success: true, output: `✅ wrote ${relPath} (${content.length} bytes)` };
|
|
894
|
-
}
|
|
895
|
-
catch (e) {
|
|
896
|
-
return { success: false, error: `写文件失败: ${String(e)}` };
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
this.tools.set('edit_file', {
|
|
901
|
-
name: 'edit_file',
|
|
902
|
-
description: '编辑一个文件: 在 path 处查找 old_text, 替换为 new_text. 找不到 old_text 会失败 (避免静默不替换). 路径同样受护栏限制.',
|
|
903
|
-
parameters: { path: '相对路径 (必填)', old_text: '要替换的文本 (必填, 全文匹配)', new_text: '新文本 (必填)' },
|
|
904
|
-
execute: async (args) => {
|
|
905
|
-
const relPath = String(args.path || '').trim();
|
|
906
|
-
const oldText = String(args.old_text ?? '');
|
|
907
|
-
const newText = String(args.new_text ?? '');
|
|
908
|
-
if (!relPath)
|
|
909
|
-
return { success: false, error: 'path 必填' };
|
|
910
|
-
if (!oldText)
|
|
911
|
-
return { success: false, error: 'old_text 必填' };
|
|
912
|
-
const pathResult = checkWritePath(relPath);
|
|
913
|
-
if (!pathResult.allowed) {
|
|
914
|
-
return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
|
|
915
|
-
}
|
|
916
|
-
try {
|
|
917
|
-
const absPath = path.resolve(this.cwd, relPath);
|
|
918
|
-
const original = await fs.readFile(absPath, 'utf-8');
|
|
919
|
-
if (!original.includes(oldText)) {
|
|
920
|
-
return { success: false, error: `old_text 在 ${relPath} 中未找到, 拒绝静默写入. 请先用 read_document 读最新内容.` };
|
|
921
|
-
}
|
|
922
|
-
const updated = original.replace(oldText, newText);
|
|
923
|
-
await fs.writeFile(absPath, updated, 'utf-8');
|
|
924
|
-
return { success: true, output: `✅ edited ${relPath} (${oldText.length} → ${newText.length} 字节)` };
|
|
925
|
-
}
|
|
926
|
-
catch (e) {
|
|
927
|
-
return { success: false, error: `编辑文件失败: ${String(e)}` };
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
this.tools.set('git_diff', {
|
|
932
|
-
name: 'git_diff',
|
|
933
|
-
description: '查看 git diff. 默认显示未提交改动 (staged + unstaged), 可指定 ref1..ref2 看两个 commit/分支之间的 diff. 输出会截到 8000 字符避免超长.',
|
|
934
|
-
parameters: { range: '可选. e.g. "HEAD~3..HEAD" 或 "master..agent/feat-x". 省略则看未提交改动.' },
|
|
935
|
-
execute: async (args) => {
|
|
936
|
-
const range = String(args.range || '').trim();
|
|
937
|
-
const argv = range ? ['diff', range] : ['diff'];
|
|
938
|
-
const result = await shellExec('git', argv, { timeoutMs: 10_000 });
|
|
939
|
-
if (result.deniedByGuard)
|
|
940
|
-
return { success: false, error: result.error };
|
|
941
|
-
if (!result.success)
|
|
942
|
-
return { success: false, error: result.error, output: result.output };
|
|
943
|
-
const out = (result.output || '').slice(0, 8000);
|
|
944
|
-
return { success: true, output: out || '(空 diff — 没有未提交改动)' };
|
|
945
|
-
}
|
|
946
|
-
});
|
|
947
|
-
this.tools.set('git_commit', {
|
|
948
|
-
name: 'git_commit',
|
|
949
|
-
description: 'git add -A + git commit. 提交信息由 LLM 提供. 不会 push — push 是 agent 显式调 git_commit_and_push 触发. 命中护栏 (push to master/main, force-push) 仍会被拒. 内部自动设 BOLLOON_AUTO_EVOLVE=1 让 pre-commit hook 跳过 vitest/tsc (auto-evolve 模式, CI 兜底).',
|
|
950
|
-
parameters: { message: 'commit message (必填, 用 HEREDOC 多行)' },
|
|
951
|
-
execute: async (args) => {
|
|
952
|
-
const message = String(args.message || '').trim();
|
|
953
|
-
if (!message)
|
|
954
|
-
return { success: false, error: 'message 必填' };
|
|
955
|
-
// M3.4 (2026-06-17): agent git_commit 自动设 BOLLOON_AUTO_EVOLVE=1, 让 lefthook pre-commit 跳过 vitest/tsc
|
|
956
|
-
// 这样频繁 commit 不会被 30s+ 的 pre-commit 阻塞. CI 会兜底 (push 后 GitHub Actions 跑测试)
|
|
957
|
-
// 先 add
|
|
958
|
-
const addResult = await shellExec('git', ['add', '-A'], { timeoutMs: 10_000 });
|
|
959
|
-
if (addResult.deniedByGuard)
|
|
960
|
-
return { success: false, error: addResult.error };
|
|
961
|
-
if (!addResult.success)
|
|
962
|
-
return { success: false, error: `git add 失败: ${addResult.error}` };
|
|
963
|
-
// 再 commit (message 用 -m, 避免 HEREDOC 注入)
|
|
964
|
-
// Windows shell 不支持 inline env var, 改用 child_process 直接 spawn
|
|
965
|
-
// 简单方案: 用 spawn + 临时设 env
|
|
966
|
-
try {
|
|
967
|
-
const { spawn: spawnFn } = await import('child_process');
|
|
968
|
-
const env = { ...process.env, BOLLOON_AUTO_EVOLVE: '1' };
|
|
969
|
-
const output = await new Promise((resolve, reject) => {
|
|
970
|
-
const proc = spawnFn('git', ['commit', '-m', message], {
|
|
971
|
-
cwd: this.cwd, env, stdio: ['ignore', 'pipe', 'pipe'],
|
|
972
|
-
});
|
|
973
|
-
let stdout = '';
|
|
974
|
-
let stderr = '';
|
|
975
|
-
proc.stdout.on('data', (d) => stdout += d.toString());
|
|
976
|
-
proc.stderr.on('data', (d) => stderr += d.toString());
|
|
977
|
-
proc.on('close', (code) => code === 0 ? resolve(stdout) : reject(new Error(stderr || `git commit exited ${code}`)));
|
|
978
|
-
proc.on('error', reject);
|
|
979
|
-
});
|
|
980
|
-
return { success: true, output: `✅ committed: ${message.split('\n')[0]}\n${output}` };
|
|
981
|
-
}
|
|
982
|
-
catch (e) {
|
|
983
|
-
return { success: false, error: `git commit 失败: ${String(e.message || e).slice(0, 500)}` };
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
});
|
|
987
|
-
// M3.4: git_push — 改完直接 push (Q3-B 决策, 修改了 pre-push hook 让它别卡)
|
|
988
|
-
// 命中护栏 (push to master/main, force-push) 仍会被拒 — 这两条底线不变
|
|
989
|
-
this.tools.set('git_push', {
|
|
990
|
-
name: 'git_push',
|
|
991
|
-
description: 'git push 当前分支到 origin. 命中护栏 (push to master/main, --force) 仍会被拒. 用于长期项目自动 commit + push 循环.',
|
|
992
|
-
parameters: { remote: '可选, 默认 origin', branch: '可选, 默认当前分支' },
|
|
993
|
-
execute: async (args) => {
|
|
994
|
-
const remote = String(args.remote || 'origin').trim();
|
|
995
|
-
const branch = String(args.branch || '').trim();
|
|
996
|
-
const argv = branch ? ['push', remote, branch] : ['push', remote];
|
|
997
|
-
const result = await shellExec('git', argv, { timeoutMs: 60_000 });
|
|
998
|
-
if (result.deniedByGuard)
|
|
999
|
-
return { success: false, error: result.error };
|
|
1000
|
-
if (!result.success)
|
|
1001
|
-
return { success: false, error: result.error, output: result.output };
|
|
1002
|
-
return { success: true, output: `✅ pushed to ${remote}${branch ? `/${branch}` : ''}\n${result.output || ''}` };
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
// M3.4: git_branch — 创建/切换分支 (用于多任务并行隔离)
|
|
1006
|
-
this.tools.set('git_branch', {
|
|
1007
|
-
name: 'git_branch',
|
|
1008
|
-
description: 'git checkout -b <name> 或 git checkout <name>. 用于多任务并行隔离 — 改前先 checkout 到 agent/<task-id> 分支.',
|
|
1009
|
-
parameters: { name: '分支名 (必填, e.g. "agent/task-123")', create: '可选, "true" 表示创建新分支 (默认 false = 切到已有)' },
|
|
1010
|
-
execute: async (args) => {
|
|
1011
|
-
const name = String(args.name || '').trim();
|
|
1012
|
-
const create = String(args.create || 'false') === 'true';
|
|
1013
|
-
if (!name)
|
|
1014
|
-
return { success: false, error: 'name 必填' };
|
|
1015
|
-
const argv = create ? ['checkout', '-b', name] : ['checkout', name];
|
|
1016
|
-
const result = await shellExec('git', argv, { timeoutMs: 10_000 });
|
|
1017
|
-
if (result.deniedByGuard)
|
|
1018
|
-
return { success: false, error: result.error };
|
|
1019
|
-
if (!result.success)
|
|
1020
|
-
return { success: false, error: result.error, output: result.output };
|
|
1021
|
-
return { success: true, output: `✅ ${create ? 'created + checked out' : 'checked out'} ${name}\n${result.output || ''}` };
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
// M3.2: 任务状态机工具 — 让 agent 自己维护 multi_step 任务的状态
|
|
1025
|
-
this.tools.set('create_task', {
|
|
1026
|
-
name: 'create_task',
|
|
1027
|
-
description: '创建一个新多步任务, 初始 steps 列表由 LLM 给出. 返回 task-id, 后续用 update_task / get_task 跟踪进度. 任务状态写 ~/.bolloon/tasks/<id>.yaml 持久化.',
|
|
1028
|
-
parameters: {
|
|
1029
|
-
goal: '任务目标 (1 句话, 必填)',
|
|
1030
|
-
steps: '步骤列表 (数组, 必填, 至少 1 步)',
|
|
1031
|
-
sessionKey: '可选, 关联的 session key (channel:sessionId)',
|
|
1032
|
-
branch: '可选, 关联的 git 分支名',
|
|
1033
|
-
},
|
|
1034
|
-
execute: async (args) => {
|
|
1035
|
-
const goal = String(args.goal || '').trim();
|
|
1036
|
-
const stepsRaw = args.steps;
|
|
1037
|
-
if (!goal)
|
|
1038
|
-
return { success: false, error: 'goal 必填' };
|
|
1039
|
-
let steps = [];
|
|
1040
|
-
if (Array.isArray(stepsRaw)) {
|
|
1041
|
-
steps = stepsRaw.map((s) => String(s).trim()).filter(Boolean);
|
|
1042
|
-
}
|
|
1043
|
-
else if (typeof stepsRaw === 'string') {
|
|
1044
|
-
// 支持 "step1\nstep2\nstep3" 或 "step1,step2,step3"
|
|
1045
|
-
steps = stepsRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean);
|
|
1046
|
-
}
|
|
1047
|
-
if (steps.length === 0)
|
|
1048
|
-
return { success: false, error: 'steps 必填且至少 1 步' };
|
|
1049
|
-
const sessionKey = String(args.sessionKey || '').trim() || undefined;
|
|
1050
|
-
const branch = String(args.branch || '').trim() || undefined;
|
|
1051
|
-
try {
|
|
1052
|
-
const { createTask } = await import('./task-state.js');
|
|
1053
|
-
const task = await createTask({ goal, steps, sessionKey, branch });
|
|
1054
|
-
return { success: true, output: `✅ task created: ${task.id}\nbranch: ${branch || '(未指定)'}\nsteps:\n${steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}` };
|
|
1055
|
-
}
|
|
1056
|
-
catch (e) {
|
|
1057
|
-
return { success: false, error: `create_task 失败: ${String(e)}` };
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
this.tools.set('update_task', {
|
|
1062
|
-
name: 'update_task',
|
|
1063
|
-
description: '更新任务的某一步状态 (pending → running → done/failed/skipped). 系统会自动推进下一步 pending → running (当当前步 done 时).',
|
|
1064
|
-
parameters: {
|
|
1065
|
-
task_id: '任务 id (必填)',
|
|
1066
|
-
step_id: '步骤 id (必填, e.g. "step-1")',
|
|
1067
|
-
status: '新状态 (running | done | failed | skipped)',
|
|
1068
|
-
result_summary: '可选, 结果摘要',
|
|
1069
|
-
error: '可选, 失败原因 (status=failed 时填)',
|
|
1070
|
-
},
|
|
1071
|
-
execute: async (args) => {
|
|
1072
|
-
const taskId = String(args.task_id || '').trim();
|
|
1073
|
-
const stepId = String(args.step_id || '').trim();
|
|
1074
|
-
const status = String(args.status || '').trim();
|
|
1075
|
-
if (!taskId || !stepId)
|
|
1076
|
-
return { success: false, error: 'task_id + step_id 必填' };
|
|
1077
|
-
if (!['running', 'done', 'failed', 'skipped'].includes(status)) {
|
|
1078
|
-
return { success: false, error: `status 必须是 running|done|failed|skipped` };
|
|
1079
|
-
}
|
|
1080
|
-
try {
|
|
1081
|
-
const { updateStep } = await import('./task-state.js');
|
|
1082
|
-
const patch = { status };
|
|
1083
|
-
if (args.result_summary)
|
|
1084
|
-
patch.resultSummary = String(args.result_summary);
|
|
1085
|
-
if (args.error)
|
|
1086
|
-
patch.error = String(args.error);
|
|
1087
|
-
const updated = await updateStep(taskId, stepId, patch);
|
|
1088
|
-
if (!updated)
|
|
1089
|
-
return { success: false, error: `任务 ${taskId} 未找到` };
|
|
1090
|
-
const nextRunning = updated.steps.find((s) => s.status === 'running');
|
|
1091
|
-
return {
|
|
1092
|
-
success: true,
|
|
1093
|
-
output: `✅ step ${stepId} → ${status}\ntask 状态: ${updated.status}${nextRunning ? `\n下一步: ${nextRunning.id} — ${nextRunning.description}` : ''}`,
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
catch (e) {
|
|
1097
|
-
return { success: false, error: `update_task 失败: ${String(e)}` };
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
this.tools.set('get_task', {
|
|
1102
|
-
name: 'get_task',
|
|
1103
|
-
description: '查任务的当前状态和步骤进度. 用于跨 loop / 跨 session 恢复.',
|
|
1104
|
-
parameters: { task_id: '任务 id (必填)' },
|
|
1105
|
-
execute: async (args) => {
|
|
1106
|
-
const taskId = String(args.task_id || '').trim();
|
|
1107
|
-
if (!taskId)
|
|
1108
|
-
return { success: false, error: 'task_id 必填' };
|
|
1109
|
-
try {
|
|
1110
|
-
const { getTask } = await import('./task-state.js');
|
|
1111
|
-
const t = await getTask(taskId);
|
|
1112
|
-
if (!t)
|
|
1113
|
-
return { success: false, error: `任务 ${taskId} 未找到` };
|
|
1114
|
-
const lines = [
|
|
1115
|
-
`任务: ${t.id}`,
|
|
1116
|
-
`目标: ${t.goal}`,
|
|
1117
|
-
`状态: ${t.status}`,
|
|
1118
|
-
`branch: ${t.branch || '(未指定)'}`,
|
|
1119
|
-
`sessionKey: ${t.sessionKey || '(未指定)'}`,
|
|
1120
|
-
`创建: ${t.createdAt}`,
|
|
1121
|
-
`更新: ${t.updatedAt}`,
|
|
1122
|
-
``,
|
|
1123
|
-
`步骤:`,
|
|
1124
|
-
...t.steps.map((s) => ` ${s.status === 'done' ? '✅' : s.status === 'running' ? '🔄' : s.status === 'failed' ? '❌' : s.status === 'skipped' ? '⏭️' : '⏳'} ${s.id} — ${s.description}${s.resultSummary ? `\n 结果: ${s.resultSummary}` : ''}${s.error ? `\n 错误: ${s.error}` : ''}`),
|
|
1125
|
-
];
|
|
1126
|
-
return { success: true, output: lines.join('\n') };
|
|
1127
|
-
}
|
|
1128
|
-
catch (e) {
|
|
1129
|
-
return { success: false, error: `get_task 失败: ${String(e)}` };
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
});
|
|
1133
|
-
this.tools.set('list_tasks', {
|
|
1134
|
-
name: 'list_tasks',
|
|
1135
|
-
description: '列出最近 N 个任务 (默认 10). 用于多任务并行管理.',
|
|
1136
|
-
parameters: { limit: '可选, 默认 10' },
|
|
1137
|
-
execute: async (args) => {
|
|
1138
|
-
const limit = Number(args.limit) || 10;
|
|
1139
|
-
try {
|
|
1140
|
-
const { listTasks } = await import('./task-state.js');
|
|
1141
|
-
const tasks = await listTasks(limit);
|
|
1142
|
-
if (tasks.length === 0) {
|
|
1143
|
-
return { success: true, output: '当前没有任务. 用 create_task 创建一个.' };
|
|
1144
|
-
}
|
|
1145
|
-
const lines = tasks.map((t) => {
|
|
1146
|
-
const done = t.steps.filter((s) => s.status === 'done').length;
|
|
1147
|
-
const total = t.steps.length;
|
|
1148
|
-
return `${t.status === 'completed' ? '✅' : t.status === 'failed' ? '❌' : '🔄'} ${t.id} — ${t.goal} (${done}/${total} steps, branch: ${t.branch || '-'})`;
|
|
1149
|
-
});
|
|
1150
|
-
return { success: true, output: `最近 ${tasks.length} 个任务:\n${lines.join('\n')}` };
|
|
1151
|
-
}
|
|
1152
|
-
catch (e) {
|
|
1153
|
-
return { success: false, error: `list_tasks 失败: ${String(e)}` };
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
// M3.3 (2026-06-17): 工具幂等性 — 显式 cache 防止重试时副作用执行两次
|
|
1158
|
-
// 设计: 在 registerTools 末尾 wrap 所有 this.tools, 每个调用走 cache:
|
|
1159
|
-
// - 算 (toolName + JSON.stringify(args)) 的 hash
|
|
1160
|
-
// - 命中 cache → 返缓存结果 (不重跑副作用, 关键对 write_file / edit_file / shell_exec)
|
|
1161
|
-
// - 失败结果不缓存 (避免缓存 transient 错误)
|
|
1162
|
-
// - 缓存容量 200 条, 超过就清空
|
|
1163
|
-
this.wrapToolsWithIdempotency();
|
|
1164
|
-
}
|
|
1165
|
-
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
1166
|
-
idempotencyCache = new Map();
|
|
1167
|
-
IDEMPOTENCY_TTL_MS = 5 * 60 * 1000; // 5 分钟内同 (tool, args) 走 cache
|
|
1168
|
-
IDEMPOTENCY_MAX = 200;
|
|
1169
|
-
wrapToolsWithIdempotency() {
|
|
1170
|
-
// 只 wrap 会产生副作用的工具 — 读类工具 (list_files, read_document, get_task) 不 wrap
|
|
1171
|
-
// (让 LLM 拿到最新数据, 不会因为缓存读到旧 task 状态)
|
|
1172
|
-
const SIDE_EFFECT_TOOLS = new Set([
|
|
1173
|
-
'write_file', 'edit_file', 'shell_exec', 'git_commit', 'git_push', 'git_branch',
|
|
1174
|
-
'create_task', 'update_task',
|
|
1175
|
-
]);
|
|
363
|
+
// 2026-07-06: 工具注册抽到 ./pi-sdk-tools.ts, 这里只调 + 镜像到 ToolRegistry
|
|
364
|
+
this._inboxMessages = [];
|
|
365
|
+
const toolCtx = {
|
|
366
|
+
tools: this.tools,
|
|
367
|
+
cwd: this.cwd,
|
|
368
|
+
identity: this.identity,
|
|
369
|
+
persona: this.persona,
|
|
370
|
+
minimaxAvailable: this.minimaxAvailable,
|
|
371
|
+
setPersona: async (p) => { await this.setPersona(p); },
|
|
372
|
+
sessionManager: this.sessionManager,
|
|
373
|
+
constraintLayer: this.constraintLayer,
|
|
374
|
+
_inboxMessages: this._inboxMessages,
|
|
375
|
+
};
|
|
376
|
+
registerBuiltinTools(toolCtx);
|
|
377
|
+
registerWalletTools(toolCtx);
|
|
378
|
+
setupInboxListener(toolCtx);
|
|
379
|
+
// 镜像到 ToolRegistry (alias resolve 用)
|
|
1176
380
|
for (const [name, tool] of this.tools.entries()) {
|
|
1177
|
-
|
|
1178
|
-
continue;
|
|
1179
|
-
const original = tool.execute;
|
|
1180
|
-
tool.execute = async (args) => {
|
|
1181
|
-
const key = `${name}|${JSON.stringify(args)}`;
|
|
1182
|
-
const cached = this.idempotencyCache.get(key);
|
|
1183
|
-
if (cached && Date.now() - cached.ts < this.IDEMPOTENCY_TTL_MS) {
|
|
1184
|
-
// 命中 — 加标记让 LLM 知道这是 cache (不会真执行副作用)
|
|
1185
|
-
return { ...cached.result, output: (cached.result.output || '') + '\n[↻ idempotency cache hit]' };
|
|
1186
|
-
}
|
|
1187
|
-
const result = await original(args);
|
|
1188
|
-
// 只缓存成功结果, 避免缓存 transient 错误
|
|
1189
|
-
if (result && result.success) {
|
|
1190
|
-
if (this.idempotencyCache.size >= this.IDEMPOTENCY_MAX) {
|
|
1191
|
-
this.idempotencyCache.clear();
|
|
1192
|
-
}
|
|
1193
|
-
this.idempotencyCache.set(key, { result, ts: Date.now() });
|
|
1194
|
-
}
|
|
1195
|
-
return result;
|
|
1196
|
-
};
|
|
381
|
+
this._toolRegistry.register(tool);
|
|
1197
382
|
}
|
|
383
|
+
// M3.3: 副作用工具走幂等性 cache
|
|
384
|
+
this._idempotencyCache.wrap(this.tools);
|
|
1198
385
|
}
|
|
1199
386
|
/** 清幂等性缓存 — 强制下次调用真正执行 (用于 agent 显式需要重新跑的场景) */
|
|
1200
387
|
clearIdempotencyCache() {
|
|
1201
|
-
this.
|
|
388
|
+
this._idempotencyCache.clear();
|
|
1202
389
|
}
|
|
390
|
+
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
391
|
+
_idempotencyCache = new IdempotencyCache();
|
|
1203
392
|
async registerP2PDocumentReceiver() {
|
|
1204
393
|
await initDocumentReceiver();
|
|
1205
394
|
}
|
|
@@ -1207,10 +396,13 @@ class PiAgentSession {
|
|
|
1207
396
|
// M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
|
|
1208
397
|
if (this.cachedToolDefinitions)
|
|
1209
398
|
return this.cachedToolDefinitions;
|
|
1210
|
-
const defs = ['
|
|
399
|
+
const defs = ['可用工具 (name(params) - 简介):'];
|
|
1211
400
|
for (const tool of this.tools.values()) {
|
|
1212
|
-
|
|
1213
|
-
|
|
401
|
+
// 2026-06-19: 压缩 tool 定义 — 只显示参数名 (不显示描述, 减少 60% 长度)
|
|
402
|
+
// 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
|
|
403
|
+
// 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
|
|
404
|
+
const paramNames = Object.keys(tool.parameters).join(',');
|
|
405
|
+
defs.push(`- ${tool.name}(${paramNames})`);
|
|
1214
406
|
}
|
|
1215
407
|
this.cachedToolDefinitions = defs.join('\n');
|
|
1216
408
|
return this.cachedToolDefinitions;
|
|
@@ -1307,15 +499,27 @@ class PiAgentSession {
|
|
|
1307
499
|
}
|
|
1308
500
|
}
|
|
1309
501
|
async promptStream(input, onStream, signal, channelId) {
|
|
502
|
+
console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
|
|
1310
503
|
this.minimaxAvailable = this.checkMinimax();
|
|
504
|
+
console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
|
|
1311
505
|
this.currentChannelId = channelId ?? this.currentChannelId;
|
|
506
|
+
// 2026-06-18 (supervisor): web server 把 46K markedPrompt 喂过来
|
|
507
|
+
// (【本轮用户请求】\n<text>\n【请求结束】\n\n<contextHint>).
|
|
508
|
+
// 整个 input 走下游, pivot loop 之前拿 47K buildContext 当 user message 发出去,
|
|
509
|
+
// 模型撞 context window. 提取 userText 替代 input, contextHint 拼到 systemPrompt 末尾.
|
|
510
|
+
const markerMatch = input.match(/【本轮用户请求】\s*([\s\S]*?)\s*【请求结束】/);
|
|
511
|
+
const userText = markerMatch ? markerMatch[1].trim() : input;
|
|
512
|
+
const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
|
|
513
|
+
console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
|
|
1312
514
|
this.messageHistory.push({
|
|
1313
515
|
role: 'user',
|
|
1314
|
-
content:
|
|
516
|
+
content: userText
|
|
1315
517
|
});
|
|
518
|
+
// 2026-06-18: web server 喂的 markedPrompt 外的 contextHint 拼到 system 末尾 (而不是当 user message)
|
|
519
|
+
this.contextHintAddition = contextHint;
|
|
1316
520
|
onStream({ type: 'thinking', content: '🤔 开始思考...' });
|
|
1317
521
|
if (!this.minimaxAvailable) {
|
|
1318
|
-
const response = await this.handleFallback(
|
|
522
|
+
const response = await this.handleFallback(userText);
|
|
1319
523
|
this.messageHistory.push({ role: 'assistant', content: response });
|
|
1320
524
|
onStream({ type: 'done', content: '' });
|
|
1321
525
|
return response;
|
|
@@ -1323,11 +527,11 @@ class PiAgentSession {
|
|
|
1323
527
|
// P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
|
|
1324
528
|
this.currentOnStream = onStream;
|
|
1325
529
|
this.currentSignal = signal ?? null;
|
|
1326
|
-
await this.computeJudgmentGate(
|
|
530
|
+
await this.computeJudgmentGate(userText);
|
|
1327
531
|
// M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
|
|
1328
532
|
try {
|
|
1329
533
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
1330
|
-
this.currentIntent = classifyIntent(
|
|
534
|
+
this.currentIntent = classifyIntent(userText);
|
|
1331
535
|
this.currentIntentHint = intentHint(this.currentIntent);
|
|
1332
536
|
if (this.currentIntent !== 'chitchat') {
|
|
1333
537
|
onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
|
|
@@ -1348,14 +552,68 @@ class PiAgentSession {
|
|
|
1348
552
|
}
|
|
1349
553
|
// Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
|
|
1350
554
|
// (失败静默, 5s 限流防止循环)
|
|
555
|
+
// 2026-07-04: 透传 agentId 让 onSessionStart 加载 persona 文档
|
|
1351
556
|
let bootstrapAddition = '';
|
|
1352
557
|
try {
|
|
1353
|
-
const ss = await onSessionStart({
|
|
558
|
+
const ss = await onSessionStart({
|
|
559
|
+
channelId: this.currentChannelId || undefined,
|
|
560
|
+
agentId: this.currentAgentId || undefined,
|
|
561
|
+
});
|
|
1354
562
|
bootstrapAddition = ss.systemAddition || '';
|
|
1355
563
|
}
|
|
1356
564
|
catch (err) {
|
|
1357
565
|
console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
|
|
1358
566
|
}
|
|
567
|
+
// 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
|
|
568
|
+
// 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
|
|
569
|
+
if (this.currentChannelId) {
|
|
570
|
+
try {
|
|
571
|
+
const { getRecentEvents } = await import('../bootstrap/event-log.js');
|
|
572
|
+
const events = await getRecentEvents(this.currentChannelId, 5);
|
|
573
|
+
if (events.length > 0) {
|
|
574
|
+
const eventBlock = [
|
|
575
|
+
'## 最近项目事件 (最近 5 条, 倒序)',
|
|
576
|
+
...events.map(e => `- [${e.ts.slice(0, 16)}] [${e.type}] ${e.summary}`),
|
|
577
|
+
].join('\n');
|
|
578
|
+
bootstrapAddition = (bootstrapAddition + '\n\n' + eventBlock).slice(-2000);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
console.warn('[PiAgent] getRecentEvents failed (non-fatal):', err);
|
|
583
|
+
}
|
|
584
|
+
// 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
|
|
585
|
+
try {
|
|
586
|
+
const { readState, formatStateForPrompt } = await import('../bootstrap/project-state.js');
|
|
587
|
+
const state = await readState({ channelId: this.currentChannelId });
|
|
588
|
+
const stateText = formatStateForPrompt(state);
|
|
589
|
+
if (stateText) {
|
|
590
|
+
bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
console.warn('[PiAgent] readState failed (non-fatal):', err);
|
|
595
|
+
}
|
|
596
|
+
// 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
|
|
597
|
+
try {
|
|
598
|
+
const { searchIndex } = await import('../bootstrap/vector-index.js');
|
|
599
|
+
const indexName = `channel-${this.currentChannelId}`;
|
|
600
|
+
const results = await searchIndex({
|
|
601
|
+
indexName,
|
|
602
|
+
query: userText,
|
|
603
|
+
topK: 3,
|
|
604
|
+
});
|
|
605
|
+
if (results.length > 0) {
|
|
606
|
+
const hitBlock = [
|
|
607
|
+
'## 相关历史片段 (top-3, TF-IDF cosine)',
|
|
608
|
+
...results.map((r, i) => `- [${i + 1}] score=${r.score.toFixed(3)}: ${r.text.slice(0, 200).replace(/\n/g, ' ')}`),
|
|
609
|
+
].join('\n');
|
|
610
|
+
bootstrapAddition = (bootstrapAddition + '\n\n' + hitBlock).slice(-3000);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
// 索引不存在是常见情况 (新 channel), 不打 warn
|
|
615
|
+
}
|
|
616
|
+
}
|
|
1359
617
|
this.bootstrapAddition = bootstrapAddition;
|
|
1360
618
|
// P2: 解析当前 permission mode (BootstrapOptions > env BOLLOON_PERM_MODE > default)
|
|
1361
619
|
try {
|
|
@@ -1372,7 +630,7 @@ class PiAgentSession {
|
|
|
1372
630
|
if (this.usePivotLoop) {
|
|
1373
631
|
let pivotResult = '';
|
|
1374
632
|
try {
|
|
1375
|
-
const lr = await this.promptWithPivotLoop(
|
|
633
|
+
const lr = await this.promptWithPivotLoop(userText, undefined, channelId);
|
|
1376
634
|
pivotResult = lr.response || '';
|
|
1377
635
|
onStream({ type: 'done', content: '' });
|
|
1378
636
|
}
|
|
@@ -1396,7 +654,7 @@ class PiAgentSession {
|
|
|
1396
654
|
}
|
|
1397
655
|
catch { }
|
|
1398
656
|
}
|
|
1399
|
-
monitorAfterReply(
|
|
657
|
+
monitorAfterReply(userText, pivotResult);
|
|
1400
658
|
const stopStartTime = this.promptStartTime || Date.now();
|
|
1401
659
|
onStop({
|
|
1402
660
|
channelId: this.currentChannelId || 'unknown',
|
|
@@ -1407,6 +665,7 @@ class PiAgentSession {
|
|
|
1407
665
|
this.currentOnStream = null;
|
|
1408
666
|
this.currentSignal = null;
|
|
1409
667
|
this.bootstrapAddition = '';
|
|
668
|
+
this.contextHintAddition = '';
|
|
1410
669
|
this.promptStartTime = 0;
|
|
1411
670
|
}
|
|
1412
671
|
return pivotResult;
|
|
@@ -1540,37 +799,53 @@ class PiAgentSession {
|
|
|
1540
799
|
}
|
|
1541
800
|
// M2.4: persona 缓存
|
|
1542
801
|
if (!this.cachedPersonaSection && this.persona) {
|
|
1543
|
-
this.cachedPersonaSection = `
|
|
1544
|
-
角色描述: ${this.persona.description || '无'}
|
|
1545
|
-
性格特点: ${this.persona.personality || '无'}
|
|
1546
|
-
问候语: ${this.persona.greeting || '无'}
|
|
802
|
+
this.cachedPersonaSection = `
|
|
803
|
+
角色描述: ${this.persona.description || '无'}
|
|
804
|
+
性格特点: ${this.persona.personality || '无'}
|
|
805
|
+
问候语: ${this.persona.greeting || '无'}
|
|
1547
806
|
`;
|
|
1548
807
|
}
|
|
1549
|
-
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${this.cachedPersonaSection}
|
|
1550
|
-
当前工作目录: ${this.cwd}
|
|
1551
|
-
当前身份: ${this.identity.name} (${this.identity.did})
|
|
1552
|
-
${this.currentIntentHint}
|
|
1553
|
-
|
|
1554
|
-
${this.getToolDefinitions()}
|
|
1555
|
-
|
|
1556
|
-
工作模式:
|
|
1557
|
-
1. 理解用户自然语言请求
|
|
1558
|
-
2. 分析需要哪些工具来完成
|
|
1559
|
-
3. 按顺序调用工具并观察结果
|
|
1560
|
-
4. 根据观察结果决定下一步
|
|
1561
|
-
5. 最终给出完整回答
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
- 每次只调用一个工具
|
|
1565
|
-
- 仔细分析工具返回结果
|
|
1566
|
-
- 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
|
|
1567
|
-
-
|
|
808
|
+
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${this.cachedPersonaSection}
|
|
809
|
+
当前工作目录: ${this.cwd}
|
|
810
|
+
当前身份: ${this.identity.name} (${this.identity.did})
|
|
811
|
+
${this.currentIntentHint}
|
|
812
|
+
|
|
813
|
+
${this.getToolDefinitions()}
|
|
814
|
+
|
|
815
|
+
工作模式:
|
|
816
|
+
1. 理解用户自然语言请求
|
|
817
|
+
2. 分析需要哪些工具来完成
|
|
818
|
+
3. 按顺序调用工具并观察结果
|
|
819
|
+
4. 根据观察结果决定下一步
|
|
820
|
+
5. 最终给出完整回答
|
|
821
|
+
|
|
822
|
+
重要 (一次命中要求):
|
|
823
|
+
- 每次只调用一个工具
|
|
824
|
+
- 仔细分析工具返回结果
|
|
825
|
+
- 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
|
|
826
|
+
- 如果需要更多信息,继续调用工具
|
|
827
|
+
|
|
828
|
+
【工具调用格式 (严格遵守, 否则系统无法解析)】
|
|
829
|
+
- 你只能输出**一个**工具调用, 不要堆叠多个 invoke
|
|
830
|
+
- 工具调用格式: {"name":"<tool_name>","input":{"arg1":"value1"}}
|
|
831
|
+
- 用 markdown json code block 包裹: \`\`\`json\n{"name":"X","input":{...}}\n\`\`\`
|
|
832
|
+
- 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
|
|
833
|
+
- 工具调用后必须等结果, 不要在同一个回复里继续输出
|
|
834
|
+
- <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
1568
835
|
// 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
|
|
1569
836
|
// 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
|
|
1570
837
|
// promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
|
|
1571
838
|
// 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
|
|
1572
839
|
// 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
|
|
1573
|
-
|
|
840
|
+
// 2026-07-06: pivot 内 token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
|
|
841
|
+
// pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 this.messageHistory 同步.
|
|
842
|
+
// 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
|
|
843
|
+
// 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
|
|
844
|
+
// 不再撞预算. 这条路径留作技术债.
|
|
845
|
+
const onCompact = async () => {
|
|
846
|
+
// no-op (best-effort hook for future pi-sdk/pivot history sync)
|
|
847
|
+
};
|
|
848
|
+
const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
|
|
1574
849
|
this.messageHistory.push({ role: 'user', content: input });
|
|
1575
850
|
if (result.response) {
|
|
1576
851
|
this.messageHistory.push({ role: 'assistant', content: result.response });
|
|
@@ -1614,10 +889,12 @@ ${this.getToolDefinitions()}
|
|
|
1614
889
|
while (iteration < this.MAX_REACT_ITERATIONS) {
|
|
1615
890
|
iteration++;
|
|
1616
891
|
// 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
|
|
1617
|
-
|
|
892
|
+
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
|
|
893
|
+
const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
|
|
894
|
+
if (maxIterDecision.shouldExit) {
|
|
1618
895
|
console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
|
|
1619
896
|
onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
|
|
1620
|
-
finalResponse = finalResponse ||
|
|
897
|
+
finalResponse = finalResponse || maxIterDecision.finalAnswer;
|
|
1621
898
|
break;
|
|
1622
899
|
}
|
|
1623
900
|
// 停止条件 2: signal.aborted (显式 abort / 用户中断)
|
|
@@ -1631,14 +908,23 @@ ${this.getToolDefinitions()}
|
|
|
1631
908
|
if (totalErrors >= this.MAX_TOTAL_ERRORS) {
|
|
1632
909
|
console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
|
|
1633
910
|
onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
|
|
1634
|
-
|
|
911
|
+
// 2026-06-19: 即使 LLM 一直失败, 也汇总之前成功执行的 tool result 给用户
|
|
912
|
+
if (this.successfulToolResults.length > 0) {
|
|
913
|
+
finalResponse = `✅ 之前步骤成功执行了 ${this.successfulToolResults.length} 个工具 (但 LLM 后续 ${totalErrors} 次调用失败):\n` +
|
|
914
|
+
this.successfulToolResults.map((r, i) => ` ${i + 1}. ${r.tool}: ${r.outputPreview}`).join('\n') +
|
|
915
|
+
`\n\n⚠️ (LLM 连续失败, 可能是 minimax 上游限流/网络问题, 工具已成功执行但 LLM 没能继续总结)`;
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
|
|
919
|
+
}
|
|
1635
920
|
break;
|
|
1636
921
|
}
|
|
1637
922
|
// 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
|
|
1638
923
|
// compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
|
|
924
|
+
// 2026-07-01 (v0.2.4 子任务 1): 触发判定走 shouldCompactBeforeIteration 纯函数
|
|
1639
925
|
const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
|
|
1640
926
|
const estimatedTokensBefore = this.estimateHistoryTokens();
|
|
1641
|
-
if (estimatedTokensBefore
|
|
927
|
+
if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
|
|
1642
928
|
const tokensBeforeCompact = estimatedTokensBefore;
|
|
1643
929
|
console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
|
|
1644
930
|
onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
@@ -1651,11 +937,13 @@ ${this.getToolDefinitions()}
|
|
|
1651
937
|
}
|
|
1652
938
|
}
|
|
1653
939
|
// 停止条件 3: context overflow (compact 后还超, 强制终止)
|
|
940
|
+
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideContextOverflow 纯函数
|
|
1654
941
|
const estimatedTokens = this.estimateHistoryTokens();
|
|
1655
|
-
|
|
942
|
+
const overflowDecision = decideContextOverflow(estimatedTokens, this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD);
|
|
943
|
+
if (overflowDecision.shouldExit) {
|
|
1656
944
|
console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
|
|
1657
945
|
onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
|
|
1658
|
-
finalResponse = finalResponse ||
|
|
946
|
+
finalResponse = finalResponse || overflowDecision.finalAnswer;
|
|
1659
947
|
break;
|
|
1660
948
|
}
|
|
1661
949
|
// 调试日志:显示每次循环开始
|
|
@@ -1680,33 +968,33 @@ ${this.getToolDefinitions()}
|
|
|
1680
968
|
}
|
|
1681
969
|
// M2.4: persona section 缓存 — persona 在 loadPersona() 时一次设定, 此后不变
|
|
1682
970
|
if (!this.cachedPersonaSection && this.persona) {
|
|
1683
|
-
this.cachedPersonaSection = `
|
|
1684
|
-
角色描述: ${this.persona.description || '无'}
|
|
1685
|
-
性格特点: ${this.persona.personality || '无'}
|
|
1686
|
-
问候语: ${this.persona.greeting || '无'}
|
|
971
|
+
this.cachedPersonaSection = `
|
|
972
|
+
角色描述: ${this.persona.description || '无'}
|
|
973
|
+
性格特点: ${this.persona.personality || '无'}
|
|
974
|
+
问候语: ${this.persona.greeting || '无'}
|
|
1687
975
|
`;
|
|
1688
976
|
}
|
|
1689
977
|
const personaSection = this.cachedPersonaSection;
|
|
1690
|
-
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
|
|
1691
|
-
当前工作目录: ${this.cwd}
|
|
1692
|
-
当前身份: ${this.identity.name} (${this.identity.did})
|
|
1693
|
-
${refineContext}
|
|
1694
|
-
${this.currentIntentHint}
|
|
1695
|
-
|
|
1696
|
-
${toolDefs}
|
|
1697
|
-
|
|
1698
|
-
工作模式:
|
|
1699
|
-
1. 理解用户自然语言请求
|
|
1700
|
-
2. 分析需要哪些工具来完成
|
|
1701
|
-
3. 按顺序调用工具并观察结果
|
|
1702
|
-
4. 根据观察结果决定下一步
|
|
1703
|
-
5. 最终给出完整回答
|
|
1704
|
-
|
|
1705
|
-
重要:
|
|
1706
|
-
- 每次只调用一个工具
|
|
1707
|
-
- 仔细分析工具返回结果
|
|
1708
|
-
- 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
|
|
1709
|
-
- 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
|
|
978
|
+
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
|
|
979
|
+
当前工作目录: ${this.cwd}
|
|
980
|
+
当前身份: ${this.identity.name} (${this.identity.did})
|
|
981
|
+
${refineContext}
|
|
982
|
+
${this.currentIntentHint}
|
|
983
|
+
|
|
984
|
+
${toolDefs}
|
|
985
|
+
|
|
986
|
+
工作模式:
|
|
987
|
+
1. 理解用户自然语言请求
|
|
988
|
+
2. 分析需要哪些工具来完成
|
|
989
|
+
3. 按顺序调用工具并观察结果
|
|
990
|
+
4. 根据观察结果决定下一步
|
|
991
|
+
5. 最终给出完整回答
|
|
992
|
+
|
|
993
|
+
重要:
|
|
994
|
+
- 每次只调用一个工具
|
|
995
|
+
- 仔细分析工具返回结果
|
|
996
|
+
- 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
|
|
997
|
+
- 如果需要更多信息,继续调用工具${this.judgmentGateAddition}${this.contextHintAddition}`;
|
|
1710
998
|
// 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
|
|
1711
999
|
// 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
|
|
1712
1000
|
// 2. reactive compaction (prompt 估算超阈值, 跑压缩)
|
|
@@ -1714,18 +1002,61 @@ ${toolDefs}
|
|
|
1714
1002
|
// 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
|
|
1715
1003
|
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream);
|
|
1716
1004
|
const reply = (response.reply || '').trim();
|
|
1717
|
-
// 2026-06-
|
|
1718
|
-
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
1005
|
+
// 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
|
|
1006
|
+
const nativeToolCalls = response.toolCalls;
|
|
1007
|
+
// 2026-06-19 架构 fix: 不再因 [AI 服务调用失败] break
|
|
1008
|
+
// 旧逻辑: sentinel → aiFailed=true → break → 外层 retry 整个 loop (重置 history)
|
|
1009
|
+
// 新逻辑: 把错误当 tool_result push 进 history → 下一轮 LLM 看到错误能反思重试
|
|
1010
|
+
// 这是 dive-into 文档的"fail-open error recovery" — 错误进入 context, 不让 LLM 重复犯同样错
|
|
1011
|
+
// 2026-07-06: 对不可恢复的 API 错误直接终止, 不再无限重试
|
|
1721
1012
|
if (reply.startsWith('[AI 服务调用失败]')) {
|
|
1722
|
-
console.log(`[PiAgent] 收到 AI 错误 sentinel
|
|
1723
|
-
|
|
1013
|
+
console.log(`[PiAgent] 收到 AI 错误 sentinel`);
|
|
1014
|
+
console.log(`[sentinel DEBUG] 完整 reply: ${reply}`);
|
|
1015
|
+
console.log(`[sentinel DEBUG] 上一轮 messages 数量: ${Array.isArray(messages) ? messages.length : 'N/A'}, systemPrompt 长度: ${systemPrompt.length}`);
|
|
1724
1016
|
aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
|
|
1017
|
+
totalErrors++;
|
|
1018
|
+
consecutiveErrors++;
|
|
1019
|
+
// 2026-07-06: 检测不可恢复的 API 错误 — 这些错误 LLM 无法通过反思修复, 重试无意义
|
|
1020
|
+
const isFatalApiError = reply.includes('chat content is empty') ||
|
|
1021
|
+
reply.includes('invalid params') ||
|
|
1022
|
+
reply.includes('401') ||
|
|
1023
|
+
reply.includes('403') ||
|
|
1024
|
+
reply.includes('quota') ||
|
|
1025
|
+
reply.includes('rate limit') ||
|
|
1026
|
+
reply.includes('API key') ||
|
|
1027
|
+
reply.includes('authentication') ||
|
|
1028
|
+
reply.includes('unauthorized');
|
|
1029
|
+
if (isFatalApiError) {
|
|
1030
|
+
console.log(`[PiAgent] 检测到不可恢复的 API 错误, 终止 loop: ${aiFailureReason}`);
|
|
1031
|
+
if (onStream) {
|
|
1032
|
+
onStream({ type: 'error', content: `⛔ API 错误无法恢复: ${aiFailureReason}`, tool: 'system' });
|
|
1033
|
+
}
|
|
1034
|
+
finalResponse = `❌ AI 服务调用失败: ${aiFailureReason}\n\n这是一个底层 API 错误, 不是任务本身的问题。请检查 API 配置或稍后重试。`;
|
|
1035
|
+
aiFailed = true;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
// 连续错误过多也终止, 防止 LLM 陷入死循环
|
|
1039
|
+
if (consecutiveErrors >= 3) {
|
|
1040
|
+
console.log(`[PiAgent] 连续 ${consecutiveErrors} 次 AI 错误, 终止 loop`);
|
|
1041
|
+
if (onStream) {
|
|
1042
|
+
onStream({ type: 'error', content: `⛔ 连续 ${consecutiveErrors} 次 AI 错误, 终止循环`, tool: 'system' });
|
|
1043
|
+
}
|
|
1044
|
+
finalResponse = `❌ AI 连续调用失败 ${consecutiveErrors} 次, 已终止。\n\n失败原因: ${aiFailureReason}\n\n请检查 API 配置或简化任务后重试。`;
|
|
1045
|
+
aiFailed = true;
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
// 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
|
|
1049
|
+
this.messageHistory.push({
|
|
1050
|
+
role: 'system',
|
|
1051
|
+
content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
|
|
1052
|
+
});
|
|
1725
1053
|
if (onStream) {
|
|
1726
|
-
onStream({ type: 'status', content: `⚠️ AI
|
|
1054
|
+
onStream({ type: 'status', content: `⚠️ AI 调用失败 ${totalErrors}/${this.MAX_TOTAL_ERRORS}, 已 push 错误到 history 让 LLM 反思`, tool: 'system' });
|
|
1727
1055
|
}
|
|
1728
|
-
|
|
1056
|
+
// 退避 2s 后继续 — 临时 minimax 限流避开, 不让 loop 终止
|
|
1057
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1058
|
+
// 关键: 不设 aiFailed=true, 让外层不重试整个 loop (重置 history), 继续内层循环
|
|
1059
|
+
continue;
|
|
1729
1060
|
}
|
|
1730
1061
|
console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
|
|
1731
1062
|
console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
|
|
@@ -1733,20 +1064,14 @@ ${toolDefs}
|
|
|
1733
1064
|
if (onStream) {
|
|
1734
1065
|
onStream({ type: 'token', content: reply.substring(0, 100) });
|
|
1735
1066
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
// 如果质量太低且还有改进机会,进入改进循环
|
|
1740
|
-
if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
|
|
1741
|
-
refineAttempts++;
|
|
1742
|
-
console.log(`[PiAgent] 质量评分 ${(lastQualityScore * 10).toFixed(1)}/10 < ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10,自动改进中 (${refineAttempts}/${this.MAX_REFINE_ATTEMPTS})`);
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
finalResponse = this.extractFinalAnswer(reply);
|
|
1746
|
-
break;
|
|
1747
|
-
}
|
|
1067
|
+
// 2026-06-19 架构 fix: parseToolCall 优先于 isFinalResponse
|
|
1068
|
+
// 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
|
|
1069
|
+
// 现在: 先尝试解析 tool_call, 有就执行; 没有才检查是不是真正的 final gen
|
|
1748
1070
|
const toolCall = this.parseToolCall(reply);
|
|
1071
|
+
// 2026-06-30 修: 给 toolCall 分配稳定 id, 让后续 tool result 能引用同一个 id
|
|
1072
|
+
// OpenAI 协议要求 messages 里 tool result 必须有对应的 tool_call_id, 否则 400
|
|
1749
1073
|
if (toolCall) {
|
|
1074
|
+
toolCall.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1750
1075
|
this.messageHistory.push({
|
|
1751
1076
|
role: 'assistant',
|
|
1752
1077
|
content: reply,
|
|
@@ -1912,7 +1237,7 @@ ${toolDefs}
|
|
|
1912
1237
|
catch (err) {
|
|
1913
1238
|
console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
|
|
1914
1239
|
}
|
|
1915
|
-
this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
|
|
1240
|
+
this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
|
|
1916
1241
|
this.logToHarness(toolCall.name, toolCall.args, result);
|
|
1917
1242
|
// 通知前端工具执行结果
|
|
1918
1243
|
if (onStream) {
|
|
@@ -1944,6 +1269,16 @@ ${toolDefs}
|
|
|
1944
1269
|
}
|
|
1945
1270
|
if (result.success) {
|
|
1946
1271
|
consecutiveErrors = 0; // 重置连续错误计数
|
|
1272
|
+
// 2026-06-19: 记录成功结果, 用于 LLM 失败退出时汇总给用户
|
|
1273
|
+
if (result.output) {
|
|
1274
|
+
this.successfulToolResults.push({
|
|
1275
|
+
tool: toolCall.name,
|
|
1276
|
+
outputPreview: result.output.substring(0, 200) + (result.output.length > 200 ? '...' : '')
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
this.successfulToolResults.push({ tool: toolCall.name, outputPreview: '(无输出)' });
|
|
1281
|
+
}
|
|
1947
1282
|
// 检查工具执行质量
|
|
1948
1283
|
lastQualityScore = this.estimateToolResultQuality(result);
|
|
1949
1284
|
if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
|
|
@@ -2015,6 +1350,36 @@ ${toolDefs}
|
|
|
2015
1350
|
if (onStream) {
|
|
2016
1351
|
onStream({ type: 'token', content: reply.substring(0, 150) });
|
|
2017
1352
|
}
|
|
1353
|
+
// 2026-06-19 架构 fix: 只有 strip <think> 后才检查 isFinalResponse
|
|
1354
|
+
// (parseToolCall 已先尝试, 既然没解析出 tool_call, 现在检查 final gen 是否真的在最终回答区)
|
|
1355
|
+
if (this.isFinalResponse(reply)) {
|
|
1356
|
+
// 2026-06-19 dive-into 风格修复: 如果还有 successful tool results 没汇报,
|
|
1357
|
+
// LLM 不能提前 final_gen — harness 自动注入"请汇报剩余工具结果" hint 再 continue
|
|
1358
|
+
// 这是 dive-into 文档"step 9 stop condition check" 的具体化:
|
|
1359
|
+
// stop condition = (有工具结果未汇报) ? continue : break
|
|
1360
|
+
// 检查回复是否包含工具结果内容(避免无限循环)
|
|
1361
|
+
const hasToolResultContent = this.successfulToolResults.some(r => reply.includes(r.tool) || reply.includes(r.outputPreview.substring(0, 50)));
|
|
1362
|
+
// 如果回复包含工具结果内容,清除 successfulToolResults
|
|
1363
|
+
if (hasToolResultContent) {
|
|
1364
|
+
console.log(`[PiAgent] 回复包含工具结果内容, 清除 successfulToolResults (${this.successfulToolResults.length} 个)`);
|
|
1365
|
+
this.successfulToolResults = [];
|
|
1366
|
+
}
|
|
1367
|
+
if (this.successfulToolResults.length > 0 && iteration < this.MAX_REACT_ITERATIONS) {
|
|
1368
|
+
const unreported = this.successfulToolResults.length;
|
|
1369
|
+
console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报, push hint 让其继续`);
|
|
1370
|
+
this.messageHistory.push({
|
|
1371
|
+
role: 'system',
|
|
1372
|
+
content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
|
|
1373
|
+
});
|
|
1374
|
+
if (onStream) {
|
|
1375
|
+
onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结`, tool: 'system' });
|
|
1376
|
+
}
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
lastQualityScore = this.estimateResponseQuality(reply);
|
|
1380
|
+
finalResponse = this.extractFinalAnswer(reply);
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
2018
1383
|
// 检查是否需要继续循环处理
|
|
2019
1384
|
// 更严格的判断:只有当回复明确表示需要更多信息时才继续
|
|
2020
1385
|
const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
|
|
@@ -2060,12 +1425,12 @@ ${toolDefs}
|
|
|
2060
1425
|
onStream({ type: 'status', content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
|
|
2061
1426
|
}
|
|
2062
1427
|
const now = new Date().toISOString();
|
|
2063
|
-
const identityPrefix = `${this.identity.name} | bolloon 智能体
|
|
2064
|
-
<environment_details>
|
|
2065
|
-
Current time: ${now}
|
|
2066
|
-
Working directory: ${this.cwd}
|
|
2067
|
-
Workspace root folder: ${this.cwd}
|
|
2068
|
-
</environment_details>
|
|
1428
|
+
const identityPrefix = `${this.identity.name} | bolloon 智能体
|
|
1429
|
+
<environment_details>
|
|
1430
|
+
Current time: ${now}
|
|
1431
|
+
Working directory: ${this.cwd}
|
|
1432
|
+
Workspace root folder: ${this.cwd}
|
|
1433
|
+
</environment_details>
|
|
2069
1434
|
`;
|
|
2070
1435
|
finalResponse = identityPrefix + finalResponse;
|
|
2071
1436
|
this.messageHistory.push({ role: 'assistant', content: finalResponse });
|
|
@@ -2157,18 +1522,45 @@ Workspace root folder: ${this.cwd}
|
|
|
2157
1522
|
for (const m of recentMessages) {
|
|
2158
1523
|
const role = m.role;
|
|
2159
1524
|
let content = m.content;
|
|
2160
|
-
// tool role
|
|
1525
|
+
// 2026-06-30 修: OpenAI 协议 tool role 必须带 tool_call_id, 否则 minimax (OpenAI-compatible) 报 400
|
|
1526
|
+
// bolloon 之前把所有 tool result 包成 "[工具结果] ..." 当 user/assistant role 发, minimax 严格校验失败
|
|
1527
|
+
// 现在: 保留 role='tool' + 加 tool_call_id 字段 (用 messageHistory 里自己生成的 id)
|
|
2161
1528
|
if (role === 'tool') {
|
|
2162
1529
|
const result = m.toolResult ? JSON.stringify(m.toolResult) : content;
|
|
2163
1530
|
content = `[工具结果] ${result}`;
|
|
1531
|
+
// MiniMax 等 API 不支持 tool role, 转为 user role
|
|
1532
|
+
out.push({ role: 'user', content });
|
|
1533
|
+
continue;
|
|
2164
1534
|
}
|
|
2165
1535
|
// system role (router hint 等) 直接保留
|
|
2166
1536
|
if (role === 'system') {
|
|
2167
1537
|
out.push({ role: 'system', content });
|
|
2168
1538
|
continue;
|
|
2169
1539
|
}
|
|
2170
|
-
// assistant
|
|
2171
|
-
|
|
1540
|
+
// 2026-06-30 修: assistant 消息如果带 toolCall (bolloon 内部对象), emit OpenAI 协议的 tool_calls 数组
|
|
1541
|
+
// minimax 严格要求 assistant 消息含 tool_calls 字段, 后续 tool result 才能引用 tool_call_id
|
|
1542
|
+
if (role === 'assistant') {
|
|
1543
|
+
const tc = m.toolCall;
|
|
1544
|
+
if (tc && tc.id) {
|
|
1545
|
+
out.push({
|
|
1546
|
+
role: 'assistant',
|
|
1547
|
+
content: content || '',
|
|
1548
|
+
tool_calls: [{
|
|
1549
|
+
id: tc.id,
|
|
1550
|
+
type: 'function',
|
|
1551
|
+
function: {
|
|
1552
|
+
name: tc.name,
|
|
1553
|
+
arguments: JSON.stringify(tc.args || {}),
|
|
1554
|
+
},
|
|
1555
|
+
}],
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
out.push({ role, content });
|
|
1560
|
+
}
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (role === 'user') {
|
|
2172
1564
|
out.push({ role, content });
|
|
2173
1565
|
}
|
|
2174
1566
|
}
|
|
@@ -2255,7 +1647,8 @@ Workspace root folder: ${this.cwd}
|
|
|
2255
1647
|
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2256
1648
|
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2257
1649
|
const response = await llm.chat(contextOrMessages, systemPrompt, signal);
|
|
2258
|
-
|
|
1650
|
+
// 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
1651
|
+
return { reply: response.reply || '', toolCalls: response.toolCalls };
|
|
2259
1652
|
}
|
|
2260
1653
|
catch (err) {
|
|
2261
1654
|
// 用户主动 abort: 不重试, 立即抛
|
|
@@ -2381,100 +1774,47 @@ Workspace root folder: ${this.cwd}
|
|
|
2381
1774
|
}
|
|
2382
1775
|
}
|
|
2383
1776
|
isFinalResponse(content) {
|
|
2384
|
-
//
|
|
2385
|
-
return content.
|
|
1777
|
+
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
1778
|
+
return isFinalResponseImpl(content, this._parseCtx());
|
|
2386
1779
|
}
|
|
2387
1780
|
extractFinalAnswer(content) {
|
|
2388
|
-
//
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
content = after;
|
|
2397
|
-
}
|
|
2398
|
-
else {
|
|
2399
|
-
content = content.substring(0, markerIndex).trim();
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
// 移除任何 tool call 标记
|
|
2403
|
-
let cleaned = content
|
|
2404
|
-
.replace(/调用工具[::]\s*\w+\s*\([^)]*\)/g, '')
|
|
2405
|
-
.replace(/使用工具[::]\s*\w+\s*\([^)]*\)/g, '')
|
|
2406
|
-
.replace(/tool[_\w]*[::]\s*\w+\s*\([^)]*\)/gi, '')
|
|
2407
|
-
.trim();
|
|
2408
|
-
return cleaned;
|
|
1781
|
+
// 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
|
|
1782
|
+
return extractFinalAnswerImpl(content);
|
|
1783
|
+
}
|
|
1784
|
+
_parseCtx() {
|
|
1785
|
+
return {
|
|
1786
|
+
tools: new Set(Array.from(this.tools.keys())),
|
|
1787
|
+
resolveAlias: (name) => this.resolveToolName(name),
|
|
1788
|
+
};
|
|
2409
1789
|
}
|
|
2410
1790
|
parseToolCall(content) {
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
catch {
|
|
2441
|
-
// 解析失败就当字符串处理
|
|
2442
|
-
const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
|
|
2443
|
-
for (const pair of argPairs) {
|
|
2444
|
-
const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
|
|
2445
|
-
if (key)
|
|
2446
|
-
args[key] = valueParts.join(':') || '';
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
|
|
2451
|
-
// 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
|
|
2452
|
-
// 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
|
|
2453
|
-
const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
|
2454
|
-
let xmlMatch;
|
|
2455
|
-
while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
|
|
2456
|
-
const argName = xmlMatch[1];
|
|
2457
|
-
const argValue = xmlMatch[2].trim();
|
|
2458
|
-
if (argName && argValue) {
|
|
2459
|
-
args[argName] = argValue;
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
else if (rawArgs) {
|
|
2464
|
-
// 形参串, 形如 key: value, key2: value2
|
|
2465
|
-
const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
|
|
2466
|
-
for (const pair of argPairs) {
|
|
2467
|
-
const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
|
|
2468
|
-
if (key)
|
|
2469
|
-
args[key] = valueParts.join(':') || '';
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
if (this.tools.has(name) || this.tools.has(name.replace(/_/g, '_'))) {
|
|
2473
|
-
return { name, args };
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
return null;
|
|
1791
|
+
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
1792
|
+
return parseToolCallImpl(content, this._parseCtx());
|
|
1793
|
+
}
|
|
1794
|
+
// [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
|
|
1795
|
+
_dbgParseToolCall(content) {
|
|
1796
|
+
const r = this.parseToolCall(content);
|
|
1797
|
+
console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
|
|
1798
|
+
return r;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
|
|
1805
|
+
* LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
|
|
1806
|
+
* bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
|
|
1807
|
+
* 返回 this.tools 里的标准名, 或 null 表示未识别
|
|
1808
|
+
*/
|
|
1809
|
+
/**
|
|
1810
|
+
* 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
|
|
1811
|
+
* bolloon 注册的标准工具名.
|
|
1812
|
+
*
|
|
1813
|
+
* 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
|
|
1814
|
+
* 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
|
|
1815
|
+
*/
|
|
1816
|
+
resolveToolName(name) {
|
|
1817
|
+
return this._toolRegistry.resolve(name);
|
|
2478
1818
|
}
|
|
2479
1819
|
estimateResponseQuality(response) {
|
|
2480
1820
|
let score = 0.5;
|
|
@@ -2565,37 +1905,37 @@ Workspace root folder: ${this.cwd}
|
|
|
2565
1905
|
getDefaultResponse(input) {
|
|
2566
1906
|
const operationsRef = PiAgentSession.getOperationsReference();
|
|
2567
1907
|
if (operationsRef) {
|
|
2568
|
-
return `收到了: "${input}"
|
|
2569
|
-
|
|
2570
|
-
我是一个判断力处理智能体,支持自然语言交互。
|
|
2571
|
-
|
|
2572
|
-
可用操作(直接说出即可):
|
|
2573
|
-
${this.extractOperationsFromRef(operationsRef)}
|
|
2574
|
-
|
|
2575
|
-
示例请求:
|
|
2576
|
-
- "读取 src/index.ts 文件"
|
|
2577
|
-
- "总结一下 README.md"
|
|
2578
|
-
- "查看当前连接了哪些节点"
|
|
1908
|
+
return `收到了: "${input}"
|
|
1909
|
+
|
|
1910
|
+
我是一个判断力处理智能体,支持自然语言交互。
|
|
1911
|
+
|
|
1912
|
+
可用操作(直接说出即可):
|
|
1913
|
+
${this.extractOperationsFromRef(operationsRef)}
|
|
1914
|
+
|
|
1915
|
+
示例请求:
|
|
1916
|
+
- "读取 src/index.ts 文件"
|
|
1917
|
+
- "总结一下 README.md"
|
|
1918
|
+
- "查看当前连接了哪些节点"
|
|
2579
1919
|
- "向 QmABC... 发送测试消息"`;
|
|
2580
1920
|
}
|
|
2581
|
-
return `收到了: "${input}"
|
|
2582
|
-
|
|
2583
|
-
我是一个判断力处理智能体,支持自然语言交互。
|
|
2584
|
-
|
|
2585
|
-
可用操作(直接说出即可):
|
|
2586
|
-
- "读取 README.md" - 读取并分析文档
|
|
2587
|
-
- "总结文档" - 总结文档内容
|
|
2588
|
-
- "改进文档,按照X要求" - 改进文档
|
|
2589
|
-
- "查看节点" - 查看已连接的对等节点
|
|
2590
|
-
- "向X发送消息Y" - 向对等节点发送消息
|
|
2591
|
-
- "广播消息X" - 广播消息到所有节点
|
|
2592
|
-
- "查看身份" - 查看当前智能体身份
|
|
2593
|
-
- "查看日志" - 查看最近操作日志
|
|
2594
|
-
|
|
2595
|
-
示例请求:
|
|
2596
|
-
- "读取 src/index.ts 文件"
|
|
2597
|
-
- "总结一下 README.md"
|
|
2598
|
-
- "查看当前连接了哪些节点"
|
|
1921
|
+
return `收到了: "${input}"
|
|
1922
|
+
|
|
1923
|
+
我是一个判断力处理智能体,支持自然语言交互。
|
|
1924
|
+
|
|
1925
|
+
可用操作(直接说出即可):
|
|
1926
|
+
- "读取 README.md" - 读取并分析文档
|
|
1927
|
+
- "总结文档" - 总结文档内容
|
|
1928
|
+
- "改进文档,按照X要求" - 改进文档
|
|
1929
|
+
- "查看节点" - 查看已连接的对等节点
|
|
1930
|
+
- "向X发送消息Y" - 向对等节点发送消息
|
|
1931
|
+
- "广播消息X" - 广播消息到所有节点
|
|
1932
|
+
- "查看身份" - 查看当前智能体身份
|
|
1933
|
+
- "查看日志" - 查看最近操作日志
|
|
1934
|
+
|
|
1935
|
+
示例请求:
|
|
1936
|
+
- "读取 src/index.ts 文件"
|
|
1937
|
+
- "总结一下 README.md"
|
|
1938
|
+
- "查看当前连接了哪些节点"
|
|
2599
1939
|
- "向 QmABC... 发送测试消息"`;
|
|
2600
1940
|
}
|
|
2601
1941
|
extractOperationsFromRef(ref) {
|
|
@@ -2620,13 +1960,13 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
2620
1960
|
}
|
|
2621
1961
|
}
|
|
2622
1962
|
return operationLines.length > 0 ? operationLines.join('\n') :
|
|
2623
|
-
` - "读取 README.md" - 读取并分析文档
|
|
2624
|
-
- "总结文档" - 总结文档内容
|
|
2625
|
-
- "改进文档,按照X要求" - 改进文档
|
|
2626
|
-
- "查看节点" - 查看已连接的对等节点
|
|
2627
|
-
- "向X发送消息Y" - 向对等节点发送消息
|
|
2628
|
-
- "广播消息X" - 广播消息到所有节点
|
|
2629
|
-
- "查看身份" - 查看当前智能体身份
|
|
1963
|
+
` - "读取 README.md" - 读取并分析文档
|
|
1964
|
+
- "总结文档" - 总结文档内容
|
|
1965
|
+
- "改进文档,按照X要求" - 改进文档
|
|
1966
|
+
- "查看节点" - 查看已连接的对等节点
|
|
1967
|
+
- "向X发送消息Y" - 向对等节点发送消息
|
|
1968
|
+
- "广播消息X" - 广播消息到所有节点
|
|
1969
|
+
- "查看身份" - 查看当前智能体身份
|
|
2630
1970
|
- "查看日志" - 查看最近操作日志`;
|
|
2631
1971
|
}
|
|
2632
1972
|
async suggestRename(messages) {
|
|
@@ -2955,98 +2295,5 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
2955
2295
|
return [...this.operationLog];
|
|
2956
2296
|
}
|
|
2957
2297
|
}
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
// 独立的 session 实例缓存(用于多 session 支持)
|
|
2961
|
-
const independentSessions = new Map();
|
|
2962
|
-
export async function createAgentSession(config, forceNew) {
|
|
2963
|
-
const incomingDid = config.identityDoc?.did;
|
|
2964
|
-
// 如果有独立的 peerId (包含 :),使用它作为 key
|
|
2965
|
-
if (config.peerId && config.peerId.includes(':')) {
|
|
2966
|
-
const key = config.peerId;
|
|
2967
|
-
if (!forceNew && independentSessions.has(key)) {
|
|
2968
|
-
console.log(`[createAgentSession] 找到现有独立 session, key=${key}`);
|
|
2969
|
-
return independentSessions.get(key);
|
|
2970
|
-
}
|
|
2971
|
-
const session = new PiAgentSession(config);
|
|
2972
|
-
independentSessions.set(key, session);
|
|
2973
|
-
console.log(`[createAgentSession] 创建独立 session, key=${key}, DID=${incomingDid}`);
|
|
2974
|
-
return session;
|
|
2975
|
-
}
|
|
2976
|
-
// 如果指定了 forceNew 但没有 peerId,生成带时间戳的 key
|
|
2977
|
-
if (forceNew) {
|
|
2978
|
-
const key = `force:${Date.now()}`;
|
|
2979
|
-
const session = new PiAgentSession(config);
|
|
2980
|
-
independentSessions.set(key, session);
|
|
2981
|
-
console.log(`[createAgentSession] 创建强制新 session, key=${key}`);
|
|
2982
|
-
return session;
|
|
2983
|
-
}
|
|
2984
|
-
// 如果有新的 DID,强制重建 session
|
|
2985
|
-
if (sessionInstance && lastIdentityDid && incomingDid && lastIdentityDid !== incomingDid) {
|
|
2986
|
-
console.log(`[createAgentSession] DID 变化 ${lastIdentityDid} -> ${incomingDid},重建 session`);
|
|
2987
|
-
sessionInstance = null;
|
|
2988
|
-
}
|
|
2989
|
-
if (sessionInstance) {
|
|
2990
|
-
// 检查是否需要更新 identity
|
|
2991
|
-
const currentDid = sessionInstance.getIdentity().did;
|
|
2992
|
-
if (incomingDid && currentDid !== incomingDid) {
|
|
2993
|
-
console.log(`[createAgentSession] 更新 identity: ${currentDid} -> ${incomingDid}`);
|
|
2994
|
-
sessionInstance.updateIdentity({
|
|
2995
|
-
did: incomingDid,
|
|
2996
|
-
name: config.identityDoc?.name || sessionInstance.getIdentity().name,
|
|
2997
|
-
publicKey: config.identityDoc?.publicKey || '',
|
|
2998
|
-
createdAt: Date.now()
|
|
2999
|
-
});
|
|
3000
|
-
}
|
|
3001
|
-
return sessionInstance;
|
|
3002
|
-
}
|
|
3003
|
-
sessionInstance = new PiAgentSession(config);
|
|
3004
|
-
lastIdentityDid = config.identityDoc?.did || null;
|
|
3005
|
-
console.log(`[createAgentSession] 新建 session, DID=${lastIdentityDid}`);
|
|
3006
|
-
return sessionInstance;
|
|
3007
|
-
}
|
|
3008
|
-
export function getAgentSession() {
|
|
3009
|
-
return sessionInstance;
|
|
3010
|
-
}
|
|
3011
|
-
export function resetAgentSession() {
|
|
3012
|
-
sessionInstance = null;
|
|
3013
|
-
lastIdentityDid = null;
|
|
3014
|
-
}
|
|
3015
|
-
/**
|
|
3016
|
-
* 自我改进循环: 在沙箱分支上工作, 输出结果给用户审.
|
|
3017
|
-
*
|
|
3018
|
-
* 不在 PiAgent 实例上的原因: 心跳回调可能没有 agent 实例, 单独函数更易复用.
|
|
3019
|
-
*
|
|
3020
|
-
* **关键不变量**:
|
|
3021
|
-
* 1. AI 不能 push 到 master (shell-guard 黑名单 + git 受保护分支)
|
|
3022
|
-
* 2. 改动必须走沙箱分支 (SELF_IMPROVE_BRANCH_PREFIX)
|
|
3023
|
-
* 3. 6 小时冷却期 (SELF_IMPROVE_COOLDOWN_MS)
|
|
3024
|
-
* 4. 写文件必须经过 shell_exec + 护栏检查
|
|
3025
|
-
*/
|
|
3026
|
-
let lastSelfImproveAt = null;
|
|
3027
|
-
export async function runSelfImproveLoop(goal) {
|
|
3028
|
-
const cooldownMs = getCooldownMs();
|
|
3029
|
-
// 1. 冷却期检查
|
|
3030
|
-
if (lastSelfImproveAt && Date.now() - lastSelfImproveAt < cooldownMs) {
|
|
3031
|
-
const waitHrs = Math.ceil((cooldownMs - (Date.now() - lastSelfImproveAt)) / 3600000);
|
|
3032
|
-
return { success: false, error: `自改冷却中, 还需要约 ${waitHrs} 小时` };
|
|
3033
|
-
}
|
|
3034
|
-
// 2. 选源分支 + 新分支名
|
|
3035
|
-
const sourceBranch = 'master';
|
|
3036
|
-
const newBranch = `${getBranchPrefix()}${Date.now()}`;
|
|
3037
|
-
console.log(`[self-improve] 启动自改循环, 目标: ${goal}, 新分支: ${newBranch}`);
|
|
3038
|
-
// 3. 创建分支
|
|
3039
|
-
const r1 = await shellExec('git', ['checkout', sourceBranch]);
|
|
3040
|
-
if (!r1.success)
|
|
3041
|
-
return { success: false, error: `切换到 ${sourceBranch} 失败: ${r1.error}` };
|
|
3042
|
-
const r2 = await shellExec('git', ['checkout', '-b', newBranch]);
|
|
3043
|
-
if (!r2.success)
|
|
3044
|
-
return { success: false, error: `创建分支失败: ${r2.error}` };
|
|
3045
|
-
// 4. 走 task queue: 把"自改"作为一个 task 抛回去, AI 拿到后会用 shell_exec 改
|
|
3046
|
-
// 护栏已经阻止所有禁区改动, 这里只负责登记
|
|
3047
|
-
lastSelfImproveAt = Date.now();
|
|
3048
|
-
return {
|
|
3049
|
-
success: true,
|
|
3050
|
-
output: `✅ 自改分支已创建: ${newBranch}\n目标: ${goal}\n\n**护栏已激活**:\n - 仅允许白名单命令 (git/npm/tsc/vitest/cat/ls/...)\n - 禁止改 src/agents/pi-sdk.ts, shell-guard.ts, src/heartbeat/, src/network/, src/pi-ecosystem-judgment/, package.json, .env 等\n - 6 小时冷却期\n\nAI 接下来会用 shell_exec 工具改源码. 完成后你会在对话里看到 diff 摘要, 手动 git diff master..${newBranch} 审, 满意再 merge.`
|
|
3051
|
-
};
|
|
3052
|
-
}
|
|
2298
|
+
// createAgentSession / getAgentSession / resetAgentSession / runSelfImproveLoop
|
|
2299
|
+
// 已抽到 ./pi-sdk-session-factory.ts (2026-07-06), 从顶部 import 并 re-export
|