@johpaz/hive-sdk 0.1.3 → 0.1.5
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 +97 -0
- package/README.md +78 -23
- 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 +9 -4
- 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 +5 -4
- package/packages/core/src/agent/acceptance-checks.ts +166 -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 +71 -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 -20
- 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 -17
- 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-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +5 -0
- 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/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/agent-loop-terminal-synthesis.test.ts +32 -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/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
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger
|
|
2
|
-
import { sanitizeMessages } from "./interface"
|
|
1
|
+
import { logger } from "../../utils/logger"
|
|
2
|
+
import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
|
|
3
3
|
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
4
|
-
import type { ContentPart, LLMMessage } from "
|
|
4
|
+
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
5
5
|
|
|
6
6
|
const log = logger.child("llm-client")
|
|
7
7
|
|
|
8
|
+
// Local models known to support Ollama's `think` request flag (returns
|
|
9
|
+
// message.thinking separate from message.content). Conservative allowlist —
|
|
10
|
+
// unrecognized models don't get `think:true` since some may error on it
|
|
11
|
+
// rather than silently ignore it.
|
|
12
|
+
const THINKING_CAPABLE_MODEL_PATTERNS = [/deepseek-r1/i, /^qwq/i, /qwen3.*think/i]
|
|
13
|
+
|
|
14
|
+
function supportsThinking(model: string): boolean {
|
|
15
|
+
return THINKING_CAPABLE_MODEL_PATTERNS.some((re) => re.test(model))
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export class OllamaProvider implements LLMProvider {
|
|
9
19
|
private _convertMessage(msg: LLMMessage): any {
|
|
10
20
|
if (typeof msg.content === "string") {
|
|
@@ -48,6 +58,9 @@ export class OllamaProvider implements LLMProvider {
|
|
|
48
58
|
host,
|
|
49
59
|
...(Object.keys(headers).length ? { headers } : {}),
|
|
50
60
|
})
|
|
61
|
+
// Ollama's client aborts all of its own in-flight streams — safe 1:1 with
|
|
62
|
+
// options.signal since a fresh client is created per call.
|
|
63
|
+
if (options.signal) options.signal.addEventListener("abort", () => client.abort(), { once: true })
|
|
51
64
|
|
|
52
65
|
const messages = sanitizeMessages(options.messages).map((m): any => {
|
|
53
66
|
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
@@ -73,7 +86,7 @@ export class OllamaProvider implements LLMProvider {
|
|
|
73
86
|
function: {
|
|
74
87
|
name: t.function.name,
|
|
75
88
|
description: t.function.description,
|
|
76
|
-
parameters: t.function.parameters,
|
|
89
|
+
parameters: ensureArrayItems(t.function.parameters),
|
|
77
90
|
},
|
|
78
91
|
}))
|
|
79
92
|
|
|
@@ -85,6 +98,8 @@ export class OllamaProvider implements LLMProvider {
|
|
|
85
98
|
}
|
|
86
99
|
if (options.numGpu !== undefined) runtimeOptions.num_gpu = options.numGpu
|
|
87
100
|
if (options.temperature !== undefined) runtimeOptions.temperature = options.temperature
|
|
101
|
+
const maxTokens = resolveMaxTokens(options.maxTokens, options.contextWindow)
|
|
102
|
+
if (maxTokens) runtimeOptions.num_predict = maxTokens
|
|
88
103
|
|
|
89
104
|
try {
|
|
90
105
|
|
|
@@ -94,20 +109,30 @@ export class OllamaProvider implements LLMProvider {
|
|
|
94
109
|
` num_ctx=${runtimeOptions.num_ctx}`
|
|
95
110
|
)
|
|
96
111
|
|
|
112
|
+
const thinkEnabled = options.thinking?.enabled && supportsThinking(modelName)
|
|
113
|
+
|
|
97
114
|
const stream = await client.chat({
|
|
98
115
|
model: modelName,
|
|
99
116
|
messages,
|
|
100
117
|
tools: tools?.length ? tools : undefined,
|
|
101
118
|
options: Object.keys(runtimeOptions).length ? runtimeOptions : undefined,
|
|
102
119
|
stream: true,
|
|
103
|
-
|
|
120
|
+
...(thinkEnabled ? { think: true } : {}),
|
|
121
|
+
} as any)
|
|
104
122
|
|
|
105
123
|
let content = ""
|
|
124
|
+
let reasoning_content = ""
|
|
106
125
|
let promptEvalCount = 0
|
|
107
126
|
let evalCount = 0
|
|
108
127
|
const tool_calls: LLMToolCall[] = []
|
|
109
128
|
|
|
110
129
|
for await (const part of stream) {
|
|
130
|
+
const thinkingDelta = (part.message as any)?.thinking ?? ""
|
|
131
|
+
if (thinkingDelta) {
|
|
132
|
+
reasoning_content += thinkingDelta
|
|
133
|
+
options.onReasoningToken?.(thinkingDelta)
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
const delta = part.message?.content ?? ""
|
|
112
137
|
if (delta) {
|
|
113
138
|
content += delta
|
|
@@ -133,6 +158,7 @@ export class OllamaProvider implements LLMProvider {
|
|
|
133
158
|
|
|
134
159
|
return {
|
|
135
160
|
content,
|
|
161
|
+
reasoning_content: reasoning_content || undefined,
|
|
136
162
|
tool_calls: tool_calls.length ? tool_calls : undefined,
|
|
137
163
|
stop_reason: tool_calls.length > 0 ? "tool_calls" : "stop",
|
|
138
164
|
usage:
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger"
|
|
2
|
+
import {
|
|
3
|
+
sanitizeMessages, requiresTemperature1, OPENAI_COMPAT_BASE_URLS,
|
|
4
|
+
getProviderProfile, modelSupportsTools, normalizeToolName, normalizeToolSchema,
|
|
5
|
+
resolveMaxTokens,
|
|
6
|
+
} from "./interface"
|
|
7
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
8
|
+
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
9
|
+
|
|
10
|
+
const log = logger.child("llm-client")
|
|
11
|
+
|
|
12
|
+
/** Matches both generic "context length exceeded" phrasing and llama.cpp's exceed_context_size_error shape. */
|
|
13
|
+
function isContextOverflowError(err: any, errMsg: string): boolean {
|
|
14
|
+
const status = err?.status ?? err?.response?.status
|
|
15
|
+
if (status !== 400) return false
|
|
16
|
+
if (err?.error?.type === "exceed_context_size_error" || err?.type === "exceed_context_size_error") return true
|
|
17
|
+
return errMsg.includes("context length") || errMsg.includes("input_tokens")
|
|
18
|
+
|| errMsg.includes("maximum input length") || errMsg.includes("context size")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** llama.cpp-style errors report the server's real context size in n_ctx — use it when present. */
|
|
22
|
+
function extractRealContextSize(err: any): number | undefined {
|
|
23
|
+
return err?.error?.n_ctx ?? err?.n_ctx
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Keeps system + last ~33% of messages, and shrinks max_tokens — using the real n_ctx when the error provided one. */
|
|
27
|
+
function compactBodyForContextOverflow(body: any, err: any): void {
|
|
28
|
+
const kept: any[] = []
|
|
29
|
+
let systemMsg: any = null
|
|
30
|
+
for (const m of body.messages) {
|
|
31
|
+
if (m.role === "system") { systemMsg = m; continue }
|
|
32
|
+
kept.push(m)
|
|
33
|
+
}
|
|
34
|
+
const keepRatio = Math.max(1, Math.floor(kept.length / 3))
|
|
35
|
+
const trimmed = kept.slice(-keepRatio)
|
|
36
|
+
body.messages = systemMsg ? [systemMsg, ...trimmed] : trimmed
|
|
37
|
+
|
|
38
|
+
const realCtx = extractRealContextSize(err)
|
|
39
|
+
if (realCtx) {
|
|
40
|
+
body.max_tokens = Math.min(body.max_tokens ?? realCtx, Math.floor(realCtx * 0.25))
|
|
41
|
+
} else if (body.max_tokens) {
|
|
42
|
+
body.max_tokens = Math.min(body.max_tokens, 4096)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export abstract class OpenAICompatBase implements LLMProvider {
|
|
47
|
+
constructor(protected readonly providerName: string) {}
|
|
48
|
+
|
|
49
|
+
/** Override to true when the provider requires reasoning_content to be round-tripped. */
|
|
50
|
+
protected needsReasoningRoundtrip(): boolean { return false }
|
|
51
|
+
|
|
52
|
+
/** Override to true for providers running on localhost. */
|
|
53
|
+
protected isLocalProvider(): boolean { return false }
|
|
54
|
+
|
|
55
|
+
/** Override to customize the OpenAI client (e.g. strip unwanted headers, add custom fetch). */
|
|
56
|
+
protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined): Promise<any> {
|
|
57
|
+
const { default: OpenAI } = await import("openai")
|
|
58
|
+
return new OpenAI({ apiKey, baseURL })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Hook called before each request. Override for e.g. auto-starting a local server. */
|
|
62
|
+
protected async beforeCall(_options: LLMCallOptions): Promise<void> {}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hook called after tools are prepared when sendTools is true.
|
|
66
|
+
* Override to inject tool descriptions into the system prompt
|
|
67
|
+
* (for local models whose chat templates don't support native tool calling).
|
|
68
|
+
*/
|
|
69
|
+
protected injectToolsIntoPrompt(_body: any, _preparedTools: any[]): void {}
|
|
70
|
+
|
|
71
|
+
/** Override to add provider-specific fields to the request body (e.g. extra_body for llama.cpp chat_template_kwargs). */
|
|
72
|
+
protected modifyRequestBody(body: any, _options: LLMCallOptions): any { return body }
|
|
73
|
+
|
|
74
|
+
private _convertContentPart(part: ContentPart): any {
|
|
75
|
+
switch (part.type) {
|
|
76
|
+
case "text":
|
|
77
|
+
return { type: "text", text: part.text }
|
|
78
|
+
case "image_url":
|
|
79
|
+
return { type: "image_url", image_url: { url: part.image_url.url } }
|
|
80
|
+
case "image_base64":
|
|
81
|
+
return { type: "image_url", image_url: { url: `data:${part.mimeType};base64,${part.base64}` } }
|
|
82
|
+
case "document":
|
|
83
|
+
log.warn(`[llm-client] ${this.providerName}: document content parts are not supported — content will be omitted`)
|
|
84
|
+
return { type: "text", text: `[Document: ${(part as any).fileName || "file"}] (content not supported for this provider)` }
|
|
85
|
+
default:
|
|
86
|
+
return { type: "text", text: JSON.stringify(part) }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private _convertMessage(msg: LLMMessage): any {
|
|
91
|
+
if (Array.isArray(msg.content)) {
|
|
92
|
+
return { ...msg, content: msg.content.map(p => this._convertContentPart(p)) }
|
|
93
|
+
}
|
|
94
|
+
return msg
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async call(options: LLMCallOptions): Promise<LLMResponse> {
|
|
98
|
+
const baseURL = options.baseUrl?.trim() || OPENAI_COMPAT_BASE_URLS[this.providerName] || undefined
|
|
99
|
+
const isLocal = this.isLocalProvider()
|
|
100
|
+
|
|
101
|
+
await this.beforeCall(options)
|
|
102
|
+
|
|
103
|
+
const apiKey = options.apiKey || (isLocal ? "ollama" : undefined)
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
throw new Error(`API key missing for provider: ${this.providerName}. Configure it in Settings → Providers.`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const client = await this.resolveOpenAIClient(apiKey, baseURL)
|
|
109
|
+
|
|
110
|
+
const sanitized = sanitizeMessages(options.messages)
|
|
111
|
+
const rawMessages = this.needsReasoningRoundtrip()
|
|
112
|
+
? sanitized
|
|
113
|
+
: sanitized.map(({ reasoning_content: _rc, ...rest }) => rest as typeof sanitized[number])
|
|
114
|
+
const messagesForProvider = rawMessages.map(m => this._convertMessage(m))
|
|
115
|
+
|
|
116
|
+
const providerPrefix = new RegExp(`^${this.providerName}\\/`, "i")
|
|
117
|
+
const body: any = {
|
|
118
|
+
model: options.model.replace(providerPrefix, ""),
|
|
119
|
+
messages: messagesForProvider,
|
|
120
|
+
temperature: requiresTemperature1(this.providerName, options.model) ? 1 : (options.temperature ?? 0.7),
|
|
121
|
+
}
|
|
122
|
+
const maxTokens = resolveMaxTokens(options.maxTokens, options.contextWindow)
|
|
123
|
+
if (maxTokens) body.max_tokens = maxTokens
|
|
124
|
+
if (options.numCtx && isLocal) body.num_ctx = options.numCtx
|
|
125
|
+
|
|
126
|
+
const profile = getProviderProfile(this.providerName)
|
|
127
|
+
const sendTools = modelSupportsTools(this.providerName, options.model) && !!(options.tools?.length)
|
|
128
|
+
|
|
129
|
+
const toolNameMap = new Map<string, string>()
|
|
130
|
+
|
|
131
|
+
if (sendTools) {
|
|
132
|
+
const preparedTools = options.tools!.map((t) => {
|
|
133
|
+
const originalName = t.function.name
|
|
134
|
+
const wireName = profile.normalizeToolNames
|
|
135
|
+
? normalizeToolName(originalName, profile.toolNameReplacement)
|
|
136
|
+
: originalName
|
|
137
|
+
if (wireName !== originalName) toolNameMap.set(wireName, originalName)
|
|
138
|
+
return {
|
|
139
|
+
...t,
|
|
140
|
+
function: {
|
|
141
|
+
...t.function,
|
|
142
|
+
name: wireName,
|
|
143
|
+
parameters: normalizeToolSchema(t.function.parameters as Record<string, unknown>, profile),
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
body.tools = preparedTools
|
|
148
|
+
body.tool_choice = profile.toolChoiceAuto
|
|
149
|
+
if (profile.disableParallelToolCalls) body.parallel_tool_calls = false
|
|
150
|
+
|
|
151
|
+
this.injectToolsIntoPrompt(body, preparedTools)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log.info(`[llm-client] ${this.providerName}/${body.model} — ${options.messages.length} msgs, ${options.tools?.length ?? 0} tools${sendTools ? "" : " (tools suppressed)"}`)
|
|
155
|
+
|
|
156
|
+
if (options.onToken) {
|
|
157
|
+
return this._streamCall(client, body, options, toolNameMap, sendTools, profile)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let response
|
|
161
|
+
try {
|
|
162
|
+
response = await client.chat.completions.create(this.modifyRequestBody(body, options), { signal: options.signal })
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
const status = err?.status ?? err?.response?.status
|
|
165
|
+
const errMsg = (err?.error?.message ?? err?.message ?? "").toLowerCase()
|
|
166
|
+
|
|
167
|
+
// Retry 1: context overflow — compact messages and retry. Checked BEFORE the
|
|
168
|
+
// tools-rejected branch below: a status-code-only check would otherwise catch
|
|
169
|
+
// a genuine context-overflow 400 first (many providers share 400 for both
|
|
170
|
+
// cases) and retry by stripping tools, which does nothing for an oversized
|
|
171
|
+
// prompt and just fails again the same way.
|
|
172
|
+
if (isContextOverflowError(err, errMsg)) {
|
|
173
|
+
log.warn(`[llm-client] ${this.providerName}: context overflow — compacting messages and retrying`)
|
|
174
|
+
const originalCount = body.messages.length
|
|
175
|
+
compactBodyForContextOverflow(body, err)
|
|
176
|
+
log.info(`[llm-client] ${this.providerName}: compacted ${originalCount} msgs → ${body.messages.length} msgs, max_tokens=${body.max_tokens}`)
|
|
177
|
+
response = await client.chat.completions.create(this.modifyRequestBody(body, options), { signal: options.signal })
|
|
178
|
+
}
|
|
179
|
+
// Retry 2: tools rejected by provider — remove tools and retry
|
|
180
|
+
else if (sendTools && profile.retryWithoutToolsOnCodes.includes(status)) {
|
|
181
|
+
log.warn(`[llm-client] ${this.providerName}: tools rejected (HTTP ${status}) — retrying without tools`)
|
|
182
|
+
const bodyNoTools = { ...body }
|
|
183
|
+
delete bodyNoTools.tools
|
|
184
|
+
delete bodyNoTools.tool_choice
|
|
185
|
+
delete bodyNoTools.parallel_tool_calls
|
|
186
|
+
response = await client.chat.completions.create(this.modifyRequestBody(bodyNoTools, options), { signal: options.signal })
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
throw err
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const choice = response.choices[0]
|
|
194
|
+
const msg = choice.message
|
|
195
|
+
|
|
196
|
+
let final_tool_calls: LLMToolCall[] | undefined = (msg.tool_calls as any[])?.map((tc: any) => ({
|
|
197
|
+
id: tc.id,
|
|
198
|
+
type: "function" as const,
|
|
199
|
+
function: {
|
|
200
|
+
name: toolNameMap.get(tc.function.name) ?? tc.function.name,
|
|
201
|
+
arguments: tc.function.arguments,
|
|
202
|
+
},
|
|
203
|
+
}))
|
|
204
|
+
|
|
205
|
+
let final_content = msg.content ?? ""
|
|
206
|
+
|
|
207
|
+
if (sendTools && (!final_tool_calls || final_tool_calls.length === 0) && final_content) {
|
|
208
|
+
const extracted = extractToolCallsFromText(final_content, toolNameMap)
|
|
209
|
+
if (extracted.tool_calls.length > 0) {
|
|
210
|
+
final_tool_calls = extracted.tool_calls
|
|
211
|
+
final_content = extracted.content
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
content: final_content,
|
|
217
|
+
tool_calls: final_tool_calls,
|
|
218
|
+
reasoning_content: (msg as any).reasoning_content ?? undefined,
|
|
219
|
+
stop_reason:
|
|
220
|
+
choice.finish_reason === "tool_calls" ? "tool_calls"
|
|
221
|
+
: choice.finish_reason === "length" ? "max_tokens"
|
|
222
|
+
: "stop",
|
|
223
|
+
usage: response.usage ? {
|
|
224
|
+
input_tokens: response.usage.prompt_tokens,
|
|
225
|
+
output_tokens: response.usage.completion_tokens,
|
|
226
|
+
} : undefined,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async _streamCall(
|
|
231
|
+
client: any,
|
|
232
|
+
body: any,
|
|
233
|
+
options: LLMCallOptions,
|
|
234
|
+
toolNameMap: Map<string, string>,
|
|
235
|
+
sendTools: boolean,
|
|
236
|
+
profile: ReturnType<typeof getProviderProfile>,
|
|
237
|
+
): Promise<LLMResponse> {
|
|
238
|
+
let stream
|
|
239
|
+
try {
|
|
240
|
+
stream = await client.chat.completions.create({ ...this.modifyRequestBody(body, options), stream: true }, { signal: options.signal })
|
|
241
|
+
} catch (err: any) {
|
|
242
|
+
const status = err?.status ?? err?.response?.status
|
|
243
|
+
const errMsg = (err?.error?.message ?? err?.message ?? "").toLowerCase()
|
|
244
|
+
|
|
245
|
+
if (isContextOverflowError(err, errMsg)) {
|
|
246
|
+
log.warn(`[llm-client] ${this.providerName}: context overflow — compacting messages and retrying stream`)
|
|
247
|
+
const originalCount = body.messages.length
|
|
248
|
+
compactBodyForContextOverflow(body, err)
|
|
249
|
+
log.info(`[llm-client] ${this.providerName}: compacted ${originalCount} msgs → ${body.messages.length} msgs, max_tokens=${body.max_tokens}`)
|
|
250
|
+
stream = await client.chat.completions.create({ ...this.modifyRequestBody(body, options), stream: true }, { signal: options.signal })
|
|
251
|
+
} else if (sendTools && profile.retryWithoutToolsOnCodes.includes(status)) {
|
|
252
|
+
log.warn(`[llm-client] ${this.providerName}: tools rejected (HTTP ${status}) — retrying stream without tools`)
|
|
253
|
+
const bodyNoTools = { ...body }
|
|
254
|
+
delete bodyNoTools.tools
|
|
255
|
+
delete bodyNoTools.tool_choice
|
|
256
|
+
delete bodyNoTools.parallel_tool_calls
|
|
257
|
+
stream = await client.chat.completions.create({ ...this.modifyRequestBody(bodyNoTools, options), stream: true }, { signal: options.signal })
|
|
258
|
+
} else {
|
|
259
|
+
throw err
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let content = ""
|
|
264
|
+
let reasoning_content = ""
|
|
265
|
+
let finish_reason = "stop"
|
|
266
|
+
const toolCallMap: Map<number, { id: string; name: string; arguments: string }> = new Map()
|
|
267
|
+
let input_tokens = 0
|
|
268
|
+
let output_tokens = 0
|
|
269
|
+
|
|
270
|
+
for await (const chunk of stream) {
|
|
271
|
+
const choice = chunk.choices?.[0]
|
|
272
|
+
if (!choice) continue
|
|
273
|
+
|
|
274
|
+
const delta = choice.delta as any
|
|
275
|
+
if (delta.content) {
|
|
276
|
+
content += delta.content
|
|
277
|
+
options.onToken!(delta.content)
|
|
278
|
+
}
|
|
279
|
+
if (delta.reasoning_content) {
|
|
280
|
+
reasoning_content += delta.reasoning_content
|
|
281
|
+
options.onReasoningToken?.(delta.reasoning_content)
|
|
282
|
+
}
|
|
283
|
+
if (delta.tool_calls) {
|
|
284
|
+
for (const tc of delta.tool_calls) {
|
|
285
|
+
const idx: number = tc.index
|
|
286
|
+
if (!toolCallMap.has(idx)) {
|
|
287
|
+
toolCallMap.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" })
|
|
288
|
+
}
|
|
289
|
+
const entry = toolCallMap.get(idx)!
|
|
290
|
+
if (tc.id) entry.id = tc.id
|
|
291
|
+
if (tc.function?.name) entry.name = tc.function.name
|
|
292
|
+
if (tc.function?.arguments) entry.arguments += tc.function.arguments
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (choice.finish_reason) finish_reason = choice.finish_reason
|
|
296
|
+
|
|
297
|
+
if (chunk.usage) {
|
|
298
|
+
input_tokens = chunk.usage.prompt_tokens ?? 0
|
|
299
|
+
output_tokens = chunk.usage.completion_tokens ?? 0
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const tool_calls: LLMToolCall[] = [...toolCallMap.values()].map((tc) => ({
|
|
304
|
+
id: tc.id,
|
|
305
|
+
type: "function" as const,
|
|
306
|
+
function: {
|
|
307
|
+
name: toolNameMap.get(tc.name) ?? tc.name,
|
|
308
|
+
arguments: tc.arguments || "{}",
|
|
309
|
+
},
|
|
310
|
+
}))
|
|
311
|
+
|
|
312
|
+
let final_tool_calls: LLMToolCall[] | undefined = tool_calls.length ? tool_calls : undefined
|
|
313
|
+
let final_content = content
|
|
314
|
+
|
|
315
|
+
if (sendTools && !final_tool_calls && final_content) {
|
|
316
|
+
const extracted = extractToolCallsFromText(final_content, toolNameMap)
|
|
317
|
+
if (extracted.tool_calls.length > 0) {
|
|
318
|
+
final_tool_calls = extracted.tool_calls
|
|
319
|
+
final_content = extracted.content
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
content: final_content,
|
|
325
|
+
tool_calls: final_tool_calls,
|
|
326
|
+
reasoning_content: reasoning_content || undefined,
|
|
327
|
+
stop_reason:
|
|
328
|
+
finish_reason === "tool_calls" ? "tool_calls"
|
|
329
|
+
: finish_reason === "length" ? "max_tokens"
|
|
330
|
+
: "stop",
|
|
331
|
+
usage: input_tokens > 0 || output_tokens > 0
|
|
332
|
+
? { input_tokens, output_tokens }
|
|
333
|
+
: undefined,
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Extracts tool_calls from text when the model fails to emit native tool_calls.
|
|
340
|
+
* Supports common formats used by Gemma, Qwen, and other local models.
|
|
341
|
+
*/
|
|
342
|
+
function extractToolCallsFromText(
|
|
343
|
+
content: string,
|
|
344
|
+
toolNameMap: Map<string, string>,
|
|
345
|
+
knownToolNames?: Set<string>,
|
|
346
|
+
): { content: string; tool_calls: LLMToolCall[] } {
|
|
347
|
+
const tool_calls: LLMToolCall[] = []
|
|
348
|
+
let extractedContent = content
|
|
349
|
+
|
|
350
|
+
// Regexes for wrapped tool-call blocks.
|
|
351
|
+
const regexes = [
|
|
352
|
+
/<tool_call>\s*({[\s\S]*?})\s*<\/tool_call>/g,
|
|
353
|
+
/<function_call>\s*({[\s\S]*?})\s*<\/function_call>/g,
|
|
354
|
+
/```(?:tool_call|json)\s*({[\s\S]*?})\s*```/g,
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
for (const regex of regexes) {
|
|
358
|
+
let match
|
|
359
|
+
while ((match = regex.exec(content)) !== null) {
|
|
360
|
+
try {
|
|
361
|
+
const json = JSON.parse(match[1])
|
|
362
|
+
const calls = Array.isArray(json) ? json : [json]
|
|
363
|
+
for (const call of calls) {
|
|
364
|
+
if (!call) continue
|
|
365
|
+
// Accept both { name, arguments } and { function: { name, arguments } }
|
|
366
|
+
const fn = call.function || call
|
|
367
|
+
const name = fn.name ?? call.name
|
|
368
|
+
let args = fn.arguments ?? call.arguments ?? call.parameters
|
|
369
|
+
if (!name) continue
|
|
370
|
+
tool_calls.push({
|
|
371
|
+
id: crypto.randomUUID(),
|
|
372
|
+
type: "function",
|
|
373
|
+
function: {
|
|
374
|
+
name: toolNameMap.get(name) ?? name,
|
|
375
|
+
arguments: typeof args === "object" ? JSON.stringify(args) : (args || "{}"),
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
extractedContent = extractedContent.replace(match[0], "").trim()
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
// ignore parse errors
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fallback: entire output is a bare JSON tool call — only if name matches a known tool
|
|
387
|
+
if (tool_calls.length === 0 && knownToolNames && knownToolNames.size > 0) {
|
|
388
|
+
try {
|
|
389
|
+
const trimmed = content.trim()
|
|
390
|
+
// Strip common markdown fences before parsing.
|
|
391
|
+
const jsonText = trimmed.replace(/^```(?:json|tool_call)?\s*|\s*```$/g, "").trim()
|
|
392
|
+
const json = JSON.parse(jsonText)
|
|
393
|
+
const calls = Array.isArray(json) ? json : [json]
|
|
394
|
+
for (const call of calls) {
|
|
395
|
+
if (!call) continue
|
|
396
|
+
const fn = call.function || call
|
|
397
|
+
const name = fn.name ?? call.name
|
|
398
|
+
let args = fn.arguments ?? call.arguments ?? call.parameters
|
|
399
|
+
const resolvedName = toolNameMap.get(name) ?? name
|
|
400
|
+
if (name && knownToolNames.has(resolvedName) && (args !== undefined || calls.length === 1)) {
|
|
401
|
+
tool_calls.push({
|
|
402
|
+
id: crypto.randomUUID(),
|
|
403
|
+
type: "function",
|
|
404
|
+
function: {
|
|
405
|
+
name: resolvedName,
|
|
406
|
+
arguments: typeof args === "object" ? JSON.stringify(args) : (args || "{}"),
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
extractedContent = ""
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
// not valid JSON
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { content: extractedContent, tool_calls }
|
|
418
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal loadout — the single source of truth for what the coordinator starts
|
|
3
|
+
* a turn with, and the rule that derives which skills ride along with it.
|
|
4
|
+
*
|
|
5
|
+
* Everything else is discovered at runtime through `search_knowledge`, which
|
|
6
|
+
* injects both the tool and its associated skill into the loadout
|
|
7
|
+
* (agent-loop.ts). So a skill that documents tools outside this set has nothing
|
|
8
|
+
* to teach a turn that hasn't discovered them yet — it only spends context.
|
|
9
|
+
*
|
|
10
|
+
* These used to be two hand-maintained lists (one here, one in skill-selector)
|
|
11
|
+
* and they drifted: 3 of the 4 "minimal" skills documented tools the agent did
|
|
12
|
+
* not have. The skill set is now derived from the tool set so it cannot.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Tools always present in the coordinator's loadout, without discovery. */
|
|
16
|
+
export const MINIMAL_TOOLS = new Set([
|
|
17
|
+
// Discovery — the entry point to everything else
|
|
18
|
+
"search_knowledge",
|
|
19
|
+
// Communication with the user
|
|
20
|
+
"notify",
|
|
21
|
+
"report_progress",
|
|
22
|
+
// Notes that survive context compaction
|
|
23
|
+
"save_note",
|
|
24
|
+
// Orchestration: the coordinator's own competency, not an optional capability
|
|
25
|
+
"task_delegate",
|
|
26
|
+
"task_revise",
|
|
27
|
+
"agent_find",
|
|
28
|
+
"task_status",
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
/** Splits a SkillDoc's comma-separated `tools` column into tool names. */
|
|
32
|
+
export function parseSkillTools(toolsCsv: string | null | undefined): string[] {
|
|
33
|
+
return (toolsCsv ?? "")
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((name) => name.trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A skill is minimal when every tool it documents is already in the loadout.
|
|
41
|
+
* A skill that declares no tools is not minimal: it has no anchor to the
|
|
42
|
+
* always-available set, so it belongs to discovery like any other.
|
|
43
|
+
*/
|
|
44
|
+
export function isMinimalSkill(toolsCsv: string | null | undefined): boolean {
|
|
45
|
+
const tools = parseSkillTools(toolsCsv)
|
|
46
|
+
return tools.length > 0 && tools.every((name) => MINIMAL_TOOLS.has(name))
|
|
47
|
+
}
|