@nomad-e/bluma-cli 0.1.74 → 0.1.75

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 CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  > **Credit:** BluMa was conceived and architected by **Alex Fonseca**.
13
13
 
14
- **Current Version:** 0.1.60
14
+ **Current Version:** 0.1.74
15
15
 
16
16
  ---
17
17
 
@@ -42,7 +42,7 @@ BluMa operates as a **conversational agent** in the terminal, combining:
42
42
  - **Rich UI Layer**: React/Ink 5 components for interactive prompts, live overlays, and real-time feedback
43
43
  - **Agent Layer**: LLM orchestration via FactorRouter with tool invocation and context management
44
44
  - **Runtime Layer**: Task tracking, plugin system, hooks, diagnostics, session management, and coordinator mode
45
- - **Tool Layer**: 40+ native tools + MCP SDK integration for external tools
45
+ - **Tool Layer**: 45+ native tools + MCP SDK integration for external tools
46
46
 
47
47
  The agent maintains persistent conversation history, workspace snapshots, and coding memory across sessions.
48
48
 
@@ -67,7 +67,7 @@ The agent maintains persistent conversation history, workspace snapshots, and co
67
67
  - **Tool Execution Policy**: Intelligent decisions based on sandbox mode and safety
68
68
 
69
69
  ### Tools & Skills
70
- - **40+ Native Tools**: File operations, search, shell commands, web fetch, agent coordination, task management, MCP resources, cron scheduling, LSP queries, notebook editing
70
+ - **45+ Native Tools**: File operations, search, shell commands, web fetch, agent coordination, task management, MCP resources, cron scheduling, LSP queries, notebook editing
71
71
  - **Coordinator Mode**: Orchestrator playbook for delegating work to background workers
72
72
  - **MCP Integration**: Model Context Protocol SDK for external tool servers
73
73
  - **Skills System**: Pluggable knowledge modules (git, PDF, Excel, etc.)
@@ -237,7 +237,7 @@ npm start
237
237
  ┌────────────────────────┼────────────────────────────────────┐
238
238
  │ Tools Layer │
239
239
  │ ┌──────────────────────────────────────────────────────┐ │
240
- │ │ Native Tools (40+) │ │
240
+ │ │ Native Tools (45+) │ │
241
241
  │ │ edit_tool, file_write, shell_command, grep_search, │ │
242
242
  │ │ spawn_agent, wait_agent, list_agents, │ │
243
243
  │ │ todo, task_boundary, task_create, task_list, │ │
@@ -260,7 +260,7 @@ npm start
260
260
 
261
261
  ## Native Tools
262
262
 
263
- BluMa includes **40+ built-in tools** organized by category:
263
+ BluMa includes **45+ built-in tools** organized by category:
264
264
 
265
265
  ### File Operations
266
266
  | Tool | Description |
@@ -341,6 +341,18 @@ BluMa includes **40+ built-in tools** organized by category:
341
341
  | `repl` | Interactive code execution (Python, Node, Bash) |
342
342
  | `task_output` | Read task output in real-time |
343
343
 
344
+ ### Specialized
345
+ | Tool | Description |
346
+ |------|-------------|
347
+ | `context_collapse` | Collapse context window |
348
+ | `dream_engine` | Auto-dream feature (coding memory consolidation) |
349
+ | `brief` | Generate project brief |
350
+ | `ctx_inspect` | Inspect current context |
351
+ | `snip` | Code snippet management |
352
+ | `coordinator_tools` | Coordinator mode utilities |
353
+ | `create-next-app` | Scaffold Next.js project with shadcn/ui + Tailwind |
354
+ | `deploy-app` | Deploy Next.js to Severino hosting |
355
+
344
356
  ### Plan Mode
345
357
  | Tool | Description |
346
358
  |------|-------------|
@@ -349,7 +361,7 @@ BluMa includes **40+ built-in tools** organized by category:
349
361
 
350
362
  ---
351
363
 
352
- ## New Features in v0.1.60
364
+ ## Current Features
353
365
 
354
366
  ### Mailbox System (Bidirectional Communication)
355
367
  **Push-based communication** between coordinator and workers using EventEmitter:
