@johpaz/hive-sdk 0.1.4 → 0.1.6
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/CHANGELOG.md +129 -0
- package/README.md +78 -23
- package/bun.lock +55 -29
- package/bunfig.toml +4 -2
- package/docs/API-AGENTS.md +78 -27
- package/docs/API-CONTEXT-COMPILER.md +31 -34
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
- package/docs/HIVE-HARNESS.md +1 -1
- package/docs/INDEX.md +4 -4
- package/docs/TEMPLATE-HIVE-APP.md +10 -10
- package/package.json +17 -12
- package/packages/cli/package.json +2 -2
- package/packages/cli/src/commands/create-app.test.ts +36 -7
- package/packages/cli/src/commands/init.ts +3 -3
- package/packages/cli/src/commands/run.ts +1 -1
- package/packages/cli/src/commands/test.ts +37 -25
- package/packages/cli/src/commands/trace.ts +30 -28
- package/packages/cli/templates/hive-app/.env.example +10 -2
- package/packages/cli/templates/hive-app/README.md +103 -0
- package/packages/cli/templates/hive-app/hive.config.ts +9 -3
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
- package/packages/cli/templates/hive-app/src/main.ts +12 -19
- package/packages/core/package.json +13 -12
- package/packages/core/src/agent/acceptance-checks.ts +172 -0
- package/packages/core/src/agent/agent-catalog.ts +348 -0
- package/packages/core/src/agent/agent-loop.ts +1373 -0
- package/packages/core/src/agent/capability-search.ts +186 -0
- package/packages/core/src/agent/catalog-selector.ts +103 -0
- package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
- package/packages/core/src/agent/context-compiler.ts +689 -0
- package/packages/core/src/agent/conversation-store.ts +381 -0
- package/packages/core/src/agent/curator.ts +276 -0
- package/packages/core/src/agent/delegation-runtime.ts +241 -0
- package/packages/core/src/agent/goal-runner.ts +323 -0
- package/packages/core/src/agent/index.ts +17 -12
- package/packages/core/src/agent/llm-client.ts +266 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
- package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
- package/packages/core/src/agent/minimal-loadout.ts +47 -0
- package/packages/core/src/agent/playbook-selector.ts +119 -0
- package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
- package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
- package/packages/core/src/agent/providers/index.ts +35 -16
- package/packages/core/src/agent/reflector.ts +320 -0
- package/packages/core/src/agent/routing-intent.ts +22 -0
- package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
- package/packages/core/src/{harness → agent}/run-store.ts +142 -81
- package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
- package/packages/core/src/agent/skill-selector.ts +374 -0
- package/packages/core/src/agent/stuck-loop.ts +209 -0
- package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
- package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
- package/packages/core/src/api/createAgent.test.ts +139 -27
- package/packages/core/src/api/createAgent.ts +232 -44
- package/packages/core/src/artifacts/store.ts +162 -0
- package/packages/core/src/canvas/canvas-manager.ts +161 -0
- package/packages/core/src/canvas/canvas.test.ts +8 -4
- package/packages/core/src/canvas/emitter.ts +131 -80
- package/packages/core/src/canvas/index.ts +1 -3
- package/packages/core/src/channels/base.ts +9 -1
- package/packages/core/src/channels/discord.ts +5 -4
- package/packages/core/src/channels/manager.ts +122 -30
- package/packages/core/src/channels/slack.ts +5 -4
- package/packages/core/src/channels/telegram.ts +36 -6
- package/packages/core/src/channels/webchat.ts +11 -10
- package/packages/core/src/channels/whatsapp.ts +23 -7
- package/packages/core/src/config/index.ts +13 -2
- package/packages/core/src/config/loader.ts +76 -29
- package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
- package/packages/core/src/ethics/EthicsGuard.ts +51 -47
- package/packages/core/src/events/agent-bus.ts +44 -68
- package/packages/core/src/events/channel-narration.ts +150 -0
- package/packages/core/src/events/narration.ts +82 -0
- package/packages/core/src/events/tool-narration.ts +62 -0
- package/packages/core/src/gateway/delegation-groups.ts +258 -0
- package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
- package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
- package/packages/core/src/gateway/lane-queue.ts +173 -0
- package/packages/core/src/gateway/notification-inbox.ts +57 -0
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/harness/index.ts +46 -27
- package/packages/core/src/index.ts +33 -27
- package/packages/core/src/mcp/hot-reload.ts +32 -23
- package/packages/core/src/mcp/index.ts +6 -3
- package/packages/core/src/mcp/singleton.ts +1 -4
- package/packages/core/src/mcp/tool-sync.ts +138 -0
- package/packages/core/src/memory/Scratchpad.test.ts +39 -20
- package/packages/core/src/memory/Scratchpad.ts +27 -34
- package/packages/core/src/multimodal/vision-service.ts +44 -38
- package/packages/core/src/resilience/retry.ts +95 -0
- package/packages/core/src/scheduler/CronScheduler.ts +334 -287
- package/packages/core/src/scheduler/index.ts +9 -7
- package/packages/core/src/scheduler/integration.ts +46 -26
- package/packages/core/src/scheduler/scheduler.test.ts +9 -13
- package/packages/core/src/scheduler/types.ts +7 -2
- package/packages/core/src/security/Pairing.ts +1 -1
- package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
- package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
- package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
- package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
- package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
- package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
- package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
- package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
- package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
- package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
- package/packages/core/src/skills/skills.test.ts +52 -11
- package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
- package/packages/core/src/storage/bootstrap.ts +151 -0
- package/packages/core/src/storage/causal-events.ts +84 -0
- package/packages/core/src/storage/collections.ts +680 -0
- package/packages/core/src/storage/crypto.ts +205 -74
- package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
- package/packages/core/src/storage/hivedb.ts +61 -0
- package/packages/core/src/storage/index.ts +111 -18
- package/packages/core/src/storage/model-id.ts +53 -0
- package/packages/core/src/storage/onboarding.ts +540 -972
- package/packages/core/src/storage/reconcile.ts +238 -0
- package/packages/core/src/storage/seed.ts +572 -406
- package/packages/core/src/storage/usage.ts +285 -225
- package/packages/core/src/storage/user-email.ts +11 -0
- package/packages/core/src/swarm/AgentExecutor.ts +1 -1
- package/packages/core/src/swarm/EventBridge.ts +1 -1
- package/packages/core/src/swarm/index.ts +12 -9
- package/packages/core/src/tool-runtime/index.ts +146 -23
- package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
- package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
- package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
- package/packages/core/src/tools/agents/get-available-models.ts +36 -54
- package/packages/core/src/tools/agents/index.ts +784 -292
- package/packages/core/src/tools/api/api-request.test.ts +164 -0
- package/packages/core/src/tools/api/api-request.ts +174 -0
- package/packages/core/src/tools/api/index.ts +16 -0
- package/packages/core/src/tools/cli/index.ts +4 -0
- package/packages/core/src/tools/core/index.ts +281 -112
- package/packages/core/src/tools/cron/index.ts +121 -124
- package/packages/core/src/tools/index.ts +63 -78
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
- package/packages/core/src/tools/types.ts +3 -1
- package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
- package/packages/core/src/tools/web/browser-backend.ts +129 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +80 -35
- package/packages/core/src/tools/web/browser-type.ts +3 -8
- package/packages/core/src/tools/web/index.ts +4 -4
- package/packages/core/src/tools/web/webview-backend.ts +412 -0
- package/packages/core/src/voice/index.ts +89 -63
- package/packages/core/src/workers/agent.worker.ts +2 -2
- package/packages/core/src/workers/workers.test.ts +3 -10
- package/scripts/bump-version.ts +248 -0
- package/scripts/generate-skill-bundle.ts +108 -0
- package/test/acceptance-checks.test.ts +403 -0
- package/test/agent-loop-terminal-synthesis.test.ts +32 -0
- package/test/browser-backend.test.ts +308 -0
- package/test/catalog-agents-stay-enabled.test.ts +117 -0
- package/test/causal-events.test.ts +117 -0
- package/test/compaction.test.ts +105 -0
- package/test/context-compiler.test.ts +269 -0
- package/test/curator.test.ts +130 -0
- package/test/durable-queue.test.ts +114 -0
- package/test/harness-barrel.test.ts +64 -0
- package/test/hive-helpers.test.ts +130 -0
- package/test/hivedb-search.test.ts +189 -0
- package/test/internal-turns.test.ts +166 -0
- package/test/job-idempotency.test.ts +68 -0
- package/test/job-retry-backoff.test.ts +184 -0
- package/test/job-store.test.ts +381 -0
- package/test/llm-retry.test.ts +97 -0
- package/test/memory-perf.test.ts +774 -0
- package/test/minimal-loadout.test.ts +78 -0
- package/test/model-catalog.test.ts +105 -0
- package/test/preload.ts +12 -0
- package/test/reflector.test.ts +320 -0
- package/test/retention-cap.test.ts +91 -0
- package/test/retired-capabilities-pruned.test.ts +192 -0
- package/test/run-store.test.ts +355 -0
- package/test/scratchpad.test.ts +74 -0
- package/test/secrets-durability.test.ts +119 -0
- package/test/seed-model-reseed.test.ts +155 -0
- package/test/setup-agent-seed.test.ts +264 -0
- package/test/tool-inventory.test.ts +65 -0
- package/test/tool-runtime.test.ts +258 -0
- package/test/tool-selector-runtime-tools.test.ts +117 -0
- package/test/toon.test.ts +429 -0
- package/tsconfig.json +2 -0
- package/packages/core/src/ace/Curator.ts +0 -158
- package/packages/core/src/ace/Reflector.ts +0 -200
- package/packages/core/src/ace/index.ts +0 -4
- package/packages/core/src/agent/AgentRunner.ts +0 -711
- package/packages/core/src/agent/ContextCompiler.ts +0 -567
- package/packages/core/src/agent/ContextGuard.ts +0 -91
- package/packages/core/src/agent/ConversationStore.ts +0 -254
- package/packages/core/src/agent/Hooks.ts +0 -166
- package/packages/core/src/agent/StuckLoop.ts +0 -133
- package/packages/core/src/agent/providers/LLMClient.ts +0 -149
- package/packages/core/src/agent/providers/anthropic.ts +0 -212
- package/packages/core/src/agent/providers/openai-compat.ts +0 -231
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
- package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
- package/packages/core/src/agent/selectors/index.ts +0 -6
- package/packages/core/src/auth/auth.ts +0 -121
- package/packages/core/src/auth/index.ts +0 -1
- package/packages/core/src/canvas/CanvasManager.ts +0 -390
- package/packages/core/src/canvas/canvas-tools.ts +0 -448
- package/packages/core/src/harness/collections.ts +0 -98
- package/packages/core/src/harness/goal-verifier.ts +0 -141
- package/packages/core/src/harness/harness.test.ts +0 -236
- package/packages/core/src/harness/reconcile.ts +0 -149
- package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
- package/packages/core/src/multimodal/VisionService.ts +0 -293
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
- package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
- package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
- package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
- package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
- package/packages/core/src/scheduler/dag/errors.ts +0 -37
- package/packages/core/src/scheduler/dag/index.ts +0 -26
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
- package/packages/core/src/storage/HiveDBStorage.ts +0 -64
- package/packages/core/src/storage/SQLiteStorage.ts +0 -414
- package/packages/core/src/storage/hiveSeed.ts +0 -308
- package/packages/core/src/storage/hiveStorage.test.ts +0 -38
- package/packages/core/src/storage/schema.ts +0 -689
- package/packages/core/src/storage/storage.test.ts +0 -37
- package/packages/core/src/swarm/AgentBus.ts +0 -460
- package/packages/core/src/swarm/EventBus.ts +0 -169
- package/packages/core/src/swarm/WorkerPool.ts +0 -236
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -375
- package/packages/core/src/tools/codebridge/index.ts +0 -342
- package/packages/core/src/tools/meeting/index.ts +0 -353
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/api-request.test.ts +0 -170
- package/packages/core/src/tools/web/api-request.ts +0 -239
- package/test/setup-db.ts +0 -216
- /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HiveDB-based Dynamic Skill Selector Module
|
|
3
|
+
*
|
|
4
|
+
* Context Compiler Level 4 - Intelligent Skill Selection
|
|
5
|
+
*
|
|
6
|
+
* This module uses HiveDB BM25 scoring (Spanish stemming + accent folding)
|
|
7
|
+
* to select the most relevant skills based on the user message, similar to
|
|
8
|
+
* tool selection.
|
|
9
|
+
*
|
|
10
|
+
* DESIGN DECISIONS:
|
|
11
|
+
*
|
|
12
|
+
* 1. Reads from skills table in database (not hardcoded catalog)
|
|
13
|
+
* 2. Maximum skills per turn for balanced context injection
|
|
14
|
+
* 3. Explicit triggers checked first; search is the semantic fallback
|
|
15
|
+
* 4. Relative score cutoff (fraction of the top hit), never absolute
|
|
16
|
+
* 5. Returns skill content for injection into system prompt
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { col } from "../storage/hive"
|
|
20
|
+
import type { SkillDoc } from "../storage/collections"
|
|
21
|
+
import { logger } from "../utils/logger"
|
|
22
|
+
import { isMinimalSkill } from "./minimal-loadout"
|
|
23
|
+
import { isCalendarOperation } from "./routing-intent"
|
|
24
|
+
import {
|
|
25
|
+
searchCapabilities,
|
|
26
|
+
applyRelativeCutoff,
|
|
27
|
+
replaceCapabilityDocs,
|
|
28
|
+
type CapabilityDoc,
|
|
29
|
+
} from "./capability-search"
|
|
30
|
+
|
|
31
|
+
const log = logger.child("skill-selector")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// ─── Types ───────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface SkillDescriptor {
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
description: string
|
|
41
|
+
category: string
|
|
42
|
+
tools: string
|
|
43
|
+
triggers: string
|
|
44
|
+
preferred_agents: string
|
|
45
|
+
body: string
|
|
46
|
+
version: string
|
|
47
|
+
version_num: number
|
|
48
|
+
active: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every bundled SKILL.md self-titles right after its frontmatter (e.g.
|
|
53
|
+
* "# Canvas Report Skill\n\n## Cuándo se Activa...") for standalone
|
|
54
|
+
* readability — but every system-prompt injection site wraps the body with
|
|
55
|
+
* its own `## <skill.name>` header, so the leading H1 becomes a *higher*
|
|
56
|
+
* heading level nested under a lower one (malformed hierarchy) and repeats
|
|
57
|
+
* the same title twice, for free tokens on every turn. Strip it here, once,
|
|
58
|
+
* so the 29 source files keep their own title for anyone reading them
|
|
59
|
+
* standalone, while every injected copy stays clean.
|
|
60
|
+
*/
|
|
61
|
+
function stripRedundantTitle(body: string): string {
|
|
62
|
+
// Bodies carry a leading blank line from the frontmatter's closing "---",
|
|
63
|
+
// so the H1 isn't at position 0 — allow leading whitespace before it.
|
|
64
|
+
return body.replace(/^\s*# .+\n+/, "")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toSkillDescriptor(doc: SkillDoc): SkillDescriptor {
|
|
68
|
+
return {
|
|
69
|
+
id: doc.id, name: doc.name, description: doc.description ?? "", category: doc.category,
|
|
70
|
+
tools: doc.tools, triggers: doc.triggers, preferred_agents: doc.preferred_agents,
|
|
71
|
+
body: stripRedundantTitle(doc.body), version: doc.version, version_num: doc.version_num, active: doc.active,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SelectedSkill {
|
|
76
|
+
id: string
|
|
77
|
+
name: string
|
|
78
|
+
score: number
|
|
79
|
+
category: string
|
|
80
|
+
description: string
|
|
81
|
+
body: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SkillSelectorResult {
|
|
85
|
+
skills: SkillDescriptor[]
|
|
86
|
+
selected: SelectedSkill[]
|
|
87
|
+
reasoning: string
|
|
88
|
+
timingMs: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Configuration ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Maximum skills to return per message */
|
|
94
|
+
const MAX_SKILLS_PER_TURN = 4 // Increased from 2 to allow more skills
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Relative relevance cutoff: keep a hit only if it scores at least this
|
|
98
|
+
* fraction of the top hit. HiveDB BM25 scores are positive (higher = better)
|
|
99
|
+
* but corpus-dependent, so absolute thresholds don't transfer — a ratio does.
|
|
100
|
+
*/
|
|
101
|
+
const RELEVANCE_RATIO = 0.3
|
|
102
|
+
|
|
103
|
+
/** Stopwords to filter out before BM25 query construction */
|
|
104
|
+
const STOPWORDS = new Set([
|
|
105
|
+
"que", "con", "para", "por", "una", "uno", "los", "las", "del",
|
|
106
|
+
"como", "esta", "esto", "ese", "eso", "the", "and", "for",
|
|
107
|
+
"with", "this", "that", "have", "will", "also", "de", "en",
|
|
108
|
+
"el", "la", "se", "su", "sus", "al", "es", "son", "pero",
|
|
109
|
+
"más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
|
|
110
|
+
"hola", "hi", "hello", "hey", "gracias", "thank", "please",
|
|
111
|
+
"ok", "okay", "yes", "si", "no", "bien", "good", "great",
|
|
112
|
+
"puedes", "necesito", "quiero", "podés", "necesitás", "querés",
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
/** Conversational patterns that should return empty skill list */
|
|
116
|
+
const CONVERSATIONAL_PATTERNS = [
|
|
117
|
+
/^(hola|hi|hello|hey|buenos? días?|buenas? noches?|qué tal|howdy)/i,
|
|
118
|
+
/^(gracias|thank you|thanks|muchas gracias|muchas thanks)/i,
|
|
119
|
+
/^(cómo estás?|how are you?|qué流水|you doing|qué cuentas)/i,
|
|
120
|
+
/^(sí|yes|ok|okay|de acuerdo|perfecto|claro|por supuesto)/i,
|
|
121
|
+
/^(adiós|bye|nos vemos|see you|later|chau)/i,
|
|
122
|
+
/^(entiendo|understand|i see|ya veo|got it)/i,
|
|
123
|
+
/^(bien|good|great|excelente|awesome|perfect)/i,
|
|
124
|
+
/^(?:\?|¿)$/, // Just a question mark
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
// ─── Helper Functions ───────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if message is purely conversational (no skills needed)
|
|
131
|
+
*/
|
|
132
|
+
function isConversational(message: string): boolean {
|
|
133
|
+
const trimmed = message.trim()
|
|
134
|
+
|
|
135
|
+
// Empty or very short messages
|
|
136
|
+
if (trimmed.length < 2) return true
|
|
137
|
+
|
|
138
|
+
// Check conversational patterns
|
|
139
|
+
for (const pattern of CONVERSATIONAL_PATTERNS) {
|
|
140
|
+
if (pattern.test(trimmed)) {
|
|
141
|
+
log.debug(`[skill-selector] Message matched conversational pattern: ${pattern}`)
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if all words are stopwords (likely conversational)
|
|
147
|
+
const words = trimmed.toLowerCase().split(/\s+/)
|
|
148
|
+
const meaningfulWords = words.filter(w => w.length > 2 && !STOPWORDS.has(w))
|
|
149
|
+
if (meaningfulWords.length === 0) {
|
|
150
|
+
log.debug(`[skill-selector] All words are stopwords - conversational`)
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if message matches explicit triggers from a skill
|
|
159
|
+
*/
|
|
160
|
+
function matchTriggers(message: string, triggersJson: string | null): boolean {
|
|
161
|
+
if (!triggersJson) return false
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Triggers are stored as comma-separated string in DB (e.g., "trigger1,trigger2")
|
|
165
|
+
const triggers: string[] = triggersJson.split(",").map(t => t.trim()).filter(t => t.length > 0)
|
|
166
|
+
if (triggers.length === 0) return false
|
|
167
|
+
|
|
168
|
+
const lowerMessage = message.toLowerCase()
|
|
169
|
+
return triggers.some(trigger =>
|
|
170
|
+
lowerMessage.includes(trigger.toLowerCase())
|
|
171
|
+
)
|
|
172
|
+
} catch (err) {
|
|
173
|
+
log.warn(`[skill-selector] Failed to parse triggers: ${(err as Error).message}`)
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Main Selection Function ─────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Select skills for a given user message using hybrid matching:
|
|
182
|
+
* 1. First check explicit triggers (high confidence match)
|
|
183
|
+
* 2. Fallback to HiveDB BM25 scoring for semantic matching
|
|
184
|
+
*
|
|
185
|
+
* @param userMessage - The raw user message
|
|
186
|
+
* @returns Array of selected skills
|
|
187
|
+
*
|
|
188
|
+
* ALGORITHM:
|
|
189
|
+
* 1. If conversational → return []
|
|
190
|
+
* 2. Check explicit triggers from all enabled skills
|
|
191
|
+
* 3. If trigger match found → return matching skill immediately
|
|
192
|
+
* 4. Query the HiveDB capability index with the raw message
|
|
193
|
+
* 5. Keep hits scoring at least RELEVANCE_RATIO of the top hit
|
|
194
|
+
* 6. Hydrate skill details from HiveDB and return top MAX_SKILLS_PER_TURN
|
|
195
|
+
*/
|
|
196
|
+
export async function selectSkills(userMessage: string): Promise<SkillDescriptor[]> {
|
|
197
|
+
const startTime = performance.now()
|
|
198
|
+
|
|
199
|
+
log.debug(`[skill-selector] Processing user message: "${userMessage.substring(0, 100)}"`)
|
|
200
|
+
|
|
201
|
+
// Step 1: Check if conversational
|
|
202
|
+
if (isConversational(userMessage)) {
|
|
203
|
+
log.debug(`[skill-selector] Conversational message, returning empty array`)
|
|
204
|
+
return []
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Step 2: Check explicit triggers first (high priority)
|
|
208
|
+
const skillsCol = await col<SkillDoc>("skills")
|
|
209
|
+
const calendarOperation = isCalendarOperation(userMessage)
|
|
210
|
+
const allSkills = (await skillsCol.scan({}))
|
|
211
|
+
.filter(e => e.doc.active && !(calendarOperation && e.doc.category === "cron"))
|
|
212
|
+
.map(e => toSkillDescriptor(e.doc))
|
|
213
|
+
|
|
214
|
+
// Check trigger match - if found, return immediately with high confidence
|
|
215
|
+
for (const skill of allSkills) {
|
|
216
|
+
if (skill.triggers && matchTriggers(userMessage, skill.triggers)) {
|
|
217
|
+
log.info(`[skill-selector] Trigger match found: ${skill.name}`)
|
|
218
|
+
return [skill]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Step 3: Semantic matching via the HiveDB capability index
|
|
223
|
+
let hits
|
|
224
|
+
try {
|
|
225
|
+
hits = await searchCapabilities(userMessage, { types: ["skill"], k: 20 })
|
|
226
|
+
} catch (err) {
|
|
227
|
+
log.error(`[skill-selector] Capability search failed:`, err)
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (hits.length === 0) {
|
|
232
|
+
log.debug(`[skill-selector] No matches, returning empty array`)
|
|
233
|
+
return []
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Log raw scores for debugging
|
|
237
|
+
log.info(`[skill-selector] Raw scores: ${hits.slice(0, 10).map(h => `id=${h.rawId}, score=${h.score.toFixed(2)}`).join(", ")}`)
|
|
238
|
+
|
|
239
|
+
// Step 4: Keep only hits close enough to the best match
|
|
240
|
+
const relevantHits = applyRelativeCutoff(hits, RELEVANCE_RATIO)
|
|
241
|
+
if (relevantHits.length === 0) {
|
|
242
|
+
log.debug(`[skill-selector] All results below ratio cutoff, returning empty`)
|
|
243
|
+
return []
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Step 5: Hydrate full skill details from HiveDB (already loaded above)
|
|
247
|
+
const skillMap = new Map(allSkills.map(s => [s.id, s]))
|
|
248
|
+
const result: SkillDescriptor[] = []
|
|
249
|
+
|
|
250
|
+
for (const hit of relevantHits) {
|
|
251
|
+
const skill = skillMap.get(hit.rawId)
|
|
252
|
+
if (skill) {
|
|
253
|
+
result.push(skill)
|
|
254
|
+
if (result.length === MAX_SKILLS_PER_TURN) break
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const timing = performance.now() - startTime
|
|
259
|
+
|
|
260
|
+
if (result.length > 0) {
|
|
261
|
+
log.info(`[skill-selector] Selected ${result.length} skills in ${timing.toFixed(2)}ms:`,
|
|
262
|
+
result.map(s => ({ name: s.name, category: s.category })))
|
|
263
|
+
} else {
|
|
264
|
+
log.debug(`[skill-selector] No skills selected, returning empty array in ${timing.toFixed(2)}ms`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Minimal Skills Loader ───────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Load the skills that ride along with the minimal tool loadout.
|
|
274
|
+
*
|
|
275
|
+
* Derived, not listed: a skill qualifies when every tool it documents is in
|
|
276
|
+
* MINIMAL_TOOLS (see minimal-loadout.ts). Any other skill reaches the agent
|
|
277
|
+
* through discovery, which injects it together with the tools it teaches.
|
|
278
|
+
*/
|
|
279
|
+
export async function getMinimalSkills(): Promise<SkillDescriptor[]> {
|
|
280
|
+
try {
|
|
281
|
+
const skillsCol = await col<SkillDoc>("skills")
|
|
282
|
+
const skills = (await skillsCol.scan({}))
|
|
283
|
+
.filter(e => e.doc.active && isMinimalSkill(e.doc.tools))
|
|
284
|
+
.map(e => toSkillDescriptor(e.doc))
|
|
285
|
+
|
|
286
|
+
log.info(`[skill-selector] Loaded ${skills.length} minimal skills: ${skills.map(s => s.name).join(", ")}`)
|
|
287
|
+
return skills
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.error(`[skill-selector] Failed to load minimal skills:`, err)
|
|
290
|
+
return []
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Sync Skills to HiveDB ───────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Sync all enabled skills from database to the HiveDB capability index.
|
|
298
|
+
* Should be called on initialization from gateway/initializer.ts.
|
|
299
|
+
* Replaces all `type=skill` documents atomically (delete-by-filter + one
|
|
300
|
+
* batch commit).
|
|
301
|
+
*/
|
|
302
|
+
export async function syncSkillsToIndex(): Promise<void> {
|
|
303
|
+
try {
|
|
304
|
+
// Step 1: Get all enabled skills from database
|
|
305
|
+
const skillsCol = await col<SkillDoc>("skills")
|
|
306
|
+
const dbSkills = (await skillsCol.scan({})).map(e => e.doc).filter(s => s.active)
|
|
307
|
+
|
|
308
|
+
if (dbSkills.length === 0) {
|
|
309
|
+
log.debug(`[skill-selector] No skills found in DB to sync`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Step 2: Replace all skill documents in the HiveDB capability index.
|
|
313
|
+
// name (boost 4.0) = skill name; tags (3.0) = triggers + category +
|
|
314
|
+
// tools (the strongest semantic signals); body (2.0) = description +
|
|
315
|
+
// full skill body.
|
|
316
|
+
const docs: CapabilityDoc[] = dbSkills.map(skill => ({
|
|
317
|
+
type: "skill" as const,
|
|
318
|
+
rawId: skill.id,
|
|
319
|
+
name: skill.name,
|
|
320
|
+
tags: [skill.triggers, skill.category, skill.tools].filter(Boolean).join(" "),
|
|
321
|
+
body: [skill.description, skill.body].filter(Boolean).join("\n"),
|
|
322
|
+
}))
|
|
323
|
+
|
|
324
|
+
await replaceCapabilityDocs("skill", docs)
|
|
325
|
+
|
|
326
|
+
log.info(`[skill-selector] Sync complete: ${dbSkills.length} skills indexed in HiveDB`)
|
|
327
|
+
|
|
328
|
+
} catch (err) {
|
|
329
|
+
log.error(`[skill-selector] Skill index sync failed:`, err)
|
|
330
|
+
throw err // Re-throw to inform initializer
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ─── Debug/Test Helpers ─────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get all enabled skills from database (for debugging/testing)
|
|
337
|
+
*/
|
|
338
|
+
export async function getAllSkillsFromDB(): Promise<SkillDescriptor[]> {
|
|
339
|
+
try {
|
|
340
|
+
const skillsCol = await col<SkillDoc>("skills")
|
|
341
|
+
return (await skillsCol.scan({})).filter(e => e.doc.active).map(e => toSkillDescriptor(e.doc))
|
|
342
|
+
} catch (err) {
|
|
343
|
+
log.error(`[skill-selector] Failed to fetch skills:`, err)
|
|
344
|
+
return []
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get skill by name
|
|
350
|
+
*/
|
|
351
|
+
export async function getSkillByName(name: string): Promise<SkillDescriptor | undefined> {
|
|
352
|
+
try {
|
|
353
|
+
const skillsCol = await col<SkillDoc>("skills")
|
|
354
|
+
const entry = await skillsCol.get(name)
|
|
355
|
+
return entry && entry.doc.active ? toSkillDescriptor(entry.doc) : undefined
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log.error(`[skill-selector] Failed to fetch skill by name:`, err)
|
|
358
|
+
return undefined
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get skills by category
|
|
364
|
+
*/
|
|
365
|
+
export async function getSkillsByCategory(category: string): Promise<SkillDescriptor[]> {
|
|
366
|
+
try {
|
|
367
|
+
const skillsCol = await col<SkillDoc>("skills")
|
|
368
|
+
const entries = await skillsCol.findBy("category", category)
|
|
369
|
+
return entries.filter(e => e.doc.active).map(e => toSkillDescriptor(e.doc))
|
|
370
|
+
} catch (err) {
|
|
371
|
+
log.error(`[skill-selector] Failed to fetch skills by category:`, err)
|
|
372
|
+
return []
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Config } from "../config/loader.ts";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import { hashObject } from "../utils/crypto.ts";
|
|
4
|
+
|
|
5
|
+
interface ToolCallRecord {
|
|
6
|
+
toolName: string;
|
|
7
|
+
argsHash: string;
|
|
8
|
+
errorMessage?: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ProgressRecord {
|
|
13
|
+
hash: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StuckLoopState {
|
|
18
|
+
detected: boolean;
|
|
19
|
+
toolName: string;
|
|
20
|
+
count: number;
|
|
21
|
+
lastError?: string;
|
|
22
|
+
kind: "loop" | "stall";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class StuckLoopDetector {
|
|
26
|
+
private log = logger.child("stuck-loop");
|
|
27
|
+
private history: Map<string, ToolCallRecord[]> = new Map();
|
|
28
|
+
private progressHistory: Map<string, ProgressRecord[]> = new Map();
|
|
29
|
+
private readonly maxHistoryPerSession = 50;
|
|
30
|
+
private readonly triggerThreshold = 3;
|
|
31
|
+
private readonly progressThreshold = 3;
|
|
32
|
+
|
|
33
|
+
constructor(_config: Config) {}
|
|
34
|
+
|
|
35
|
+
recordToolCall(
|
|
36
|
+
sessionId: string,
|
|
37
|
+
toolName: string,
|
|
38
|
+
args: Record<string, unknown>,
|
|
39
|
+
error?: string
|
|
40
|
+
): void {
|
|
41
|
+
let sessionHistory = this.history.get(sessionId);
|
|
42
|
+
if (!sessionHistory) {
|
|
43
|
+
sessionHistory = [];
|
|
44
|
+
this.history.set(sessionId, sessionHistory);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const record: ToolCallRecord = {
|
|
48
|
+
toolName,
|
|
49
|
+
argsHash: hashObject(args),
|
|
50
|
+
errorMessage: error,
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
sessionHistory.push(record);
|
|
55
|
+
|
|
56
|
+
if (sessionHistory.length > this.maxHistoryPerSession) {
|
|
57
|
+
sessionHistory.shift();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.log.debug(`Recorded tool call: ${toolName} for session ${sessionId}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
check(sessionId: string): StuckLoopState {
|
|
64
|
+
const sessionHistory = this.history.get(sessionId) ?? [];
|
|
65
|
+
|
|
66
|
+
if (sessionHistory.length < this.triggerThreshold) {
|
|
67
|
+
return { detected: false, toolName: "", count: 0, kind: "loop" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const recent = sessionHistory.slice(-10);
|
|
71
|
+
const counts = new Map<string, { count: number; error?: string }>();
|
|
72
|
+
|
|
73
|
+
for (const record of recent) {
|
|
74
|
+
const key = `${record.toolName}:${record.argsHash}`;
|
|
75
|
+
const existing = counts.get(key);
|
|
76
|
+
|
|
77
|
+
if (existing) {
|
|
78
|
+
existing.count++;
|
|
79
|
+
if (record.errorMessage) {
|
|
80
|
+
existing.error = record.errorMessage;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
counts.set(key, { count: 1, error: record.errorMessage });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [key, data] of counts) {
|
|
88
|
+
if (data.count >= this.triggerThreshold && data.error) {
|
|
89
|
+
const toolName = key.split(":")[0] ?? "unknown";
|
|
90
|
+
|
|
91
|
+
this.log.warn(`Stuck loop detected: ${toolName} called ${data.count} times with same args and error`);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
detected: true,
|
|
95
|
+
toolName,
|
|
96
|
+
count: data.count,
|
|
97
|
+
lastError: data.error,
|
|
98
|
+
kind: "loop",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { detected: false, toolName: "", count: 0, kind: "loop" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Record a progress snapshot hash. Use this to detect when the agent is not
|
|
108
|
+
* advancing even though no tool errors are occurring.
|
|
109
|
+
*/
|
|
110
|
+
recordProgress(sessionId: string, progressHash: string): void {
|
|
111
|
+
let sessionProgress = this.progressHistory.get(sessionId);
|
|
112
|
+
if (!sessionProgress) {
|
|
113
|
+
sessionProgress = [];
|
|
114
|
+
this.progressHistory.set(sessionId, sessionProgress);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sessionProgress.push({ hash: progressHash, timestamp: Date.now() });
|
|
118
|
+
|
|
119
|
+
if (sessionProgress.length > this.maxHistoryPerSession) {
|
|
120
|
+
sessionProgress.shift();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.log.debug(`Recorded progress hash for session ${sessionId}: ${progressHash}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detect lack of progress when the same hash appears repeatedly.
|
|
128
|
+
*/
|
|
129
|
+
checkProgress(sessionId: string, threshold?: number): StuckLoopState {
|
|
130
|
+
const sessionProgress = this.progressHistory.get(sessionId) ?? [];
|
|
131
|
+
const required = threshold ?? this.progressThreshold;
|
|
132
|
+
|
|
133
|
+
if (sessionProgress.length < required) {
|
|
134
|
+
return { detected: false, toolName: "", count: 0, kind: "stall" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const recent = sessionProgress.slice(-required);
|
|
138
|
+
const firstHash = recent[0]?.hash;
|
|
139
|
+
const allSame = recent.every((r) => r.hash === firstHash);
|
|
140
|
+
|
|
141
|
+
if (allSame && firstHash) {
|
|
142
|
+
this.log.warn(`Stall detected: no progress for ${required} checks (hash ${firstHash})`);
|
|
143
|
+
return {
|
|
144
|
+
detected: true,
|
|
145
|
+
toolName: "NO_PROGRESS",
|
|
146
|
+
count: required,
|
|
147
|
+
kind: "stall",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { detected: false, toolName: "", count: 0, kind: "stall" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
clear(sessionId: string): void {
|
|
155
|
+
this.history.delete(sessionId);
|
|
156
|
+
this.progressHistory.delete(sessionId);
|
|
157
|
+
this.log.debug(`Cleared stuck loop history for session ${sessionId}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
prune(maxAgeMs: number = 30 * 60 * 1000): number {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
let pruned = 0;
|
|
163
|
+
|
|
164
|
+
for (const [sessionId, history] of this.history) {
|
|
165
|
+
const filtered = history.filter((r) => now - r.timestamp < maxAgeMs);
|
|
166
|
+
|
|
167
|
+
if (filtered.length === 0) {
|
|
168
|
+
this.history.delete(sessionId);
|
|
169
|
+
pruned++;
|
|
170
|
+
} else if (filtered.length !== history.length) {
|
|
171
|
+
this.history.set(sessionId, filtered);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [sessionId, history] of this.progressHistory) {
|
|
176
|
+
const filtered = history.filter((r) => now - r.timestamp < maxAgeMs);
|
|
177
|
+
|
|
178
|
+
if (filtered.length === 0) {
|
|
179
|
+
this.progressHistory.delete(sessionId);
|
|
180
|
+
pruned++;
|
|
181
|
+
} else if (filtered.length !== history.length) {
|
|
182
|
+
this.progressHistory.set(sessionId, filtered);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return pruned;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createStuckLoopDetector(config: Config): StuckLoopDetector {
|
|
191
|
+
return new StuckLoopDetector(config);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build a human-readable intervention message for the model.
|
|
196
|
+
*/
|
|
197
|
+
export function getInterventionMessage(state: StuckLoopState): string {
|
|
198
|
+
if (!state.detected) return "";
|
|
199
|
+
|
|
200
|
+
if (state.kind === "stall") {
|
|
201
|
+
return `WARNING: Has estado sin avanzar durante ${state.count} ciclos consecutivos. Revisa tu plan, intenta una herramienta diferente o pide aclaración al usuario en lugar de repetir la misma secuencia.`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (state.count >= 4) {
|
|
205
|
+
return `CRITICAL: Has llamado ${state.toolName} ${state.count} veces con los mismos argumentos y sigue fallando con: "${state.lastError}". El usuario será notificado. Debes cambiar completamente de estrategia o pedir ayuda.`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `WARNING: Has llamado ${state.toolName} ${state.count} veces con los mismos argumentos y sigue fallando. Debes probar un enfoque diferente en lugar de repetir la misma acción.`;
|
|
209
|
+
}
|