@bolloon/bolloon-agent 0.2.2 → 0.2.4
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/bin/bolloon-cli.cjs +15 -4
- package/bin/ipfs +0 -0
- package/dist/agents/parse-tool-call.js +304 -0
- package/dist/agents/pi-sdk.js +201 -291
- package/dist/agents/react-loop.js +120 -0
- package/dist/agents/session-store.js +157 -0
- package/dist/agents/tool-registry.js +136 -0
- package/dist/agents/workflow-pivot-loop.js +40 -0
- package/dist/bollharness/src/index.js +5 -0
- package/dist/bollharness/src/scripts/checks/check_adr_plan_numbering.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_api_types.js +45 -0
- package/dist/bollharness/src/scripts/checks/check_artifact_link.js +146 -0
- package/dist/bollharness/src/scripts/checks/check_bridge_deps.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_bugfix_binding.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_bugfix_binding_ci.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_doc_file_references.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +135 -0
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +31 -0
- package/dist/bollharness/src/scripts/checks/check_file_existence_claims.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_fragment_integrity.js +34 -0
- package/dist/bollharness/src/scripts/checks/check_hook_installed.js +63 -0
- package/dist/bollharness/src/scripts/checks/check_issue_closure.js +41 -0
- package/dist/bollharness/src/scripts/checks/check_mcp_parity.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_security.js +48 -0
- package/dist/bollharness/src/scripts/checks/check_skill_parity.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_versions.js +6 -0
- package/dist/bollharness/src/scripts/checks/finding.js +13 -0
- package/dist/bollharness/src/scripts/checks/next_decision_number.js +20 -0
- package/dist/bollharness/src/scripts/checks/regenerate_magic_docs.js +6 -0
- package/dist/bollharness/src/scripts/ci/detect_rebaseline_triggers.js +8 -0
- package/dist/bollharness/src/scripts/ci/scan_subprocess_cfg.js +8 -0
- package/dist/bollharness/src/scripts/ci/scan_verify_artifacts.js +8 -0
- package/dist/bollharness/src/scripts/ci/scan_yaml_schema.js +8 -0
- package/dist/bollharness/src/scripts/context_router.js +67 -0
- package/dist/bollharness/src/scripts/deploy-guard.js +157 -0
- package/dist/bollharness/src/scripts/guard-feedback.js +192 -0
- package/dist/bollharness/src/scripts/guard_router.js +158 -0
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +6 -0
- package/dist/bollharness/src/scripts/hooks/auto-python3.js +6 -0
- package/dist/bollharness/src/scripts/hooks/deploy-progress-on-session-end.js +6 -0
- package/dist/bollharness/src/scripts/hooks/failure-analyzer.js +6 -0
- package/dist/bollharness/src/scripts/hooks/gate-judgment-inject.js +92 -0
- package/dist/bollharness/src/scripts/hooks/gate-transition-judgment.js +63 -0
- package/dist/bollharness/src/scripts/hooks/inbox-ack.js +6 -0
- package/dist/bollharness/src/scripts/hooks/inbox-inject-on-start.js +6 -0
- package/dist/bollharness/src/scripts/hooks/inbox-validate.js +6 -0
- package/dist/bollharness/src/scripts/hooks/inbox-write-ledger.js +6 -0
- package/dist/bollharness/src/scripts/hooks/initializer-agent.js +6 -0
- package/dist/bollharness/src/scripts/hooks/loop-detection.js +73 -0
- package/dist/bollharness/src/scripts/hooks/owner-guard.js +6 -0
- package/dist/bollharness/src/scripts/hooks/precompact.js +6 -0
- package/dist/bollharness/src/scripts/hooks/review-agent-gatekeeper.js +6 -0
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +108 -0
- package/dist/bollharness/src/scripts/hooks/sanitize-on-read.js +6 -0
- package/dist/bollharness/src/scripts/hooks/session-reflection.js +7 -0
- package/dist/bollharness/src/scripts/hooks/session-start-magic-docs.js +7 -0
- package/dist/bollharness/src/scripts/hooks/session-start-reset-risk.js +7 -0
- package/dist/bollharness/src/scripts/hooks/session-start-toolkit-reminder.js +7 -0
- package/dist/bollharness/src/scripts/hooks/stop-evaluator.js +157 -0
- package/dist/bollharness/src/scripts/hooks/tool-call-counter.js +6 -0
- package/dist/bollharness/src/scripts/hooks/trace-analyzer.js +10 -0
- package/dist/bollharness/src/scripts/install/install-trust-token.js +7 -0
- package/dist/bollharness/src/scripts/install/multi_project_registry.js +9 -0
- package/dist/bollharness/src/scripts/install/phase2_auto.js +21 -0
- package/dist/bollharness/src/scripts/install/pre_commit_installer.js +6 -0
- package/dist/bollharness/src/scripts/install/tier_selector.js +7 -0
- package/dist/bollharness/src/scripts/install/transcript_miner.js +7 -0
- package/dist/bollharness/src/scripts/lib/claim_patterns.js +10 -0
- package/dist/bollharness/src/scripts/lib/sanitize_patterns.js +12 -0
- package/dist/bollharness/src/scripts/sanitize.js +6 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js +6 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/createOrder.js +6 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/getMarket.js +5 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/getOrders.js +6 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/listMarkets.js +4 -0
- package/dist/constraint-runtime/src/tools/SafeSDK/deploySafe.js +6 -0
- package/dist/constraint-runtime/src/tools/WalletTools/autoPay.js +29 -0
- package/dist/constraint-runtime/src/tools/WalletTools/createWallet.js +10 -0
- package/dist/constraint-runtime/src/tools/WalletTools/getBalance.js +13 -0
- package/dist/constraint-runtime/src/tools/WalletTools/importWallet.js +22 -0
- package/dist/constraint-runtime/src/tools/WalletTools/sendTransaction.js +25 -0
- package/dist/constraint-runtime/src/tools/WalletTools/signMessage.js +10 -0
- package/dist/constraint-runtime/src/tools/WalletTools/transferToken.js +25 -0
- package/dist/electron/config.js.map +1 -0
- package/dist/electron/dialogs.js.map +1 -0
- package/dist/electron/first-run.js.map +1 -0
- package/dist/electron/ipc.js.map +1 -0
- package/dist/electron/logger.js.map +1 -0
- package/dist/electron/main.js.map +1 -0
- package/dist/electron/menu.js.map +1 -0
- package/dist/electron/paths.js.map +1 -0
- package/dist/electron/server.js.map +1 -0
- package/dist/electron/tray.js.map +1 -0
- package/dist/electron/window.js.map +1 -0
- package/dist/electron-preload.js +29 -0
- package/dist/electron-preload.js.map +1 -0
- package/dist/electron.js +5 -0
- package/dist/electron.js.map +1 -0
- package/dist/llm/config-store.js +24 -8
- package/dist/llm/pi-ai.js +33 -19
- package/dist/web/server.js +16 -1
- package/dist/workflows/collaboration.js +374 -0
- package/dist/workflows/index.js +54 -0
- package/package.json +12 -3
- package/CLAUDE.md +0 -3
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/tray.png +0 -0
- package/build/trayTemplate.png +0 -0
- package/ios/App/App/AppDelegate.swift +0 -49
- package/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png +0 -0
- package/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -14
- package/ios/App/App/Assets.xcassets/Contents.json +0 -6
- package/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +0 -23
- package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png +0 -0
- package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png +0 -0
- package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png +0 -0
- package/ios/App/App/Base.lproj/LaunchScreen.storyboard +0 -32
- package/ios/App/App/Base.lproj/Main.storyboard +0 -19
- package/ios/App/App/Info.plist +0 -88
- package/ios/App/App.xcodeproj/project.pbxproj +0 -376
- package/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/App/CapApp-SPM/Package.swift +0 -25
- package/ios/App/CapApp-SPM/README.md +0 -5
- package/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift +0 -1
- package/ios/debug.xcconfig +0 -1
- package/scripts/auto-evolve-oneshot.sh +0 -155
- package/scripts/auto-evolve-snapshot.sh +0 -136
- package/scripts/build-app-bundle.cjs +0 -170
- package/scripts/build-cli.js +0 -216
- package/scripts/detect-schema-changes.sh +0 -48
- package/scripts/lefthook-helper.sh +0 -69
- package/scripts/postinstall.js +0 -153
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -31,6 +31,10 @@ import { onPostToolUse } from '../bootstrap/lifecycle-hooks.js';
|
|
|
31
31
|
import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
|
|
32
32
|
// React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
|
|
33
33
|
import { ReactHarness } from '../security/react-harness.js';
|
|
34
|
+
import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
|
|
35
|
+
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
36
|
+
import { ToolRegistry } from './tool-registry.js';
|
|
37
|
+
import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
|
|
34
38
|
const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
|
|
35
39
|
const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
|
|
36
40
|
export class PiSessionManager {
|
|
@@ -344,11 +348,17 @@ class PiAgentSession {
|
|
|
344
348
|
socialHeartbeat = null;
|
|
345
349
|
messageHistory = [];
|
|
346
350
|
tools = new Map();
|
|
351
|
+
/** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
|
|
352
|
+
_toolRegistry = new ToolRegistry();
|
|
347
353
|
skillRegistry = new SkillRegistry();
|
|
348
354
|
/** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
|
|
349
355
|
cachedToolDefinitions = '';
|
|
350
356
|
/** M2.4: 缓存 persona section */
|
|
351
357
|
cachedPersonaSection = '';
|
|
358
|
+
/** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
|
|
359
|
+
_sessionStore;
|
|
360
|
+
/** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
|
|
361
|
+
_readyPromise = null;
|
|
352
362
|
// 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
|
|
353
363
|
// 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
|
|
354
364
|
// 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
|
|
@@ -444,6 +454,8 @@ class PiAgentSession {
|
|
|
444
454
|
this.peerId = config.peerId || 'local';
|
|
445
455
|
this.identity = config.identityDoc || this.createDefaultIdentity();
|
|
446
456
|
this.minimaxAvailable = this.checkMinimax();
|
|
457
|
+
// 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
|
|
458
|
+
this._sessionStore = config.sessionStore ?? defaultSessionStore;
|
|
447
459
|
this.constraintLayer = new ConstraintLayer();
|
|
448
460
|
this.workflowEngine = new WorkflowEngine(this.constraintLayer);
|
|
449
461
|
this.sessionManager = new PiSessionManager(this.identity.did, this.cwd);
|
|
@@ -459,38 +471,37 @@ class PiAgentSession {
|
|
|
459
471
|
// 之前 messageHistory 是空的, 服务重启后 LLM 看到的是新对话
|
|
460
472
|
// 现在 loadSessionKey 形如 "channel-xxx:default" 走 ~/.bolloon/sessions/cache/<key>.json
|
|
461
473
|
if (config.loadSessionKey) {
|
|
462
|
-
this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30)
|
|
474
|
+
this._readyPromise = this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30).catch((err) => {
|
|
475
|
+
// 失败静默, 但不让 whenReady 永久 hang
|
|
476
|
+
console.warn(`[PiAgent] hydrateMessageHistory failed: ${err.message?.slice(0, 100)}`);
|
|
477
|
+
});
|
|
463
478
|
}
|
|
464
479
|
}
|
|
465
480
|
/**
|
|
466
|
-
*
|
|
481
|
+
* 2026-06-30: 让外部 await 构造期间的 hydrate 完成.
|
|
482
|
+
* 解决 fire-and-forget 让 messageHistory 不可预测的问题.
|
|
483
|
+
* 不传 loadSessionKey 时立即返回.
|
|
484
|
+
*/
|
|
485
|
+
whenReady() {
|
|
486
|
+
return this._readyPromise ?? Promise.resolve();
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* M2.3 (2026-06-30 重构): 从 SessionStore 加载历史, 转成 messageHistory 格式
|
|
467
490
|
* - 失败静默 (历史加载失败不应该阻塞 agent 启动)
|
|
468
491
|
* - 限制 max 条数, 防止 context 爆
|
|
469
|
-
* -
|
|
470
|
-
* -
|
|
492
|
+
* - 跳过错误消息 ([AI 服务调用失败] / [错误:...]) 不污染 LLM
|
|
493
|
+
* - 委托 SessionStore 完成 IO, 保证 save/load 路径对称
|
|
494
|
+
*
|
|
495
|
+
* 历史格式兼容旧 schema ({type, content}) 和新 schema (PersistedMessage[])
|
|
471
496
|
*/
|
|
472
497
|
async hydrateMessageHistory(sessionKey, maxMessages) {
|
|
473
498
|
try {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// 保留最后 N 条, 转换 role 字段
|
|
479
|
-
const tail = messages.slice(-maxMessages);
|
|
480
|
-
const hydrated = [];
|
|
481
|
-
for (const m of tail) {
|
|
482
|
-
// 跳过错误的 AI 消息 (M1.2 之后, AI 错误时 reply 是 [AI 服务调用失败] 字符串, 不该进 history)
|
|
483
|
-
if (m?.type === 'ai' && typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
|
|
484
|
-
continue;
|
|
485
|
-
if (m?.type === 'ai' && typeof m.content === 'string' && m.content.startsWith('[错误:'))
|
|
486
|
-
continue;
|
|
487
|
-
if (!m?.content)
|
|
488
|
-
continue;
|
|
489
|
-
const role = m.type === 'user' ? 'user' : m.type === 'ai' ? 'assistant' : null;
|
|
490
|
-
if (!role)
|
|
491
|
-
continue;
|
|
492
|
-
hydrated.push({ role, content: String(m.content) });
|
|
499
|
+
const loaded = await this._sessionStore.loadMessages(sessionKey);
|
|
500
|
+
if (!loaded) {
|
|
501
|
+
console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
|
|
502
|
+
return;
|
|
493
503
|
}
|
|
504
|
+
const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
|
|
494
505
|
if (hydrated.length > 0) {
|
|
495
506
|
this.messageHistory = hydrated;
|
|
496
507
|
console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
|
|
@@ -500,6 +511,84 @@ class PiAgentSession {
|
|
|
500
511
|
console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${err.message?.slice(0, 100)}`);
|
|
501
512
|
}
|
|
502
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* 2026-06-30: 把当前 messageHistory 持久化到 SessionStore.
|
|
516
|
+
* 公开方法 — claude code / 外部 harness 在每次 prompt 完成后调一下,
|
|
517
|
+
* 即可获得"重启 / 跨进程接续"的语义.
|
|
518
|
+
*/
|
|
519
|
+
async saveCurrentSession(key) {
|
|
520
|
+
const persisted = this.messageHistory.map((m) => ({
|
|
521
|
+
role: m.role,
|
|
522
|
+
content: m.content,
|
|
523
|
+
toolCall: m.toolCall,
|
|
524
|
+
toolResult: m.toolResult,
|
|
525
|
+
toolCallId: m.toolCallId,
|
|
526
|
+
timestamp: Date.now(),
|
|
527
|
+
source: 'pi-session',
|
|
528
|
+
}));
|
|
529
|
+
await this._sessionStore.saveMessages(key, persisted);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* 2026-06-30: 从 disk 拉历史覆盖当前 messageHistory.
|
|
533
|
+
* 返回加载条数 — 失败或空则返回 0.
|
|
534
|
+
* 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
|
|
535
|
+
*/
|
|
536
|
+
async resumeSession(key, maxMessages = 30) {
|
|
537
|
+
const before = this.messageHistory.length;
|
|
538
|
+
await this.hydrateMessageHistory(key, maxMessages);
|
|
539
|
+
return this.messageHistory.length - before;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* 2026-06-30: 读历史不修改 messageHistory.
|
|
543
|
+
* 给 claude code / 测试做"先看一下历史"用 — 不破坏当前会话.
|
|
544
|
+
* 返回 Message[] 数组 (空数组表示无历史).
|
|
545
|
+
*/
|
|
546
|
+
async peekSessionHistory(key, maxMessages = 30) {
|
|
547
|
+
try {
|
|
548
|
+
const loaded = await this._sessionStore.loadMessages(key);
|
|
549
|
+
if (!loaded)
|
|
550
|
+
return [];
|
|
551
|
+
return this._filterToMessage(loaded).slice(-maxMessages);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** hydrateMessageHistory 用的过滤逻辑 — 提到外面复用 */
|
|
558
|
+
_filterToMessage(loaded) {
|
|
559
|
+
const hydrated = [];
|
|
560
|
+
const VALID_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
|
|
561
|
+
for (const m of loaded) {
|
|
562
|
+
// role 必须合法 (拒绝旧 schema {type:'user'} 没 role 字段的)
|
|
563
|
+
if (!VALID_ROLES.has(m.role))
|
|
564
|
+
continue;
|
|
565
|
+
// 跳过污染消息
|
|
566
|
+
if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
|
|
567
|
+
continue;
|
|
568
|
+
if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
|
|
569
|
+
continue;
|
|
570
|
+
// 注意: '!m.content' 会跳过 content='' 的 tool call 消息 (assistant role + toolCall 字段),
|
|
571
|
+
// 这种是合法的 (LLM 输出只有 tool call, 没有正文) — 必须保留.
|
|
572
|
+
// 这里只跳过"无内容 + 也没 tool call/tool result"的废消息.
|
|
573
|
+
if (!m.content && !m.toolCall && !m.toolResult)
|
|
574
|
+
continue;
|
|
575
|
+
// 跳过空 tool role (tool result 占位但没有任何内容)
|
|
576
|
+
if (m.role === 'tool' && !m.toolResult)
|
|
577
|
+
continue;
|
|
578
|
+
hydrated.push({
|
|
579
|
+
role: m.role,
|
|
580
|
+
content: m.content ?? '',
|
|
581
|
+
toolCall: m.toolCall,
|
|
582
|
+
toolResult: m.toolResult,
|
|
583
|
+
toolCallId: m.toolCallId,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return hydrated;
|
|
587
|
+
}
|
|
588
|
+
/** 暴露 store 给测试 / 高级集成用. */
|
|
589
|
+
get sessionStoreInstance() {
|
|
590
|
+
return this._sessionStore;
|
|
591
|
+
}
|
|
503
592
|
/**
|
|
504
593
|
* 从 SKILL.md 目录加载 skills 进 skillRegistry.
|
|
505
594
|
*
|
|
@@ -1617,6 +1706,11 @@ class PiAgentSession {
|
|
|
1617
1706
|
// - 失败结果不缓存 (避免缓存 transient 错误)
|
|
1618
1707
|
// - 缓存容量 200 条, 超过就清空
|
|
1619
1708
|
this.wrapToolsWithIdempotency();
|
|
1709
|
+
// 2026-06-30: 镜像到 ToolRegistry — resolveToolName 走 registry 的 alias 表.
|
|
1710
|
+
// 被 wrap 的 tool (idempotency cache) 也直接拿来用, registry 不重复 wrap.
|
|
1711
|
+
for (const [name, tool] of this.tools.entries()) {
|
|
1712
|
+
this._toolRegistry.register(tool);
|
|
1713
|
+
}
|
|
1620
1714
|
}
|
|
1621
1715
|
/** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
|
|
1622
1716
|
idempotencyCache = new Map();
|
|
@@ -2094,10 +2188,12 @@ ${this.getToolDefinitions()}
|
|
|
2094
2188
|
while (iteration < this.MAX_REACT_ITERATIONS) {
|
|
2095
2189
|
iteration++;
|
|
2096
2190
|
// 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
|
|
2097
|
-
|
|
2191
|
+
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
|
|
2192
|
+
const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
|
|
2193
|
+
if (maxIterDecision.shouldExit) {
|
|
2098
2194
|
console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
|
|
2099
2195
|
onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
|
|
2100
|
-
finalResponse = finalResponse ||
|
|
2196
|
+
finalResponse = finalResponse || maxIterDecision.finalAnswer;
|
|
2101
2197
|
break;
|
|
2102
2198
|
}
|
|
2103
2199
|
// 停止条件 2: signal.aborted (显式 abort / 用户中断)
|
|
@@ -2124,9 +2220,10 @@ ${this.getToolDefinitions()}
|
|
|
2124
2220
|
}
|
|
2125
2221
|
// 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
|
|
2126
2222
|
// compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
|
|
2223
|
+
// 2026-07-01 (v0.2.4 子任务 1): 触发判定走 shouldCompactBeforeIteration 纯函数
|
|
2127
2224
|
const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
|
|
2128
2225
|
const estimatedTokensBefore = this.estimateHistoryTokens();
|
|
2129
|
-
if (estimatedTokensBefore
|
|
2226
|
+
if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
|
|
2130
2227
|
const tokensBeforeCompact = estimatedTokensBefore;
|
|
2131
2228
|
console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
|
|
2132
2229
|
onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
@@ -2139,11 +2236,13 @@ ${this.getToolDefinitions()}
|
|
|
2139
2236
|
}
|
|
2140
2237
|
}
|
|
2141
2238
|
// 停止条件 3: context overflow (compact 后还超, 强制终止)
|
|
2239
|
+
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideContextOverflow 纯函数
|
|
2142
2240
|
const estimatedTokens = this.estimateHistoryTokens();
|
|
2143
|
-
|
|
2241
|
+
const overflowDecision = decideContextOverflow(estimatedTokens, this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD);
|
|
2242
|
+
if (overflowDecision.shouldExit) {
|
|
2144
2243
|
console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
|
|
2145
2244
|
onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
|
|
2146
|
-
finalResponse = finalResponse ||
|
|
2245
|
+
finalResponse = finalResponse || overflowDecision.finalAnswer;
|
|
2147
2246
|
break;
|
|
2148
2247
|
}
|
|
2149
2248
|
// 调试日志:显示每次循环开始
|
|
@@ -2202,12 +2301,16 @@ ${toolDefs}
|
|
|
2202
2301
|
// 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
|
|
2203
2302
|
const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream);
|
|
2204
2303
|
const reply = (response.reply || '').trim();
|
|
2304
|
+
// 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
|
|
2305
|
+
const nativeToolCalls = response.toolCalls;
|
|
2205
2306
|
// 2026-06-19 架构 fix: 不再因 [AI 服务调用失败] break
|
|
2206
2307
|
// 旧逻辑: sentinel → aiFailed=true → break → 外层 retry 整个 loop (重置 history)
|
|
2207
2308
|
// 新逻辑: 把错误当 tool_result push 进 history → 下一轮 LLM 看到错误能反思重试
|
|
2208
2309
|
// 这是 dive-into 文档的"fail-open error recovery" — 错误进入 context, 不让 LLM 重复犯同样错
|
|
2209
2310
|
if (reply.startsWith('[AI 服务调用失败]')) {
|
|
2210
2311
|
console.log(`[PiAgent] 收到 AI 错误 sentinel, 推到 history 让 LLM 反思, 继续 loop`);
|
|
2312
|
+
console.log(`[sentinel DEBUG] 完整 reply: ${reply}`);
|
|
2313
|
+
console.log(`[sentinel DEBUG] 上一轮 messages 数量: ${Array.isArray(messages) ? messages.length : 'N/A'}, systemPrompt 长度: ${systemPrompt.length}`);
|
|
2211
2314
|
aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
|
|
2212
2315
|
totalErrors++;
|
|
2213
2316
|
consecutiveErrors++;
|
|
@@ -2234,9 +2337,10 @@ ${toolDefs}
|
|
|
2234
2337
|
// 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
|
|
2235
2338
|
// 现在: 先尝试解析 tool_call, 有就执行; 没有才检查是不是真正的 final gen
|
|
2236
2339
|
const toolCall = this.parseToolCall(reply);
|
|
2237
|
-
// 2026-06-
|
|
2238
|
-
//
|
|
2340
|
+
// 2026-06-30 修: 给 toolCall 分配稳定 id, 让后续 tool result 能引用同一个 id
|
|
2341
|
+
// OpenAI 协议要求 messages 里 tool result 必须有对应的 tool_call_id, 否则 400
|
|
2239
2342
|
if (toolCall) {
|
|
2343
|
+
toolCall.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2240
2344
|
this.messageHistory.push({
|
|
2241
2345
|
role: 'assistant',
|
|
2242
2346
|
content: reply,
|
|
@@ -2402,7 +2506,7 @@ ${toolDefs}
|
|
|
2402
2506
|
catch (err) {
|
|
2403
2507
|
console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
|
|
2404
2508
|
}
|
|
2405
|
-
this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
|
|
2509
|
+
this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
|
|
2406
2510
|
this.logToHarness(toolCall.name, toolCall.args, result);
|
|
2407
2511
|
// 通知前端工具执行结果
|
|
2408
2512
|
if (onStream) {
|
|
@@ -2522,6 +2626,13 @@ ${toolDefs}
|
|
|
2522
2626
|
// LLM 不能提前 final_gen — harness 自动注入"请汇报剩余工具结果" hint 再 continue
|
|
2523
2627
|
// 这是 dive-into 文档"step 9 stop condition check" 的具体化:
|
|
2524
2628
|
// stop condition = (有工具结果未汇报) ? continue : break
|
|
2629
|
+
// 检查回复是否包含工具结果内容(避免无限循环)
|
|
2630
|
+
const hasToolResultContent = this.successfulToolResults.some(r => reply.includes(r.tool) || reply.includes(r.outputPreview.substring(0, 50)));
|
|
2631
|
+
// 如果回复包含工具结果内容,清除 successfulToolResults
|
|
2632
|
+
if (hasToolResultContent) {
|
|
2633
|
+
console.log(`[PiAgent] 回复包含工具结果内容, 清除 successfulToolResults (${this.successfulToolResults.length} 个)`);
|
|
2634
|
+
this.successfulToolResults = [];
|
|
2635
|
+
}
|
|
2525
2636
|
if (this.successfulToolResults.length > 0 && iteration < this.MAX_REACT_ITERATIONS) {
|
|
2526
2637
|
const unreported = this.successfulToolResults.length;
|
|
2527
2638
|
console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报, push hint 让其继续`);
|
|
@@ -2680,18 +2791,45 @@ Workspace root folder: ${this.cwd}
|
|
|
2680
2791
|
for (const m of recentMessages) {
|
|
2681
2792
|
const role = m.role;
|
|
2682
2793
|
let content = m.content;
|
|
2683
|
-
// tool role
|
|
2794
|
+
// 2026-06-30 修: OpenAI 协议 tool role 必须带 tool_call_id, 否则 minimax (OpenAI-compatible) 报 400
|
|
2795
|
+
// bolloon 之前把所有 tool result 包成 "[工具结果] ..." 当 user/assistant role 发, minimax 严格校验失败
|
|
2796
|
+
// 现在: 保留 role='tool' + 加 tool_call_id 字段 (用 messageHistory 里自己生成的 id)
|
|
2684
2797
|
if (role === 'tool') {
|
|
2685
2798
|
const result = m.toolResult ? JSON.stringify(m.toolResult) : content;
|
|
2686
2799
|
content = `[工具结果] ${result}`;
|
|
2800
|
+
// MiniMax 等 API 不支持 tool role, 转为 user role
|
|
2801
|
+
out.push({ role: 'user', content });
|
|
2802
|
+
continue;
|
|
2687
2803
|
}
|
|
2688
2804
|
// system role (router hint 等) 直接保留
|
|
2689
2805
|
if (role === 'system') {
|
|
2690
2806
|
out.push({ role: 'system', content });
|
|
2691
2807
|
continue;
|
|
2692
2808
|
}
|
|
2693
|
-
// assistant
|
|
2694
|
-
|
|
2809
|
+
// 2026-06-30 修: assistant 消息如果带 toolCall (bolloon 内部对象), emit OpenAI 协议的 tool_calls 数组
|
|
2810
|
+
// minimax 严格要求 assistant 消息含 tool_calls 字段, 后续 tool result 才能引用 tool_call_id
|
|
2811
|
+
if (role === 'assistant') {
|
|
2812
|
+
const tc = m.toolCall;
|
|
2813
|
+
if (tc && tc.id) {
|
|
2814
|
+
out.push({
|
|
2815
|
+
role: 'assistant',
|
|
2816
|
+
content: content || '',
|
|
2817
|
+
tool_calls: [{
|
|
2818
|
+
id: tc.id,
|
|
2819
|
+
type: 'function',
|
|
2820
|
+
function: {
|
|
2821
|
+
name: tc.name,
|
|
2822
|
+
arguments: JSON.stringify(tc.args || {}),
|
|
2823
|
+
},
|
|
2824
|
+
}],
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
else {
|
|
2828
|
+
out.push({ role, content });
|
|
2829
|
+
}
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
if (role === 'user') {
|
|
2695
2833
|
out.push({ role, content });
|
|
2696
2834
|
}
|
|
2697
2835
|
}
|
|
@@ -2778,7 +2916,8 @@ Workspace root folder: ${this.cwd}
|
|
|
2778
2916
|
// M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
|
|
2779
2917
|
// 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
|
|
2780
2918
|
const response = await llm.chat(contextOrMessages, systemPrompt, signal);
|
|
2781
|
-
|
|
2919
|
+
// 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
|
|
2920
|
+
return { reply: response.reply || '', toolCalls: response.toolCalls };
|
|
2782
2921
|
}
|
|
2783
2922
|
catch (err) {
|
|
2784
2923
|
// 用户主动 abort: 不重试, 立即抛
|
|
@@ -2904,225 +3043,22 @@ Workspace root folder: ${this.cwd}
|
|
|
2904
3043
|
}
|
|
2905
3044
|
}
|
|
2906
3045
|
isFinalResponse(content) {
|
|
2907
|
-
// 2026-06-
|
|
2908
|
-
|
|
2909
|
-
// 也会被识别成终止信号, 工具调用直接跳过了.
|
|
2910
|
-
const stripped = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
|
2911
|
-
return stripped.includes('<final gen>');
|
|
3046
|
+
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
3047
|
+
return isFinalResponseImpl(content, this._parseCtx());
|
|
2912
3048
|
}
|
|
2913
3049
|
extractFinalAnswer(content) {
|
|
2914
|
-
//
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
content = after;
|
|
2923
|
-
}
|
|
2924
|
-
else {
|
|
2925
|
-
content = content.substring(0, markerIndex).trim();
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
// 移除任何 tool call 标记
|
|
2929
|
-
let cleaned = content
|
|
2930
|
-
.replace(/调用工具[::]\s*\w+\s*\([^)]*\)/g, '')
|
|
2931
|
-
.replace(/使用工具[::]\s*\w+\s*\([^)]*\)/g, '')
|
|
2932
|
-
.replace(/tool[_\w]*[::]\s*\w+\s*\([^)]*\)/gi, '')
|
|
2933
|
-
.trim();
|
|
2934
|
-
return cleaned;
|
|
3050
|
+
// 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
|
|
3051
|
+
return extractFinalAnswerImpl(content);
|
|
3052
|
+
}
|
|
3053
|
+
_parseCtx() {
|
|
3054
|
+
return {
|
|
3055
|
+
tools: new Set(Array.from(this.tools.keys())),
|
|
3056
|
+
resolveAlias: (name) => this.resolveToolName(name),
|
|
3057
|
+
};
|
|
2935
3058
|
}
|
|
2936
3059
|
parseToolCall(content) {
|
|
2937
|
-
// 2026-06-
|
|
2938
|
-
|
|
2939
|
-
// {"name": "shell_exec", "arguments": {"command": "git", "args": ["status"]}} (OpenAI 标准)
|
|
2940
|
-
// {"name": "shell_exec", "input": {"command": "git", "args": ["status"]}} (Anthropic/Minimax 风格)
|
|
2941
|
-
// 常见包裹: ```json\n{...}\n``` (markdown code block), <tool_call>{...}</tool_call> (OpenAI Hermes)
|
|
2942
|
-
const jsonPatterns = [
|
|
2943
|
-
// markdown json code block + OpenAI <tool_call> 块, 同时匹配 arguments/input 字段
|
|
2944
|
-
/(?:```(?:json|json5)?\s*\n?)?\{[\s\S]*?"name"\s*:\s*["'](\w+)["']\s*,\s*["']?(?:arguments|input)["']?\s*:\s*(\{[\s\S]*?\})\s*\}/,
|
|
2945
|
-
];
|
|
2946
|
-
for (const p of jsonPatterns) {
|
|
2947
|
-
const m = content.match(p);
|
|
2948
|
-
console.log(`[parseToolCall DEBUG] jsonPattern match: ${m ? `name=${m[1]}, args=${JSON.stringify(m[2]).slice(0, 80)}` : 'null'}`);
|
|
2949
|
-
if (m) {
|
|
2950
|
-
const name = m[1];
|
|
2951
|
-
let args = {};
|
|
2952
|
-
try {
|
|
2953
|
-
const parsed = JSON.parse(m[2]);
|
|
2954
|
-
if (parsed && typeof parsed === 'object') {
|
|
2955
|
-
args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
catch { /* 解析失败保持 args={} */ }
|
|
2959
|
-
// 2026-06-19: 自动 split command 字段 — LLM 常把 "git status" 整体当 command
|
|
2960
|
-
// shell_exec execute 要求 command="git", args="status" 拆分形式
|
|
2961
|
-
// 这里检测 command 含空格且 args 字段空, 自动按空格 split
|
|
2962
|
-
if (typeof args.command === 'string' && args.command.includes(' ') && !args.args) {
|
|
2963
|
-
const parts = args.command.split(/\s+/);
|
|
2964
|
-
args.command = parts[0];
|
|
2965
|
-
args.args = parts.slice(1).join(' ');
|
|
2966
|
-
}
|
|
2967
|
-
const resolved = this.resolveToolName(name);
|
|
2968
|
-
if (resolved) {
|
|
2969
|
-
return { name: resolved, args };
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
// 2026-06-19 修: fallback — LLM 有时输出 <tool_name>shell_exec</tool_name> 这种伪 XML 标签
|
|
2974
|
-
// 旧正则 /<(\w+)>([\s\S]*?)<\/\1>/ 会匹配, 但 name="tool_name" 不在 tools 里
|
|
2975
|
-
// 这里 detect 内容是否像 tool_call (有 <command>/<args>/<path> 子标签) 然后按 <command> 第一词推断工具
|
|
2976
|
-
const xmlTagMatch = content.match(/<(\w+)>([\s\S]*?)<\/\1>/);
|
|
2977
|
-
if (xmlTagMatch) {
|
|
2978
|
-
const outerTag = xmlTagMatch[1];
|
|
2979
|
-
const inner = xmlTagMatch[2];
|
|
2980
|
-
// 如果外层标签不在 tools 里但有 <command>/<args> 子标签, 尝试按内容推断
|
|
2981
|
-
if (!this.resolveToolName(outerTag)) {
|
|
2982
|
-
const cmdMatch = inner.match(/<command>(\w+)<\/command>/);
|
|
2983
|
-
if (cmdMatch) {
|
|
2984
|
-
const cmd = cmdMatch[1];
|
|
2985
|
-
// 推测工具: git/npx/npm/tsx → shell_exec; cat/head/tail → read_document
|
|
2986
|
-
if (['git', 'npx', 'npm', 'tsx', 'tsc', 'vitest', 'node', 'mkdir', 'touch', 'ls', 'echo', 'cat', 'head', 'tail', 'wc', 'pwd', 'date'].includes(cmd)) {
|
|
2987
|
-
const args = {};
|
|
2988
|
-
const argsMatch = inner.match(/<args>([\s\S]*?)<\/args>/);
|
|
2989
|
-
if (argsMatch)
|
|
2990
|
-
args.args = argsMatch[1].trim();
|
|
2991
|
-
const cmdsMatch = inner.match(/<command>([\s\S]*?)<\/command>/);
|
|
2992
|
-
if (cmdsMatch)
|
|
2993
|
-
args.command = cmdsMatch[1].trim();
|
|
2994
|
-
return { name: 'shell_exec', args };
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
// 2026-06-19 修: <tools:call name="X">...</tools:call> 嵌套格式
|
|
3000
|
-
// LLM 实际产出变体: <tools:call name="shell_exec"><tools:call name="command">git</tools:call>...</tools:call>
|
|
3001
|
-
const toolsCallMatch = content.match(/<tools:call\s+name=["'](\w+)["']>([\s\S]*?)<\/tools:call>/);
|
|
3002
|
-
if (toolsCallMatch) {
|
|
3003
|
-
const name = toolsCallMatch[1];
|
|
3004
|
-
const inner = toolsCallMatch[2];
|
|
3005
|
-
const args = {};
|
|
3006
|
-
const argTags = inner.matchAll(/<tools:call\s+name=["'](\w+)["']>([\s\S]*?)<\/tools:call>/g);
|
|
3007
|
-
for (const m of argTags) {
|
|
3008
|
-
args[m[1]] = m[2].trim();
|
|
3009
|
-
}
|
|
3010
|
-
const resolved = this.resolveToolName(name);
|
|
3011
|
-
if (resolved) {
|
|
3012
|
-
return { name: resolved, args };
|
|
3013
|
-
}
|
|
3014
|
-
// 兜底: 如果外层 tool name 不在 tools, 但内层有 command, 推断 shell_exec
|
|
3015
|
-
if (args.command) {
|
|
3016
|
-
const cmdFirst = args.command.split(/\s+/)[0];
|
|
3017
|
-
if (['git', 'npx', 'npm', 'tsx', 'tsc', 'vitest', 'node', 'mkdir', 'touch', 'ls', 'echo', 'cat', 'head', 'tail', 'wc', 'pwd', 'date'].includes(cmdFirst)) {
|
|
3018
|
-
return { name: 'shell_exec', args };
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
// 2026-06-19 修: 先剥离 <think>...</think> 思考块, 避免旧正则 `<(\w+)>([\s\S]*?)<\/\1>` 优先匹配到 <think>
|
|
3023
|
-
// 之前 LLM 输出含 <think>...</think><invoke name="X">...</invoke> 时, <think> 先 match, name="think" 不在 tools → return null → 后续 <invoke> 没机会 match
|
|
3024
|
-
const strippedContent = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
|
3025
|
-
const patterns = [
|
|
3026
|
-
// 2026-06-19 修: minimax/Hermes 自闭合 XML 格式 <invoke name="X">...</invoke>
|
|
3027
|
-
// 实际 LLM 习惯: <invoke name="shell_exec"><command>sed</command><args>["-n","1060,1095p"]</args></invoke>
|
|
3028
|
-
// 必须放在最前 — 旧正则 /<(\w+)>([\s\S]*?)<\/\1>/ 会匹配到外层 <invoke> 但 name 是 "invoke" 而不是 "shell_exec"
|
|
3029
|
-
new RegExp('<invoke\\s+name=["\']([\\w]+)["\']>([\\s\\S]*?)</invoke>'),
|
|
3030
|
-
// 2026-06-19 修: <function_calls> 包裹
|
|
3031
|
-
new RegExp('<function_calls>[\\s\\S]*?<invoke\\s+name=["\']([\\w]+)["\']>([\\s\\S]*?)</invoke>[\\s\\S]*?</function_calls>'),
|
|
3032
|
-
/调用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
|
|
3033
|
-
/使用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
|
|
3034
|
-
/tool[_\w]*[::]\s*(\w+)\s*\(([^)]*)\)/i,
|
|
3035
|
-
/(\w+)\s*\(\s*([^)]*)\s*\)/,
|
|
3036
|
-
// 兼容 LLM 输出的对象字面量格式: {tool => "get_identity", args => {...}}
|
|
3037
|
-
/\{\s*tool\s*=>\s*["'](\w+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/,
|
|
3038
|
-
// 兼容: tool => "get_identity" (无 args 包裹)
|
|
3039
|
-
/\btool\s*=>\s*["'](\w+)["']/,
|
|
3040
|
-
// 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
|
|
3041
|
-
/\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
|
|
3042
|
-
// 2026-06-19: 兼容 LLM 输出 "tool_name {json_args}" 形式 (单行, 无 name 字段)
|
|
3043
|
-
// 实际输出: write_file {"path":"docs/test.md","content":"..."}
|
|
3044
|
-
// 只在行首/新行匹配, 避免误匹配普通文本里的 "foo {...}"
|
|
3045
|
-
/(?:^|\n)(\w+)\s+(\{[\s\S]*?\})(?=\n|$)/,
|
|
3046
|
-
// 2026-06-15 修: 兼容 LLM 输出的 XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
|
|
3047
|
-
// 实际 LLM 习惯: <shell_exec>\n<command>ls</command>\n<args>["-la", "..."]</args>\n</shell_exec>
|
|
3048
|
-
/<(\w+)>([\s\S]*?)<\/\1>/,
|
|
3049
|
-
];
|
|
3050
|
-
for (const pattern of patterns) {
|
|
3051
|
-
const match = strippedContent.match(pattern);
|
|
3052
|
-
if (match) {
|
|
3053
|
-
const name = match[1];
|
|
3054
|
-
let args = {};
|
|
3055
|
-
console.log(`[parseToolCall DEBUG] pattern matched: name=${name}, rawArgs=${(match[2] || '').slice(0, 100)}`);
|
|
3056
|
-
const rawArgs = match[2] || '';
|
|
3057
|
-
if (rawArgs && rawArgs.trim().startsWith('{')) {
|
|
3058
|
-
// JSON 形式, 尝试解析
|
|
3059
|
-
try {
|
|
3060
|
-
const parsed = JSON.parse(rawArgs);
|
|
3061
|
-
if (parsed && typeof parsed === 'object') {
|
|
3062
|
-
args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
catch {
|
|
3066
|
-
// 解析失败就当字符串处理
|
|
3067
|
-
const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
|
|
3068
|
-
for (const pair of argPairs) {
|
|
3069
|
-
const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
|
|
3070
|
-
if (key)
|
|
3071
|
-
args[key] = valueParts.join(':') || '';
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
|
|
3076
|
-
// 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
|
|
3077
|
-
// 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
|
|
3078
|
-
// 2026-06-19 修: 也支持 <parameter name="X">value</parameter> 形式 (OpenAI Hermes function_call)
|
|
3079
|
-
const paramRe = /<parameter\s+name=["'](\w+)["']>([\s\S]*?)<\/parameter>/g;
|
|
3080
|
-
let pMatch;
|
|
3081
|
-
while ((pMatch = paramRe.exec(rawArgs)) !== null) {
|
|
3082
|
-
const argName = pMatch[1];
|
|
3083
|
-
const argValue = pMatch[2].trim();
|
|
3084
|
-
if (argName && argValue) {
|
|
3085
|
-
args[argName] = argValue;
|
|
3086
|
-
}
|
|
3087
|
-
}
|
|
3088
|
-
// 如果没匹配到 <parameter>, 用普通 XML 子标签
|
|
3089
|
-
if (Object.keys(args).length === 0) {
|
|
3090
|
-
const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
|
3091
|
-
let xmlMatch;
|
|
3092
|
-
while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
|
|
3093
|
-
const argName = xmlMatch[1];
|
|
3094
|
-
const argValue = xmlMatch[2].trim();
|
|
3095
|
-
if (argName && argValue) {
|
|
3096
|
-
args[argName] = argValue;
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
else if (rawArgs) {
|
|
3102
|
-
// 形参串, 形如 key: value, key2: value2
|
|
3103
|
-
const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
|
|
3104
|
-
for (const pair of argPairs) {
|
|
3105
|
-
const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
|
|
3106
|
-
if (key)
|
|
3107
|
-
args[key] = valueParts.join(':') || '';
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
if (this.tools.has(name)) {
|
|
3111
|
-
// 2026-06-19: auto-split command field — shell_exec expects command='git', args='status' split
|
|
3112
|
-
if (typeof args.command === 'string' && args.command.includes(' ') && !args.args) {
|
|
3113
|
-
const parts = args.command.split(/\s+/);
|
|
3114
|
-
args.command = parts[0];
|
|
3115
|
-
args.args = parts.slice(1).join(' ');
|
|
3116
|
-
}
|
|
3117
|
-
return { name, args };
|
|
3118
|
-
}
|
|
3119
|
-
const resolved = this.resolveToolName(name);
|
|
3120
|
-
if (resolved) {
|
|
3121
|
-
return { name: resolved, args };
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
return null;
|
|
3060
|
+
// 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
|
|
3061
|
+
return parseToolCallImpl(content, this._parseCtx());
|
|
3126
3062
|
}
|
|
3127
3063
|
// [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
|
|
3128
3064
|
_dbgParseToolCall(content) {
|
|
@@ -3384,47 +3320,15 @@ Workspace root folder: ${this.cwd}
|
|
|
3384
3320
|
* bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
|
|
3385
3321
|
* 返回 this.tools 里的标准名, 或 null 表示未识别
|
|
3386
3322
|
*/
|
|
3323
|
+
/**
|
|
3324
|
+
* 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
|
|
3325
|
+
* bolloon 注册的标准工具名.
|
|
3326
|
+
*
|
|
3327
|
+
* 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
|
|
3328
|
+
* 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
|
|
3329
|
+
*/
|
|
3387
3330
|
resolveToolName(name) {
|
|
3388
|
-
|
|
3389
|
-
return name;
|
|
3390
|
-
const lower = name.toLowerCase();
|
|
3391
|
-
const aliasMap = {
|
|
3392
|
-
// Claude Code 风格 → bolloon 工具
|
|
3393
|
-
read: 'read_file',
|
|
3394
|
-
edit: 'edit_file',
|
|
3395
|
-
write: 'write_file',
|
|
3396
|
-
rm: 'delete_file',
|
|
3397
|
-
mv: 'move_file',
|
|
3398
|
-
bash: 'shell_exec',
|
|
3399
|
-
shell: 'shell_exec',
|
|
3400
|
-
sh: 'shell_exec',
|
|
3401
|
-
// grep / glob 现在是独立工具
|
|
3402
|
-
cat: 'read_file',
|
|
3403
|
-
// 测试/编译 → 独立工具
|
|
3404
|
-
test: 'vitest_run',
|
|
3405
|
-
vitest: 'vitest_run',
|
|
3406
|
-
typecheck: 'tsc_check',
|
|
3407
|
-
tsc: 'tsc_check',
|
|
3408
|
-
// git 操作
|
|
3409
|
-
log: 'git_log',
|
|
3410
|
-
show: 'git_show',
|
|
3411
|
-
diff: 'git_diff',
|
|
3412
|
-
commit: 'git_commit',
|
|
3413
|
-
push: 'git_push',
|
|
3414
|
-
branch: 'git_branch',
|
|
3415
|
-
checkout: 'git_branch',
|
|
3416
|
-
stash: 'git_stash',
|
|
3417
|
-
// 任务管理 (todo)
|
|
3418
|
-
todo_write: 'create_task',
|
|
3419
|
-
todowrite: 'create_task',
|
|
3420
|
-
task: 'create_task',
|
|
3421
|
-
};
|
|
3422
|
-
const aliased = aliasMap[lower];
|
|
3423
|
-
if (aliased && this.tools.has(aliased))
|
|
3424
|
-
return aliased;
|
|
3425
|
-
if (this.tools.has(lower))
|
|
3426
|
-
return lower;
|
|
3427
|
-
return null;
|
|
3331
|
+
return this._toolRegistry.resolve(name);
|
|
3428
3332
|
}
|
|
3429
3333
|
/**
|
|
3430
3334
|
* 2026-06-19: 注册 P2P 消息 listener, 把收到的消息存到 _inboxMessages 供 check_inbox 读.
|
|
@@ -3999,11 +3903,14 @@ export async function createAgentSession(config, forceNew) {
|
|
|
3999
3903
|
const key = config.peerId;
|
|
4000
3904
|
if (!forceNew && independentSessions.has(key)) {
|
|
4001
3905
|
console.log(`[createAgentSession] 找到现有独立 session, key=${key}`);
|
|
4002
|
-
|
|
3906
|
+
const existing = independentSessions.get(key);
|
|
3907
|
+
await existing.whenReady();
|
|
3908
|
+
return existing;
|
|
4003
3909
|
}
|
|
4004
3910
|
const session = new PiAgentSession(config);
|
|
4005
3911
|
independentSessions.set(key, session);
|
|
4006
3912
|
console.log(`[createAgentSession] 创建独立 session, key=${key}, DID=${incomingDid}`);
|
|
3913
|
+
await session.whenReady();
|
|
4007
3914
|
return session;
|
|
4008
3915
|
}
|
|
4009
3916
|
// 如果指定了 forceNew 但没有 peerId,生成带时间戳的 key
|
|
@@ -4012,6 +3919,7 @@ export async function createAgentSession(config, forceNew) {
|
|
|
4012
3919
|
const session = new PiAgentSession(config);
|
|
4013
3920
|
independentSessions.set(key, session);
|
|
4014
3921
|
console.log(`[createAgentSession] 创建强制新 session, key=${key}`);
|
|
3922
|
+
await session.whenReady();
|
|
4015
3923
|
return session;
|
|
4016
3924
|
}
|
|
4017
3925
|
// 如果有新的 DID,强制重建 session
|
|
@@ -4031,11 +3939,13 @@ export async function createAgentSession(config, forceNew) {
|
|
|
4031
3939
|
createdAt: Date.now()
|
|
4032
3940
|
});
|
|
4033
3941
|
}
|
|
3942
|
+
await sessionInstance.whenReady();
|
|
4034
3943
|
return sessionInstance;
|
|
4035
3944
|
}
|
|
4036
3945
|
sessionInstance = new PiAgentSession(config);
|
|
4037
3946
|
lastIdentityDid = config.identityDoc?.did || null;
|
|
4038
3947
|
console.log(`[createAgentSession] 新建 session, DID=${lastIdentityDid}`);
|
|
3948
|
+
await sessionInstance.whenReady();
|
|
4039
3949
|
return sessionInstance;
|
|
4040
3950
|
}
|
|
4041
3951
|
export function getAgentSession() {
|