@@ -570,6 +582,7 @@ Level 3b: scripts/*.py
570
582
  |-------|-------------|
571
583
  | `git-commit` | Conventional commits, staging, commit messages |
572
584
  | `git-pr` | Pull requests, code review preparation |
585
+ | `git-release` | Version bumps, changelogs, git tags, GitHub releases |
573
586
  | `pdf` | PDF creation, extraction, merging, OCR |
574
587
  | `xlsx` | Spreadsheet operations, formulas, charts |
575
588
  | `skill-creator` | Author new BluMa skills |
@@ -765,11 +778,29 @@ src/
765
778
  │ │ ├── agent.ts # Main orchestrator
766
779
  │ │ ├── bluma/ # Core agent logic
767
780
  │ │ │ ├── core/
768
- │ │ │ │ └── bluma.ts # BluMaAgent class
769
- │ │ │ └── turn_start_payload.ts # Backend turn payload
781
+ │ │ │ │ ├── bluma.ts # BluMaAgent class
782
+ │ │ │ └── turn_start_payload.ts
783
+ │ │ │ ├── context/
784
+ │ │ │ │ └── auto_compact.ts # Automatic context compaction
785
+ │ │ │ ├── memory/
786
+ │ │ │ │ └── session_memory.ts # Session memory
787
+ │ │ │ └── types/
788
+ │ │ │ └── errors.ts # Error type definitions
770
789
  │ │ ├── config/
771
- │ │ │ ├── native_tools.json # Tool definitions (40+)
772
- │ │ │ └── skills/ # Bundled skills (git-commit, git-pr, pdf, xlsx, skill-creator)
790
+ │ │ │ ├── native_tools.json # Tool definitions (45+)
791
+ │ │ │ ├── skills/ # Bundled skills
792
+ │ │ │ │ ├── git-commit/
793
+ │ │ │ │ ├── git-pr/
794
+ │ │ │ │ ├── git-release/ # Version bumps, changelogs, releases
795
+ │ │ │ │ ├── pdf/
796
+ │ │ │ │ ├── skill-creator/
797
+ │ │ │ │ └── xlsx/
798
+ │ │ │ │ └── scripts/
799
+ │ │ │ │ └── office/ # Office document handling
800
+ │ │ │ │ ├── pack.py
801
+ │ │ │ │ ├── unpack.py
802
+ │ │ │ │ ├── validate.py
803
+ │ │ │ │ └── soffice.py
773
804
  │ │ ├── core/ # LLM, context, prompts
774
805
  │ │ │ ├── context-api/ # Context management
775
806
  │ │ │ │ ├── context_manager.ts # Token-aware context
@@ -777,17 +808,32 @@ src/
777
808
  │ │ │ │ └── token_counter.ts # Tiktoken integration
778
809
  │ │ │ ├── llm/ # LLM client
779
810
  │ │ │ │ ├── llm.ts # FactorRouter client
811
+ │ │ │ │ ├── llm_errors.ts # LLM error formatting
812
+ │ │ │ │ ├── streaming_delta.ts # Streaming delta handling
780
813
  │ │ │ │ └── tool_call_normalizer.ts
781
- │ │ │ └── prompt/ # Prompt engineering
782
- │ │ │ ├── prompt_builder.ts # Dynamic prompts
783
- │ │ │ └── workspace_snapshot.ts
814
+ │ │ │ ├── prompt/ # Prompt engineering
815
+ │ │ │├── prompt_builder.ts # Dynamic prompts
816
+ │ │ │ │ ├── workspace_snapshot.ts
817
+ │ │ │ │ ├── mcp_instructions.ts
818
+ │ │ │ │ ├── model_info.ts
819
+ │ │ │ │ ├── production_sandbox_prompt.ts
820
+ │ │ │ │ ├── system_prompt_sections.ts
821
+ │ │ │ │ ├── system_reminders.ts
822
+ │ │ │ │ └── tool_guidance.ts
823
+ │ │ │ └── context_viz.ts # Context visualization
824
+ │ │ ├── docs/
825
+ │ │ │ └── TOOL_PARITY.md # Tool parity documentation
784
826
  │ │ ├── feedback/
785
827
  │ │ │ └── feedback_system.ts # Smart feedback system
828
+ │ │ │ └── feedback_scoring.ts # User feedback scoring
786
829
  │ │ ├── runtime/ # Orchestration layer (v0.1.41+)
787
830
  │ │ │ ├── diagnostics.ts # System snapshots
788
831
  │ │ │ ├── feature_flags.ts # Feature gates
789
832
  │ │ │ ├── hook_registry.ts # Event-driven hooks
790
833
  │ │ │ ├── native_tool_catalog.ts # Tool registry
834
+ │ │ │ ├── permission_bridge.ts # Leader↔Worker permission system
835
+ │ │ │ ├── permission_rules.ts # Permission rule definitions
836
+ │ │ │ ├── tool_permission_classifier.ts
791
837
  │ │ │ ├── plan_mode_session.ts # Plan mode state
792
838
  │ │ │ ├── plugin_registry.ts # Plugin system
793
839
  │ │ │ ├── plugin_runtime.ts # Plugin execution
@@ -799,21 +845,29 @@ src/
799
845
  │ │ │ ├── tool_auto_approve.ts # Auto-approve rules
800
846
  │ │ │ ├── tool_execution_policy.ts
801
847
  │ │ │ ├── tool_orchestration.ts # Parallel read eligibility
802
- │ │ │ └── tool_permission_classifier.ts
848
+ │ │ │ ├── mailbox_registry.ts # Mailbox system
849
+ │ │ │ ├── worker_context.ts # Worker context management
850
+ │ │ │ └── factorai_context.ts # FactorAI app context
803
851
  │ │ ├── session_manager/
804
852
  │ │ │ └── session_manager.ts # Session persistence
805
853
  │ │ ├── skills/
806
- │ │ │ └── skill_loader.ts # Skill loading
854
+ │ │ │ └── skill_loader.ts # 3-source skill loading
807
855
  │ │ ├── subagents/ # Subagent system
808
856
  │ │ │ ├── base_llm_subagent.ts
809
857
  │ │ │ ├── init/ # Init subagent (BluMa.md)
858
+ │ │ │ │ ├── init_subagent.ts # Deep project analysis
859
+ │ │ │ │ ├── init_system_prompt.ts
860
+ │ │ │ │ └── contracts.ts
810
861
  │ │ │ ├── registry.ts
811
- │ │ │ ├── subagents_bluma.ts
812
- │ │ │ └── types.ts
862
+ │ │ │ ├── subagents_bluma.ts # SubAgent orchestration
863
+ │ │ │ ├── types.ts # SubAgent type definitions
864
+ │ │ │ └── worker_system_prompt.ts
813
865
  │ │ ├── tools/
814
866
  │ │ │ ├── mcp/
815
867
  │ │ │ │ └── mcp_client.ts # MCP SDK client
816
- │ │ │ └── natives/ # 27 native tool implementations
868
+ │ │ │ ├── shared/
869
+ │ │ │ │ └── token_utils.ts # Token utilities
870
+ │ │ │ └── natives/ # 45+ native tool implementations
817
871
  │ │ │ ├── agent_coordination.ts
818
872
  │ │ │ ├── ask_user_question.ts
819
873
  │ │ │ ├── async_command.ts
@@ -844,29 +898,35 @@ src/
844
898
  │ │ ├── types/
845
899
  │ │ │ └── index.ts
846
900
  │ │ └── utils/
901
+ │ │ ├── blumamd.ts # BluMa markdown utilities
847
902
  │ │ ├── coordinator_prompt.ts # Coordinator mode playbook
903
+ │ │ ├── logger.ts # Logging utilities
848
904
  │ │ ├── update_check.ts
849
905
  │ │ └── user_message_images.ts
850
906
  │ └── ui/
851
907
  │ ├── App.tsx # Main UI component
852
908
  │ ├── Asci/
853
909
  │ │ └── AsciiArt.ts
854
- │ ├── components/ # 24+ UI components
910
+ │ ├── components/ # 25+ UI components
855
911
  │ │ ├── AnimatedBorder.tsx
856
912
  │ │ ├── AskUserQuestionPrompt.tsx
857
913
  │ │ ├── AssistantMessageDisplay.tsx
858
914
  │ │ ├── CollapsibleResult.tsx
915
+ │ │ ├── ConfirmationPrompt.tsx # Permission confirmation dialog
859
916
  │ │ ├── EditToolDiffPanel.tsx # Diff preview for edits
860
917
  │ │ ├── ErrorMessage.tsx
861
918
  │ │ ├── ExpandedPreviewBlock.tsx
862
919
  │ │ ├── InputPrompt.tsx # User input
920
+ │ │ ├── InteractiveMenu.tsx # Interactive menu component
863
921
  │ │ ├── MarkdownRenderer.tsx
864
922
  │ │ ├── ProgressBar.tsx
865
923
  │ │ ├── ReasoningDisplay.tsx # LLM reasoning
924
+ │ │ │ ├── SessionInfoConnectingMCP.tsx
866
925
  │ │ ├── SessionStats.tsx
867
926
  │ │ ├── SimpleDiff.tsx
868
927
  │ │ ├── SlashCommands.tsx # 30+ commands
869
928
  │ │ ├── StatusNotification.tsx
929
+ │ │ ├── StartupUpdateGate.tsx # Update check gate
870
930
  │ │ ├── StreamingText.tsx # Live text output
871
931
  │ │ ├── TodoPlanDisplay.tsx # Task visualization
872
932
  │ │ ├── ToolCallDisplay.tsx
@@ -875,17 +935,41 @@ src/
875
935
  │ │ ├── ToolResultDisplay.tsx
876
936
  │ │ ├── TypewriterText.tsx
877
937
  │ │ ├── UpdateNotice.tsx
938
+ │ │ ├── WorkerOverlay.tsx # Worker status overlay
939
+ │ │ ├── WorkerStatusList.tsx # Active workers list
940
+ │ │ ├── WorkerTranscript.tsx # Worker conversation transcript
941
+ │ │ ├── WorkingTimer.tsx # Work duration timer
878
942
  │ │ ├── streamingTextFlush.ts
879
943
  │ │ └── toolCallRenderers.tsx
880
944
  │ ├── constants/
881
- │ │ ├── historyLayout.ts
882
- │ │ ├── inputPaste.ts
883
- │ │ └── toolUiPreview.ts
945
+ │ │ ├── historyLayout.ts # History layout constants
946
+ │ │ ├── inputPaste.ts # Input paste constants
947
+ │ │ └── toolUiPreview.ts # Tool UI preview constants
948
+ │ │ └── toolUiSymbols.ts # Tool UI symbols
884
949
  │ ├── hooks/
885
- │ │ └── useAtCompletion.ts
950
+ │ │ ├── useAtCompletion.ts # Completion hook
951
+ │ │ └── useWorkerProgress.ts # Worker progress hook
952
+ │ ├── prompts/
953
+ │ │ └── initCommandPrompt.ts # Init command prompt
954
+ │ ├── slash-commands/
955
+ │ │ ├── SlashCommands.types.ts # Type definitions
956
+ │ │ ├── commandHelpers.tsx # Command helpers
957
+ │ │ ├── constants.ts # Slash command constants
958
+ │ │ ├── SessionLivePanel.tsx # Session live panel
959
+ │ │ ├── streamingTextFlush.ts # Streaming text flush
960
+ │ │ └── renderers/
961
+ │ │ ├── index.ts
962
+ │ │ ├── configRenderers.tsx
963
+ │ │ ├── infoRenderers.tsx
964
+ │ │ ├── permissionRenderers.tsx
965
+ │ │ ├── pluginRenderers.tsx
966
+ │ │ ├── sessionRenderers.tsx
967
+ │ │ ├── staticRenderers.tsx
968
+ │ │ └── taskRenderers.tsx
886
969
  │ ├── theme/
887
970
  │ │ ├── blumaTerminal.ts
888
- │ │ └── m3Layout.tsx
971
+ │ │ ├── themes.ts # Theme definitions
972
+ │ │ └── m3Layout.tsx # Material Design 3 layout
889
973
  │ └── utils/
890
974
  │ ├── clipboardImage.ts
891
975
  │ ├── editToolDiffUtils.ts
@@ -896,8 +980,9 @@ src/
896
980
  │ ├── pathDisplay.ts
897
981
  │ ├── shellToolNames.ts
898
982
  │ ├── slashRegistry.ts
899
- │ ├── terminalTitle.ts
900
- │ ├── toolDisplayLabels.ts
983
+ │ ├── terminalTitle.ts # Terminal title keeper
984
+ │ ├── toolActionLabels.ts # Tool action labels
985
+ │ ├── toolDisplayLabels.ts # Tool display labels
901
986
  │ ├── toolInvocationPairing.ts
902
987
  │ └── useSimpleInputBuffer.ts
903
988
  ├── main.ts # Entry point
@@ -917,7 +1002,7 @@ npm run test:watch # Watch mode
917
1002
  ### Test Structure
918
1003
 
919
1004
  ```
920
- tests/ # 33 test files (flat structure)
1005
+ tests/ # 35+ test files (flat structure)
921
1006
  ├── agent_*.spec.ts # Agent routing, overlays, coordination
922
1007
  ├── edit_tool.spec.ts # File editing operations
923
1008
  ├── file_write.spec.ts # File write operations
@@ -939,6 +1024,9 @@ tests/ # 33 test files (flat structure)
939
1024
  ├── web_fetch.spec.ts # Web fetching
940
1025
  ├── workspace_snapshot.spec.ts # Workspace analysis
941
1026
  ├── ui_*.spec.ts(x) # UI component tests
1027
+ ├── llm_stream_partial.spec.ts # LLM streaming partial handling
1028
+ ├── llm_errors.spec.ts # LLM error handling
1029
+ ├── jest-resolver.cjs # Jest resolver configuration
942
1030
  └── ... # Additional integration and unit tests
943
1031
  ```
944
1032
 
@@ -636,13 +636,13 @@
636
636
  "type": "function",
637
637
  "function": {
638
638
  "name": "search_web",
639
- "description": "Search for programming solutions across developer-focused sources: Reddit (programming subreddits), GitHub (issues/discussions), StackOverflow, and X (link only). Use this to find solutions, best practices, and community discussions about errors or implementation approaches.",
639
+ "description": "Search for programming solutions across developer-focused sources: Reddit (programming subreddits), GitHub (issues/discussions), StackOverflow, and X (link only). Use this to find solutions, best practices, and community discussions about errors or implementation approaches. NEW: Supports domain filtering and rate limiting.",
640
640
  "parameters": {
641
641
  "type": "object",
642
642
  "properties": {
643
643
  "query": {
644
644
  "type": "string",
645
- "description": "Search query, e.g. 'typescript cannot find module', 'react useEffect infinite loop fix'"
645
+ "description": "Search query, e.g. 'typescript cannot find module', 'react useEffect infinite loop fix'. Must be 2-500 characters."
646
646
  },
647
647
  "sources": {
648
648
  "type": "array",
@@ -664,8 +664,31 @@
664
664
  },
665
665
  "max_results": {
666
666
  "type": "integer",
667
- "description": "Maximum total results to return. Default is 10.",
668
- "default": 10
667
+ "description": "Maximum total results to return. Default is 10, max is 50.",
668
+ "default": 10,
669
+ "maximum": 50,
670
+ "minimum": 1
671
+ },
672
+ "allowed_domains": {
673
+ "type": "array",
674
+ "items": {
675
+ "type": "string"
676
+ },
677
+ "description": "Only include search results from these domains (e.g., ['github.com', 'reddit.com']). Cannot be used with blocked_domains."
678
+ },
679
+ "blocked_domains": {
680
+ "type": "array",
681
+ "items": {
682
+ "type": "string"
683
+ },
684
+ "description": "Never include search results from these domains (e.g., ['spam-site.com']). Cannot be used with allowed_domains."
685
+ },
686
+ "max_uses": {
687
+ "type": "integer",
688
+ "description": "Maximum number of sources to search (rate limiting). Default is 8, max is 20.",
689
+ "default": 8,
690
+ "maximum": 20,
691
+ "minimum": 1
669
692
  }
670
693
  },
671
694
  "required": [
package/dist/main.js CHANGED
@@ -327,7 +327,7 @@ var init_sandbox_policy = __esm({
327
327
  BLOCKED_COMMAND_PATTERNS = [
328
328
  { pattern: /\bsudo\b/, reason: "Privilege escalation is not allowed." },
329
329
  { pattern: /\bsu\b\s/, reason: "User switching is not allowed." },
330
- { pattern: /\brm\s+-rf\s+\/\b/, reason: "Deleting root filesystem is blocked." },
330
+ { pattern: /rm\s+-rf\s+\/\s*$/, reason: "Deleting root filesystem is blocked." },
331
331
  { pattern: /\bcurl\b.*\|\s*(bash|sh|zsh)/i, reason: "Pipe-to-shell execution is blocked." },
332
332
  { pattern: /\bwget\b.*\|\s*(bash|sh|zsh)/i, reason: "Pipe-to-shell execution is blocked." },
333
333
  { pattern: /\beval\b\s*\(/, reason: "Eval execution is blocked." },
@@ -6980,9 +6980,106 @@ async function readArtifact(args) {
6980
6980
  import https from "https";
6981
6981
  import http from "http";
6982
6982
  var DEFAULT_SOURCES = ["reddit", "github", "stackoverflow"];
6983
- var MAX_RESULTS_DEFAULT = 5;
6983
+ var MAX_RESULTS_DEFAULT = 10;
6984
+ var MAX_USES_DEFAULT = 8;
6984
6985
  var REQUEST_TIMEOUT = 15e3;
6985
6986
  var MAX_CONTENT_LENGTH = 4e3;
6987
+ function validateInput(args) {
6988
+ if (!args.query || typeof args.query !== "string") {
6989
+ return { valid: false, error: "query is required and must be a string" };
6990
+ }
6991
+ if (args.query.trim().length < 2) {
6992
+ return { valid: false, error: "query must be at least 2 characters long" };
6993
+ }
6994
+ if (args.query.length > 500) {
6995
+ return { valid: false, error: "query must be less than 500 characters" };
6996
+ }
6997
+ if (args.allowed_domains && args.blocked_domains && args.allowed_domains.length > 0 && args.blocked_domains.length > 0) {
6998
+ return {
6999
+ valid: false,
7000
+ error: "Cannot specify both allowed_domains and blocked_domains in the same request"
7001
+ };
7002
+ }
7003
+ if (args.sources && args.sources.length > 0) {
7004
+ const validSources = ["reddit", "github", "stackoverflow", "x"];
7005
+ for (const source of args.sources) {
7006
+ if (!validSources.includes(source)) {
7007
+ return {
7008
+ valid: false,
7009
+ error: `Invalid source: ${source}. Valid sources are: ${validSources.join(", ")}`
7010
+ };
7011
+ }
7012
+ }
7013
+ }
7014
+ if (args.max_results !== void 0) {
7015
+ if (typeof args.max_results !== "number" || args.max_results < 1) {
7016
+ return { valid: false, error: "max_results must be a positive integer" };
7017
+ }
7018
+ if (args.max_results > 50) {
7019
+ return { valid: false, error: "max_results cannot exceed 50" };
7020
+ }
7021
+ }
7022
+ if (args.max_uses !== void 0) {
7023
+ if (typeof args.max_uses !== "number" || args.max_uses < 1) {
7024
+ return { valid: false, error: "max_uses must be a positive integer" };
7025
+ }
7026
+ if (args.max_uses > 20) {
7027
+ return { valid: false, error: "max_uses cannot exceed 20" };
7028
+ }
7029
+ }
7030
+ const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$/;
7031
+ if (args.allowed_domains) {
7032
+ for (const domain of args.allowed_domains) {
7033
+ if (!domainRegex.test(domain)) {
7034
+ return { valid: false, error: `Invalid domain format: ${domain}` };
7035
+ }
7036
+ }
7037
+ }
7038
+ if (args.blocked_domains) {
7039
+ for (const domain of args.blocked_domains) {
7040
+ if (!domainRegex.test(domain)) {
7041
+ return { valid: false, error: `Invalid domain format: ${domain}` };
7042
+ }
7043
+ }
7044
+ }
7045
+ return { valid: true };
7046
+ }
7047
+ function extractDomain(url) {
7048
+ try {
7049
+ const urlObj = new URL(url);
7050
+ const hostname = urlObj.hostname.toLowerCase();
7051
+ const parts = hostname.split(".");
7052
+ if (parts.length >= 2) {
7053
+ return parts.slice(-2).join(".");
7054
+ }
7055
+ return hostname;
7056
+ } catch {
7057
+ return null;
7058
+ }
7059
+ }
7060
+ function passesDomainFilter(url, allowedDomains, blockedDomains) {
7061
+ const domain = extractDomain(url);
7062
+ if (!domain) {
7063
+ return { passes: false, reason: "Could not extract domain from URL" };
7064
+ }
7065
+ if (allowedDomains && allowedDomains.length > 0) {
7066
+ const isAllowed = allowedDomains.some(
7067
+ (allowed) => domain === allowed.toLowerCase() || domain.endsWith("." + allowed.toLowerCase())
7068
+ );
7069
+ if (!isAllowed) {
7070
+ return { passes: false, reason: `Domain ${domain} not in allowed list` };
7071
+ }
7072
+ }
7073
+ if (blockedDomains && blockedDomains.length > 0) {
7074
+ const isBlocked = blockedDomains.some(
7075
+ (blocked) => domain === blocked.toLowerCase() || domain.endsWith("." + blocked.toLowerCase())
7076
+ );
7077
+ if (isBlocked) {
7078
+ return { passes: false, reason: `Domain ${domain} is in blocked list` };
7079
+ }
7080
+ }
7081
+ return { passes: true };
7082
+ }
6986
7083
  function httpGet(url, customHeaders) {
6987
7084
  return new Promise((resolve2, reject) => {
6988
7085
  const protocol = url.startsWith("https") ? https : http;
@@ -7028,17 +7125,29 @@ function cleanContent(text, maxLength = MAX_CONTENT_LENGTH) {
7028
7125
  }
7029
7126
  return cleaned;
7030
7127
  }
7031
- async function searchReddit(query, limit) {
7128
+ async function searchReddit(query, limit, allowedDomains, blockedDomains) {
7032
7129
  const results = [];
7130
+ const warnings = [];
7033
7131
  try {
7034
7132
  const subreddits = "programming+webdev+javascript+typescript+python+node+reactjs+learnprogramming+rust+golang+devops";
7035
7133
  const encodedQuery = encodeURIComponent(query);
7036
- const url = `https://www.reddit.com/r/${subreddits}/search.json?q=${encodedQuery}&sort=relevance&limit=${limit}&restrict_sr=on`;
7134
+ const url = `https://www.reddit.com/r/${subreddits}/search.json?q=${encodedQuery}&sort=relevance&limit=${limit * 2}&restrict_sr=on`;
7135
+ const domainCheck = passesDomainFilter(url, allowedDomains, blockedDomains);
7136
+ if (!domainCheck.passes) {
7137
+ warnings.push(`Reddit search skipped: ${domainCheck.reason}`);
7138
+ return results;
7139
+ }
7037
7140
  const response = await httpGet(url);
7038
7141
  const data = JSON.parse(response);
7039
7142
  if (data.data?.children) {
7040
- for (const child of data.data.children.slice(0, limit)) {
7143
+ for (const child of data.data.children.slice(0, limit * 2)) {
7144
+ if (results.length >= limit) break;
7041
7145
  const post = child.data;
7146
+ const postUrl = `https://reddit.com${post.permalink}`;
7147
+ const urlCheck = passesDomainFilter(postUrl, allowedDomains, blockedDomains);
7148
+ if (!urlCheck.passes) {
7149
+ continue;
7150
+ }
7042
7151
  let content = `# ${post.title}
7043
7152
 
7044
7153
  `;
@@ -7070,7 +7179,7 @@ ${cleanContent(post.selftext, 2e3)}
7070
7179
  }
7071
7180
  results.push({
7072
7181
  title: post.title || "",
7073
- url: `https://reddit.com${post.permalink}`,
7182
+ url: postUrl,
7074
7183
  source: "reddit",
7075
7184
  content: cleanContent(content),
7076
7185
  score: post.score,
@@ -7084,18 +7193,34 @@ ${cleanContent(post.selftext, 2e3)}
7084
7193
  }
7085
7194
  } catch (error) {
7086
7195
  console.error(`[search_web] Reddit error: ${error.message}`);
7196
+ if (error.message.includes("403") || error.message.includes("429")) {
7197
+ warnings.push("Reddit rate limit encountered - results may be incomplete");
7198
+ }
7087
7199
  }
7088
7200
  return results;
7089
7201
  }
7090
- async function searchGitHub(query, limit) {
7202
+ async function searchGitHub(query, limit, allowedDomains, blockedDomains) {
7091
7203
  const results = [];
7204
+ const warnings = [];
7092
7205
  try {
7093
7206
  const encodedQuery = encodeURIComponent(query);
7094
- const url = `https://api.github.com/search/issues?q=${encodedQuery}+is:issue&sort=reactions&order=desc&per_page=${limit}`;
7095
- const response = await httpGet(url);
7207
+ const url = `https://api.github.com/search/issues?q=${encodedQuery}+is:issue&sort=reactions&order=desc&per_page=${limit * 2}`;
7208
+ const domainCheck = passesDomainFilter(url, allowedDomains, blockedDomains);
7209
+ if (!domainCheck.passes) {
7210
+ warnings.push(`GitHub search skipped: ${domainCheck.reason}`);
7211
+ return results;
7212
+ }
7213
+ const response = await httpGet(url, {
7214
+ "Accept": "application/vnd.github+json"
7215
+ });
7096
7216
  const data = JSON.parse(response);
7097
7217
  if (data.items) {
7098
- for (const item of data.items.slice(0, limit)) {
7218
+ for (const item of data.items.slice(0, limit * 2)) {
7219
+ if (results.length >= limit) break;
7220
+ const urlCheck = passesDomainFilter(item.html_url, allowedDomains, blockedDomains);
7221
+ if (!urlCheck.passes) {
7222
+ continue;
7223
+ }
7099
7224
  let content = `# ${item.title}
7100
7225
 
7101
7226
  `;
@@ -7130,18 +7255,32 @@ ${cleanContent(item.body, 2500)}
7130
7255
  }
7131
7256
  } catch (error) {
7132
7257
  console.error(`[search_web] GitHub error: ${error.message}`);
7258
+ if (error.message.includes("403")) {
7259
+ warnings.push("GitHub API rate limit may have been reached");
7260
+ }
7133
7261
  }
7134
7262
  return results;
7135
7263
  }
7136
- async function searchStackOverflow(query, limit) {
7264
+ async function searchStackOverflow(query, limit, allowedDomains, blockedDomains) {
7137
7265
  const results = [];
7266
+ const warnings = [];
7138
7267
  try {
7139
7268
  const encodedQuery = encodeURIComponent(query);
7140
- const url = `https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&q=${encodedQuery}&site=stackoverflow&pagesize=${limit}&filter=withbody`;
7269
+ const url = `https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&q=${encodedQuery}&site=stackoverflow&pagesize=${limit * 2}&filter=withbody`;
7270
+ const domainCheck = passesDomainFilter(url, allowedDomains, blockedDomains);
7271
+ if (!domainCheck.passes) {
7272
+ warnings.push(`StackOverflow search skipped: ${domainCheck.reason}`);
7273
+ return results;
7274
+ }
7141
7275
  const response = await httpGet(url);
7142
7276
  const data = JSON.parse(response);
7143
7277
  if (data.items) {
7144
- for (const item of data.items.slice(0, limit)) {
7278
+ for (const item of data.items.slice(0, limit * 2)) {
7279
+ if (results.length >= limit) break;
7280
+ const urlCheck = passesDomainFilter(item.link, allowedDomains, blockedDomains);
7281
+ if (!urlCheck.passes) {
7282
+ continue;
7283
+ }
7145
7284
  let content = `# ${item.title}
7146
7285
 
7147
7286
  `;
@@ -7195,64 +7334,96 @@ ${cleanContent(cleanAnswer, 2e3)}
7195
7334
  }
7196
7335
  return results;
7197
7336
  }
7198
- async function searchWeb(args) {
7337
+ async function searchX(query, limit, allowedDomains, blockedDomains) {
7338
+ const results = [];
7339
+ const warnings = [];
7340
+ warnings.push("X (Twitter) search is not available - API requires authentication");
7199
7341
  try {
7200
- const {
7201
- query,
7202
- sources = DEFAULT_SOURCES,
7203
- max_results = MAX_RESULTS_DEFAULT
7204
- } = args;
7205
- if (!query || typeof query !== "string") {
7206
- return {
7207
- success: false,
7208
- query: query || "",
7209
- results: [],
7210
- sources_searched: [],
7211
- total_results: 0,
7212
- error: "query is required and must be a string"
7213
- };
7342
+ const encodedQuery = encodeURIComponent(`${query} site:twitter.com OR site:x.com`);
7343
+ const url = `https://www.google.com/search?q=${encodedQuery}&num=${limit}`;
7344
+ const domainCheck = passesDomainFilter(url, allowedDomains, blockedDomains);
7345
+ if (!domainCheck.passes) {
7346
+ warnings.push(`X search skipped: ${domainCheck.reason}`);
7347
+ return results;
7214
7348
  }
7215
- const allResults = [];
7216
- const sourcesSearched = [];
7217
- const resultsPerSource = Math.ceil(max_results / sources.length);
7218
- const searches = [];
7219
- for (const source of sources) {
7220
- sourcesSearched.push(source);
7221
- switch (source) {
7222
- case "reddit":
7223
- searches.push(searchReddit(query, resultsPerSource));
7224
- break;
7225
- case "github":
7226
- searches.push(searchGitHub(query, resultsPerSource));
7227
- break;
7228
- case "stackoverflow":
7229
- searches.push(searchStackOverflow(query, resultsPerSource));
7230
- break;
7231
- }
7232
- }
7233
- const searchResults = await Promise.all(searches);
7234
- for (const results of searchResults) {
7235
- allResults.push(...results);
7236
- }
7237
- allResults.sort((a, b) => (b.score || 0) - (a.score || 0));
7238
- const limitedResults = allResults.slice(0, max_results);
7239
- return {
7240
- success: true,
7241
- query,
7242
- results: limitedResults,
7243
- sources_searched: sourcesSearched,
7244
- total_results: limitedResults.length
7245
- };
7246
7349
  } catch (error) {
7350
+ console.error(`[search_web] X error: ${error.message}`);
7351
+ }
7352
+ return results;
7353
+ }
7354
+ async function searchWeb(args) {
7355
+ const startTime = performance.now();
7356
+ const warnings = [];
7357
+ let searchesPerformed = 0;
7358
+ const validation = validateInput(args);
7359
+ if (!validation.valid) {
7247
7360
  return {
7248
7361
  success: false,
7249
7362
  query: args.query || "",
7250
7363
  results: [],
7251
7364
  sources_searched: [],
7252
7365
  total_results: 0,
7253
- error: `Unexpected error: ${error.message}`
7366
+ duration_seconds: 0,
7367
+ searches_performed: 0,
7368
+ error: validation.error
7254
7369
  };
7255
7370
  }
7371
+ const {
7372
+ query,
7373
+ sources = DEFAULT_SOURCES,
7374
+ max_results = MAX_RESULTS_DEFAULT,
7375
+ allowed_domains,
7376
+ blocked_domains,
7377
+ max_uses = MAX_USES_DEFAULT
7378
+ } = args;
7379
+ if (sources.length > max_uses) {
7380
+ warnings.push(`Requested ${sources.length} sources but max_uses is ${max_uses} - limiting to ${max_uses} sources`);
7381
+ }
7382
+ const allResults = [];
7383
+ const sourcesSearched = [];
7384
+ const limitedSources = sources.slice(0, max_uses);
7385
+ const resultsPerSource = Math.ceil(max_results / limitedSources.length);
7386
+ const searches = [];
7387
+ for (const source of limitedSources) {
7388
+ sourcesSearched.push(source);
7389
+ switch (source) {
7390
+ case "reddit":
7391
+ searches.push(searchReddit(query, resultsPerSource, allowed_domains, blocked_domains));
7392
+ break;
7393
+ case "github":
7394
+ searches.push(searchGitHub(query, resultsPerSource, allowed_domains, blocked_domains));
7395
+ break;
7396
+ case "stackoverflow":
7397
+ searches.push(searchStackOverflow(query, resultsPerSource, allowed_domains, blocked_domains));
7398
+ break;
7399
+ case "x":
7400
+ searches.push(searchX(query, resultsPerSource, allowed_domains, blocked_domains));
7401
+ break;
7402
+ }
7403
+ }
7404
+ const searchResults = await Promise.all(searches);
7405
+ searchesPerformed = searchResults.length;
7406
+ for (const results of searchResults) {
7407
+ allResults.push(...results);
7408
+ }
7409
+ allResults.sort((a, b) => (b.score || 0) - (a.score || 0));
7410
+ const limitedResults = allResults.slice(0, max_results);
7411
+ const endTime = performance.now();
7412
+ const durationSeconds = (endTime - startTime) / 1e3;
7413
+ if (limitedResults.length === 0) {
7414
+ warnings.push("No results found - try adjusting your query or domain filters");
7415
+ }
7416
+ return {
7417
+ success: true,
7418
+ query,
7419
+ results: limitedResults,
7420
+ sources_searched: sourcesSearched,
7421
+ total_results: limitedResults.length,
7422
+ duration_seconds: Math.round(durationSeconds * 100) / 100,
7423
+ // 2 casas decimais
7424
+ searches_performed: searchesPerformed,
7425
+ warnings: warnings.length > 0 ? warnings : void 0
7426
+ };
7256
7427
  }
7257
7428
 
7258
7429
  // src/app/agent/tools/natives/load_skill.ts
@@ -13036,7 +13207,7 @@ You are the **BluMa Coordinator** \u2014 a **Product Owner + Engineering Manager
13036
13207
 
13037
13208
  ## 0. Core Philosophy: Team > Solo
13038
13209
 
13039
- **One AI is good. A coordinated team of 3-7 AIs is exponentially better.**
13210
+ **One AI is good. A coordinated team of 3-7 AIs can be better when the task truly benefits from delegation.**
13040
13211
 
13041
13212
  Think of yourself as a **rigorous PO** who:
13042
13213
  - Receives a request from the user (the "client")
@@ -13044,7 +13215,7 @@ Think of yourself as a **rigorous PO** who:
13044
13215
  - Assigns each task to the right specialist worker
13045
13216
  - Coordinates their work in parallel
13046
13217
  - Verifies quality before delivering to the client
13047
- - **Always prefers team execution over solo work** for anything non-trivial
13218
+ - **Prefer team execution** when the task is non-trivial, parallelizable, risky, or needs independent verification
13048
13219
 
13049
13220
  **Why this matters:**
13050
13221
  - **Quality**: Each worker focuses deeply on one aspect \u2192 fewer mistakes
@@ -13054,14 +13225,15 @@ Think of yourself as a **rigorous PO** who:
13054
13225
  - **CEO appreciation**: Systematic, professional approach that scales
13055
13226
 
13056
13227
  **Default behavior**: When a task arrives, your first instinct should be:
13057
- > "How can I break this into parallel worker tasks?"
13228
+ > "Can I answer or handle this directly?"
13058
13229
 
13059
- Not: "How do I do this myself?"
13230
+ Only if the answer is no, ask:
13231
+ > "How can I break this into parallel worker tasks?"
13060
13232
 
13061
13233
  ## 1. Your Role
13062
13234
 
13063
13235
  You do **NOT execute tasks directly** (except trivial reads). Your job is to:
13064
- - **Orchestrate workers** to research, implement, and verify changes
13236
+ - **Orchestrate workers** to research, implement, and verify changes when that materially improves speed, quality, or confidence
13065
13237
  - **Synthesize results** and communicate with the user
13066
13238
  - **Answer questions directly** when possible \u2014 don't delegate work you can handle without tools
13067
13239
  - **Read-only tools** (\`read_file_lines\`, \`grep\`, etc.) are fine for **light** coordinator checks (e.g. verify a path before writing a worker spec); heavy exploration belongs in workers
@@ -14233,7 +14405,7 @@ Use **both** API **reasoning** (when available) **and** the \`message\` tool. Re
14233
14405
  - Never claim success without tool output that proves it.
14234
14406
  - **Stay audible:** Your **default** in multi-step work is to call \`message\` with \`message_type: "info"\` **early and often** \u2014 not optional polish. **Bias toward sending \`info\`** after discoveries, failures, and before long tool chains; **several \`info\` calls per turn** is normal and expected. Do **not** hide behind tools or reasoning only; \`info\` is how the user follows along.
14235
14407
  - **Ask when uncertain:** Use \`ask_user_question\` when you encounter ambiguity, need clarification, or face multiple valid approaches. Do not assume \u2014 ask the user to make decisions about their preferences, requirements, or implementation choices. This tool is your primary mechanism for resolving uncertainty.
14236
- - **Team-first mindset:** For any non-trivial task, prefer spawning parallel workers over doing it yourself. One AI is good; a coordinated team of 3-7 workers is exponentially better. Break work into research \u2192 implementation \u2192 verification phases, each handled by specialist workers. You are the PO \u2014 orchestrate, synthesize, verify, deliver.
14408
+ - **Worker policy:** Use workers surgically, not by default. Do the work directly when the task is simple, local, or already well-scoped. Spawn workers when the task is genuinely non-trivial, parallelizable, risky, or needs independent verification. Break larger efforts into research \u2192 implementation \u2192 verification phases when that creates real value. You are the PO \u2014 orchestrate when it helps, synthesize, verify, deliver.
14237
14409
  - **Engineer mindset \u2014 question anomalies:** When something seems wrong (memory loss, unexpected behavior, aggressive defaults), **investigate deeply**. Do not accept "it's working as designed". Trace the code, find the root cause, and **have courage to revert** if a feature breaks core functionality. Protect the session, memory, and stability above all.
14238
14410
  - **Courage to reverse:** If you discover a path is wrong (e.g., a "feature" that destroys context, a default that's too aggressive), **stop immediately**, explain why it's broken, and revert/remove it. Better to undo a bad change than to let it cause harm. This is what separates a **thinking engineer** from a **blind executor**.
14239
14411
  - Large efforts: \`todo\`; parallel subtasks: \`spawn_agent\` with a clear scope + \`wait_agent\` / \`list_agents\`.
@@ -14654,6 +14826,29 @@ async function createApiContextWindow(fullHistory, currentAnchor, compressedTurn
14654
14826
  init_runtime_config();
14655
14827
  import os23 from "os";
14656
14828
  import OpenAI from "openai";
14829
+
14830
+ // src/app/agent/core/llm/streaming_delta.ts
14831
+ function extractStreamingDelta(previous, next) {
14832
+ const prev = String(previous ?? "");
14833
+ const curr = String(next ?? "");
14834
+ if (!curr) return "";
14835
+ if (!prev) return curr;
14836
+ if (curr.startsWith(prev)) {
14837
+ return curr.slice(prev.length);
14838
+ }
14839
+ if (prev.startsWith(curr)) {
14840
+ return "";
14841
+ }
14842
+ const maxOverlap = Math.min(prev.length, curr.length);
14843
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
14844
+ if (prev.slice(-overlap) === curr.slice(0, overlap)) {
14845
+ return curr.slice(overlap);
14846
+ }
14847
+ }
14848
+ return curr;
14849
+ }
14850
+
14851
+ // src/app/agent/core/llm/llm.ts
14657
14852
  function defaultBlumaUserContextInput(sessionId, userMessage) {
14658
14853
  const msg = String(userMessage || "").slice(0, 300);
14659
14854
  return {
@@ -14845,12 +15040,17 @@ var LLMService = class {
14845
15040
  { headers: this.requestHeaders(params.userContext) }
14846
15041
  );
14847
15042
  const toolCallsAccumulator = /* @__PURE__ */ new Map();
