@bolloon/bolloon-agent 0.1.34 → 0.1.36
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/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +144 -0
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +687 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +117 -0
- package/dist/bootstrap/context-collector.js +312 -0
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +171 -0
- package/dist/bootstrap/project-context.js +158 -0
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +49 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +125 -28
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +208 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/input-scanner.js +223 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +3810 -3830
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +60 -49
- package/dist/web/server.js +827 -124
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +33 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +481 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/validate-system-prompt.ts +142 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +741 -30
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +135 -0
- package/src/bootstrap/context-collector.ts +371 -0
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +289 -0
- package/src/bootstrap/project-context.ts +171 -0
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +47 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +135 -29
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/input-scanner.ts +287 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +105 -2187
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +60 -49
- package/src/web/server.ts +894 -148
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -0
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
- package/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
package/src/agents/pi-sdk.ts
CHANGED
|
@@ -41,6 +41,18 @@ import {
|
|
|
41
41
|
import { Session, SkillRegistry, saveSession, loadSession, type Skill, type StoredSession } from '@bolloon/constraint-runtime';
|
|
42
42
|
import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
|
|
43
43
|
|
|
44
|
+
// Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
|
|
45
|
+
// 失败静默, 不阻塞主对话
|
|
46
|
+
import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
|
|
47
|
+
// 持续监控门 (P3): AI 回复后审计是否违反原则
|
|
48
|
+
import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
|
|
49
|
+
// Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
|
|
50
|
+
import { onSessionStart, onStop, onPreToolUse } from '../pi-ecosystem-judgment/human-value-pipeline.js';
|
|
51
|
+
import { onPostToolUse, onJudgmentInjected, onMonitorViolation } from '../bootstrap/lifecycle-hooks.js';
|
|
52
|
+
import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
|
|
53
|
+
// React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
|
|
54
|
+
import { ReactHarness } from '../security/react-harness.js';
|
|
55
|
+
|
|
44
56
|
// Pi Ecosystem Integration (lazy imports - initialized on demand)
|
|
45
57
|
// Functions from: createGoal, getCurrentGoal, completeCurrentGoal, failCurrentGoal, getGoalStats, getQueueSummary
|
|
46
58
|
|
|
@@ -470,10 +482,18 @@ export interface StreamCallback {
|
|
|
470
482
|
}
|
|
471
483
|
|
|
472
484
|
export interface StreamEvent {
|
|
473
|
-
type: 'status' | 'thinking' | 'tool' | 'token' | 'done' | 'error'
|
|
485
|
+
type: 'status' | 'thinking' | 'tool' | 'token' | 'done' | 'error'
|
|
486
|
+
| 'step_start' | 'step_done' | 'step_error';
|
|
474
487
|
content: string;
|
|
475
488
|
tool?: string;
|
|
476
489
|
data?: unknown;
|
|
490
|
+
// step_* 专用: success / output / error
|
|
491
|
+
success?: boolean;
|
|
492
|
+
output?: string;
|
|
493
|
+
error?: string;
|
|
494
|
+
args?: Record<string, unknown>;
|
|
495
|
+
// step_* 可选: 步骤耗时 (server 端用来展示 in 状态条 + 性能分析)
|
|
496
|
+
durationMs?: number;
|
|
477
497
|
}
|
|
478
498
|
|
|
479
499
|
const TOOL_DEFINITIONS = `
|
|
@@ -499,9 +519,9 @@ export interface HeartbeatConfig {
|
|
|
499
519
|
}
|
|
500
520
|
|
|
501
521
|
export interface AgentSession {
|
|
502
|
-
prompt(input: string): Promise<string>;
|
|
503
|
-
promptStream(input: string, onStream: StreamCallback): Promise<string>;
|
|
504
|
-
promptWithPivotLoop(input: string, config?: PivotLoopConfig): Promise<LoopResult>;
|
|
522
|
+
prompt(input: string, options?: { onStream?: StreamCallback; signal?: AbortSignal; channelId?: string }): Promise<string>;
|
|
523
|
+
promptStream(input: string, onStream: StreamCallback, signal?: AbortSignal, channelId?: string): Promise<string>;
|
|
524
|
+
promptWithPivotLoop(input: string, config?: PivotLoopConfig, channelId?: string): Promise<LoopResult>;
|
|
505
525
|
suggestRename(messages: { type: string; content: string }[]): Promise<string | null>;
|
|
506
526
|
readDocument(filePath: string): Promise<string>;
|
|
507
527
|
summarizeDocument(filePath: string, context?: string): Promise<{
|
|
@@ -520,6 +540,7 @@ export interface AgentSession {
|
|
|
520
540
|
broadcast(message: string): Promise<void>;
|
|
521
541
|
getIdentity(): IdentityDoc;
|
|
522
542
|
updateIdentity(updates: Partial<IdentityDoc>): void;
|
|
543
|
+
setCurrentChannelId(channelId: string): void;
|
|
523
544
|
getSessionState(): PiSessionState;
|
|
524
545
|
getMemory(): PiMemory;
|
|
525
546
|
getPersona(): PersonaDoc | null;
|
|
@@ -561,15 +582,89 @@ class PiAgentSession implements AgentSession {
|
|
|
561
582
|
private messageHistory: Message[] = [];
|
|
562
583
|
private tools: Map<string, Tool> = new Map();
|
|
563
584
|
private skillRegistry: SkillRegistry = new SkillRegistry();
|
|
564
|
-
|
|
585
|
+
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
586
|
+
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
587
|
+
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
588
|
+
private readonly MAX_REACT_ITERATIONS = 10_000;
|
|
565
589
|
private readonly MAX_REFINE_ATTEMPTS = 3;
|
|
566
590
|
private readonly QUALITY_THRESHOLD = 0.6;
|
|
591
|
+
/** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long) */
|
|
592
|
+
private readonly MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // 60K tokens 上限
|
|
593
|
+
/** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
|
|
594
|
+
* 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
|
|
595
|
+
private readonly MAX_TOTAL_ERRORS = 20;
|
|
596
|
+
/** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
|
|
597
|
+
private readonly LOOP_COMPACT_RATIO = 0.8;
|
|
598
|
+
/** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
|
|
599
|
+
private readonly MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
|
|
567
600
|
private thinkingEngine = new DeepThinkingEngine(3);
|
|
568
601
|
private coordinator = new AgentCoordinator(3);
|
|
569
602
|
private harness: any = null;
|
|
570
603
|
private harnessEnabled = false;
|
|
604
|
+
/** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
|
|
605
|
+
private reactHarness: ReactHarness = new ReactHarness();
|
|
571
606
|
private usePivotLoop: boolean = false;
|
|
572
607
|
private pivotLoopConfig?: PivotLoopConfig;
|
|
608
|
+
/** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
|
|
609
|
+
private currentPermissionMode: import('./permission-mode.js').PermissionMode = 'default';
|
|
610
|
+
/** P1.2: Context Collapse 读时投影结果 (feature flag 开启时由 maybeAutoCompact 写入, buildContext 优先用) */
|
|
611
|
+
private projectedHistory: Message[] | null = null;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
|
|
615
|
+
* 每次调用都会重置 (避免上一轮遗留)
|
|
616
|
+
*/
|
|
617
|
+
private judgmentGateAddition: string = '';
|
|
618
|
+
private judgmentGateUsedIds: string[] = [];
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
|
|
622
|
+
* 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
|
|
623
|
+
*/
|
|
624
|
+
private currentOnStream: StreamCallback | null = null;
|
|
625
|
+
private currentSignal: AbortSignal | null = null;
|
|
626
|
+
/** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
|
|
627
|
+
private bootstrapAddition: string = '';
|
|
628
|
+
/** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
|
|
629
|
+
private promptStartTime: number = 0;
|
|
630
|
+
/** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
|
|
631
|
+
private currentChannelId: string = '';
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 算 judgment 注入门: 失败静默, 不阻塞主对话
|
|
635
|
+
* 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
|
|
636
|
+
* 调用方负责用完即清 (judgmentGateAddition='')
|
|
637
|
+
*/
|
|
638
|
+
private async computeJudgmentGate(input: string): Promise<void> {
|
|
639
|
+
const safePhase = (phase: string, extra: Record<string, unknown> = {}) => {
|
|
640
|
+
try {
|
|
641
|
+
if (this.currentOnStream) {
|
|
642
|
+
this.currentOnStream({ type: 'phase', phase, ...extra, content: '' } as any);
|
|
643
|
+
}
|
|
644
|
+
} catch { /* 静默 */ }
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
safePhase('gate_compute', { detail: '正在检索相关判断力...' });
|
|
648
|
+
try {
|
|
649
|
+
// P-Action 4 (2026-06-15) 路径 1 整合: 透传 maxChars=1500 (≈ 375 tokens 硬上限)
|
|
650
|
+
// 路径 2/3 检测由 injection-gate 内部 alreadyInjectedSources 处理 (目前 assembleSystemPrompt 还没注入 value-store 标记, 所以这里不传)
|
|
651
|
+
const gate = await injectJudgmentGate(input, {}, { maxChars: 1500 });
|
|
652
|
+
this.judgmentGateAddition = gate.systemAddition;
|
|
653
|
+
this.judgmentGateUsedIds = gate.usedIds;
|
|
654
|
+
if (gate.usedIds.length > 0) {
|
|
655
|
+
safePhase('gate_done', { usedCount: gate.usedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
|
|
656
|
+
}
|
|
657
|
+
} catch (err) {
|
|
658
|
+
console.warn('[PiAgent] judgment gate failed (non-fatal):', err);
|
|
659
|
+
this.judgmentGateAddition = '';
|
|
660
|
+
this.judgmentGateUsedIds = [];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private clearJudgmentGate(): void {
|
|
665
|
+
this.judgmentGateAddition = '';
|
|
666
|
+
this.judgmentGateUsedIds = [];
|
|
667
|
+
}
|
|
573
668
|
|
|
574
669
|
constructor(config: AgentSessionConfig) {
|
|
575
670
|
this.cwd = config.cwd;
|
|
@@ -656,9 +751,13 @@ class PiAgentSession implements AgentSession {
|
|
|
656
751
|
const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
|
|
657
752
|
this.harness = createBollharnessIntegration();
|
|
658
753
|
this.harnessEnabled = true;
|
|
754
|
+
// ReactHarness 已用 bollharness, 这里也记一份以供 archive 调用
|
|
755
|
+
this.reactHarness = new ReactHarness({ harnessEnabled: true, gateEnabled: true });
|
|
659
756
|
} catch (e) {
|
|
660
757
|
console.warn('[PiAgentSession] Harness initialization failed:', e);
|
|
661
758
|
this.harnessEnabled = false;
|
|
759
|
+
// 失败 fallback: 走纯 8-gate (不带 bollharness 的 8-gate 工作流)
|
|
760
|
+
this.reactHarness = new ReactHarness({ harnessEnabled: false, gateEnabled: true });
|
|
662
761
|
}
|
|
663
762
|
}
|
|
664
763
|
|
|
@@ -666,7 +765,7 @@ class PiAgentSession implements AgentSession {
|
|
|
666
765
|
this.tools.set('read_document', {
|
|
667
766
|
name: 'read_document',
|
|
668
767
|
description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
|
|
669
|
-
parameters: { path: '
|
|
768
|
+
parameters: { path: 'string' },
|
|
670
769
|
execute: async (args) => {
|
|
671
770
|
try {
|
|
672
771
|
const content = await documentReader.read(args.path);
|
|
@@ -683,7 +782,7 @@ class PiAgentSession implements AgentSession {
|
|
|
683
782
|
this.tools.set('summarize_document', {
|
|
684
783
|
name: 'summarize_document',
|
|
685
784
|
description: '总结文档内容,分析并生成摘要',
|
|
686
|
-
parameters: { path: '
|
|
785
|
+
parameters: { path: 'string', context: 'string' },
|
|
687
786
|
execute: async (args) => {
|
|
688
787
|
try {
|
|
689
788
|
if (!this.minimaxAvailable) {
|
|
@@ -703,7 +802,7 @@ class PiAgentSession implements AgentSession {
|
|
|
703
802
|
this.tools.set('improve_document', {
|
|
704
803
|
name: 'improve_document',
|
|
705
804
|
description: '根据要求改进文档内容',
|
|
706
|
-
parameters: { path: '
|
|
805
|
+
parameters: { path: 'string', requirements: 'string' },
|
|
707
806
|
execute: async (args) => {
|
|
708
807
|
try {
|
|
709
808
|
if (!this.minimaxAvailable) {
|
|
@@ -1004,8 +1103,9 @@ class PiAgentSession implements AgentSession {
|
|
|
1004
1103
|
}
|
|
1005
1104
|
}
|
|
1006
1105
|
|
|
1007
|
-
async prompt(input: string): Promise<string> {
|
|
1106
|
+
async prompt(input: string, options?: { onStream?: StreamCallback; signal?: AbortSignal; channelId?: string }): Promise<string> {
|
|
1008
1107
|
this.minimaxAvailable = this.checkMinimax();
|
|
1108
|
+
this.currentChannelId = options?.channelId ?? this.currentChannelId;
|
|
1009
1109
|
|
|
1010
1110
|
this.messageHistory.push({
|
|
1011
1111
|
role: 'user',
|
|
@@ -1018,11 +1118,39 @@ class PiAgentSession implements AgentSession {
|
|
|
1018
1118
|
return response;
|
|
1019
1119
|
}
|
|
1020
1120
|
|
|
1021
|
-
|
|
1121
|
+
// P0 注入门
|
|
1122
|
+
this.currentSignal = options?.signal ?? null;
|
|
1123
|
+
this.currentOnStream = options?.onStream ?? null;
|
|
1124
|
+
await this.computeJudgmentGate(input);
|
|
1125
|
+
|
|
1126
|
+
// P2: 解析当前 permission mode
|
|
1127
|
+
try {
|
|
1128
|
+
const { resolvePermissionMode } = await import('./permission-mode.js');
|
|
1129
|
+
this.currentPermissionMode = resolvePermissionMode();
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
console.warn('[PiAgent] resolvePermissionMode failed (non-fatal):', err);
|
|
1132
|
+
this.currentPermissionMode = 'default';
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
// 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
|
|
1137
|
+
const loopResult = await this.runReActLoop(undefined, options?.signal);
|
|
1138
|
+
return loopResult.reply;
|
|
1139
|
+
} finally {
|
|
1140
|
+
if (this.judgmentGateUsedIds.length > 0) {
|
|
1141
|
+
recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
|
|
1142
|
+
console.warn('[PiAgent] recordJudgmentUsage failed:', err)
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
this.clearJudgmentGate();
|
|
1146
|
+
this.currentSignal = null;
|
|
1147
|
+
this.currentOnStream = null;
|
|
1148
|
+
}
|
|
1022
1149
|
}
|
|
1023
1150
|
|
|
1024
|
-
async promptStream(input: string, onStream: StreamCallback): Promise<string> {
|
|
1151
|
+
async promptStream(input: string, onStream: StreamCallback, signal?: AbortSignal, channelId?: string): Promise<string> {
|
|
1025
1152
|
this.minimaxAvailable = this.checkMinimax();
|
|
1153
|
+
this.currentChannelId = channelId ?? this.currentChannelId;
|
|
1026
1154
|
|
|
1027
1155
|
this.messageHistory.push({
|
|
1028
1156
|
role: 'user',
|
|
@@ -1038,12 +1166,134 @@ class PiAgentSession implements AgentSession {
|
|
|
1038
1166
|
return response;
|
|
1039
1167
|
}
|
|
1040
1168
|
|
|
1041
|
-
|
|
1169
|
+
// P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
|
|
1170
|
+
this.currentOnStream = onStream;
|
|
1171
|
+
this.currentSignal = signal ?? null;
|
|
1172
|
+
await this.computeJudgmentGate(input);
|
|
1173
|
+
|
|
1174
|
+
// P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
|
|
1175
|
+
// 复用 computeJudgmentGate 的 onStream 广播 phase, 跟 judgment 注入门风格一致
|
|
1176
|
+
try {
|
|
1177
|
+
await this.maybeAutoCompact(onStream, signal);
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
console.warn('[PiAgent] maybeAutoCompact failed (non-fatal):', err);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
|
|
1183
|
+
// (失败静默, 5s 限流防止循环)
|
|
1184
|
+
let bootstrapAddition = '';
|
|
1185
|
+
try {
|
|
1186
|
+
const ss = await onSessionStart({ channelId: this.currentChannelId || undefined });
|
|
1187
|
+
bootstrapAddition = ss.systemAddition || '';
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
|
|
1190
|
+
}
|
|
1191
|
+
this.bootstrapAddition = bootstrapAddition;
|
|
1192
|
+
|
|
1193
|
+
// P2: 解析当前 permission mode (BootstrapOptions > env BOLLOON_PERM_MODE > default)
|
|
1194
|
+
try {
|
|
1195
|
+
const { resolvePermissionMode } = await import('./permission-mode.js');
|
|
1196
|
+
this.currentPermissionMode = resolvePermissionMode();
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
console.warn('[PiAgent] resolvePermissionMode failed (non-fatal, using default):', err);
|
|
1199
|
+
this.currentPermissionMode = 'default';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
this.promptStartTime = Date.now();
|
|
1203
|
+
|
|
1204
|
+
// 2026-06-16: loop 自动重试 — runReActLoop 内部遇到 [AI 服务调用失败] sentinel 时,
|
|
1205
|
+
// 会设 aiFailed=true 并提前 break. 这里在外层重跑整个 loop (不是单次 LLM 调用),
|
|
1206
|
+
// 临时网络抖动 / 配额瞬时超限可自愈. 最多 3 次, 指数退避 1s/2s/4s.
|
|
1207
|
+
// 用户看到 status bar 显示 "自动重试中 X/N" — 不暴露按钮.
|
|
1208
|
+
const MAX_LOOP_RETRIES = 3;
|
|
1209
|
+
let attempt = 0;
|
|
1210
|
+
let result: string = '';
|
|
1211
|
+
let lastAiFailureReason = '';
|
|
1212
|
+
while (attempt <= MAX_LOOP_RETRIES) {
|
|
1213
|
+
try {
|
|
1214
|
+
const loopResult = await this.runReActLoop(onStream, signal);
|
|
1215
|
+
result = loopResult.reply;
|
|
1216
|
+
if (!loopResult.aiFailed) break; // 正常完成, 退出 retry 循环
|
|
1217
|
+
lastAiFailureReason = loopResult.aiFailureReason || 'AI 调用失败';
|
|
1218
|
+
} catch (err: any) {
|
|
1219
|
+
// abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
|
|
1220
|
+
this.currentOnStream = null;
|
|
1221
|
+
this.currentSignal = null;
|
|
1222
|
+
throw err;
|
|
1223
|
+
}
|
|
1224
|
+
attempt++;
|
|
1225
|
+
if (attempt > MAX_LOOP_RETRIES) {
|
|
1226
|
+
console.warn(`[PiAgent] loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败, 终止`);
|
|
1227
|
+
if (onStream) {
|
|
1228
|
+
onStream({ type: 'status', content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
|
|
1229
|
+
}
|
|
1230
|
+
result = lastAiFailureReason || 'AI 服务调用失败, 自动重试后仍不可用';
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
const backoffMs = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s
|
|
1234
|
+
console.log(`[PiAgent] loop 自动重试 ${attempt}/${MAX_LOOP_RETRIES}, 等待 ${backoffMs}ms`);
|
|
1235
|
+
if (onStream) {
|
|
1236
|
+
onStream({ type: 'status', content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
|
|
1237
|
+
}
|
|
1238
|
+
// 中途 abort 也要响应
|
|
1239
|
+
await new Promise<void>((resolve, reject) => {
|
|
1240
|
+
const t = setTimeout(() => {
|
|
1241
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
1242
|
+
resolve();
|
|
1243
|
+
}, backoffMs);
|
|
1244
|
+
const onAbort = () => {
|
|
1245
|
+
clearTimeout(t);
|
|
1246
|
+
reject(new Error('aborted during retry backoff'));
|
|
1247
|
+
};
|
|
1248
|
+
if (signal?.aborted) {
|
|
1249
|
+
clearTimeout(t);
|
|
1250
|
+
reject(new Error('aborted during retry backoff'));
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
1254
|
+
});
|
|
1255
|
+
// 重试时要把这条 user message 从 history 里移除 (避免下一次 runReActLoop 又重复加入),
|
|
1256
|
+
// 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
|
|
1257
|
+
// 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
|
|
1258
|
+
// 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
|
|
1259
|
+
if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
|
|
1260
|
+
this.messageHistory.pop();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1042
1263
|
onStream({ type: 'done', content: '' });
|
|
1264
|
+
|
|
1265
|
+
// 回溯: 异步记录 usage (不等)
|
|
1266
|
+
if (this.judgmentGateUsedIds.length > 0) {
|
|
1267
|
+
recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
|
|
1268
|
+
console.warn('[PiAgent] recordJudgmentUsage failed:', err)
|
|
1269
|
+
);
|
|
1270
|
+
// P0.5: 把 usedIds 通过 stream 事件回传给调用方 (server.ts 写到 session message)
|
|
1271
|
+
try { onStream({ type: 'used_judgments', usedIds: this.judgmentGateUsedIds, content: '' } as any); } catch {}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// P3 监控门: fire-and-forget 审计 AI 回复是否违反原则
|
|
1275
|
+
monitorAfterReply(input, result);
|
|
1276
|
+
|
|
1277
|
+
// Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
|
|
1278
|
+
const stopStartTime = this.promptStartTime || Date.now();
|
|
1279
|
+
onStop({
|
|
1280
|
+
channelId: this.currentChannelId || 'unknown',
|
|
1281
|
+
durationMs: Date.now() - stopStartTime,
|
|
1282
|
+
usedJudgmentIds: [...this.judgmentGateUsedIds],
|
|
1283
|
+
}).catch((err) => console.warn('[PiAgent] onStop failed:', err));
|
|
1284
|
+
|
|
1285
|
+
// 用完即清, 避免污染下一轮
|
|
1286
|
+
this.clearJudgmentGate();
|
|
1287
|
+
this.currentOnStream = null;
|
|
1288
|
+
this.currentSignal = null;
|
|
1289
|
+
this.bootstrapAddition = '';
|
|
1290
|
+
this.promptStartTime = 0;
|
|
1291
|
+
|
|
1043
1292
|
return result;
|
|
1044
1293
|
}
|
|
1045
1294
|
|
|
1046
|
-
async promptWithPivotLoop(input: string, config?: PivotLoopConfig): Promise<LoopResult> {
|
|
1295
|
+
async promptWithPivotLoop(input: string, config?: PivotLoopConfig, channelId?: string): Promise<LoopResult> {
|
|
1296
|
+
this.currentChannelId = channelId ?? this.currentChannelId;
|
|
1047
1297
|
if (!this.minimaxAvailable) {
|
|
1048
1298
|
const response = await this.handleFallback(input);
|
|
1049
1299
|
return {
|
|
@@ -1073,13 +1323,16 @@ class PiAgentSession implements AgentSession {
|
|
|
1073
1323
|
loop.registerTool(tool);
|
|
1074
1324
|
}
|
|
1075
1325
|
|
|
1326
|
+
// P0 注入门: 在构造 systemPrompt 之前算一次, 拼到末尾
|
|
1327
|
+
await this.computeJudgmentGate(input);
|
|
1328
|
+
|
|
1076
1329
|
const personaSection = this.persona ? `
|
|
1077
1330
|
角色描述: ${this.persona.description || '无'}
|
|
1078
1331
|
性格特点: ${this.persona.personality || '无'}
|
|
1079
1332
|
问候语: ${this.persona.greeting || '无'}
|
|
1080
1333
|
` : '';
|
|
1081
1334
|
|
|
1082
|
-
const systemPrompt =
|
|
1335
|
+
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
|
|
1083
1336
|
当前工作目录: ${this.cwd}
|
|
1084
1337
|
当前身份: ${this.identity.name} (${this.identity.did})
|
|
1085
1338
|
|
|
@@ -1096,27 +1349,44 @@ ${this.getToolDefinitions()}
|
|
|
1096
1349
|
- 每次只调用一个工具
|
|
1097
1350
|
- 仔细分析工具返回结果
|
|
1098
1351
|
- 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
|
|
1099
|
-
-
|
|
1352
|
+
- 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
|
|
1100
1353
|
|
|
1101
|
-
|
|
1354
|
+
// 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
|
|
1355
|
+
// 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
|
|
1356
|
+
// promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
|
|
1357
|
+
const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined);
|
|
1102
1358
|
|
|
1103
1359
|
this.messageHistory.push({ role: 'user', content: input });
|
|
1104
1360
|
if (result.response) {
|
|
1105
1361
|
this.messageHistory.push({ role: 'assistant', content: result.response });
|
|
1106
1362
|
}
|
|
1107
1363
|
|
|
1364
|
+
// 回溯 + 清场
|
|
1365
|
+
if (this.judgmentGateUsedIds.length > 0) {
|
|
1366
|
+
recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
|
|
1367
|
+
console.warn('[PiAgent] recordJudgmentUsage failed:', err)
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
this.clearJudgmentGate();
|
|
1371
|
+
|
|
1108
1372
|
return result;
|
|
1109
1373
|
}
|
|
1110
1374
|
|
|
1111
|
-
private async runReActLoop(onStream?: StreamCallback): Promise<string> {
|
|
1375
|
+
private async runReActLoop(onStream?: StreamCallback, signal?: AbortSignal): Promise<{ reply: string; aiFailed: boolean; aiFailureReason?: string }> {
|
|
1112
1376
|
const llm = getMinimax();
|
|
1113
1377
|
let iteration = 0;
|
|
1114
1378
|
let finalResponse = '';
|
|
1115
1379
|
let lastQualityScore = 0;
|
|
1116
1380
|
let refineAttempts = 0;
|
|
1117
1381
|
let consecutiveErrors = 0;
|
|
1382
|
+
// 2026-06-16 新增: 累计错误数 (跨工具, 兜底防 LLM 轮换工具名死循环)
|
|
1383
|
+
let totalErrors = 0;
|
|
1118
1384
|
let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
|
|
1119
1385
|
let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
|
|
1386
|
+
// 2026-06-16: AI sentinel 标志 — runReActLoop 返回 aiFailed=true,
|
|
1387
|
+
// promptStream 据此自动重跑整个 loop 最多 N 次 (不是单次 LLM 重试)
|
|
1388
|
+
let aiFailed = false;
|
|
1389
|
+
let aiFailureReason = '';
|
|
1120
1390
|
const MAX_CONSECUTIVE_ERRORS = 3;
|
|
1121
1391
|
const MAX_SAME_TOOL_FAILURES = 3; // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案
|
|
1122
1392
|
|
|
@@ -1125,9 +1395,66 @@ ${this.getToolDefinitions()}
|
|
|
1125
1395
|
onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
|
|
1126
1396
|
}
|
|
1127
1397
|
|
|
1398
|
+
// React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
|
|
1399
|
+
// 失败静默 (fail-open), 不阻塞主循环
|
|
1400
|
+
try {
|
|
1401
|
+
await this.reactHarness.onSessionStart(this.currentChannelId || undefined);
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
console.warn('[PiAgent] reactHarness.onSessionStart failed (non-fatal):', err);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1128
1406
|
while (iteration < this.MAX_REACT_ITERATIONS) {
|
|
1129
1407
|
iteration++;
|
|
1130
1408
|
|
|
1409
|
+
// 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
|
|
1410
|
+
if (iteration >= this.MAX_REACT_ITERATIONS) {
|
|
1411
|
+
console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
|
|
1412
|
+
onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
|
|
1413
|
+
finalResponse = finalResponse || '(本轮 ReAct 循环达到最大步数, 强制结束)';
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// 停止条件 2: signal.aborted (显式 abort / 用户中断)
|
|
1418
|
+
if (signal?.aborted) {
|
|
1419
|
+
console.warn('[PiAgent] runReActLoop aborted by signal');
|
|
1420
|
+
onStream?.({ type: 'error', content: '⏹️ 用户中断', tool: 'loop' });
|
|
1421
|
+
finalResponse = finalResponse || '(用户中断)';
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// 2026-06-16 新增: 累计错误兜底 — 跨工具, 防 LLM 轮换工具名绕过 MAX_SAME_TOOL_FAILURES
|
|
1426
|
+
if (totalErrors >= this.MAX_TOTAL_ERRORS) {
|
|
1427
|
+
console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
|
|
1428
|
+
onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
|
|
1429
|
+
finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
|
|
1434
|
+
// compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
|
|
1435
|
+
const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
|
|
1436
|
+
const estimatedTokensBefore = this.estimateHistoryTokens();
|
|
1437
|
+
if (estimatedTokensBefore > compactThreshold) {
|
|
1438
|
+
const tokensBeforeCompact = estimatedTokensBefore;
|
|
1439
|
+
console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
|
|
1440
|
+
onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
1441
|
+
try {
|
|
1442
|
+
await this.maybeAutoCompact(onStream, signal);
|
|
1443
|
+
} catch (compactErr) {
|
|
1444
|
+
// C 路径: compact 失败不 break, 让 token 阈值检查兜底
|
|
1445
|
+
console.warn(`[PiAgent] loop 内 maybeAutoCompact 失败 (non-fatal, 继续走 token 阈值):`, compactErr);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// 停止条件 3: context overflow (compact 后还超, 强制终止)
|
|
1450
|
+
const estimatedTokens = this.estimateHistoryTokens();
|
|
1451
|
+
if (estimatedTokens > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD) {
|
|
1452
|
+
console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
|
|
1453
|
+
onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
|
|
1454
|
+
finalResponse = finalResponse || '(本轮 ReAct 循环因上下文溢出终止)';
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1131
1458
|
// 调试日志:显示每次循环开始
|
|
1132
1459
|
console.log(`[PiAgent] 循环 ${iteration}/${this.MAX_REACT_ITERATIONS} 开始`);
|
|
1133
1460
|
if (onStream) {
|
|
@@ -1154,7 +1481,7 @@ ${this.getToolDefinitions()}
|
|
|
1154
1481
|
问候语: ${this.persona.greeting || '无'}
|
|
1155
1482
|
` : '';
|
|
1156
1483
|
|
|
1157
|
-
const systemPrompt =
|
|
1484
|
+
const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
|
|
1158
1485
|
当前工作目录: ${this.cwd}
|
|
1159
1486
|
当前身份: ${this.identity.name} (${this.identity.did})
|
|
1160
1487
|
${refineContext}
|
|
@@ -1172,10 +1499,29 @@ ${toolDefs}
|
|
|
1172
1499
|
- 每次只调用一个工具
|
|
1173
1500
|
- 仔细分析工具返回结果
|
|
1174
1501
|
- 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
|
|
1175
|
-
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1502
|
+
- 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
|
|
1503
|
+
|
|
1504
|
+
// 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
|
|
1505
|
+
// 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
|
|
1506
|
+
// 2. reactive compaction (prompt 估算超阈值, 跑压缩)
|
|
1507
|
+
// 3. prompt-too-long (LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次)
|
|
1508
|
+
// 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
|
|
1509
|
+
const response = await this.callLlmWithRecovery(llm, context, systemPrompt, signal, onStream);
|
|
1510
|
+
const reply = (response.reply || '').trim();
|
|
1511
|
+
|
|
1512
|
+
// 2026-06-16: 看到 [AI 服务调用失败] sentinel → 不再立即 break,
|
|
1513
|
+
// 而是设 aiFailed=true, 让外层 promptStream 自动重跑整个 loop 最多 N 次
|
|
1514
|
+
// (LLM API 401 / 网络错 / 配额满时, pi-ai 返回这个 prefix;
|
|
1515
|
+
// 自动 retry 兜底: 临时网络抖动可自愈, 真挂 N 次后才报失败)
|
|
1516
|
+
if (reply.startsWith('[AI 服务调用失败]')) {
|
|
1517
|
+
console.log(`[PiAgent] 收到 AI 错误 sentinel, 标记 aiFailed, 外层会自动重试整个 loop`);
|
|
1518
|
+
aiFailed = true;
|
|
1519
|
+
aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
|
|
1520
|
+
if (onStream) {
|
|
1521
|
+
onStream({ type: 'status', content: `⚠️ AI 调用失败, 将自动重试整个 loop`, tool: 'system' });
|
|
1522
|
+
}
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1179
1525
|
|
|
1180
1526
|
console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
|
|
1181
1527
|
console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
|
|
@@ -1214,21 +1560,172 @@ ${toolDefs}
|
|
|
1214
1560
|
if (toolCall.args && Object.keys(toolCall.args).length > 0) {
|
|
1215
1561
|
onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
|
|
1216
1562
|
}
|
|
1563
|
+
// 2026-06-15: step-timeline 状态机 — 开新节点
|
|
1564
|
+
onStream({
|
|
1565
|
+
type: 'step_start',
|
|
1566
|
+
content: `调用 ${toolCall.name}`,
|
|
1567
|
+
tool: toolCall.name,
|
|
1568
|
+
args: toolCall.args || {},
|
|
1569
|
+
});
|
|
1217
1570
|
}
|
|
1218
1571
|
|
|
1219
1572
|
const tool = this.tools.get(toolCall.name);
|
|
1220
1573
|
if (!tool) {
|
|
1221
1574
|
consecutiveErrors++;
|
|
1575
|
+
// 2026-06-16 新增: 未知工具也要累计 (LLM 幻觉高频场景)
|
|
1576
|
+
totalErrors++;
|
|
1222
1577
|
const errorResult: ToolResult = { success: false, error: `未知工具: ${toolCall.name}` };
|
|
1223
1578
|
this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
|
|
1224
1579
|
this.logToHarness(toolCall.name, toolCall.args, errorResult);
|
|
1225
|
-
console.warn(`[PiAgent] 未知工具: ${toolCall.name},跳过并继续`);
|
|
1580
|
+
console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
|
|
1226
1581
|
continue;
|
|
1227
1582
|
}
|
|
1228
1583
|
|
|
1584
|
+
// Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
|
|
1585
|
+
// 失败静默 — hook 自身挂掉 = 放行
|
|
1586
|
+
// P2: 透传 permissionMode (从 BootstrapOptions / env BOLLOON_PERM_MODE 解析)
|
|
1587
|
+
let toolToExecute = tool;
|
|
1588
|
+
try {
|
|
1589
|
+
const pre = await onPreToolUse({
|
|
1590
|
+
tool: toolCall.name,
|
|
1591
|
+
args: toolCall.args || {},
|
|
1592
|
+
permissionMode: this.currentPermissionMode,
|
|
1593
|
+
});
|
|
1594
|
+
if (!pre.allowed) {
|
|
1595
|
+
const deniedResult: ToolResult = {
|
|
1596
|
+
success: false,
|
|
1597
|
+
error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
|
|
1598
|
+
};
|
|
1599
|
+
this.messageHistory.push({
|
|
1600
|
+
role: 'tool',
|
|
1601
|
+
content: JSON.stringify(deniedResult),
|
|
1602
|
+
toolResult: deniedResult,
|
|
1603
|
+
});
|
|
1604
|
+
this.logToHarness(toolCall.name, toolCall.args, deniedResult);
|
|
1605
|
+
if (onStream) {
|
|
1606
|
+
onStream({
|
|
1607
|
+
type: 'error',
|
|
1608
|
+
content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
|
|
1609
|
+
tool: toolCall.name,
|
|
1610
|
+
});
|
|
1611
|
+
// 2026-06-15: step-timeline — 拦在 PreToolUse, 标 step_error
|
|
1612
|
+
onStream({
|
|
1613
|
+
type: 'step_error',
|
|
1614
|
+
content: `PreToolUse 拒绝 ${toolCall.name}`,
|
|
1615
|
+
tool: toolCall.name,
|
|
1616
|
+
error: pre.reason || '安全校验失败',
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
|
|
1620
|
+
// 不调 tool.execute, 也不计 consecutiveErrors (这是用户级拒绝, 不是工具错)
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
console.warn('[PiAgent] onPreToolUse failed (non-fatal, allowing):', err);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// React Harness: 8-gate + builtin-guards 校验 (在 PreToolUse 之后, 串接双层)
|
|
1628
|
+
// 失败静默, 拒绝时不调 tool.execute
|
|
1629
|
+
try {
|
|
1630
|
+
const pre = await this.reactHarness.preToolCall(
|
|
1631
|
+
toolCall.name,
|
|
1632
|
+
toolCall.args || {},
|
|
1633
|
+
this.currentChannelId || undefined
|
|
1634
|
+
);
|
|
1635
|
+
if (!pre.allowed) {
|
|
1636
|
+
const deniedResult: ToolResult = {
|
|
1637
|
+
success: false,
|
|
1638
|
+
error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}`,
|
|
1639
|
+
};
|
|
1640
|
+
this.messageHistory.push({
|
|
1641
|
+
role: 'tool',
|
|
1642
|
+
content: JSON.stringify(deniedResult),
|
|
1643
|
+
toolResult: deniedResult,
|
|
1644
|
+
});
|
|
1645
|
+
this.logToHarness(toolCall.name, toolCall.args, deniedResult);
|
|
1646
|
+
if (onStream) {
|
|
1647
|
+
onStream({
|
|
1648
|
+
type: 'error',
|
|
1649
|
+
content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
|
|
1650
|
+
tool: toolCall.name,
|
|
1651
|
+
});
|
|
1652
|
+
// 2026-06-15: step-timeline — Harness gate 拒绝, 标 step_error
|
|
1653
|
+
onStream({
|
|
1654
|
+
type: 'step_error',
|
|
1655
|
+
content: `Harness 拒绝 ${toolCall.name}`,
|
|
1656
|
+
tool: toolCall.name,
|
|
1657
|
+
error: pre.reason || '安全校验失败',
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
console.warn('[PiAgent] reactHarness.preToolCall failed (non-fatal, allowing):', err);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1229
1667
|
try {
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1668
|
+
const toolStart = Date.now();
|
|
1669
|
+
let result = await tool.execute(toolCall.args);
|
|
1670
|
+
const toolDurationMs = Date.now() - toolStart;
|
|
1671
|
+
console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
|
|
1672
|
+
|
|
1673
|
+
// PostToolUse 审计 hook: 写 audit log, 默认 continue
|
|
1674
|
+
try {
|
|
1675
|
+
await onPostToolUse({
|
|
1676
|
+
tool: toolCall.name,
|
|
1677
|
+
args: toolCall.args || {},
|
|
1678
|
+
result: {
|
|
1679
|
+
success: result.success,
|
|
1680
|
+
output: result.output?.substring(0, 500),
|
|
1681
|
+
error: result.error,
|
|
1682
|
+
},
|
|
1683
|
+
durationMs: toolDurationMs,
|
|
1684
|
+
});
|
|
1685
|
+
} catch (postErr) {
|
|
1686
|
+
console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Context router: 拿最近一次 preToolCall 算的 hint, 拼到 tool result messageHistory
|
|
1690
|
+
// (LLM 下次看到 tool result 时, 能"记得"这次调用的安全约束)
|
|
1691
|
+
const routeHint = this.reactHarness.getLastRouteHint();
|
|
1692
|
+
if (routeHint && routeHint.systemAddition) {
|
|
1693
|
+
this.messageHistory.push({
|
|
1694
|
+
role: 'system',
|
|
1695
|
+
content: `[Harness Router Hint: ${routeHint.reason}]\n${routeHint.systemAddition}`,
|
|
1696
|
+
});
|
|
1697
|
+
this.reactHarness.clearRouteHint();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// React Harness: post-tool call (output 审计: secret leak 等)
|
|
1701
|
+
// 拒绝时 result.output 含敏感 → 替换为 generic message, 不污染 messageHistory
|
|
1702
|
+
try {
|
|
1703
|
+
const post = await this.reactHarness.postToolCall(
|
|
1704
|
+
toolCall.name,
|
|
1705
|
+
String(result.output || ''),
|
|
1706
|
+
this.currentChannelId || undefined
|
|
1707
|
+
);
|
|
1708
|
+
if (!post.allowed) {
|
|
1709
|
+
if (onStream) {
|
|
1710
|
+
onStream({
|
|
1711
|
+
type: 'error',
|
|
1712
|
+
content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`,
|
|
1713
|
+
tool: toolCall.name,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
|
|
1717
|
+
// 替换 result: success 仍保留 (tool 本身没错), 但 output 改成 generic
|
|
1718
|
+
// 这样 LLM 下轮看 output 不会拿到秘密, 但 success 标志让它知道 "工具执行了"
|
|
1719
|
+
result = {
|
|
1720
|
+
...result,
|
|
1721
|
+
output: `[harness output gate: output 含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`,
|
|
1722
|
+
_harnessDenied: true,
|
|
1723
|
+
} as typeof result;
|
|
1724
|
+
}
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1232
1729
|
this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
|
|
1233
1730
|
this.logToHarness(toolCall.name, toolCall.args, result);
|
|
1234
1731
|
|
|
@@ -1240,8 +1737,23 @@ ${toolDefs}
|
|
|
1240
1737
|
const outputPreview = result.output.substring(0, 200);
|
|
1241
1738
|
onStream({ type: 'tool', content: `📤 结果: ${outputPreview}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
|
|
1242
1739
|
}
|
|
1740
|
+
// 2026-06-15: step-timeline 状态机 — 关闭当前节点 (成功)
|
|
1741
|
+
onStream({
|
|
1742
|
+
type: 'step_done',
|
|
1743
|
+
content: `${toolCall.name} 执行成功`,
|
|
1744
|
+
tool: toolCall.name,
|
|
1745
|
+
success: true,
|
|
1746
|
+
output: result.output,
|
|
1747
|
+
});
|
|
1243
1748
|
} else {
|
|
1244
1749
|
onStream({ type: 'error', content: `❌ ${toolCall.name} 执行失败: ${result.error}`, tool: toolCall.name });
|
|
1750
|
+
// 2026-06-15: step-timeline 状态机 — 关闭当前节点 (失败)
|
|
1751
|
+
onStream({
|
|
1752
|
+
type: 'step_error',
|
|
1753
|
+
content: `${toolCall.name} 执行失败`,
|
|
1754
|
+
tool: toolCall.name,
|
|
1755
|
+
error: result.error,
|
|
1756
|
+
});
|
|
1245
1757
|
}
|
|
1246
1758
|
}
|
|
1247
1759
|
|
|
@@ -1264,6 +1776,8 @@ ${toolDefs}
|
|
|
1264
1776
|
// 不 break,继续下一次循环
|
|
1265
1777
|
} else {
|
|
1266
1778
|
consecutiveErrors++;
|
|
1779
|
+
// 2026-06-16 新增: 累计错误 (跨工具, 兜底防 LLM 轮换工具名死循环)
|
|
1780
|
+
totalErrors++;
|
|
1267
1781
|
// 跟踪同一工具连续失败次数
|
|
1268
1782
|
if (toolCall.name === lastFailedTool) {
|
|
1269
1783
|
lastFailedToolCount++;
|
|
@@ -1271,7 +1785,7 @@ ${toolDefs}
|
|
|
1271
1785
|
lastFailedTool = toolCall.name;
|
|
1272
1786
|
lastFailedToolCount = 1;
|
|
1273
1787
|
}
|
|
1274
|
-
console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}): ${result.error}`);
|
|
1788
|
+
console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}, 累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${result.error}`);
|
|
1275
1789
|
|
|
1276
1790
|
// 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
|
|
1277
1791
|
if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
|
|
@@ -1298,10 +1812,12 @@ ${toolDefs}
|
|
|
1298
1812
|
}
|
|
1299
1813
|
} catch (execError) {
|
|
1300
1814
|
consecutiveErrors++;
|
|
1815
|
+
// 2026-06-16 新增: 异常分支也要累计
|
|
1816
|
+
totalErrors++;
|
|
1301
1817
|
const errorResult: ToolResult = { success: false, error: String(execError) };
|
|
1302
1818
|
this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
|
|
1303
1819
|
this.logToHarness(toolCall.name, toolCall.args, errorResult);
|
|
1304
|
-
console.error(`[PiAgent]
|
|
1820
|
+
console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
|
|
1305
1821
|
}
|
|
1306
1822
|
} else {
|
|
1307
1823
|
// LLM 返回的不是 tool call 格式
|
|
@@ -1320,7 +1836,10 @@ ${toolDefs}
|
|
|
1320
1836
|
const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
|
|
1321
1837
|
reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
|
|
1322
1838
|
// 兼容 LLM 用对象字面量输出 tool call (上轮没解析成功时, 至少要继续)
|
|
1323
|
-
reply.includes('tool =>') || reply.includes('[TOOL_CALL]')
|
|
1839
|
+
reply.includes('tool =>') || reply.includes('[TOOL_CALL]') ||
|
|
1840
|
+
// 2026-06-15 修: 兼容 LLM 用 XML 标签输出 tool call (<shell_exec>...</shell_exec>)
|
|
1841
|
+
// 这时 parseToolCall 失败, 至少要让 loop 继续
|
|
1842
|
+
/<\w+>[\s\S]*?<\/\w+>/.test(reply);
|
|
1324
1843
|
const hasError = ['不存在', '找不到', '无法找到', 'not found', 'does not exist',
|
|
1325
1844
|
'错误', 'error', '失败', 'failed'].some(k => reply.includes(k));
|
|
1326
1845
|
const isTooShort = reply.length < 50 && reply.length > 0;
|
|
@@ -1373,7 +1892,16 @@ Workspace root folder: ${this.cwd}
|
|
|
1373
1892
|
finalResponse = identityPrefix + finalResponse;
|
|
1374
1893
|
|
|
1375
1894
|
this.messageHistory.push({ role: 'assistant', content: finalResponse });
|
|
1376
|
-
|
|
1895
|
+
|
|
1896
|
+
// React Harness: 循环结束
|
|
1897
|
+
try {
|
|
1898
|
+
await this.reactHarness.onSessionEnd();
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
console.warn('[PiAgent] reactHarness.onSessionEnd failed (non-fatal):', err);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
|
|
1904
|
+
return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
|
|
1377
1905
|
}
|
|
1378
1906
|
|
|
1379
1907
|
async deepThink(prompt: string): Promise<{ result: ThinkResult; response: string }> {
|
|
@@ -1429,7 +1957,13 @@ Workspace root folder: ${this.cwd}
|
|
|
1429
1957
|
}
|
|
1430
1958
|
|
|
1431
1959
|
private buildContext(): string {
|
|
1432
|
-
|
|
1960
|
+
// P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
|
|
1961
|
+
// 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
|
|
1962
|
+
// 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
|
|
1963
|
+
//
|
|
1964
|
+
// P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
|
|
1965
|
+
const source = this.projectedHistory ?? this.messageHistory;
|
|
1966
|
+
const recentMessages = this.compressHistorySync(source).slice(-10);
|
|
1433
1967
|
return recentMessages.map(m => {
|
|
1434
1968
|
if (m.role === 'user') return `用户: ${m.content}`;
|
|
1435
1969
|
if (m.role === 'assistant') return `助手: ${m.content}`;
|
|
@@ -1441,6 +1975,164 @@ Workspace root folder: ${this.cwd}
|
|
|
1441
1975
|
}).join('\n');
|
|
1442
1976
|
}
|
|
1443
1977
|
|
|
1978
|
+
/**
|
|
1979
|
+
* 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
|
|
1980
|
+
* 失败静默: 任何异常 → 0 (不阻塞)
|
|
1981
|
+
*/
|
|
1982
|
+
private estimateHistoryTokens(): number {
|
|
1983
|
+
try {
|
|
1984
|
+
const { estimateTokens } = require('../context-compaction/index.js') as typeof import('../context-compaction/index.js');
|
|
1985
|
+
return estimateTokens(this.messageHistory as any);
|
|
1986
|
+
} catch {
|
|
1987
|
+
return 0;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* 3 个恢复机制合一:
|
|
1993
|
+
* 1. max output token 升级: 最多 3 次, 每次 maxOutputTokens 翻倍 (如果 llm.chat 支持)
|
|
1994
|
+
* 2. reactive compaction: 估算 > 80% 阈值, 跑 sync compressHistorySync + 必要时 maybeAutoCompact
|
|
1995
|
+
* 3. prompt-too-long: LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次
|
|
1996
|
+
*
|
|
1997
|
+
* 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
|
|
1998
|
+
*/
|
|
1999
|
+
private async callLlmWithRecovery(
|
|
2000
|
+
llm: any,
|
|
2001
|
+
context: string,
|
|
2002
|
+
systemPrompt: string,
|
|
2003
|
+
signal: AbortSignal | undefined,
|
|
2004
|
+
onStream?: (chunk: any) => void
|
|
2005
|
+
): Promise<{ reply: string }> {
|
|
2006
|
+
// Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
|
|
2007
|
+
const estimated = this.estimateHistoryTokens();
|
|
2008
|
+
if (estimated > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
|
|
2009
|
+
console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
|
|
2010
|
+
onStream?.({ type: 'status', content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
|
|
2011
|
+
try {
|
|
2012
|
+
const compacted = this.compressHistorySync(this.messageHistory);
|
|
2013
|
+
this.messageHistory = compacted;
|
|
2014
|
+
if (this.estimateHistoryTokens() > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
|
|
2015
|
+
await this.maybeAutoCompact(onStream, signal);
|
|
2016
|
+
}
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
console.warn('[PiAgent] reactive compaction pre-check failed:', err);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// 主调用 + 3 个恢复路径
|
|
2023
|
+
let lastErr: any = null;
|
|
2024
|
+
for (let attempt = 0; attempt <= this.MAX_OUTPUT_TOKEN_ESCALATION_RETRIES; attempt++) {
|
|
2025
|
+
try {
|
|
2026
|
+
const response = await llm.chat(context, systemPrompt, signal);
|
|
2027
|
+
return { reply: response.reply || '' };
|
|
2028
|
+
} catch (err: any) {
|
|
2029
|
+
lastErr = err;
|
|
2030
|
+
const errMsg = String(err?.message || err || '');
|
|
2031
|
+
const isPromptTooLong = /token|too long|exceed|length|context|4000|413|429/i.test(errMsg);
|
|
2032
|
+
if (isPromptTooLong) {
|
|
2033
|
+
console.warn(`[PiAgent] prompt-too-long 触发 (attempt ${attempt + 1}), 跑 reactive compaction`);
|
|
2034
|
+
onStream?.({ type: 'status', content: `⚠️ prompt-too-long 触发 (attempt ${attempt + 1}/${this.MAX_OUTPUT_TOKEN_ESCALATION_RETRIES + 1})`, tool: 'recovery' });
|
|
2035
|
+
try {
|
|
2036
|
+
await this.maybeAutoCompact(onStream, signal);
|
|
2037
|
+
} catch (compactionErr) {
|
|
2038
|
+
console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
|
|
2039
|
+
}
|
|
2040
|
+
// 重新生成 context (compressHistorySync + projected)
|
|
2041
|
+
context = this.buildContext();
|
|
2042
|
+
} else {
|
|
2043
|
+
// 非 prompt-too-long 错误, 1 次重试就放弃
|
|
2044
|
+
if (attempt === 0) {
|
|
2045
|
+
console.warn(`[PiAgent] LLM 调用失败 (non-prompt-too-long), 1 次重试:`, err);
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
console.warn('[PiAgent] callLlmWithRecovery 全失败 (silent):', lastErr);
|
|
2053
|
+
return { reply: '' };
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
|
|
2058
|
+
* Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
|
|
2059
|
+
* 失败静默: 任何 stage 抛错 → 返回原 history.
|
|
2060
|
+
*/
|
|
2061
|
+
private compressHistorySync(history: Message[]): Message[] {
|
|
2062
|
+
try {
|
|
2063
|
+
// context-compaction 的 Message 与 pi-sdk 的 Message 字段兼容
|
|
2064
|
+
// 这里用 any cast 跳过 structural type 严格校验 (避免双向 import)
|
|
2065
|
+
let h: any = history;
|
|
2066
|
+
const r1 = budgetReduce(h);
|
|
2067
|
+
h = r1.history;
|
|
2068
|
+
const r2 = snip(h);
|
|
2069
|
+
h = r2.history;
|
|
2070
|
+
const r3 = microcompact(h);
|
|
2071
|
+
h = r3.history;
|
|
2072
|
+
return h as Message[];
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
console.warn('[PiAgent] compressHistorySync failed (silent, using original):', err);
|
|
2075
|
+
return history;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* P1.1: 异步跑 Auto-Compact (LLM 摘要).
|
|
2081
|
+
* 入口: promptStream 入口, 在 computeJudgmentGate 之后, onSessionStart 之前.
|
|
2082
|
+
*
|
|
2083
|
+
* 逻辑:
|
|
2084
|
+
* 1. 跑完整 compactPipeline (5 层, 异步)
|
|
2085
|
+
* 2. 第 5 层 (Auto-Compact) 需要 LLM, 通过 getMinimax().chat 注入
|
|
2086
|
+
* 3. 如果 budgetGate 不超限, 5 层短路在前 3 层, 不会调 LLM → 零开销
|
|
2087
|
+
* 4. 失败静默: 任何异常 → console.warn + 保留原 messageHistory
|
|
2088
|
+
*
|
|
2089
|
+
* onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
|
|
2090
|
+
*/
|
|
2091
|
+
private async maybeAutoCompact(
|
|
2092
|
+
onStream?: (chunk: any) => void,
|
|
2093
|
+
signal?: AbortSignal
|
|
2094
|
+
): Promise<void> {
|
|
2095
|
+
if (this.messageHistory.length < 10) return; // 历史太短, 不值得压
|
|
2096
|
+
|
|
2097
|
+
onStream?.({ type: 'status', content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
|
|
2098
|
+
|
|
2099
|
+
// 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
|
|
2100
|
+
// 给 Context Collapse (虚拟投影) 和 Auto-Compact (摘要) 共用
|
|
2101
|
+
const llm = getMinimax();
|
|
2102
|
+
const llmChat = async (systemPrompt: string, userPrompt: string): Promise<string> => {
|
|
2103
|
+
const r = await llm.chat(userPrompt, systemPrompt, signal);
|
|
2104
|
+
return r.reply;
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
const { compactPipeline, isContextCollapseEnabled } = await import('../context-compaction/index.js');
|
|
2108
|
+
const result = await compactPipeline(this.messageHistory as any, {
|
|
2109
|
+
maxTokens: 8000,
|
|
2110
|
+
llmChat,
|
|
2111
|
+
collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
|
|
2112
|
+
cacheScope: this.currentChannelId || 'default',
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
if (result.compacted && result.history.length < this.messageHistory.length) {
|
|
2116
|
+
const saved = this.messageHistory.length - result.history.length;
|
|
2117
|
+
const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
|
|
2118
|
+
onStream?.({
|
|
2119
|
+
type: 'status',
|
|
2120
|
+
content: `🗜️ 上下文压缩: ${stagesApplied || 'no-op'} | 节省 ${saved} 条 (剩余 ${result.history.length}, collapse=${isContextCollapseEnabled() ? 'on' : 'off'})`,
|
|
2121
|
+
tool: 'compactor',
|
|
2122
|
+
});
|
|
2123
|
+
// 关键: 第 4 层 (Context Collapse) 是读时投影 (非破坏)
|
|
2124
|
+
// 第 5 层 (Auto-Compact) 是破坏性折叠
|
|
2125
|
+
// 这里用 if-else 区分: collapse on → 仅 buildContext 用; collapse off → 真更新
|
|
2126
|
+
if (isContextCollapseEnabled()) {
|
|
2127
|
+
this.projectedHistory = result.history as Message[]; // buildContext 用
|
|
2128
|
+
// messageHistory 不变 (非破坏)
|
|
2129
|
+
} else {
|
|
2130
|
+
this.messageHistory = result.history as Message[]; // 真破坏性更新
|
|
2131
|
+
this.projectedHistory = null;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1444
2136
|
private isFinalResponse(content: string): boolean {
|
|
1445
2137
|
// 只有明确输出 <final gen> 才认为是最终回答
|
|
1446
2138
|
return content.includes('<final gen>');
|
|
@@ -1481,6 +2173,9 @@ Workspace root folder: ${this.cwd}
|
|
|
1481
2173
|
/\btool\s*=>\s*["'](\w+)["']/,
|
|
1482
2174
|
// 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
|
|
1483
2175
|
/\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
|
|
2176
|
+
// 2026-06-15 修: 兼容 LLM 输出的 XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
|
|
2177
|
+
// 实际 LLM 习惯: <shell_exec>\n<command>ls</command>\n<args>["-la", "..."]</args>\n</shell_exec>
|
|
2178
|
+
/<(\w+)>([\s\S]*?)<\/\1>/,
|
|
1484
2179
|
];
|
|
1485
2180
|
|
|
1486
2181
|
for (const pattern of patterns) {
|
|
@@ -1505,6 +2200,18 @@ Workspace root folder: ${this.cwd}
|
|
|
1505
2200
|
if (key) args[key] = valueParts.join(':') || '';
|
|
1506
2201
|
}
|
|
1507
2202
|
}
|
|
2203
|
+
} else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
|
|
2204
|
+
// 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
|
|
2205
|
+
// 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
|
|
2206
|
+
const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
|
2207
|
+
let xmlMatch: RegExpExecArray | null;
|
|
2208
|
+
while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
|
|
2209
|
+
const argName = xmlMatch[1];
|
|
2210
|
+
const argValue = xmlMatch[2].trim();
|
|
2211
|
+
if (argName && argValue) {
|
|
2212
|
+
args[argName] = argValue;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
1508
2215
|
} else if (rawArgs) {
|
|
1509
2216
|
// 形参串, 形如 key: value, key2: value2
|
|
1510
2217
|
const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -1885,6 +2592,10 @@ ${this.extractOperationsFromRef(operationsRef)}
|
|
|
1885
2592
|
this.identity = { ...this.identity, ...updates };
|
|
1886
2593
|
}
|
|
1887
2594
|
|
|
2595
|
+
setCurrentChannelId(channelId: string): void {
|
|
2596
|
+
this.currentChannelId = channelId;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
1888
2599
|
getSessionState(): PiSessionState {
|
|
1889
2600
|
return this.sessionManager.getState();
|
|
1890
2601
|
}
|