@phuetz/code-buddy 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -164
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +101 -13
- package/dist/agent/execution/agent-executor.js +122 -12
- package/dist/agent/execution/tool-dependency-graph.js +7 -0
- package/dist/agent/execution/tool-hooks.d.ts +6 -0
- package/dist/agent/execution/tool-hooks.js +13 -5
- package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
- package/dist/agent/execution/tool-selection-strategy.js +67 -10
- package/dist/agent/extended-thinking.d.ts +8 -0
- package/dist/agent/extended-thinking.js +27 -0
- package/dist/agent/hermes-browser-backends.js +151 -11
- package/dist/agent/hermes-claw-migrate.d.ts +59 -0
- package/dist/agent/hermes-claw-migrate.js +427 -26
- package/dist/agent/hermes-parity-manifest.js +78 -71
- package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
- package/dist/agent/hermes-runtime-lifecycle.js +468 -0
- package/dist/agent/lesson-auto-proposer.js +10 -0
- package/dist/agent/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/session-duration.d.ts +36 -0
- package/dist/agent/middleware/session-duration.js +78 -0
- package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
- package/dist/agent/middleware/visual-validation-middleware.js +67 -0
- package/dist/agent/model-benchmark.d.ts +77 -0
- package/dist/agent/model-benchmark.js +309 -0
- package/dist/agent/model-tier.d.ts +14 -0
- package/dist/agent/model-tier.js +71 -0
- package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
- package/dist/agent/multi-agent/multi-agent-system.js +3 -1
- package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
- package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
- package/dist/agent/multi-agent/session-tools.js +22 -5
- package/dist/agent/reasoning/mcts.js +20 -6
- package/dist/agent/session-end-flush.d.ts +71 -0
- package/dist/agent/session-end-flush.js +233 -0
- package/dist/agent/streaming/streaming-handler.d.ts +7 -0
- package/dist/agent/streaming/streaming-handler.js +8 -1
- package/dist/agent/tool-executor.js +1 -0
- package/dist/agent/tool-handler.d.ts +5 -0
- package/dist/agent/tool-handler.js +130 -4
- package/dist/browser-automation/browser-operator-executor.js +15 -0
- package/dist/browser-automation/browser-use-runner.d.ts +96 -0
- package/dist/browser-automation/browser-use-runner.js +492 -0
- package/dist/browser-automation/camofox-runner.d.ts +107 -0
- package/dist/browser-automation/camofox-runner.js +287 -0
- package/dist/channels/dingtalk/index.js +3 -0
- package/dist/channels/discord/client.d.ts +9 -0
- package/dist/channels/discord/client.js +12 -0
- package/dist/channels/feishu/index.d.ts +167 -1
- package/dist/channels/feishu/index.js +432 -7
- package/dist/channels/gateway-lifecycle.d.ts +132 -0
- package/dist/channels/gateway-lifecycle.js +219 -0
- package/dist/channels/google-chat/index.d.ts +5 -1
- package/dist/channels/google-chat/index.js +5 -1
- package/dist/channels/imessage/index.d.ts +14 -0
- package/dist/channels/imessage/index.js +64 -17
- package/dist/channels/index.d.ts +4 -0
- package/dist/channels/index.js +4 -0
- package/dist/channels/irc/index.d.ts +73 -3
- package/dist/channels/irc/index.js +446 -11
- package/dist/channels/line/index.js +3 -0
- package/dist/channels/mattermost/index.d.ts +51 -4
- package/dist/channels/mattermost/index.js +303 -20
- package/dist/channels/nextcloud-talk/index.d.ts +89 -8
- package/dist/channels/nextcloud-talk/index.js +367 -16
- package/dist/channels/nostr/index.d.ts +121 -1
- package/dist/channels/nostr/index.js +396 -8
- package/dist/channels/ntfy/index.js +3 -0
- package/dist/channels/qq/index.js +3 -0
- package/dist/channels/slash-parity.d.ts +117 -0
- package/dist/channels/slash-parity.js +185 -0
- package/dist/channels/synology-chat/index.js +3 -0
- package/dist/channels/teams/index.d.ts +3 -0
- package/dist/channels/teams/index.js +3 -0
- package/dist/channels/telegram/client.d.ts +9 -0
- package/dist/channels/telegram/client.js +12 -0
- package/dist/channels/twilio-voice/index.js +3 -0
- package/dist/channels/webchat/index.d.ts +7 -1
- package/dist/channels/webchat/index.js +7 -1
- package/dist/channels/wecom/index.js +3 -0
- package/dist/channels/weixin/index.js +3 -0
- package/dist/channels/zalo/index.js +3 -0
- package/dist/cli/config-loader.js +1 -1
- package/dist/codebuddy/client.d.ts +30 -1
- package/dist/codebuddy/client.js +167 -20
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
- package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
- package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
- package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -1
- package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
- package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
- package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
- package/dist/codebuddy/tools.d.ts +1 -1
- package/dist/codebuddy/tools.js +27 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/native-engine-commands.js +107 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +5 -0
- package/dist/commands/goal-cli.d.ts +71 -0
- package/dist/commands/goal-cli.js +280 -0
- package/dist/commands/handlers/goal-handler.d.ts +34 -0
- package/dist/commands/handlers/goal-handler.js +155 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/headless-slash.d.ts +2 -0
- package/dist/commands/headless-slash.js +12 -0
- package/dist/commands/llm-provider-resolution.d.ts +5 -3
- package/dist/commands/llm-provider-resolution.js +87 -33
- package/dist/commands/ollama.d.ts +25 -0
- package/dist/commands/ollama.js +100 -0
- package/dist/commands/provider.d.ts +5 -0
- package/dist/commands/provider.js +106 -55
- package/dist/commands/slash/builtin-commands.js +20 -0
- package/dist/commands/spec-next.js +2 -1
- package/dist/commands/spec-plan.js +5 -16
- package/dist/commands/tunnel.d.ts +2 -0
- package/dist/commands/tunnel.js +48 -0
- package/dist/config/config-resolver.d.ts +2 -1
- package/dist/config/config-resolver.js +54 -41
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/env-schema.js +770 -0
- package/dist/config/feature-flags.js +7 -0
- package/dist/config/model-tools.js +4 -4
- package/dist/config/toml-config.d.ts +16 -0
- package/dist/config/toml-config.js +3 -0
- package/dist/context/context-manager-v2.d.ts +39 -0
- package/dist/context/context-manager-v2.js +91 -0
- package/dist/daemon/agent-task-executor.js +12 -1
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +5 -3
- package/dist/daemon/autonomous-loop.d.ts +21 -1
- package/dist/daemon/autonomous-loop.js +69 -0
- package/dist/daemon/colab-goal.d.ts +38 -0
- package/dist/daemon/colab-goal.js +81 -0
- package/dist/daemon/cron-agent-bridge.d.ts +12 -3
- package/dist/daemon/cron-agent-bridge.js +25 -9
- package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
- package/dist/desktop/codebuddy-engine-adapter.js +257 -80
- package/dist/desktop/engine-adapter.d.ts +14 -0
- package/dist/desktop-automation/automation-manager.js +16 -0
- package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
- package/dist/desktop-automation/omniparser-runner.js +115 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +72 -5
- package/dist/doctor/index.js +40 -23
- package/dist/events/types.d.ts +11 -0
- package/dist/fleet/colab-store.d.ts +21 -0
- package/dist/fleet/colab-store.js +28 -0
- package/dist/fleet/peer-session-bridge.d.ts +1 -1
- package/dist/fleet/peer-session-bridge.js +243 -2
- package/dist/fleet/peer-session-store.d.ts +3 -0
- package/dist/fleet/peer-tool-bridge.js +14 -0
- package/dist/fleet/privacy-lint.d.ts +8 -0
- package/dist/fleet/privacy-lint.js +22 -0
- package/dist/goals/goal-decomposer.d.ts +27 -0
- package/dist/goals/goal-decomposer.js +293 -0
- package/dist/goals/goal-judge-client.d.ts +15 -0
- package/dist/goals/goal-judge-client.js +45 -0
- package/dist/goals/goal-judge.d.ts +38 -0
- package/dist/goals/goal-judge.js +158 -0
- package/dist/goals/goal-loop.d.ts +37 -0
- package/dist/goals/goal-loop.js +101 -0
- package/dist/goals/goal-manager.d.ts +77 -0
- package/dist/goals/goal-manager.js +289 -0
- package/dist/goals/goal-state.d.ts +99 -0
- package/dist/goals/goal-state.js +315 -0
- package/dist/goals/goal-store.d.ts +28 -0
- package/dist/goals/goal-store.js +87 -0
- package/dist/goals/index.d.ts +5 -0
- package/dist/goals/index.js +6 -0
- package/dist/hooks/use-input-handler.js +36 -1
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/index.js +506 -25
- package/dist/input/text-to-speech.d.ts +2 -6
- package/dist/input/text-to-speech.js +2 -27
- package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
- package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
- package/dist/integrations/tailscale.d.ts +13 -0
- package/dist/integrations/tailscale.js +87 -34
- package/dist/knowledge/workspace-indexer.js +53 -9
- package/dist/mcp/client.js +1 -0
- package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
- package/dist/mcp/mcp-desktop-tools.js +158 -0
- package/dist/mcp/mcp-server.js +2 -0
- package/dist/memory/enhanced-memory.d.ts +10 -0
- package/dist/memory/enhanced-memory.js +33 -2
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.js +4 -1
- package/dist/memory/memory-auto-proposer.d.ts +23 -0
- package/dist/memory/memory-auto-proposer.js +308 -0
- package/dist/memory/memory-candidate-queue.d.ts +92 -0
- package/dist/memory/memory-candidate-queue.js +261 -0
- package/dist/memory/persistent-memory.d.ts +45 -2
- package/dist/memory/persistent-memory.js +236 -40
- package/dist/ml/bayesian-qualifier.d.ts +1 -1
- package/dist/ml/bayesian-qualifier.js +35 -2
- package/dist/observability/run-store.d.ts +1 -1
- package/dist/openclaw/gateway-bridge.js +5 -0
- package/dist/plugins/marketplace.d.ts +1 -0
- package/dist/plugins/marketplace.js +7 -0
- package/dist/prompts/execution-discipline.d.ts +14 -0
- package/dist/prompts/execution-discipline.js +29 -0
- package/dist/prompts/variation-injector.js +9 -2
- package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
- package/dist/protocols/acp/acp-agentic-runner.js +115 -18
- package/dist/protocols/acp/acp-session-store.d.ts +23 -0
- package/dist/protocols/acp/acp-session-store.js +77 -0
- package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
- package/dist/protocols/acp/acp-stdio-server.js +71 -16
- package/dist/providers/active-llm-registry.d.ts +37 -0
- package/dist/providers/active-llm-registry.js +186 -0
- package/dist/providers/auxiliary-provider.d.ts +25 -0
- package/dist/providers/auxiliary-provider.js +192 -0
- package/dist/providers/codex-oauth.d.ts +1 -1
- package/dist/providers/codex-oauth.js +27 -4
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/provider-catalog.d.ts +70 -0
- package/dist/providers/provider-catalog.js +738 -0
- package/dist/providers/provider-fallback.d.ts +35 -0
- package/dist/providers/provider-fallback.js +171 -0
- package/dist/providers/xai-oauth.d.ts +128 -0
- package/dist/providers/xai-oauth.js +735 -0
- package/dist/scheduler/cron-scheduler.d.ts +15 -3
- package/dist/scheduler/cron-scheduler.js +53 -7
- package/dist/scheduler/script-runner.d.ts +2 -0
- package/dist/scheduler/script-runner.js +19 -4
- package/dist/scheduler/watchdog-handlers.js +7 -20
- package/dist/search/usearch-index.js +7 -2
- package/dist/security/tool-policy/tool-groups.js +2 -0
- package/dist/server/index.js +17 -2
- package/dist/server/routes/mobile.d.ts +12 -4
- package/dist/server/routes/mobile.js +116 -1
- package/dist/server/tls-config.d.ts +35 -0
- package/dist/server/tls-config.js +142 -0
- package/dist/server/tunnel-manager.d.ts +20 -0
- package/dist/server/tunnel-manager.js +58 -0
- package/dist/server/websocket/fleet-bridge.d.ts +13 -1
- package/dist/server/websocket/fleet-bridge.js +16 -0
- package/dist/services/prompt-builder.d.ts +1 -0
- package/dist/services/prompt-builder.js +49 -8
- package/dist/shared/engine-types.d.ts +15 -1
- package/dist/sidecar/sidecar-bridge.d.ts +1 -0
- package/dist/sidecar/sidecar-bridge.js +21 -10
- package/dist/spec/spec-store.js +8 -1
- package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +8 -8
- package/dist/templates/project-scaffolding.js +3 -2
- package/dist/themes/theme-schema.d.ts +10 -10
- package/dist/tools/application-profiles.js +38 -0
- package/dist/tools/bash/bash-tool.d.ts +1 -0
- package/dist/tools/bash/bash-tool.js +16 -2
- package/dist/tools/bash/command-validator.js +3 -0
- package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
- package/dist/tools/bash/rtk-rewrite.js +100 -0
- package/dist/tools/bash/streaming-executor.js +14 -1
- package/dist/tools/computer-control-tool.d.ts +16 -1
- package/dist/tools/computer-control-tool.js +317 -5
- package/dist/tools/document-generator.d.ts +14 -0
- package/dist/tools/document-generator.js +79 -0
- package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
- package/dist/tools/execute-code-rpc-invoker.js +46 -5
- package/dist/tools/metadata.js +28 -0
- package/dist/tools/office-macro-tool.d.ts +10 -0
- package/dist/tools/office-macro-tool.js +93 -0
- package/dist/tools/registry/index.d.ts +2 -1
- package/dist/tools/registry/index.js +2 -1
- package/dist/tools/registry/memory-tools.d.ts +20 -0
- package/dist/tools/registry/memory-tools.js +225 -3
- package/dist/tools/registry/multimodal-tools.js +2 -2
- package/dist/tools/registry/vision-tools.d.ts +33 -0
- package/dist/tools/registry/vision-tools.js +308 -6
- package/dist/tools/registry/windows-tools.d.ts +2 -0
- package/dist/tools/registry/windows-tools.js +47 -0
- package/dist/tools/route-peer-tool.js +14 -0
- package/dist/tools/screenshot-tool.js +14 -2
- package/dist/tools/text-to-speech-tool.d.ts +1 -1
- package/dist/tools/text-to-speech-tool.js +2 -12
- package/dist/tools/tool-selector.js +3 -59
- package/dist/utils/config-validation/schema.d.ts +40 -7
- package/dist/utils/config-validation/schema.js +130 -1
- package/dist/utils/cost-tracker.js +21 -1
- package/dist/utils/disk-guard.d.ts +180 -0
- package/dist/utils/disk-guard.js +404 -0
- package/dist/utils/interactive-setup.js +35 -2
- package/dist/utils/model-utils.js +11 -2
- package/dist/utils/output-sanitizer.js +11 -0
- package/dist/utils/provider-detector.d.ts +8 -7
- package/dist/utils/provider-detector.js +19 -77
- package/dist/utils/settings-manager.d.ts +7 -0
- package/dist/utils/token-display.d.ts +3 -3
- package/dist/utils/token-display.js +22 -4
- package/dist/wizard/onboarding.d.ts +39 -0
- package/dist/wizard/onboarding.js +433 -21
- package/dist/wizard/provider-onboarding.d.ts +8 -23
- package/dist/wizard/provider-onboarding.js +39 -158
- package/package.json +4 -2
- package/dist/plugins/provider-onboarding.d.ts +0 -23
- package/dist/plugins/provider-onboarding.js +0 -116
- package/dist/utils/kokoro-tts.d.ts +0 -8
- package/dist/utils/kokoro-tts.js +0 -67
|
@@ -368,21 +368,36 @@ export class SessionToolExecutor {
|
|
|
368
368
|
const limit = this.parseSessionSearchLimit(args.limit);
|
|
369
369
|
const { getSessionStore } = await import('../../persistence/session-store.js');
|
|
370
370
|
const sessions = (await getSessionStore().searchSessions(query)).slice(0, limit);
|
|
371
|
+
const citations = [];
|
|
371
372
|
const formatted = sessions.map(session => {
|
|
372
373
|
const metadata = session.metadata && typeof session.metadata === 'object'
|
|
373
374
|
? session.metadata
|
|
374
375
|
: {};
|
|
376
|
+
const messageId = typeof metadata.searchMessageId === 'number' ? metadata.searchMessageId : undefined;
|
|
377
|
+
const role = typeof metadata.searchRole === 'string' ? metadata.searchRole : undefined;
|
|
378
|
+
const snippet = typeof metadata.searchSnippet === 'string' ? metadata.searchSnippet : '';
|
|
379
|
+
const citation = {
|
|
380
|
+
sessionId: session.id,
|
|
381
|
+
snippet,
|
|
382
|
+
label: messageId ? `session:${session.id}#message:${messageId}` : `session:${session.id}`,
|
|
383
|
+
};
|
|
384
|
+
if (messageId !== undefined)
|
|
385
|
+
citation.messageId = messageId;
|
|
386
|
+
if (role)
|
|
387
|
+
citation.role = role;
|
|
375
388
|
const match = {
|
|
376
|
-
snippet
|
|
389
|
+
snippet,
|
|
390
|
+
citation,
|
|
377
391
|
};
|
|
378
|
-
if (
|
|
379
|
-
match.role =
|
|
392
|
+
if (role)
|
|
393
|
+
match.role = role;
|
|
380
394
|
if (typeof metadata.searchScore === 'number')
|
|
381
395
|
match.score = metadata.searchScore;
|
|
382
|
-
if (
|
|
383
|
-
match.messageId =
|
|
396
|
+
if (messageId !== undefined)
|
|
397
|
+
match.messageId = messageId;
|
|
384
398
|
if (metadata.parentSessionId)
|
|
385
399
|
match.parentSessionId = metadata.parentSessionId;
|
|
400
|
+
citations.push(citation);
|
|
386
401
|
return {
|
|
387
402
|
id: session.id,
|
|
388
403
|
name: session.name,
|
|
@@ -391,6 +406,7 @@ export class SessionToolExecutor {
|
|
|
391
406
|
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
392
407
|
createdAt: session.createdAt.toISOString(),
|
|
393
408
|
lastAccessedAt: session.lastAccessedAt.toISOString(),
|
|
409
|
+
citation,
|
|
394
410
|
match,
|
|
395
411
|
};
|
|
396
412
|
});
|
|
@@ -398,6 +414,7 @@ export class SessionToolExecutor {
|
|
|
398
414
|
query,
|
|
399
415
|
total: formatted.length,
|
|
400
416
|
sessions: formatted,
|
|
417
|
+
citations,
|
|
401
418
|
};
|
|
402
419
|
return {
|
|
403
420
|
success: true,
|
|
@@ -487,21 +487,35 @@ export class MCTS {
|
|
|
487
487
|
findBestSolution() {
|
|
488
488
|
if (!this.root)
|
|
489
489
|
return null;
|
|
490
|
+
// Preferred tier: the best-scoring terminal-typed node (a concrete
|
|
491
|
+
// `implementation` or an explicit `conclusion`). Fallback tier: the
|
|
492
|
+
// best-scoring *derived* node of any type — many problems/models yield only
|
|
493
|
+
// analysis/verification/hypothesis thoughts, and a high-scoring one of those
|
|
494
|
+
// is still the best answer the search found. Without the fallback, such a
|
|
495
|
+
// search returns null even when a node scored 1.0. Both tiers exclude pruned
|
|
496
|
+
// nodes and the root (the problem statement, depth 0 — not a derived solution).
|
|
490
497
|
let best = null;
|
|
491
498
|
let bestScore = -1;
|
|
499
|
+
let fallback = null;
|
|
500
|
+
let fallbackScore = -1;
|
|
492
501
|
const traverse = (node) => {
|
|
493
|
-
if (node.state !== 'pruned' &&
|
|
494
|
-
(node.type === 'implementation' || node.type === 'conclusion') &&
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
502
|
+
if (node.state !== 'pruned' && node.depth > 0) {
|
|
503
|
+
if ((node.type === 'implementation' || node.type === 'conclusion') &&
|
|
504
|
+
node.score > bestScore) {
|
|
505
|
+
bestScore = node.score;
|
|
506
|
+
best = node;
|
|
507
|
+
}
|
|
508
|
+
if (node.score > fallbackScore) {
|
|
509
|
+
fallbackScore = node.score;
|
|
510
|
+
fallback = node;
|
|
511
|
+
}
|
|
498
512
|
}
|
|
499
513
|
for (const child of node.children) {
|
|
500
514
|
traverse(child);
|
|
501
515
|
}
|
|
502
516
|
};
|
|
503
517
|
traverse(this.root);
|
|
504
|
-
return best;
|
|
518
|
+
return best ?? fallback;
|
|
505
519
|
}
|
|
506
520
|
/**
|
|
507
521
|
* Get the path from root to a node
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-end flush (WS3-T1 — Mémoire & continuité du run).
|
|
3
|
+
*
|
|
4
|
+
* When a session ends, persist what the next run needs to not start from
|
|
5
|
+
* zero:
|
|
6
|
+
* 1. a short HANDOFF file (`.codebuddy/HANDOFF.md`) — last goal, last
|
|
7
|
+
* state, files touched, open risks — written synchronously so it also
|
|
8
|
+
* works from process `exit` handlers;
|
|
9
|
+
* 2. reusable lesson candidates via the existing review-gated
|
|
10
|
+
* auto-proposer (PENDING only — a human approves them into lessons.md,
|
|
11
|
+
* which the per-turn `<lessons_context>` injection then re-injects on
|
|
12
|
+
* future runs);
|
|
13
|
+
* 3. declarative long-term memory candidates via the review-gated memory
|
|
14
|
+
* queue. These do not enter prompt-injected persistent memory until a
|
|
15
|
+
* human accepts them.
|
|
16
|
+
*
|
|
17
|
+
* Guard-rails: trivial sessions are skipped (no LLM call, no file), and
|
|
18
|
+
* everything written here passes the privacy lint first (PII/secret spans
|
|
19
|
+
* are redacted from the handoff; tainted lesson candidates are dropped by
|
|
20
|
+
* the proposer itself).
|
|
21
|
+
*
|
|
22
|
+
* @module agent/session-end-flush
|
|
23
|
+
*/
|
|
24
|
+
import type { CodeBuddyClient } from '../codebuddy/client.js';
|
|
25
|
+
import type { ChatEntry } from './types.js';
|
|
26
|
+
export interface SessionEndFlushInput {
|
|
27
|
+
chatHistory: ChatEntry[];
|
|
28
|
+
workDir?: string;
|
|
29
|
+
/** Reuse the session's LLM client for the lesson proposal (no re-auth). */
|
|
30
|
+
client?: CodeBuddyClient;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
/** Session start (ms epoch) — used for the duration line of the handoff. */
|
|
33
|
+
startedAt?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface SessionEndFlushResult {
|
|
36
|
+
/** Lesson candidates enqueued for review (`buddy lessons` to approve). */
|
|
37
|
+
proposedLessons: number;
|
|
38
|
+
/** Declarative memory candidates enqueued for review (`/memory candidates`). */
|
|
39
|
+
proposedMemories: number;
|
|
40
|
+
/** Absolute path of the handoff file, when one was (re)written. */
|
|
41
|
+
handoffPath?: string;
|
|
42
|
+
openRisks: string[];
|
|
43
|
+
skipped?: 'disabled' | 'trivial';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Heuristic open-risk extraction: error-ish tool results, deduped per tool,
|
|
47
|
+
* newest kept. No LLM involved — deterministic and free.
|
|
48
|
+
*/
|
|
49
|
+
export declare function extractOpenRisks(history: ChatEntry[]): string[];
|
|
50
|
+
/** Files touched through write-ish tool calls, for the handoff. */
|
|
51
|
+
export declare function extractTouchedFiles(history: ChatEntry[]): string[];
|
|
52
|
+
export interface HandoffOptions {
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
startedAt?: number;
|
|
55
|
+
/** Force a write even below the size threshold (used by tests). */
|
|
56
|
+
force?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Write `.codebuddy/HANDOFF.md` synchronously. Returns the path when
|
|
60
|
+
* written, undefined when the session didn't warrant one. Idempotent per
|
|
61
|
+
* history array so the async flush and a process-exit fallback don't both
|
|
62
|
+
* write.
|
|
63
|
+
*/
|
|
64
|
+
export declare function writeHandoffSync(history: ChatEntry[], workDir?: string, options?: HandoffOptions): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Run the complete session-end flush. Safe to call from any exit path:
|
|
67
|
+
* no-ops on trivial sessions, never throws.
|
|
68
|
+
*/
|
|
69
|
+
export declare function runSessionEndFlush(input: SessionEndFlushInput): Promise<SessionEndFlushResult>;
|
|
70
|
+
/** Test hook: reset the per-history idempotence latch. */
|
|
71
|
+
export declare function resetSessionEndFlushState(): void;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-end flush (WS3-T1 — Mémoire & continuité du run).
|
|
3
|
+
*
|
|
4
|
+
* When a session ends, persist what the next run needs to not start from
|
|
5
|
+
* zero:
|
|
6
|
+
* 1. a short HANDOFF file (`.codebuddy/HANDOFF.md`) — last goal, last
|
|
7
|
+
* state, files touched, open risks — written synchronously so it also
|
|
8
|
+
* works from process `exit` handlers;
|
|
9
|
+
* 2. reusable lesson candidates via the existing review-gated
|
|
10
|
+
* auto-proposer (PENDING only — a human approves them into lessons.md,
|
|
11
|
+
* which the per-turn `<lessons_context>` injection then re-injects on
|
|
12
|
+
* future runs);
|
|
13
|
+
* 3. declarative long-term memory candidates via the review-gated memory
|
|
14
|
+
* queue. These do not enter prompt-injected persistent memory until a
|
|
15
|
+
* human accepts them.
|
|
16
|
+
*
|
|
17
|
+
* Guard-rails: trivial sessions are skipped (no LLM call, no file), and
|
|
18
|
+
* everything written here passes the privacy lint first (PII/secret spans
|
|
19
|
+
* are redacted from the handoff; tainted lesson candidates are dropped by
|
|
20
|
+
* the proposer itself).
|
|
21
|
+
*
|
|
22
|
+
* @module agent/session-end-flush
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
import { isFeatureEnabled } from '../config/feature-flags.js';
|
|
27
|
+
import { redactSecrets } from '../fleet/privacy-lint.js';
|
|
28
|
+
import { logger } from '../utils/logger.js';
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Gates & heuristics
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/** Below this many assistant turns the session is too trivial to flush. */
|
|
33
|
+
const MIN_ASSISTANT_TURNS = 2;
|
|
34
|
+
/** Handoff is only worth writing past this transcript size (chars) … */
|
|
35
|
+
const HANDOFF_MIN_TRANSCRIPT_CHARS = 8_000;
|
|
36
|
+
/** … unless open risks were detected, which always deserve a handoff. */
|
|
37
|
+
const RISK_PATTERN = /\b(error|failed|failure|exception|denied|timeout|timed out|blocked|fatal|refused)\b/i;
|
|
38
|
+
const MAX_RISKS = 5;
|
|
39
|
+
const SNIPPET_MAX = 240;
|
|
40
|
+
function meaningfulTurns(history) {
|
|
41
|
+
let assistant = 0;
|
|
42
|
+
let tools = 0;
|
|
43
|
+
for (const e of history) {
|
|
44
|
+
if (e.type === 'assistant' && e.content.trim())
|
|
45
|
+
assistant++;
|
|
46
|
+
else if (e.type === 'tool_result')
|
|
47
|
+
tools++;
|
|
48
|
+
}
|
|
49
|
+
return { assistant, tools };
|
|
50
|
+
}
|
|
51
|
+
function firstLine(text) {
|
|
52
|
+
const line = text.trim().split('\n')[0] ?? '';
|
|
53
|
+
return line.length > SNIPPET_MAX ? `${line.slice(0, SNIPPET_MAX)}…` : line;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Heuristic open-risk extraction: error-ish tool results, deduped per tool,
|
|
57
|
+
* newest kept. No LLM involved — deterministic and free.
|
|
58
|
+
*/
|
|
59
|
+
export function extractOpenRisks(history) {
|
|
60
|
+
const byKey = new Map();
|
|
61
|
+
for (const e of history) {
|
|
62
|
+
if (e.type !== 'tool_result')
|
|
63
|
+
continue;
|
|
64
|
+
if (!RISK_PATTERN.test(e.content))
|
|
65
|
+
continue;
|
|
66
|
+
const tool = e.toolCall?.function?.name || 'tool';
|
|
67
|
+
byKey.set(tool, `\`${tool}\`: ${firstLine(redactSecrets(e.content))}`);
|
|
68
|
+
}
|
|
69
|
+
return [...byKey.values()].slice(-MAX_RISKS);
|
|
70
|
+
}
|
|
71
|
+
/** Files touched through write-ish tool calls, for the handoff. */
|
|
72
|
+
export function extractTouchedFiles(history) {
|
|
73
|
+
const WRITE_TOOLS = new Set([
|
|
74
|
+
'str_replace', 'str_replace_editor', 'create_file', 'write_file',
|
|
75
|
+
'apply_patch', 'edit_file', 'text_editor',
|
|
76
|
+
]);
|
|
77
|
+
const files = new Set();
|
|
78
|
+
for (const e of history) {
|
|
79
|
+
const calls = e.toolCalls ?? (e.toolCall ? [e.toolCall] : []);
|
|
80
|
+
for (const call of calls) {
|
|
81
|
+
if (!call?.function?.name || !WRITE_TOOLS.has(call.function.name))
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const args = JSON.parse(call.function.arguments || '{}');
|
|
85
|
+
const p = args.path ?? args.file_path ?? args.filePath;
|
|
86
|
+
if (typeof p === 'string' && p.trim())
|
|
87
|
+
files.add(p.trim());
|
|
88
|
+
}
|
|
89
|
+
catch { /* unparseable args — skip */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...files].slice(0, 20);
|
|
93
|
+
}
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Handoff (sync — usable from process exit handlers)
|
|
96
|
+
// ============================================================================
|
|
97
|
+
let handoffWrittenFor = null;
|
|
98
|
+
/**
|
|
99
|
+
* Write `.codebuddy/HANDOFF.md` synchronously. Returns the path when
|
|
100
|
+
* written, undefined when the session didn't warrant one. Idempotent per
|
|
101
|
+
* history array so the async flush and a process-exit fallback don't both
|
|
102
|
+
* write.
|
|
103
|
+
*/
|
|
104
|
+
export function writeHandoffSync(history, workDir = process.cwd(), options = {}) {
|
|
105
|
+
if (handoffWrittenFor === history)
|
|
106
|
+
return undefined;
|
|
107
|
+
const { assistant } = meaningfulTurns(history);
|
|
108
|
+
if (assistant < MIN_ASSISTANT_TURNS)
|
|
109
|
+
return undefined;
|
|
110
|
+
const transcriptChars = history.reduce((n, e) => n + e.content.length, 0);
|
|
111
|
+
const risks = extractOpenRisks(history);
|
|
112
|
+
if (!options.force && transcriptChars < HANDOFF_MIN_TRANSCRIPT_CHARS && risks.length === 0) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const lastUser = [...history].reverse().find((e) => e.type === 'user' && e.content.trim());
|
|
116
|
+
const lastAssistant = [...history].reverse().find((e) => e.type === 'assistant' && e.content.trim());
|
|
117
|
+
const touched = extractTouchedFiles(history);
|
|
118
|
+
const startedAt = options.startedAt;
|
|
119
|
+
const durationMin = startedAt ? Math.round((Date.now() - startedAt) / 60_000) : undefined;
|
|
120
|
+
const lines = [
|
|
121
|
+
'# Session Handoff',
|
|
122
|
+
'',
|
|
123
|
+
`> Auto-generated at session end — latest session wins. Read this before resuming work.`,
|
|
124
|
+
'',
|
|
125
|
+
`- Date: ${new Date().toISOString()}`,
|
|
126
|
+
...(options.sessionId ? [`- Session: ${options.sessionId}`] : []),
|
|
127
|
+
...(durationMin !== undefined ? [`- Durée: ~${durationMin} min`] : []),
|
|
128
|
+
`- Échanges: ${meaningfulTurns(history).assistant} réponses assistant, ${meaningfulTurns(history).tools} résultats d'outils`,
|
|
129
|
+
'',
|
|
130
|
+
'## Dernier objectif (utilisateur)',
|
|
131
|
+
'',
|
|
132
|
+
lastUser ? firstLine(redactSecrets(lastUser.content)) : '(aucun message utilisateur)',
|
|
133
|
+
'',
|
|
134
|
+
'## Dernier état (assistant)',
|
|
135
|
+
'',
|
|
136
|
+
lastAssistant ? redactSecrets(lastAssistant.content).trim().slice(0, 800) : '(aucune réponse)',
|
|
137
|
+
'',
|
|
138
|
+
];
|
|
139
|
+
if (touched.length > 0) {
|
|
140
|
+
lines.push('## Fichiers touchés', '', ...touched.map((f) => `- \`${f}\``), '');
|
|
141
|
+
}
|
|
142
|
+
lines.push('## Risques ouverts', '');
|
|
143
|
+
if (risks.length > 0) {
|
|
144
|
+
lines.push(...risks.map((r) => `- ${r}`));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
lines.push('- Aucun détecté.');
|
|
148
|
+
}
|
|
149
|
+
lines.push('', '## Reprise', '', risks.length > 0
|
|
150
|
+
? '- Commencer par lever les risques ouverts ci-dessus.'
|
|
151
|
+
: '- Continuer le dernier objectif, ou `buddy --continue` pour recharger la session.', '- Leçons en attente de revue : `buddy lessons` (candidats proposés en fin de session).', '- Mémoires en attente de revue : `/memory candidates` puis `/memory accept <id> <reviewer>`.', '');
|
|
152
|
+
const content = redactSecrets(lines.join('\n'));
|
|
153
|
+
const dir = path.join(workDir, '.codebuddy');
|
|
154
|
+
const target = path.join(dir, 'HANDOFF.md');
|
|
155
|
+
try {
|
|
156
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
157
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
158
|
+
handoffWrittenFor = history;
|
|
159
|
+
return target;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
logger.debug('[session-end-flush] handoff write failed', { err: String(err) });
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Full async flush (handoff + lesson proposal)
|
|
168
|
+
// ============================================================================
|
|
169
|
+
/**
|
|
170
|
+
* Run the complete session-end flush. Safe to call from any exit path:
|
|
171
|
+
* no-ops on trivial sessions, never throws.
|
|
172
|
+
*/
|
|
173
|
+
export async function runSessionEndFlush(input) {
|
|
174
|
+
const empty = { proposedLessons: 0, proposedMemories: 0, openRisks: [] };
|
|
175
|
+
try {
|
|
176
|
+
if (!isFeatureEnabled('SESSION_END_FLUSH')) {
|
|
177
|
+
return { ...empty, skipped: 'disabled' };
|
|
178
|
+
}
|
|
179
|
+
const history = input.chatHistory ?? [];
|
|
180
|
+
const { assistant } = meaningfulTurns(history);
|
|
181
|
+
if (assistant < MIN_ASSISTANT_TURNS) {
|
|
182
|
+
return { ...empty, skipped: 'trivial' };
|
|
183
|
+
}
|
|
184
|
+
const workDir = input.workDir ?? process.cwd();
|
|
185
|
+
const openRisks = extractOpenRisks(history);
|
|
186
|
+
const handoffPath = writeHandoffSync(history, workDir, {
|
|
187
|
+
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
188
|
+
...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
|
|
189
|
+
});
|
|
190
|
+
// Lesson proposal last — it is the only step that may call an LLM, so a
|
|
191
|
+
// killed process still leaves the handoff behind.
|
|
192
|
+
let proposedLessons = 0;
|
|
193
|
+
try {
|
|
194
|
+
const { proposeLessonsFromSession } = await import('./lesson-auto-proposer.js');
|
|
195
|
+
const proposed = await proposeLessonsFromSession(history, workDir, input.client);
|
|
196
|
+
proposedLessons = proposed.length;
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
logger.debug('[session-end-flush] lesson proposal failed', { err: String(err) });
|
|
200
|
+
}
|
|
201
|
+
let proposedMemories = 0;
|
|
202
|
+
if (process.env.CODEBUDDY_MEMORY_AUTO_PROPOSE !== 'false') {
|
|
203
|
+
try {
|
|
204
|
+
const { proposeMemoryCandidatesFromSession } = await import('../memory/memory-auto-proposer.js');
|
|
205
|
+
const proposed = await proposeMemoryCandidatesFromSession(history, workDir, input.client, input.sessionId);
|
|
206
|
+
proposedMemories = proposed.length;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger.debug('[session-end-flush] memory proposal failed', { err: String(err) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (proposedLessons > 0 || proposedMemories > 0 || handoffPath) {
|
|
213
|
+
logger.info(`[session-end-flush] ${proposedLessons} lesson candidate(s) proposed` +
|
|
214
|
+
`, ${proposedMemories} memory candidate(s) proposed` +
|
|
215
|
+
(handoffPath ? `, handoff written to ${path.relative(workDir, handoffPath)}` : ''));
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
proposedLessons,
|
|
219
|
+
proposedMemories,
|
|
220
|
+
...(handoffPath ? { handoffPath } : {}),
|
|
221
|
+
openRisks,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
logger.debug('[session-end-flush] flush failed', { err: String(err) });
|
|
226
|
+
return empty;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Test hook: reset the per-history idempotence latch. */
|
|
230
|
+
export function resetSessionEndFlushState() {
|
|
231
|
+
handoffWrittenFor = null;
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=session-end-flush.js.map
|
|
@@ -118,6 +118,12 @@ export interface AccumulatedMessage {
|
|
|
118
118
|
content?: string;
|
|
119
119
|
/** Accumulated tool calls */
|
|
120
120
|
tool_calls?: CodeBuddyToolCall[];
|
|
121
|
+
/**
|
|
122
|
+
* The terminal `finish_reason`/`stop_reason` reported by the provider on the
|
|
123
|
+
* final chunk (`'stop'` | `'length'` | `'tool_calls'` | …). Used by the
|
|
124
|
+
* executor's in-loop recovery to detect a length-truncated turn.
|
|
125
|
+
*/
|
|
126
|
+
finishReason?: string | null;
|
|
121
127
|
/** Any additional fields from the API */
|
|
122
128
|
[key: string]: unknown;
|
|
123
129
|
}
|
|
@@ -153,6 +159,7 @@ export declare class StreamingHandler {
|
|
|
153
159
|
private accumulatedMessage;
|
|
154
160
|
private accumulatedRawContent;
|
|
155
161
|
private accumulatedReasoningContent;
|
|
162
|
+
private accumulatedFinishReason;
|
|
156
163
|
private tokenCounter;
|
|
157
164
|
private lastTokenUpdate;
|
|
158
165
|
private toolCallsYielded;
|
|
@@ -42,6 +42,7 @@ export class StreamingHandler {
|
|
|
42
42
|
accumulatedMessage = {};
|
|
43
43
|
accumulatedRawContent = '';
|
|
44
44
|
accumulatedReasoningContent = '';
|
|
45
|
+
accumulatedFinishReason = null;
|
|
45
46
|
tokenCounter = null;
|
|
46
47
|
lastTokenUpdate = 0;
|
|
47
48
|
toolCallsYielded = false;
|
|
@@ -86,6 +87,11 @@ export class StreamingHandler {
|
|
|
86
87
|
}
|
|
87
88
|
// Accumulate the message using reducer
|
|
88
89
|
this.accumulatedMessage = reduceStreamChunk(this.accumulatedMessage, chunk);
|
|
90
|
+
// Capture the terminal finish_reason (present on the final chunk). The last
|
|
91
|
+
// non-null value wins so a trailing usage-only chunk can't clear it.
|
|
92
|
+
if (chunk.choices[0].finish_reason) {
|
|
93
|
+
this.accumulatedFinishReason = chunk.choices[0].finish_reason;
|
|
94
|
+
}
|
|
89
95
|
// Extract content delta
|
|
90
96
|
const rawContentDelta = chunk.choices[0].delta?.content || '';
|
|
91
97
|
this.accumulatedRawContent += rawContentDelta;
|
|
@@ -208,7 +214,7 @@ export class StreamingHandler {
|
|
|
208
214
|
* @returns The accumulated message with all processed content and tool calls
|
|
209
215
|
*/
|
|
210
216
|
getAccumulatedMessage() {
|
|
211
|
-
return { ...this.accumulatedMessage };
|
|
217
|
+
return { ...this.accumulatedMessage, finishReason: this.accumulatedFinishReason };
|
|
212
218
|
}
|
|
213
219
|
/**
|
|
214
220
|
* Gets the raw accumulated content before sanitization.
|
|
@@ -261,6 +267,7 @@ export class StreamingHandler {
|
|
|
261
267
|
this.accumulatedMessage = {};
|
|
262
268
|
this.accumulatedRawContent = '';
|
|
263
269
|
this.accumulatedReasoningContent = '';
|
|
270
|
+
this.accumulatedFinishReason = null;
|
|
264
271
|
this.lastTokenUpdate = 0;
|
|
265
272
|
this.toolCallsYielded = false;
|
|
266
273
|
}
|
|
@@ -37,6 +37,11 @@ export interface ToolHandlerDependencies {
|
|
|
37
37
|
/** Coordinates auto-repair attempts for failed commands */
|
|
38
38
|
repairCoordinator: RepairCoordinator;
|
|
39
39
|
}
|
|
40
|
+
export interface NormalizedHallucinatedToolCall {
|
|
41
|
+
toolName: string;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
export declare function normalizeHallucinatedLocalToolCall(toolName: string, args: Record<string, unknown>): NormalizedHallucinatedToolCall | null;
|
|
40
45
|
/**
|
|
41
46
|
* ToolHandler manages tool instantiation and execution
|
|
42
47
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Tool instances are lazy-loaded on first access for optimal startup time.
|
|
17
17
|
*/
|
|
18
18
|
import { TextEditorTool, MorphEditorTool, ImageTool, BashTool, } from "../tools/index.js";
|
|
19
|
-
import { getFormalToolRegistry, createTextEditorTools, createBashTools, createLsTools, createSearchTools, createWebTools, createTodoTools, createCronjobTools, createDockerTools, createKubernetesTools, createGitTools, createMiscTools, createBrowserTools, createProcessTools, createVisionTools, createScriptTools, createPlanTools, createKnowledgeTools, createRelationshipIntelligenceTools, createInternetScoutTools, createLeadScoutTools, createBrowserOperatorTools, createMemoryTools, createParallelTools, createAttentionTools, createSkillsInspectionTools, createLessonsTools, createUserModelTools, createAliasTools, createMultimodalTools, createAdvancedTools, createCanvasTools, createLspTools, createMergeConflictTools, createVulnScannerTools, createCodebaseReplaceTools, createAdvisorTools, createFleetTools, createAskUserQuestionTools, createExitPlanModeTools, createGuiTools, createSessionTools, createGitNexusTools, createScreenpipeTools, } from "../tools/registry/index.js";
|
|
19
|
+
import { getFormalToolRegistry, createTextEditorTools, createBashTools, createLsTools, createSearchTools, createWebTools, createTodoTools, createCronjobTools, createDockerTools, createKubernetesTools, createGitTools, createMiscTools, createBrowserTools, createProcessTools, createVisionTools, createScriptTools, createPlanTools, createKnowledgeTools, createRelationshipIntelligenceTools, createInternetScoutTools, createLeadScoutTools, createBrowserOperatorTools, createWindowsTools, createMemoryTools, createParallelTools, createAttentionTools, createSkillsInspectionTools, createLessonsTools, createUserModelTools, createAliasTools, createMultimodalTools, createAdvancedTools, createCanvasTools, createLspTools, createMergeConflictTools, createVulnScannerTools, createCodebaseReplaceTools, createAdvisorTools, createFleetTools, createAskUserQuestionTools, createExitPlanModeTools, createGuiTools, createSessionTools, createGitNexusTools, createScreenpipeTools, } from "../tools/registry/index.js";
|
|
20
20
|
import { getMCPManager } from "../codebuddy/tools.js";
|
|
21
21
|
import { getErrorMessage } from "../errors/index.js";
|
|
22
22
|
import { logger } from "../utils/logger.js";
|
|
@@ -44,6 +44,67 @@ const READ_ONLY_FILE_TOOLS = new Set([
|
|
|
44
44
|
'find_definition',
|
|
45
45
|
'search_multi',
|
|
46
46
|
]);
|
|
47
|
+
export function normalizeHallucinatedLocalToolCall(toolName, args) {
|
|
48
|
+
const normalizedName = toolName.toLowerCase().replace(/\s+/g, '');
|
|
49
|
+
const thoughtToolCallSuffix = normalizedName.match(/^thought_tool_call_\d+(?:_\d+)*\.(bash|create_file|write_file|view_file|read_file)$/)?.[1];
|
|
50
|
+
if (thoughtToolCallSuffix === 'bash') {
|
|
51
|
+
const command = firstStringValue(args.command, args.cmd, args.shell_command, args.command_to_execute, args.code);
|
|
52
|
+
return {
|
|
53
|
+
toolName: 'bash',
|
|
54
|
+
args: command ? { ...args, command } : args,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (thoughtToolCallSuffix === 'create_file' || thoughtToolCallSuffix === 'write_file') {
|
|
58
|
+
const path = firstStringValue(args.path, args.file_path, args.target_file, args.filename, args.file);
|
|
59
|
+
const content = firstStringValue(args.content, args.file_content, args.contents, args.text);
|
|
60
|
+
return {
|
|
61
|
+
toolName: 'create_file',
|
|
62
|
+
args: {
|
|
63
|
+
...args,
|
|
64
|
+
...(path ? { path } : {}),
|
|
65
|
+
...(content !== undefined ? { content } : {}),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (thoughtToolCallSuffix === 'view_file' || thoughtToolCallSuffix === 'read_file') {
|
|
70
|
+
const path = firstStringValue(args.path, args.file_path, args.target_file, args.filename, args.file);
|
|
71
|
+
return {
|
|
72
|
+
toolName: 'view_file',
|
|
73
|
+
args: path ? { ...args, path } : args,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (normalizedName.includes('bash_tool:execute') ||
|
|
77
|
+
normalizedName.includes('thought-tool:execute_command') ||
|
|
78
|
+
normalizedName === 'execute_command') {
|
|
79
|
+
const command = firstStringValue(args.command, args.cmd, args.shell_command, args.command_to_execute, args.code);
|
|
80
|
+
return {
|
|
81
|
+
toolName: 'bash',
|
|
82
|
+
args: command ? { ...args, command } : args,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (normalizedName.includes('bash_tool:write_file') ||
|
|
86
|
+
normalizedName.includes('thought-tool:write_file') ||
|
|
87
|
+
normalizedName.includes('call:bash_tool:write_file')) {
|
|
88
|
+
const path = firstStringValue(args.path, args.file_path, args.target_file, args.filename, args.file);
|
|
89
|
+
const content = firstStringValue(args.content, args.file_content, args.contents, args.text);
|
|
90
|
+
return {
|
|
91
|
+
toolName: 'create_file',
|
|
92
|
+
args: {
|
|
93
|
+
...args,
|
|
94
|
+
...(path ? { path } : {}),
|
|
95
|
+
...(content !== undefined ? { content } : {}),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function firstStringValue(...values) {
|
|
102
|
+
for (const value of values) {
|
|
103
|
+
if (typeof value === 'string' && value.trim())
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
47
108
|
/**
|
|
48
109
|
* ToolHandler manages tool instantiation and execution
|
|
49
110
|
*
|
|
@@ -156,6 +217,7 @@ export class ToolHandler {
|
|
|
156
217
|
...createInternetScoutTools(),
|
|
157
218
|
...createLeadScoutTools(),
|
|
158
219
|
...createBrowserOperatorTools(),
|
|
220
|
+
...(process.platform === 'win32' ? createWindowsTools() : []),
|
|
159
221
|
...createMemoryTools(),
|
|
160
222
|
...createParallelTools(),
|
|
161
223
|
...createAttentionTools(),
|
|
@@ -244,8 +306,25 @@ export class ToolHandler {
|
|
|
244
306
|
const startTime = Date.now();
|
|
245
307
|
const hooksManager = getToolHooksManager();
|
|
246
308
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
309
|
+
let args = JSON.parse(toolCall.function.arguments);
|
|
310
|
+
let toolName = toolCall.function.name;
|
|
311
|
+
const normalized = normalizeHallucinatedLocalToolCall(toolName, args);
|
|
312
|
+
if (normalized) {
|
|
313
|
+
logger.debug('Normalized hallucinated local tool name', {
|
|
314
|
+
from: toolName,
|
|
315
|
+
to: normalized.toolName,
|
|
316
|
+
});
|
|
317
|
+
toolName = normalized.toolName;
|
|
318
|
+
args = normalized.args;
|
|
319
|
+
toolCall = {
|
|
320
|
+
...toolCall,
|
|
321
|
+
function: {
|
|
322
|
+
...toolCall.function,
|
|
323
|
+
name: toolName,
|
|
324
|
+
arguments: JSON.stringify(args),
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
249
328
|
// Create hook context
|
|
250
329
|
let hookContext = {
|
|
251
330
|
toolName,
|
|
@@ -693,6 +772,12 @@ export class ToolHandler {
|
|
|
693
772
|
*/
|
|
694
773
|
async executeBashWithHooks(args, context) {
|
|
695
774
|
const command = args.command;
|
|
775
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
776
|
+
return {
|
|
777
|
+
success: false,
|
|
778
|
+
error: 'bash: missing a non-empty "command" string argument. Put the shell command to run in the "command" field.',
|
|
779
|
+
};
|
|
780
|
+
}
|
|
696
781
|
// Execute pre-bash hooks
|
|
697
782
|
try {
|
|
698
783
|
await this.deps.hooksManager.executeHooks("pre-bash", { command });
|
|
@@ -754,6 +839,15 @@ export class ToolHandler {
|
|
|
754
839
|
const args = JSON.parse(toolCall.function.arguments);
|
|
755
840
|
const command = args.command;
|
|
756
841
|
const timeout = args.timeout || 30000;
|
|
842
|
+
// A weaker model can emit a `bash` call without a `command` (or a
|
|
843
|
+
// non-string one). Fail with a clear, recoverable message instead of
|
|
844
|
+
// crashing downstream on `command.trim()` (auto-repair can then re-prompt).
|
|
845
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
846
|
+
return {
|
|
847
|
+
success: false,
|
|
848
|
+
error: 'bash: missing a non-empty "command" string argument. Put the shell command to run in the "command" field.',
|
|
849
|
+
};
|
|
850
|
+
}
|
|
757
851
|
const gen = this.bash.executeStreaming(command, timeout);
|
|
758
852
|
let result = await gen.next();
|
|
759
853
|
while (!result.done) {
|
|
@@ -778,8 +872,15 @@ export class ToolHandler {
|
|
|
778
872
|
mode: mode,
|
|
779
873
|
});
|
|
780
874
|
reasoner.setMode(mode);
|
|
875
|
+
const problem = args.problem;
|
|
876
|
+
if (typeof problem !== 'string' || problem.trim() === '') {
|
|
877
|
+
return {
|
|
878
|
+
success: false,
|
|
879
|
+
error: 'reason: missing a non-empty "problem" string argument. Put the problem to reason about in the "problem" field.',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
781
882
|
const gen = reasoner.solveStreaming({
|
|
782
|
-
description:
|
|
883
|
+
description: problem,
|
|
783
884
|
context: args.context,
|
|
784
885
|
constraints: args.constraints,
|
|
785
886
|
});
|
|
@@ -796,6 +897,31 @@ export class ToolHandler {
|
|
|
796
897
|
return { success: false, error: `Reasoning error: ${getErrorMessage(error)}` };
|
|
797
898
|
}
|
|
798
899
|
}
|
|
900
|
+
// generate_document: stream the build phases (prepare → parse → build →
|
|
901
|
+
// write → verify → done) so the UI shows the steps unfold one by one
|
|
902
|
+
// instead of a single opaque "Created X" at the end.
|
|
903
|
+
if (toolName === 'generate_document') {
|
|
904
|
+
try {
|
|
905
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
906
|
+
const { executeGenerateDocumentStreaming } = await import('../tools/document-generator.js');
|
|
907
|
+
const gen = executeGenerateDocumentStreaming({
|
|
908
|
+
type: args.type,
|
|
909
|
+
title: args.title,
|
|
910
|
+
content: args.content,
|
|
911
|
+
outputPath: args.outputPath ?? args.output_path,
|
|
912
|
+
theme: args.theme,
|
|
913
|
+
});
|
|
914
|
+
let r = await gen.next();
|
|
915
|
+
while (!r.done) {
|
|
916
|
+
yield r.value;
|
|
917
|
+
r = await gen.next();
|
|
918
|
+
}
|
|
919
|
+
return r.value ?? { success: false, error: 'Document tool returned no result' };
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
return { success: false, error: `Document streaming error: ${getErrorMessage(error)}` };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
799
925
|
// Fallback: non-streaming execution
|
|
800
926
|
return await this.executeTool(toolCall);
|
|
801
927
|
}
|