15043
+ let reasoningSnapshot = "";
14848
15044
  for await (const chunk of stream) {
14849
15045
  const choice = chunk.choices[0];
14850
15046
  if (!choice) continue;
14851
15047
  const delta = choice.delta;
14852
15048
  applyDeltaToolCallsToAccumulator(toolCallsAccumulator, delta?.tool_calls);
14853
- const reasoning = delta?.reasoning_content || delta?.reasoning || "";
15049
+ const rawReasoning = delta?.reasoning_content || delta?.reasoning || "";
15050
+ const reasoning = extractStreamingDelta(reasoningSnapshot, rawReasoning);
15051
+ if (reasoning) {
15052
+ reasoningSnapshot += reasoning;
15053
+ }
14854
15054
  const fullToolCalls = choice.finish_reason === "tool_calls" ? Array.from(toolCallsAccumulator.values()) : void 0;
14855
15055
  yield {
14856
15056
  delta: delta?.content || "",
@@ -14873,6 +15073,32 @@ var LLMService = class {
14873
15073
  }
14874
15074
  };
14875
15075
 
15076
+ // src/app/agent/core/llm/llm_errors.ts
15077
+ function formatLlmUiError(error) {
15078
+ const rawMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error during LLM request.";
15079
+ const lower = rawMessage.toLowerCase();
15080
+ let message2 = "Ocorreu um erro inesperado ao contactar o modelo.";
15081
+ let hint = "Tente novamente. Se continuar, verifique a liga\xE7\xE3o ao FactorRouter.";
15082
+ if (lower.includes("timeout") || lower.includes("etimedout") || lower.includes("upstream_timeout")) {
15083
+ message2 = "O BluMa demorou demasiado a responder.";
15084
+ hint = "Aumente o timeout do pedido ou tente novamente.";
15085
+ } else if (lower.includes("connection") || lower.includes("econnrefused") || lower.includes("ehostunreach") || lower.includes("enotfound")) {
15086
+ message2 = "N\xE3o foi poss\xEDvel conectar ao servi\xE7o do modelo.";
15087
+ hint = "Verifique a rede, o FactorRouter_URL e o estado do gateway.";
15088
+ } else if (lower.includes("401") || lower.includes("403") || lower.includes("unauthorized") || lower.includes("forbidden")) {
15089
+ message2 = "Falha de autentica\xE7\xE3o/autoriza\xE7\xE3o ao contactar o modelo.";
15090
+ hint = "Verifique o FactorRouter_KEY e as permiss\xF5es da conta.";
15091
+ } else if (lower.includes("api")) {
15092
+ message2 = "Erro de comunica\xE7\xE3o com a API do modelo.";
15093
+ hint = "Verifique credenciais e o estado do servi\xE7o upstream.";
15094
+ }
15095
+ return {
15096
+ message: message2,
15097
+ details: rawMessage,
15098
+ hint
15099
+ };
15100
+ }
15101
+
14876
15102
  // src/app/agent/core/llm/tool_call_normalizer.ts
14877
15103
  import { randomUUID } from "crypto";
14878
15104
  var ToolCallNormalizer = class {
@@ -15331,8 +15557,8 @@ var BluMaAgent = class {
15331
15557
  factorRouterTurnClosed = false;
15332
15558
  /** Passos seguidos sem tool_calls nem texto visível (só raciocínio) — evita loop lento no mesmo turno. */
15333
15559
  emptyAssistantReplySteps = 0;
15334
- /** Passos seguidos com texto do assistente sem tool_calls (violação de protocolo) — evita loop até timeout do job. */
15335
- directTextProtocolSteps = 0;
15560
+ /** Deduplicação de reasoning chunks no streaming — evita repetição. */
15561
+ lastReasoningChunkRef = "";
15336
15562
  constructor(sessionId, eventBus, llm, mcpClient, feedbackSystem) {
15337
15563
  this.sessionId = sessionId;
15338
15564
  this.eventBus = eventBus;
@@ -15483,7 +15709,6 @@ var BluMaAgent = class {
15483
15709
  const userContent = buildUserMessageContent(inputText, process.cwd());
15484
15710
  this.history.push({ role: "user", content: userContent });
15485
15711
  this.emptyAssistantReplySteps = 0;
15486
- this.directTextProtocolSteps = 0;
15487
15712
  this.eventBus.emit(
15488
15713
  "backend_message",
15489
15714
  buildTurnStartBackendMessage({
@@ -15529,15 +15754,18 @@ var BluMaAgent = class {
15529
15754
  }
15530
15755
  } catch (parseError) {
15531
15756
  this.eventBus.emit("backend_message", {
15532
- type: "error",
15533
- message: `Failed to parse tool arguments: ${parseError.message}`
15757
+ type: "info",
15758
+ message: "O BluMa encontrou um erro ao processar. A tentar recuperar a sess\xE3o..."
15534
15759
  });
15535
15760
  toolResultContent = JSON.stringify({
15536
- error: "Invalid tool arguments format",
15537
- details: `The arguments could not be parsed as JSON: ${parseError.message}`,
15538
- raw_arguments: toolCall.function.arguments
15761
+ error: "Tool arguments could not be parsed",
15762
+ recovery: "Session recovered automatically"
15539
15763
  });
15540
15764
  this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
15765
+ this.history.push({
15766
+ role: "system",
15767
+ content: "The previous tool call had invalid JSON arguments. Please retry with properly formatted JSON arguments."
15768
+ });
15541
15769
  this.persistSession();
15542
15770
  return true;
15543
15771
  }
@@ -15697,13 +15925,12 @@ var BluMaAgent = class {
15697
15925
  parsed.push({ toolCall, toolName: toolCall.function.name, toolArgs });
15698
15926
  } catch (parseError) {
15699
15927
  const toolResultContent = JSON.stringify({
15700
- error: "Invalid tool arguments format",
15701
- details: String(parseError?.message || parseError),
15702
- raw_arguments: toolCall.function.arguments
15928
+ error: "Tool arguments could not be parsed",
15929
+ recovery: "Session recovered automatically"
15703
15930
  });
15704
15931
  this.eventBus.emit("backend_message", {
15705
- type: "error",
15706
- message: `Failed to parse tool arguments: ${parseError.message}`
15932
+ type: "info",
15933
+ message: "O BluMa encontrou um erro ao processar. A tentar recuperar a sess\xE3o..."
15707
15934
  });
15708
15935
  this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
15709
15936
  this.persistSession();
@@ -15935,8 +16162,18 @@ ${editData.error.display}`;
15935
16162
  await this._handleNonStreamingResponse(contextWindow);
15936
16163
  }
15937
16164
  } catch (error) {
15938
- const errorMessage = error instanceof Error ? error.message : "An unknown API error occurred.";
15939
- this.eventBus.emit("backend_message", { type: "error", message: errorMessage });
16165
+ const uiError = formatLlmUiError(error);
16166
+ this.eventBus.emit("backend_message", {
16167
+ type: "error",
16168
+ message: uiError.message,
16169
+ details: uiError.details,
16170
+ hint: uiError.hint
16171
+ });
16172
+ this.eventBus.emit("backend_message", {
16173
+ type: "log",
16174
+ message: "LLM request failed",
16175
+ payload: uiError.details
16176
+ });
15940
16177
  await this.notifyFactorTurnEndIfNeeded("llm_error");
15941
16178
  this.eventBus.emit("backend_message", { type: "done", status: "failed" });
15942
16179
  } finally {
@@ -15956,13 +16193,12 @@ ${editData.error.display}`;
15956
16193
  });
15957
16194
  } else if (this.emptyAssistantReplySteps >= 6) {
15958
16195
  this.eventBus.emit("backend_message", {
15959
- type: "error",
15960
- message: "The assistant produced no tool calls or visible text after several steps. Try again or use /effort low."
16196
+ type: "info",
16197
+ message: "O BluMa est\xE1 a ter dificuldade em processar. Tente novamente ou use /effort low para respostas mais r\xE1pidas."
15961
16198
  });
15962
16199
  await this.notifyFactorTurnEndIfNeeded("empty_reply_exhausted");
15963
16200
  this.eventBus.emit("backend_message", { type: "done", status: "failed" });
15964
16201
  this.emptyAssistantReplySteps = 0;
15965
- this.directTextProtocolSteps = 0;
15966
16202
  return;
15967
16203
  }
15968
16204
  await this._continueConversation();
@@ -15994,6 +16230,7 @@ ${editData.error.display}`;
15994
16230
  parallel_tool_calls: true,
15995
16231
  userContext: this.getLlmUserContext()
15996
16232
  });
16233
+ this.lastReasoningChunkRef = "";
15997
16234
  for await (const chunk of stream) {
15998
16235
  if (this.isInterrupted) {
15999
16236
  this.eventBus.emit("stream_end", {});
@@ -16005,7 +16242,11 @@ ${editData.error.display}`;
16005
16242
  this.eventBus.emit("stream_start", {});
16006
16243
  hasEmittedStart = true;
16007
16244
  }
16008
- this.eventBus.emit("stream_reasoning_chunk", { delta: chunk.reasoning });
16245
+ const reasoningKey = chunk.reasoning.trim().replace(/\s+/g, " ");
16246
+ if (reasoningKey !== this.lastReasoningChunkRef) {
16247
+ this.lastReasoningChunkRef = reasoningKey;
16248
+ this.eventBus.emit("stream_reasoning_chunk", { delta: chunk.reasoning });
16249
+ }
16009
16250
  }
16010
16251
  if (chunk.delta) {
16011
16252
  if (!hasEmittedStart) {
@@ -16044,7 +16285,6 @@ ${editData.error.display}`;
16044
16285
  this.history.push(normalizedMessage);
16045
16286
  if (normalizedMessage.tool_calls && normalizedMessage.tool_calls.length > 0) {
16046
16287
  this.emptyAssistantReplySteps = 0;
16047
- this.directTextProtocolSteps = 0;
16048
16288
  const validToolCalls = normalizedMessage.tool_calls.filter(
16049
16289
  (call) => ToolCallNormalizer.isValidToolCall(call)
16050
16290
  );
@@ -16084,28 +16324,13 @@ ${editData.error.display}`;
16084
16324
  }
16085
16325
  } else if (trimmedText) {
16086
16326
  this.emptyAssistantReplySteps = 0;
16087
- this.directTextProtocolSteps += 1;
16088
- const MAX_DIRECT_TEXT_PROTOCOL = 3;
16089
- if (!hasEmittedStart) {
16090
- this.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
16091
- }
16092
- if (this.directTextProtocolSteps >= MAX_DIRECT_TEXT_PROTOCOL) {
16093
- this.eventBus.emit("backend_message", {
16094
- type: "error",
16095
- message: 'Agent kept answering with plain assistant text instead of the `message` tool with message_type "result". Turn forcibly closed to avoid job timeout; fix prompts or model routing.'
16096
- });
16097
- await this.notifyFactorTurnEndIfNeeded("protocol_direct_text_exhausted");
16098
- this.emitTurnCompleted();
16099
- this.emptyAssistantReplySteps = 0;
16100
- this.directTextProtocolSteps = 0;
16101
- return;
16102
- }
16103
- const feedback = this.feedbackSystem.generateFeedback({
16104
- event: "protocol_violation_direct_text",
16105
- details: { violationContent: accumulatedContent }
16327
+ this.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
16328
+ this.eventBus.emit("info", {
16329
+ message: "Assistant returned plain text without tool_calls. Closing the turn to avoid protocol drift."
16106
16330
  });
16107
- this.history.push({ role: "system", content: feedback.correction });
16108
- await this._continueConversation();
16331
+ await this.notifyFactorTurnEndIfNeeded("assistant_text_without_tool_call");
16332
+ this.emitTurnCompleted();
16333
+ return;
16109
16334
  } else {
16110
16335
  await this.continueAfterEmptyAssistantResponse();
16111
16336
  }
@@ -16135,7 +16360,6 @@ ${editData.error.display}`;
16135
16360
  this.history.push(message2);
16136
16361
  if (message2.tool_calls && message2.tool_calls.length > 0) {
16137
16362
  this.emptyAssistantReplySteps = 0;
16138
- this.directTextProtocolSteps = 0;
16139
16363
  const validToolCalls = message2.tool_calls.filter(
16140
16364
  (call) => ToolCallNormalizer.isValidToolCall(call)
16141
16365
  );
@@ -16175,27 +16399,13 @@ ${editData.error.display}`;
16175
16399
  }
16176
16400
  } else if (typeof message2.content === "string" && message2.content.trim()) {
16177
16401
  this.emptyAssistantReplySteps = 0;
16178
- this.directTextProtocolSteps += 1;
16179
- const MAX_DIRECT_TEXT_PROTOCOL = 3;
16180
16402
  this.eventBus.emit("backend_message", { type: "assistant_message", content: message2.content });
16181
- if (this.directTextProtocolSteps >= MAX_DIRECT_TEXT_PROTOCOL) {
16182
- this.eventBus.emit("backend_message", {
16183
- type: "error",
16184
- message: 'Agent kept answering with plain assistant text instead of the `message` tool with message_type "result". Turn forcibly closed to avoid job timeout.'
16185
- });
16186
- await this.notifyFactorTurnEndIfNeeded("protocol_direct_text_exhausted");
16187
- this.emitTurnCompleted();
16188
- this.emptyAssistantReplySteps = 0;
16189
- this.directTextProtocolSteps = 0;
16190
- return;
16191
- }
16192
- const feedback = this.feedbackSystem.generateFeedback({
16193
- event: "protocol_violation_direct_text",
16194
- details: { violationContent: message2.content }
16403
+ this.eventBus.emit("info", {
16404
+ message: "Assistant returned plain text without tool_calls. Closing the turn to avoid protocol drift."
16195
16405
  });
16196
- this.eventBus.emit("backend_message", { type: "protocol_violation", message: feedback.message, content: message2.content });
16197
- this.history.push({ role: "system", content: feedback.correction });
16198
- await this._continueConversation();
16406
+ await this.notifyFactorTurnEndIfNeeded("assistant_text_without_tool_call");
16407
+ this.emitTurnCompleted();
16408
+ return;
16199
16409
  } else {
16200
16410
  await this.continueAfterEmptyAssistantResponse();
16201
16411
  }
@@ -16993,11 +17203,13 @@ var BaseLLMSubAgent = class {
16993
17203
  /** Um turnId por execute(); reutilizado em todo o loop de tools do subagente. */
16994
17204
  subagentTurnContext = null;
16995
17205
  lastActivityTimestamp = Date.now();
17206
+ terminalEventEmitted = false;
16996
17207
  async execute(input, ctx) {
16997
17208
  workerLog.info("Worker started", { id: this.id, pid: process.pid });
16998
17209
  this.emitEvent("worker_heartbeat", { status: "started", timestamp: Date.now(), pid: process.pid, id: this.id });
16999
17210
  this.ctx = ctx;
17000
17211
  this.isInterrupted = false;
17212
+ this.terminalEventEmitted = false;
17001
17213
  this.ctx.eventBus.on("user_interrupt", () => {
17002
17214
  this.isInterrupted = true;
17003
17215
  });
@@ -17060,13 +17272,15 @@ var BaseLLMSubAgent = class {
17060
17272
  this.emitEvent("error", {
17061
17273
  message: `Subagent tool "${message2.tool_calls[0].function.name}" requires confirmation outside sandbox mode.`
17062
17274
  });
17063
- this.emitEvent("done", { status: "blocked_confirmation" });
17275
+ this.emitDoneOnce("blocked_confirmation");
17064
17276
  break;
17065
17277
  }
17066
17278
  await this._handleToolExecution({ type: "user_decision_execute", tool_calls: message2.tool_calls });
17067
- } else if (message2.content) {
17279
+ } else if (typeof message2.content === "string" && message2.content.trim()) {
17068
17280
  this.emitEvent("assistant_message", { content: message2.content });
17069
- this.emitEvent("protocol_violation", { message: "Direct text emission detected from subagent.", content: message2.content });
17281
+ this.emitEvent("info", { message: "SubAgent returned plain text without tool_calls. Closing turn." });
17282
+ this.emitDoneOnce("completed");
17283
+ break;
17070
17284
  } else {
17071
17285
  this.emitEvent("info", { message: "SubAgent is thinking... continuing reasoning cycle." });
17072
17286
  }
@@ -17074,8 +17288,9 @@ var BaseLLMSubAgent = class {
17074
17288
  }
17075
17289
  if (turnCount >= MAX_TURNS) {
17076
17290
  this.emitEvent("info", { message: `Worker reached max turns limit (${MAX_TURNS}).` });
17077
- this.emitEvent("done", { status: "max_turns_reached" });
17291
+ this.emitDoneOnce("max_turns_reached");
17078
17292
  }
17293
+ this.emitDoneOnce("completed");
17079
17294
  return { history: this.history, turns: turnCount, status: this.isInterrupted ? "cancelled" : "completed" };
17080
17295
  };
17081
17296
  const timeoutPromise = new Promise((_, reject) => {
@@ -17088,7 +17303,7 @@ var BaseLLMSubAgent = class {
17088
17303
  if (error.message?.includes("timed out")) {
17089
17304
  workerLog.warn("Worker timed out", { id: this.id, turns: turnCount });
17090
17305
  this.emitEvent("error", { message: error.message });
17091
- this.emitEvent("done", { status: "timeout" });
17306
+ this.emitDoneOnce("timeout");
17092
17307
  } else {
17093
17308
  this.emitEvent("error", { message: error.message });
17094
17309
  }
@@ -17189,9 +17404,11 @@ ${editData.error.display}`;
17189
17404
  if (!lastToolName.includes("agent_end_turn") && !this.isInterrupted) {
17190
17405
  await this._continueConversation();
17191
17406
  }
17192
- } else if (message2.content) {
17407
+ } else if (typeof message2.content === "string" && message2.content.trim()) {
17193
17408
  this.emitEvent("assistant_message", { content: message2.content });
17194
- this.emitEvent("protocol_violation", { message: "Direct text emission detected from subagent.", content: message2.content });
17409
+ this.emitEvent("info", { message: "SubAgent returned plain text without tool_calls. Closing turn." });
17410
+ this.emitEvent("done", { status: "completed" });
17411
+ return;
17195
17412
  } else {
17196
17413
  this.emitEvent("info", { message: "SubAgent is thinking... continuing reasoning cycle." });
17197
17414
  }
@@ -17247,7 +17464,7 @@ ${editData.error.display}`;
17247
17464
  result: toolResultContent
17248
17465
  });
17249
17466
  if (toolName.includes("agent_end_turn")) {
17250
- this.emitEvent("done", { status: "completed" });
17467
+ this.emitDoneOnce("completed");
17251
17468
  }
17252
17469
  } else {
17253
17470
  toolResultContent = "Tool execution was declined.";
@@ -17286,7 +17503,12 @@ ${editData.error.display}`;
17286
17503
  } catch {
17287
17504
  }
17288
17505
  this.isInterrupted = true;
17289
- this.emitEvent("done", { status: "shutdown", reason });
17506
+ this.emitDoneOnce("shutdown", { reason });
17507
+ }
17508
+ emitDoneOnce(status, extra = {}) {
17509
+ if (this.terminalEventEmitted) return;
17510
+ this.terminalEventEmitted = true;
17511
+ this.emitEvent("done", { status, ...extra });
17290
17512
  }
17291
17513
  /**
17292
17514
  * Verifica mailbox por follow-ups do coordinator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "description": "BluMa independent agent for automation and advanced software engineering.",
5
5
  "author": "Alex Fonseca",
6
6
  "license": "Apache-2.0",