@johpaz/hive-sdk 0.1.4 → 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 -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-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
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Store — persists message history in the `conversations` HiveDB collection.
|
|
3
|
+
* Replaces the LangGraph BunSqliteSaver + lg_checkpoints approach.
|
|
4
|
+
*
|
|
5
|
+
* Also manages: summaries and scratchpad, both HiveDB document collections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { col, nextId, bumpRollup } from "../storage/hive"
|
|
9
|
+
import { getHiveDb } from "../storage/hivedb"
|
|
10
|
+
import { logger } from "../utils/logger"
|
|
11
|
+
import type { LLMMessage, ContentPart } from "./llm-client"
|
|
12
|
+
import { estimateTokens } from "../utils/toon"
|
|
13
|
+
import type { ConversationDoc, SummaryDoc, MessageSource } from "../storage/collections"
|
|
14
|
+
|
|
15
|
+
const log = logger.child("conv-store")
|
|
16
|
+
|
|
17
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface StoredMessage {
|
|
20
|
+
/** Per-thread monotonic sequence number (NOT globally unique — scope every comparison to a single threadId). */
|
|
21
|
+
id: number
|
|
22
|
+
thread_id: string
|
|
23
|
+
channel: string
|
|
24
|
+
role: "user" | "assistant" | "tool"
|
|
25
|
+
/** Provenance of this turn. Never null — legacy rows are normalized to "legacy_internal" on read. */
|
|
26
|
+
source: MessageSource
|
|
27
|
+
content: string
|
|
28
|
+
tool_calls_json: string | null
|
|
29
|
+
tool_call_id: string | null
|
|
30
|
+
reasoning_content: string | null // Kimi K2 thinking — must be round-tripped
|
|
31
|
+
content_multimodal: string | null // JSON array of ContentPart[]
|
|
32
|
+
token_count: number
|
|
33
|
+
created_at: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Internal events (delegation fan-in, etc.) ────────────────────────────────
|
|
37
|
+
//
|
|
38
|
+
// These are system-authored turns (async delegation outcomes) that must reach
|
|
39
|
+
// the model as input but must never be persisted with role:"system" — doing so
|
|
40
|
+
// causes every LLM provider to hoist them permanently into the system
|
|
41
|
+
// instruction on every subsequent turn (see gemini.ts/anthropic.ts, which
|
|
42
|
+
// concatenate ALL role:"system" messages into systemInstruction/system). They
|
|
43
|
+
// are persisted as role:"user" + a source tag instead, and wrapped with a
|
|
44
|
+
// framing marker only at serialization time (toAPIMessages), so stored content
|
|
45
|
+
// stays clean and the wording can evolve without a migration.
|
|
46
|
+
|
|
47
|
+
export const INTERNAL_SOURCES: ReadonlySet<string> =
|
|
48
|
+
new Set(["task_complete", "delegation_summary", "legacy_internal"])
|
|
49
|
+
|
|
50
|
+
export function isInternalSource(source: string | null | undefined): boolean {
|
|
51
|
+
return !!source && INTERNAL_SOURCES.has(source)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatInternalEvent(source: string, content: string): string {
|
|
55
|
+
return `<hive:internal_event source="${source}">\n` +
|
|
56
|
+
`Evento interno del sistema — NO es un mensaje del usuario. No lo cites literalmente, no expongas IDs internos (task_id, worker_id) ni JSON crudo. Respondé al usuario de forma natural y breve.\n\n` +
|
|
57
|
+
`${content}\n` +
|
|
58
|
+
`</hive:internal_event>`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function storageId(threadId: string, seq: number): string {
|
|
62
|
+
return `${threadId}:${String(seq).padStart(15, "0")}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toStoredMessage(id: string, doc: ConversationDoc): StoredMessage {
|
|
66
|
+
const seq = parseInt(id.slice(id.lastIndexOf(":") + 1), 10)
|
|
67
|
+
// Legacy rows (written before `source` existed) used role:"system" as the
|
|
68
|
+
// sole marker for internal events. Normalize them here so every downstream
|
|
69
|
+
// reader (getRecentMessages, compaction, context-compiler) sees a single
|
|
70
|
+
// consistent shape and never has to special-case role:"system" again.
|
|
71
|
+
const legacyInternal = doc.role === "system"
|
|
72
|
+
return {
|
|
73
|
+
id: seq,
|
|
74
|
+
thread_id: doc.thread_id,
|
|
75
|
+
channel: doc.channel,
|
|
76
|
+
role: legacyInternal ? "user" : (doc.role as StoredMessage["role"]),
|
|
77
|
+
source: doc.source ?? (legacyInternal ? "legacy_internal" : "message"),
|
|
78
|
+
content: doc.content,
|
|
79
|
+
tool_calls_json: doc.tool_calls_json,
|
|
80
|
+
tool_call_id: doc.tool_call_id,
|
|
81
|
+
reasoning_content: doc.reasoning_content,
|
|
82
|
+
content_multimodal: doc.content_multimodal,
|
|
83
|
+
token_count: doc.token_count,
|
|
84
|
+
created_at: doc.created_at,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Message operations ───────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const recentMessageTimestamps: number[] = []
|
|
91
|
+
|
|
92
|
+
export function getRecentMessageCount(windowMs = 5 * 60_000): number {
|
|
93
|
+
const cutoff = Date.now() - windowMs
|
|
94
|
+
while (recentMessageTimestamps.length && recentMessageTimestamps[0] < cutoff) {
|
|
95
|
+
recentMessageTimestamps.shift()
|
|
96
|
+
}
|
|
97
|
+
return recentMessageTimestamps.length
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function addMessage(
|
|
101
|
+
threadId: string,
|
|
102
|
+
role: StoredMessage["role"],
|
|
103
|
+
content: string | ContentPart[],
|
|
104
|
+
opts?: {
|
|
105
|
+
channel?: string
|
|
106
|
+
tool_calls?: LLMMessage["tool_calls"]
|
|
107
|
+
tool_call_id?: string
|
|
108
|
+
reasoning_content?: string
|
|
109
|
+
source?: MessageSource
|
|
110
|
+
}
|
|
111
|
+
): Promise<number> {
|
|
112
|
+
// Handle multimodal content by extracting text for the content column
|
|
113
|
+
const textContent = typeof content === "string"
|
|
114
|
+
? content
|
|
115
|
+
: Array.isArray(content)
|
|
116
|
+
? content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
117
|
+
: String(content)
|
|
118
|
+
|
|
119
|
+
const content_multimodal = Array.isArray(content) ? JSON.stringify(content) : null
|
|
120
|
+
const tool_calls_json = opts?.tool_calls ? JSON.stringify(opts.tool_calls) : null
|
|
121
|
+
|
|
122
|
+
const paddedSeq = await nextId(`conversations:${threadId}`)
|
|
123
|
+
const seq = parseInt(paddedSeq, 10)
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
|
|
126
|
+
const conversationsCol = await col<ConversationDoc>("conversations")
|
|
127
|
+
await conversationsCol.put(storageId(threadId, seq), {
|
|
128
|
+
id: storageId(threadId, seq),
|
|
129
|
+
thread_id: threadId,
|
|
130
|
+
channel: opts?.channel ?? "webchat",
|
|
131
|
+
role,
|
|
132
|
+
content: textContent,
|
|
133
|
+
content_multimodal,
|
|
134
|
+
tool_calls_json,
|
|
135
|
+
tool_call_id: opts?.tool_call_id ?? null,
|
|
136
|
+
reasoning_content: opts?.reasoning_content ?? null,
|
|
137
|
+
source: opts?.source ?? "message",
|
|
138
|
+
// Estimate tokens: content + tool_calls JSON
|
|
139
|
+
token_count: Math.max(1, estimateTokens(textContent) + estimateTokens(tool_calls_json ?? "")),
|
|
140
|
+
created_at: now,
|
|
141
|
+
updated_at: now,
|
|
142
|
+
}, { expectedVersion: 0 })
|
|
143
|
+
|
|
144
|
+
// Fire-and-forget — never block message persistence on the activity chart rollup.
|
|
145
|
+
const hour = new Date(now).toISOString().slice(0, 13)
|
|
146
|
+
bumpRollup("activityRollups", hour, { messageCount: 1 }).catch(() => {})
|
|
147
|
+
recentMessageTimestamps.push(now)
|
|
148
|
+
|
|
149
|
+
return seq
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns all messages for the thread ordered oldest → newest.
|
|
154
|
+
*/
|
|
155
|
+
export async function getHistory(threadId: string, limit = 200): Promise<StoredMessage[]> {
|
|
156
|
+
const conversationsCol = await col<ConversationDoc>("conversations")
|
|
157
|
+
const entries = await conversationsCol.scan({ prefix: `${threadId}:`, limit })
|
|
158
|
+
return entries.map(e => toStoredMessage(e.id, e.doc))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns only the last N messages (oldest → newest order),
|
|
163
|
+
* with leading orphaned tool messages stripped from the window start.
|
|
164
|
+
*
|
|
165
|
+
* A tool message is "orphaned" when the assistant message that issued its
|
|
166
|
+
* tool_call_id is not present in the loaded window (it was compacted away).
|
|
167
|
+
* Sending orphaned tool messages to the LLM causes provider errors.
|
|
168
|
+
*/
|
|
169
|
+
export async function getRecentMessages(threadId: string, n: number): Promise<StoredMessage[]> {
|
|
170
|
+
const conversationsCol = await col<ConversationDoc>("conversations")
|
|
171
|
+
const entries = await conversationsCol.scan({ prefix: `${threadId}:`, reverse: true })
|
|
172
|
+
const nonTool = entries.filter(e => e.doc.role !== "tool").slice(0, n)
|
|
173
|
+
const rows = nonTool.map(e => toStoredMessage(e.id, e.doc)).reverse()
|
|
174
|
+
return stripLeadingOrphanedTools(rows)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stripLeadingOrphanedTools(rows: StoredMessage[]): StoredMessage[] {
|
|
178
|
+
// Collect all tool_call_ids referenced by assistant messages in this window
|
|
179
|
+
const knownIds = new Set<string>()
|
|
180
|
+
for (const r of rows) {
|
|
181
|
+
if (r.role === "assistant" && r.tool_calls_json) {
|
|
182
|
+
try {
|
|
183
|
+
const tcs = JSON.parse(r.tool_calls_json) as Array<{ id: string }>
|
|
184
|
+
for (const tc of tcs) knownIds.add(tc.id)
|
|
185
|
+
} catch { /* ignore malformed JSON */ }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Drop tool messages at the start of the window whose assistant is missing
|
|
190
|
+
let start = 0
|
|
191
|
+
while (
|
|
192
|
+
start < rows.length &&
|
|
193
|
+
rows[start].role === "tool" &&
|
|
194
|
+
rows[start].tool_call_id !== null &&
|
|
195
|
+
!knownIds.has(rows[start].tool_call_id!)
|
|
196
|
+
) {
|
|
197
|
+
start++
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (start > 0) {
|
|
201
|
+
log.warn(`[conv-store] Stripped ${start} leading orphaned tool message(s) from window (tool_call_ids outside window)`)
|
|
202
|
+
}
|
|
203
|
+
return start > 0 ? rows.slice(start) : rows
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function getMessageCount(threadId: string): Promise<number> {
|
|
207
|
+
const conversationsCol = await col<ConversationDoc>("conversations")
|
|
208
|
+
const entries = await conversationsCol.scan({ prefix: `${threadId}:` })
|
|
209
|
+
return entries.length
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function getTotalTokens(threadId: string): Promise<number> {
|
|
213
|
+
const conversationsCol = await col<ConversationDoc>("conversations")
|
|
214
|
+
const entries = await conversationsCol.scan({ prefix: `${threadId}:` })
|
|
215
|
+
return entries.reduce((sum, e) => sum + e.doc.token_count, 0)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Messages after a given message ID (for incremental summary updates).
|
|
220
|
+
*/
|
|
221
|
+
export async function getMessagesAfter(threadId: string, afterId: number): Promise<StoredMessage[]> {
|
|
222
|
+
const conversationsCol = await col<ConversationDoc>("conversations")
|
|
223
|
+
const entries = await conversationsCol.scan({ prefix: `${threadId}:` })
|
|
224
|
+
return entries
|
|
225
|
+
.map(e => toStoredMessage(e.id, e.doc))
|
|
226
|
+
.filter(m => m.id > afterId)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Convert stored messages → LLMMessage array ───────────────────────────────
|
|
230
|
+
|
|
231
|
+
export function toAPIMessages(rows: StoredMessage[]): LLMMessage[] {
|
|
232
|
+
return rows.map((r) => {
|
|
233
|
+
let content: string | ContentPart[] = r.content
|
|
234
|
+
if (r.content_multimodal) {
|
|
235
|
+
try { content = JSON.parse(r.content_multimodal) } catch { /* ignore */ }
|
|
236
|
+
}
|
|
237
|
+
// Internal events (delegation fan-in) are wrapped at serialization time —
|
|
238
|
+
// never at authoring time — so stored content stays clean and legacy rows
|
|
239
|
+
// (normalized to source:"legacy_internal" in toStoredMessage) get wrapped
|
|
240
|
+
// for free. Internal events are never multimodal in practice.
|
|
241
|
+
if (isInternalSource(r.source) && typeof content === "string") {
|
|
242
|
+
content = formatInternalEvent(r.source, content)
|
|
243
|
+
}
|
|
244
|
+
const msg: LLMMessage = { role: r.role, content }
|
|
245
|
+
// Note: tool_calls and tool_call_id are NOT reconstructed from DB.
|
|
246
|
+
// Tool results are kept in-memory during iteration but not persisted,
|
|
247
|
+
// so historical messages only contain text conversation.
|
|
248
|
+
if (r.reasoning_content) msg.reasoning_content = r.reasoning_content
|
|
249
|
+
return msg
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Summaries ────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
export interface Summary {
|
|
256
|
+
summary: string
|
|
257
|
+
last_message_id: number
|
|
258
|
+
messages_covered: number
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function getSummary(threadId: string): Promise<Summary | null> {
|
|
262
|
+
const summariesCol = await col<SummaryDoc>("summaries")
|
|
263
|
+
const entry = await summariesCol.get(threadId)
|
|
264
|
+
if (!entry) return null
|
|
265
|
+
return {
|
|
266
|
+
summary: entry.doc.summary,
|
|
267
|
+
last_message_id: entry.doc.last_message_id ? parseInt(entry.doc.last_message_id, 10) : 0,
|
|
268
|
+
messages_covered: entry.doc.messages_covered,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function saveSummary(
|
|
273
|
+
threadId: string,
|
|
274
|
+
summary: string,
|
|
275
|
+
messagesCovered: number,
|
|
276
|
+
lastMessageId: number
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
const summariesCol = await col<SummaryDoc>("summaries")
|
|
279
|
+
const existing = await summariesCol.get(threadId)
|
|
280
|
+
await summariesCol.put(threadId, {
|
|
281
|
+
thread_id: threadId,
|
|
282
|
+
summary,
|
|
283
|
+
messages_covered: messagesCovered,
|
|
284
|
+
last_message_id: String(lastMessageId),
|
|
285
|
+
}, existing ? { expectedVersion: existing.version } : { expectedVersion: 0 })
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── Scratchpad (HiveDB collection) ────────────────────────────────────────────
|
|
289
|
+
//
|
|
290
|
+
// Persistent key-value notes per conversation. Lives in a HiveDB document
|
|
291
|
+
// collection instead of SQLite: id = "<threadId>:<key>", so a per-thread
|
|
292
|
+
// listing is a prefix scan and no secondary index is needed.
|
|
293
|
+
|
|
294
|
+
export interface ScratchpadDoc {
|
|
295
|
+
threadId: string
|
|
296
|
+
key: string
|
|
297
|
+
value: string
|
|
298
|
+
source: string | null
|
|
299
|
+
createdAt: number
|
|
300
|
+
updatedAt: number
|
|
301
|
+
/** Monotonic per-process counter — tiebreaker for notes saved within the same clock tick. */
|
|
302
|
+
seq: number
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let scratchpadSeq = 0
|
|
306
|
+
|
|
307
|
+
/** Wire shape for the admin notes panel — mirrors the old SQLite row (snake_case, epoch seconds). */
|
|
308
|
+
export interface ScratchpadNoteRow {
|
|
309
|
+
id: string
|
|
310
|
+
thread_id: string
|
|
311
|
+
key: string
|
|
312
|
+
value: string
|
|
313
|
+
source: string | null
|
|
314
|
+
created_at: number
|
|
315
|
+
updated_at: number
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function scratchpadNoteId(threadId: string, key: string): string {
|
|
319
|
+
return `${threadId}:${key}`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function scratchpadCollection() {
|
|
323
|
+
const db = await getHiveDb()
|
|
324
|
+
return db.collection<ScratchpadDoc>("scratchpad")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function saveScratchpadNote(
|
|
328
|
+
threadId: string,
|
|
329
|
+
key: string,
|
|
330
|
+
value: string,
|
|
331
|
+
source?: string
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
const col = await scratchpadCollection()
|
|
334
|
+
const id = scratchpadNoteId(threadId, key)
|
|
335
|
+
const existing = await col.get(id)
|
|
336
|
+
const now = Date.now()
|
|
337
|
+
await col.put(id, {
|
|
338
|
+
threadId,
|
|
339
|
+
key,
|
|
340
|
+
value,
|
|
341
|
+
source: source ?? null,
|
|
342
|
+
createdAt: existing?.doc.createdAt ?? now,
|
|
343
|
+
updatedAt: now,
|
|
344
|
+
seq: scratchpadSeq++,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function byMostRecent(a: { doc: ScratchpadDoc }, b: { doc: ScratchpadDoc }): number {
|
|
349
|
+
return b.doc.updatedAt - a.doc.updatedAt || b.doc.seq - a.doc.seq
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function getScratchpad(threadId: string): Promise<Array<{ key: string; value: string }>> {
|
|
353
|
+
const col = await scratchpadCollection()
|
|
354
|
+
const entries = await col.scan({ prefix: `${threadId}:` })
|
|
355
|
+
return entries
|
|
356
|
+
.sort(byMostRecent)
|
|
357
|
+
.map((e) => ({ key: e.doc.key, value: e.doc.value }))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** All notes across every thread, most recently updated first — used by the admin notes panel. */
|
|
361
|
+
export async function listAllScratchpadNotes(limit: number): Promise<ScratchpadNoteRow[]> {
|
|
362
|
+
const col = await scratchpadCollection()
|
|
363
|
+
const entries = await col.scan({})
|
|
364
|
+
return entries
|
|
365
|
+
.sort(byMostRecent)
|
|
366
|
+
.slice(0, limit)
|
|
367
|
+
.map((e) => ({
|
|
368
|
+
id: e.id,
|
|
369
|
+
thread_id: e.doc.threadId,
|
|
370
|
+
key: e.doc.key,
|
|
371
|
+
value: e.doc.value,
|
|
372
|
+
source: e.doc.source,
|
|
373
|
+
created_at: Math.floor(e.doc.createdAt / 1000),
|
|
374
|
+
updated_at: Math.floor(e.doc.updatedAt / 1000),
|
|
375
|
+
}))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function deleteScratchpadNote(threadId: string, key: string): Promise<void> {
|
|
379
|
+
const col = await scratchpadCollection()
|
|
380
|
+
await col.delete(scratchpadNoteId(threadId, key))
|
|
381
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACE Curator — converts reflections into playbook rules.
|
|
3
|
+
*
|
|
4
|
+
* Runs after the Reflector. Performs incremental edits to the playbook:
|
|
5
|
+
* - New insights → new rules
|
|
6
|
+
* - Repeated patterns → increment helpful_count
|
|
7
|
+
* - Contradicted rules → increment harmful_count or deactivate
|
|
8
|
+
* - Deactivate rules where harmful_count > helpful_count
|
|
9
|
+
*
|
|
10
|
+
* Never rewrites the whole playbook — only incremental edits.
|
|
11
|
+
*
|
|
12
|
+
* "Last processed reflection" is tracked via a `cursors` collection doc
|
|
13
|
+
* (id="curator:lastReflection") instead of SQL's MAX(source_reflection_id).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { logger } from "../utils/logger"
|
|
17
|
+
import { col, nextId, toIndexable, fromIndexable } from "../storage/hive"
|
|
18
|
+
import type {
|
|
19
|
+
ReflectionDoc,
|
|
20
|
+
PlaybookDoc,
|
|
21
|
+
AgentDoc,
|
|
22
|
+
CursorDoc,
|
|
23
|
+
AgentProposalDoc,
|
|
24
|
+
TraceDoc,
|
|
25
|
+
} from "../storage/collections"
|
|
26
|
+
|
|
27
|
+
const log = logger.child("curator")
|
|
28
|
+
|
|
29
|
+
const MAX_HARMFUL_BEFORE_PRUNE = 3
|
|
30
|
+
const CURSOR_ID = "curator:lastReflection"
|
|
31
|
+
|
|
32
|
+
/** Entry point — called by reflector.ts after it inserts new reflections */
|
|
33
|
+
export async function runCurator(): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
const cursorsCol = await col<CursorDoc>("cursors")
|
|
36
|
+
const playbookCol = await col<PlaybookDoc>("playbook")
|
|
37
|
+
const reflectionsCol = await col<ReflectionDoc>("reflections")
|
|
38
|
+
// Process unprocessed reflections (those newer than last run)
|
|
39
|
+
const cursorEntry = await cursorsCol.get(CURSOR_ID)
|
|
40
|
+
const lastProcessed = cursorEntry?.doc.value ?? null
|
|
41
|
+
|
|
42
|
+
let candidates = lastProcessed
|
|
43
|
+
? await reflectionsCol.scan({ start: lastProcessed })
|
|
44
|
+
: await reflectionsCol.scan({})
|
|
45
|
+
if (lastProcessed && candidates[0]?.id === lastProcessed) candidates = candidates.slice(1)
|
|
46
|
+
|
|
47
|
+
if (candidates.length === 0) {
|
|
48
|
+
log.debug("[curator] No new reflections to process")
|
|
49
|
+
} else {
|
|
50
|
+
log.info(`[curator] Processing ${candidates.length} new reflections`)
|
|
51
|
+
const allPlaybook = await playbookCol.scan({})
|
|
52
|
+
for (const entry of candidates) {
|
|
53
|
+
await processReflection(playbookCol, allPlaybook, entry.doc)
|
|
54
|
+
}
|
|
55
|
+
const newCursor = candidates[candidates.length - 1].id
|
|
56
|
+
await cursorsCol.put(CURSOR_ID, { value: newCursor }, cursorEntry ? { expectedVersion: cursorEntry.version } : { expectedVersion: 0 })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Prune rules where harmful > helpful (consistently bad rules)
|
|
60
|
+
const allActive = (await playbookCol.scan({})).filter(e => e.doc.active)
|
|
61
|
+
for (const entry of allActive) {
|
|
62
|
+
if (entry.doc.harmful_count > entry.doc.helpful_count && entry.doc.harmful_count >= MAX_HARMFUL_BEFORE_PRUNE) {
|
|
63
|
+
await playbookCol.put(entry.id, { ...entry.doc, active: false, updated_at: Date.now() }, { expectedVersion: entry.version })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await curateAgentStructure()
|
|
68
|
+
|
|
69
|
+
log.info("[curator] Playbook updated")
|
|
70
|
+
} catch (err) {
|
|
71
|
+
log.warn("[curator] Error:", err)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function curateAgentStructure(): Promise<void> {
|
|
76
|
+
const agentsCol = await col<AgentDoc>("agents")
|
|
77
|
+
const proposalsCol = await col<AgentProposalDoc>("agentProposals")
|
|
78
|
+
const tracesCol = await col<TraceDoc>("traces")
|
|
79
|
+
const now = Date.now()
|
|
80
|
+
|
|
81
|
+
// A catalog agent is a capability, not a disposable worker: disabling
|
|
82
|
+
// `workspace_file_operator` takes filesystem work away from the whole hive
|
|
83
|
+
// and nothing replaces it, so a bad streak can never switch one off by
|
|
84
|
+
// itself. It raises a proposal for a human to decide instead — and the ACE
|
|
85
|
+
// counters keep accumulating either way, so routing still sees the signal.
|
|
86
|
+
for (const entry of await agentsCol.findBy("source", "catalog")) {
|
|
87
|
+
if (!entry.doc.enabled) continue
|
|
88
|
+
const helpful = entry.doc.helpful_count ?? 0
|
|
89
|
+
const harmful = entry.doc.harmful_count ?? 0
|
|
90
|
+
if (harmful > helpful && harmful >= MAX_HARMFUL_BEFORE_PRUNE) {
|
|
91
|
+
await ensureAgentProposal(proposalsCol, {
|
|
92
|
+
type: "disable_agent",
|
|
93
|
+
agentId: entry.id,
|
|
94
|
+
change: { active: false, reason: "harmful_count exceeded helpful_count" },
|
|
95
|
+
evidence: [],
|
|
96
|
+
confidence: 1,
|
|
97
|
+
})
|
|
98
|
+
log.warn(`[curator] Catalog agent '${entry.doc.name}' (${entry.id}) is failing verification (helpful=${helpful}, harmful=${harmful}) — proposed for review, left enabled`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const traces = (await tracesCol.scan({})).map((entry) => entry.doc)
|
|
103
|
+
const agents = new Map((await agentsCol.scan({})).map((entry) => [entry.id, entry.doc]))
|
|
104
|
+
|
|
105
|
+
// Recurrent successful free workers are candidates for a learned template.
|
|
106
|
+
const freeSuccess = new Map<string, TraceDoc[]>()
|
|
107
|
+
for (const trace of traces) {
|
|
108
|
+
const agent = agents.get(trace.agent_id)
|
|
109
|
+
if (!trace.success || trace.tool_used || trace.catalog_agent_id || agent?.role !== "worker" || agent.source === "catalog") continue
|
|
110
|
+
const bucket = freeSuccess.get(trace.agent_id) ?? []
|
|
111
|
+
bucket.push(trace)
|
|
112
|
+
freeSuccess.set(trace.agent_id, bucket)
|
|
113
|
+
}
|
|
114
|
+
for (const [agentId, evidence] of freeSuccess) {
|
|
115
|
+
if (evidence.length < 5) continue
|
|
116
|
+
const agent = agents.get(agentId)!
|
|
117
|
+
await ensureAgentProposal(proposalsCol, {
|
|
118
|
+
type: "create_agent",
|
|
119
|
+
agentId: `candidate:${agentId}`,
|
|
120
|
+
change: {
|
|
121
|
+
suggested_name: agent.name,
|
|
122
|
+
suggested_description: agent.description,
|
|
123
|
+
observed_tasks: evidence.slice(-5).map((trace) => trace.input_summary),
|
|
124
|
+
},
|
|
125
|
+
evidence: evidence.slice(-5).map((trace) => trace.id),
|
|
126
|
+
confidence: Math.min(0.95, 0.5 + evidence.length * 0.05),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Repeated failures identify a loadout mismatch. This remains a proposal;
|
|
131
|
+
// permissions are never changed automatically.
|
|
132
|
+
const failures = new Map<string, TraceDoc[]>()
|
|
133
|
+
for (const trace of traces) {
|
|
134
|
+
if (trace.success || !trace.catalog_agent_id || !trace.tool_used) continue
|
|
135
|
+
const key = `${trace.catalog_agent_id}\0${trace.tool_used}`
|
|
136
|
+
const bucket = failures.get(key) ?? []
|
|
137
|
+
bucket.push(trace)
|
|
138
|
+
failures.set(key, bucket)
|
|
139
|
+
}
|
|
140
|
+
for (const [key, evidence] of failures) {
|
|
141
|
+
if (evidence.length < 3) continue
|
|
142
|
+
const [agentId, tool] = key.split("\0")
|
|
143
|
+
await ensureAgentProposal(proposalsCol, {
|
|
144
|
+
type: "move_tool",
|
|
145
|
+
agentId,
|
|
146
|
+
change: { tool, reason: "repeated failures; review ownership or remove from loadout" },
|
|
147
|
+
evidence: evidence.slice(-10).map((trace) => trace.id),
|
|
148
|
+
confidence: Math.min(0.95, 0.55 + evidence.length * 0.05),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { syncCatalogAgentsToIndex } = await import("./catalog-selector")
|
|
153
|
+
await syncCatalogAgentsToIndex()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function ensureAgentProposal(
|
|
157
|
+
proposalsCol: Awaited<ReturnType<typeof col<AgentProposalDoc>>>,
|
|
158
|
+
input: {
|
|
159
|
+
type: AgentProposalDoc["type"]
|
|
160
|
+
agentId: string
|
|
161
|
+
change: unknown
|
|
162
|
+
evidence: string[]
|
|
163
|
+
confidence: number
|
|
164
|
+
},
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const existing = (await proposalsCol.findBy("agent_id", input.agentId))
|
|
167
|
+
.find((entry) => entry.doc.type === input.type && entry.doc.status === "proposed")
|
|
168
|
+
if (existing) return
|
|
169
|
+
const id = await nextId("agentProposals")
|
|
170
|
+
const now = Date.now()
|
|
171
|
+
await proposalsCol.put(id, {
|
|
172
|
+
id,
|
|
173
|
+
type: input.type,
|
|
174
|
+
agent_id: input.agentId,
|
|
175
|
+
proposed_change_json: JSON.stringify(input.change),
|
|
176
|
+
evidence_trace_ids_json: JSON.stringify(input.evidence),
|
|
177
|
+
confidence: input.confidence,
|
|
178
|
+
status: "proposed",
|
|
179
|
+
created_at: now,
|
|
180
|
+
updated_at: now,
|
|
181
|
+
}, { expectedVersion: 0 })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Process a single reflection ─────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
async function processReflection(
|
|
187
|
+
playbookCol: Awaited<ReturnType<typeof col<PlaybookDoc>>>,
|
|
188
|
+
allPlaybook: Array<{ id: string; version: number; doc: PlaybookDoc }>,
|
|
189
|
+
reflection: ReflectionDoc
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const category = mapInsightTypeToCategory(reflection.insight_type)
|
|
192
|
+
const applicable: string[] = reflection.affected_tools ? JSON.parse(reflection.affected_tools) : []
|
|
193
|
+
if (reflection.affected_agents) {
|
|
194
|
+
const agentsCol = await col<AgentDoc>("agents")
|
|
195
|
+
for (const agentId of JSON.parse(reflection.affected_agents) as string[]) {
|
|
196
|
+
const agent = await agentsCol.get(agentId)
|
|
197
|
+
if (agent?.doc.source === "catalog") applicable.push(`agent:${agent.doc.id}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const applicableTo = applicable.length ? JSON.stringify([...new Set(applicable)]) : null
|
|
201
|
+
|
|
202
|
+
// Check if a similar rule already exists (fuzzy check by first 60 chars)
|
|
203
|
+
const prefix = reflection.description.substring(0, 60)
|
|
204
|
+
const existing = allPlaybook.find(e => e.doc.active && e.doc.rule.startsWith(prefix))
|
|
205
|
+
|
|
206
|
+
if (existing) {
|
|
207
|
+
// Reinforce existing rule
|
|
208
|
+
await playbookCol.put(existing.id, { ...existing.doc, helpful_count: existing.doc.helpful_count + 1, updated_at: Date.now() }, { expectedVersion: existing.version })
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Insert new rule
|
|
213
|
+
const id = await nextId("playbook")
|
|
214
|
+
const now = Date.now()
|
|
215
|
+
await playbookCol.put(id, {
|
|
216
|
+
id,
|
|
217
|
+
rule: reflection.description,
|
|
218
|
+
category,
|
|
219
|
+
applicable_to: applicableTo,
|
|
220
|
+
helpful_count: 1,
|
|
221
|
+
harmful_count: 0,
|
|
222
|
+
active: true,
|
|
223
|
+
source_reflection_id: toIndexable(reflection.id),
|
|
224
|
+
created_at: now,
|
|
225
|
+
updated_at: now,
|
|
226
|
+
}, { expectedVersion: 0 })
|
|
227
|
+
allPlaybook.push({ id, version: 1, doc: { id, rule: reflection.description, category, applicable_to: applicableTo, helpful_count: 1, harmful_count: 0, active: true, source_reflection_id: toIndexable(reflection.id), created_at: now, updated_at: now } })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function mapInsightTypeToCategory(
|
|
231
|
+
type: string
|
|
232
|
+
): "tool_selection" | "response_quality" | "error_avoidance" | "optimization" | "agent_creation" {
|
|
233
|
+
const map: Record<string, any> = {
|
|
234
|
+
success_pattern: "tool_selection",
|
|
235
|
+
failure_pattern: "error_avoidance",
|
|
236
|
+
optimization: "optimization",
|
|
237
|
+
ethics_violation: "error_avoidance",
|
|
238
|
+
// G9 evaluateHarness() insights (reflector.ts's analyzeCausalThreads)
|
|
239
|
+
root_cause: "error_avoidance",
|
|
240
|
+
learning_proposal: "response_quality",
|
|
241
|
+
}
|
|
242
|
+
return map[type] ?? "optimization"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function addOrUpdateRule(
|
|
246
|
+
playbookCol: Awaited<ReturnType<typeof col<PlaybookDoc>>>,
|
|
247
|
+
allPlaybook: Array<{ id: string; version: number; doc: PlaybookDoc }>,
|
|
248
|
+
opts: {
|
|
249
|
+
rule: string
|
|
250
|
+
category: string
|
|
251
|
+
applicable_to: string | null
|
|
252
|
+
sourceReflectionId: string | null
|
|
253
|
+
}
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
const prefix = opts.rule.substring(0, 60)
|
|
256
|
+
const existing = allPlaybook.find(e => e.doc.rule.startsWith(prefix))
|
|
257
|
+
|
|
258
|
+
if (existing) {
|
|
259
|
+
await playbookCol.put(existing.id, { ...existing.doc, helpful_count: existing.doc.helpful_count + 1, updated_at: Date.now() }, { expectedVersion: existing.version })
|
|
260
|
+
} else {
|
|
261
|
+
const id = await nextId("playbook")
|
|
262
|
+
const now = Date.now()
|
|
263
|
+
await playbookCol.put(id, {
|
|
264
|
+
id,
|
|
265
|
+
rule: opts.rule,
|
|
266
|
+
category: opts.category as PlaybookDoc["category"],
|
|
267
|
+
applicable_to: opts.applicable_to,
|
|
268
|
+
helpful_count: 1,
|
|
269
|
+
harmful_count: 0,
|
|
270
|
+
active: true,
|
|
271
|
+
source_reflection_id: toIndexable(opts.sourceReflectionId),
|
|
272
|
+
created_at: now,
|
|
273
|
+
updated_at: now,
|
|
274
|
+
}, { expectedVersion: 0 })
|
|
275
|
+
}
|
|
276
|
+
}
|