@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,150 @@
|
|
|
1
|
+
// ─── Narration delivery policy for messaging channels ─────────────────────────
|
|
2
|
+
// WebChat renders narration as ephemeral, collapsible "process" widgets. A
|
|
3
|
+
// messaging channel (WhatsApp, Telegram, Slack, Discord) has no such surface:
|
|
4
|
+
// every narration event becomes a permanent chat message. Without a filter a
|
|
5
|
+
// single turn with a few tools and two delegated workers produces dozens of
|
|
6
|
+
// messages before the actual answer — noise for the user and a rate-limit /
|
|
7
|
+
// ban risk on WhatsApp. This module decides what reaches those channels.
|
|
8
|
+
|
|
9
|
+
import { col } from "../storage/hive";
|
|
10
|
+
import type { ChannelDoc, NarrationEventDoc } from "../storage/collections";
|
|
11
|
+
import { logger } from "../utils/logger";
|
|
12
|
+
|
|
13
|
+
const log = logger.child("narration:channel");
|
|
14
|
+
|
|
15
|
+
/** `off` = silent, `milestones` = delegation lifecycle only, `all` = + per-tool steps. */
|
|
16
|
+
export type NarrationMode = "off" | "milestones" | "all";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_NARRATION_MODE: NarrationMode = "milestones";
|
|
19
|
+
|
|
20
|
+
/** Legacy `step_delivery_mode` values predate this feature — map them forward. */
|
|
21
|
+
function coerceMode(raw: string | null | undefined): NarrationMode {
|
|
22
|
+
switch (raw) {
|
|
23
|
+
case "off":
|
|
24
|
+
case "milestones":
|
|
25
|
+
case "all":
|
|
26
|
+
return raw;
|
|
27
|
+
// "new_messages" was the original (never-read) default.
|
|
28
|
+
case "new_messages":
|
|
29
|
+
return DEFAULT_NARRATION_MODE;
|
|
30
|
+
default:
|
|
31
|
+
return DEFAULT_NARRATION_MODE;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** High-level events: what the user actually cares about seeing in a chat. */
|
|
36
|
+
const MILESTONE_KINDS = new Set<NarrationEventDoc["kind"]>([
|
|
37
|
+
"delegated",
|
|
38
|
+
"worker_started",
|
|
39
|
+
"verified",
|
|
40
|
+
"failed",
|
|
41
|
+
"group_ready",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const KIND_PREFIX: Record<NarrationEventDoc["kind"], string> = {
|
|
45
|
+
delegated: "📋",
|
|
46
|
+
worker_started: "▶️",
|
|
47
|
+
tool_call: "⚙️",
|
|
48
|
+
tool_result: "⚠️",
|
|
49
|
+
verified: "✅",
|
|
50
|
+
failed: "❌",
|
|
51
|
+
group_ready: "📝",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const DETAIL_MAX_CHARS = 200;
|
|
55
|
+
|
|
56
|
+
// ─── Mode lookup (cached) ─────────────────────────────────────────────────────
|
|
57
|
+
// Resolved per channel *type* because narration events only carry the type, not
|
|
58
|
+
// the account id. Short TTL so a settings change takes effect without a restart.
|
|
59
|
+
|
|
60
|
+
const MODE_CACHE_TTL_MS = 30_000;
|
|
61
|
+
const modeCache = new Map<string, { mode: NarrationMode; expiresAt: number }>();
|
|
62
|
+
|
|
63
|
+
export function invalidateNarrationModeCache(channelType?: string): void {
|
|
64
|
+
if (channelType) modeCache.delete(channelType);
|
|
65
|
+
else modeCache.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function resolveNarrationMode(channelType: string): Promise<NarrationMode> {
|
|
69
|
+
const cached = modeCache.get(channelType);
|
|
70
|
+
if (cached && cached.expiresAt > Date.now()) return cached.mode;
|
|
71
|
+
|
|
72
|
+
let mode = DEFAULT_NARRATION_MODE;
|
|
73
|
+
try {
|
|
74
|
+
const channels = await col<ChannelDoc>("channels");
|
|
75
|
+
const row = (await channels.scan({})).find((e) => e.doc.type === channelType);
|
|
76
|
+
mode = coerceMode(row?.doc.step_delivery_mode);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
log.debug(`Could not resolve narration mode for ${channelType}: ${(error as Error).message}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
modeCache.set(channelType, { mode, expiresAt: Date.now() + MODE_CACHE_TTL_MS });
|
|
82
|
+
return mode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Filtering ────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export function shouldDeliverToChannel(event: NarrationEventDoc, mode: NarrationMode): boolean {
|
|
88
|
+
if (mode === "off") return false;
|
|
89
|
+
if (MILESTONE_KINDS.has(event.kind)) return true;
|
|
90
|
+
if (mode !== "all") return false;
|
|
91
|
+
// Even in `all`, a successful tool_result only restates the tool_call that
|
|
92
|
+
// preceded it. Errors are the part worth surfacing.
|
|
93
|
+
if (event.kind === "tool_result") return event.status === "error";
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Formatting ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** Collapses whitespace and trims — raw error details carry stacks and paths. */
|
|
100
|
+
function compactDetail(detail: string): string {
|
|
101
|
+
const flat = detail.replace(/\s+/g, " ").trim();
|
|
102
|
+
if (flat.length <= DETAIL_MAX_CHARS) return flat;
|
|
103
|
+
return `${flat.slice(0, DETAIL_MAX_CHARS - 1)}…`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatNarrationForChannel(event: NarrationEventDoc): string {
|
|
107
|
+
const prefix = KIND_PREFIX[event.kind] ?? "•";
|
|
108
|
+
const detail = event.detail ? compactDetail(event.detail) : "";
|
|
109
|
+
return detail ? `${prefix} ${event.label}\n${detail}` : `${prefix} ${event.label}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Ordered, off-critical-path delivery ──────────────────────────────────────
|
|
113
|
+
// `publishNarration` awaits the delivery adapter, so sending inline would put a
|
|
114
|
+
// round-trip to the channel's servers inside the agent loop for every event.
|
|
115
|
+
// Each conversation gets its own promise chain: the agent loop is never blocked,
|
|
116
|
+
// and messages still arrive in the order they were produced.
|
|
117
|
+
|
|
118
|
+
const sendQueues = new Map<string, Promise<void>>();
|
|
119
|
+
|
|
120
|
+
export function enqueueChannelNarration(
|
|
121
|
+
key: string,
|
|
122
|
+
send: () => Promise<void>,
|
|
123
|
+
): void {
|
|
124
|
+
const previous = sendQueues.get(key) ?? Promise.resolve();
|
|
125
|
+
const next = previous
|
|
126
|
+
.then(send)
|
|
127
|
+
.catch((error: Error) => {
|
|
128
|
+
log.warn(`Narration delivery failed for ${key}: ${error.message}`);
|
|
129
|
+
})
|
|
130
|
+
.finally(() => {
|
|
131
|
+
// Drop the entry once this send is the last one queued, so the map does
|
|
132
|
+
// not grow with one retained promise per conversation seen.
|
|
133
|
+
if (sendQueues.get(key) === next) sendQueues.delete(key);
|
|
134
|
+
});
|
|
135
|
+
sendQueues.set(key, next);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolves once the narration queued for one conversation has been sent. Call
|
|
140
|
+
* before sending a turn's final answer so it cannot overtake pending progress
|
|
141
|
+
* messages, which would show the user the outcome before the steps.
|
|
142
|
+
*/
|
|
143
|
+
export async function awaitChannelNarration(key: string): Promise<void> {
|
|
144
|
+
await (sendQueues.get(key) ?? Promise.resolve());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Test/shutdown helper: resolves once every queued narration has been sent. */
|
|
148
|
+
export async function flushChannelNarration(): Promise<void> {
|
|
149
|
+
await Promise.allSettled([...sendQueues.values()]);
|
|
150
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { col } from "../storage/hive";
|
|
3
|
+
import type { NarrationEventDoc } from "../storage/collections";
|
|
4
|
+
import { logger } from "../utils/logger";
|
|
5
|
+
|
|
6
|
+
const log = logger.child("narration");
|
|
7
|
+
|
|
8
|
+
export type NarrationDelivery = (event: NarrationEventDoc) => Promise<void>;
|
|
9
|
+
const STATE_KEY = Symbol.for("hive.narration.delivery");
|
|
10
|
+
type NarrationState = { delivery: NarrationDelivery | null };
|
|
11
|
+
|
|
12
|
+
function state(): NarrationState {
|
|
13
|
+
const root = globalThis as any;
|
|
14
|
+
root[STATE_KEY] ??= { delivery: null };
|
|
15
|
+
return root[STATE_KEY];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setNarrationDelivery(next: NarrationDelivery | null): void {
|
|
19
|
+
state().delivery = next;
|
|
20
|
+
log.info(`Delivery adapter ${next ? "registered" : "cleared"}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function eventId(turnId: string, dedupeKey: string): string {
|
|
24
|
+
return createHash("sha256").update(`${turnId}\0${dedupeKey}`).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function publishNarration(input: {
|
|
28
|
+
turnId: string;
|
|
29
|
+
threadId: string;
|
|
30
|
+
channel?: string | null;
|
|
31
|
+
userId?: string | null;
|
|
32
|
+
sessionId?: string | null;
|
|
33
|
+
agentId?: string | null;
|
|
34
|
+
agentName?: string | null;
|
|
35
|
+
kind: NarrationEventDoc["kind"];
|
|
36
|
+
status: NarrationEventDoc["status"];
|
|
37
|
+
label: string;
|
|
38
|
+
detail?: string | null;
|
|
39
|
+
dedupeKey: string;
|
|
40
|
+
}): Promise<NarrationEventDoc> {
|
|
41
|
+
const id = eventId(input.turnId, input.dedupeKey);
|
|
42
|
+
const events = await col<NarrationEventDoc>("narrationEvents");
|
|
43
|
+
const existing = await events.get(id);
|
|
44
|
+
if (existing) return existing.doc;
|
|
45
|
+
|
|
46
|
+
const event: NarrationEventDoc = {
|
|
47
|
+
id,
|
|
48
|
+
turn_id: input.turnId,
|
|
49
|
+
thread_id: input.threadId,
|
|
50
|
+
channel: input.channel ?? "",
|
|
51
|
+
user_id: input.userId ?? "",
|
|
52
|
+
session_id: input.sessionId ?? "",
|
|
53
|
+
agent_id: input.agentId ?? "",
|
|
54
|
+
agent_name: input.agentName ?? "",
|
|
55
|
+
kind: input.kind,
|
|
56
|
+
status: input.status,
|
|
57
|
+
label: input.label,
|
|
58
|
+
detail: input.detail ?? null,
|
|
59
|
+
dedupe_key: input.dedupeKey,
|
|
60
|
+
created_at: Date.now(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await events.put(id, event, { expectedVersion: 0 });
|
|
65
|
+
} catch {
|
|
66
|
+
const raced = await events.get(id);
|
|
67
|
+
if (raced) return raced.doc;
|
|
68
|
+
throw new Error(`Could not persist narration event ${id}`);
|
|
69
|
+
}
|
|
70
|
+
log.info(`Persisted ${event.kind} for turn=${event.turn_id} agent=${event.agent_id || "coordinator"}`);
|
|
71
|
+
|
|
72
|
+
const delivery = state().delivery;
|
|
73
|
+
if (delivery) {
|
|
74
|
+
try {
|
|
75
|
+
await delivery(event);
|
|
76
|
+
log.info(`Delivered ${event.kind} for turn=${event.turn_id} channel=${event.channel || "none"}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
log.warn(`Delivery failed for narration ${id}: ${(error as Error).message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return event;
|
|
82
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// ─── Tool narration map ───────────────────────────────────────────────────────
|
|
2
|
+
// Maps tool name prefixes/exact names to human-readable Spanish narrations.
|
|
3
|
+
// Shown to the user while the agent executes a tool.
|
|
4
|
+
const TOOL_NARRATIONS: Record<string, string> = {
|
|
5
|
+
// Web
|
|
6
|
+
web_search: "Buscando en la web...",
|
|
7
|
+
web_fetch: "Leyendo página web...",
|
|
8
|
+
// Files
|
|
9
|
+
read: "Leyendo archivo...",
|
|
10
|
+
write: "Escribiendo archivo...",
|
|
11
|
+
edit: "Editando archivo...",
|
|
12
|
+
exec: "Ejecutando comando...",
|
|
13
|
+
// Cron
|
|
14
|
+
"cron.create": "Programando tarea...",
|
|
15
|
+
"cron.list": "Consultando tareas programadas...",
|
|
16
|
+
"cron.update": "Actualizando tarea programada...",
|
|
17
|
+
"cron.delete": "Eliminando tarea programada...",
|
|
18
|
+
"cron.pause": "Pausando tarea programada...",
|
|
19
|
+
"cron.resume": "Reanudando tarea programada...",
|
|
20
|
+
"cron.trigger": "Ejecutando tarea ahora...",
|
|
21
|
+
"cron.history": "Consultando historial...",
|
|
22
|
+
// Agents
|
|
23
|
+
agent_create: "Creando agente worker...",
|
|
24
|
+
agent_find: "Buscando agente disponible...",
|
|
25
|
+
agent_archive: "Archivando agente...",
|
|
26
|
+
get_available_models: "Consultando modelos disponibles...",
|
|
27
|
+
task_delegate: "Delegando tarea a un agente...",
|
|
28
|
+
task_list: "Listando tareas en ejecución...",
|
|
29
|
+
task_status: "Consultando estado de la tarea...",
|
|
30
|
+
bus_publish: "Coordinando con otro agente...",
|
|
31
|
+
bus_read: "Leyendo mensajes de los agentes...",
|
|
32
|
+
// Discovery
|
|
33
|
+
search_knowledge: "Buscando capacidades disponibles...",
|
|
34
|
+
// Memory
|
|
35
|
+
save_note: "Guardando nota...",
|
|
36
|
+
memory_write: "Guardando en memoria...",
|
|
37
|
+
memory_read: "Leyendo memoria...",
|
|
38
|
+
memory_search: "Buscando en memoria...",
|
|
39
|
+
memory_delete: "Eliminando de memoria...",
|
|
40
|
+
memory_list: "Listando notas...",
|
|
41
|
+
// Browser
|
|
42
|
+
browser_navigate: "Navegando a la página...",
|
|
43
|
+
browser_click: "Haciendo clic...",
|
|
44
|
+
browser_type: "Escribiendo en la página...",
|
|
45
|
+
browser_screenshot: "Tomando captura de pantalla...",
|
|
46
|
+
browser_extract: "Extrayendo información de la página...",
|
|
47
|
+
// Canvas
|
|
48
|
+
canvas_add_node: "Actualizando canvas...",
|
|
49
|
+
canvas_update: "Actualizando canvas...",
|
|
50
|
+
// Notify
|
|
51
|
+
notify: "Enviando notificación...",
|
|
52
|
+
report_progress: "Reportando progreso...",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getNarration(toolName: string): string {
|
|
56
|
+
if (TOOL_NARRATIONS[toolName]) return TOOL_NARRATIONS[toolName]
|
|
57
|
+
// Prefix matching for MCP tools like "github__create_pr" → "Ejecutando github..."
|
|
58
|
+
const prefix = toolName.split("__")[0]
|
|
59
|
+
if (prefix && prefix !== toolName) return `Ejecutando ${prefix}...`
|
|
60
|
+
// Fallback
|
|
61
|
+
return `Ejecutando ${toolName.replace(/_/g, " ")}...`
|
|
62
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { col } from "../storage/hive";
|
|
2
|
+
import type {
|
|
3
|
+
DelegationGroupDoc,
|
|
4
|
+
DelegationGroupOutcome,
|
|
5
|
+
} from "../storage/collections";
|
|
6
|
+
import { logger } from "../utils/logger";
|
|
7
|
+
import { publishNarration } from "../events/narration";
|
|
8
|
+
|
|
9
|
+
const log = logger.child("delegation-groups");
|
|
10
|
+
const MAX_RETRIES = 8;
|
|
11
|
+
|
|
12
|
+
export type DelegationSummaryEnqueuer = (
|
|
13
|
+
group: DelegationGroupDoc,
|
|
14
|
+
content: string,
|
|
15
|
+
idempotencyKey: string,
|
|
16
|
+
) => Promise<{ id: string }>;
|
|
17
|
+
|
|
18
|
+
let summaryEnqueuer: DelegationSummaryEnqueuer | null = null;
|
|
19
|
+
|
|
20
|
+
export function setDelegationSummaryEnqueuer(enqueuer: DelegationSummaryEnqueuer | null): void {
|
|
21
|
+
summaryEnqueuer = enqueuer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function taskIds(group: DelegationGroupDoc): string[] {
|
|
25
|
+
return JSON.parse(group.task_ids_json || "[]");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function outcomes(group: DelegationGroupDoc): DelegationGroupOutcome[] {
|
|
29
|
+
return JSON.parse(group.outcomes_json || "[]");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function mutate(
|
|
33
|
+
groupId: string,
|
|
34
|
+
fn: (doc: DelegationGroupDoc) => DelegationGroupDoc,
|
|
35
|
+
): Promise<DelegationGroupDoc> {
|
|
36
|
+
const groups = await col<DelegationGroupDoc>("delegationGroups");
|
|
37
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
38
|
+
const current = await groups.get(groupId);
|
|
39
|
+
if (!current) throw new Error(`Delegation group not found: ${groupId}`);
|
|
40
|
+
const next = fn(current.doc);
|
|
41
|
+
try {
|
|
42
|
+
await groups.put(groupId, next, { expectedVersion: current.version });
|
|
43
|
+
return next;
|
|
44
|
+
} catch {
|
|
45
|
+
// optimistic concurrency conflict
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Delegation group contention: ${groupId}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function registerDelegatedTask(input: {
|
|
52
|
+
turnId: string;
|
|
53
|
+
taskId: string;
|
|
54
|
+
threadId: string;
|
|
55
|
+
channel?: string | null;
|
|
56
|
+
userId?: string | null;
|
|
57
|
+
sessionId?: string | null;
|
|
58
|
+
coordinatorAgentId?: string | null;
|
|
59
|
+
}): Promise<DelegationGroupDoc> {
|
|
60
|
+
const groups = await col<DelegationGroupDoc>("delegationGroups");
|
|
61
|
+
const existing = await groups.get(input.turnId);
|
|
62
|
+
if (!existing) {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const created: DelegationGroupDoc = {
|
|
65
|
+
id: input.turnId,
|
|
66
|
+
turn_id: input.turnId,
|
|
67
|
+
thread_id: input.threadId,
|
|
68
|
+
channel: input.channel ?? "",
|
|
69
|
+
user_id: input.userId ?? "",
|
|
70
|
+
session_id: input.sessionId ?? "",
|
|
71
|
+
coordinator_agent_id: input.coordinatorAgentId ?? "",
|
|
72
|
+
status: "open",
|
|
73
|
+
task_ids_json: JSON.stringify([input.taskId]),
|
|
74
|
+
outcomes_json: "[]",
|
|
75
|
+
summary_job_id: null,
|
|
76
|
+
created_at: now,
|
|
77
|
+
sealed_at: null,
|
|
78
|
+
ready_at: null,
|
|
79
|
+
notified_at: null,
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
await groups.put(input.turnId, created, { expectedVersion: 0 });
|
|
83
|
+
return created;
|
|
84
|
+
} catch {
|
|
85
|
+
// Another parallel task created the group; append below.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return mutate(input.turnId, (group) => {
|
|
90
|
+
if (group.status !== "open") {
|
|
91
|
+
throw new Error(`Delegation group ${input.turnId} is already sealed`);
|
|
92
|
+
}
|
|
93
|
+
const ids = taskIds(group);
|
|
94
|
+
if (!ids.includes(input.taskId)) ids.push(input.taskId);
|
|
95
|
+
return { ...group, task_ids_json: JSON.stringify(ids) };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function getDelegationGroup(turnId: string): Promise<DelegationGroupDoc | null> {
|
|
100
|
+
return (await (await col<DelegationGroupDoc>("delegationGroups")).get(turnId))?.doc ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isComplete(group: DelegationGroupDoc): boolean {
|
|
104
|
+
const expected = new Set(taskIds(group));
|
|
105
|
+
const completed = new Set(outcomes(group).map((outcome) => outcome.task_id));
|
|
106
|
+
return expected.size > 0 && [...expected].every((id) => completed.has(id));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function summarize(value: unknown, maxLen = 800): unknown {
|
|
110
|
+
if (typeof value !== "string") return value;
|
|
111
|
+
return value.length > maxLen ? `${value.slice(0, maxLen)}…` : value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function summaryPrompt(group: DelegationGroupDoc): string {
|
|
115
|
+
const factual = outcomes(group).map((outcome) => {
|
|
116
|
+
const result = outcome.result as { content?: unknown; acceptance?: unknown; checks?: { status?: string; summary?: string } } | null;
|
|
117
|
+
return {
|
|
118
|
+
task_id: outcome.task_id,
|
|
119
|
+
worker_id: outcome.worker_id,
|
|
120
|
+
task: outcome.task_name,
|
|
121
|
+
ok: outcome.ok,
|
|
122
|
+
content: summarize(result?.content ?? outcome.result),
|
|
123
|
+
acceptance: result?.acceptance ?? null,
|
|
124
|
+
checks: result?.checks ? { status: result.checks.status, summary: summarize(result.checks.summary) } : null,
|
|
125
|
+
error: outcome.error,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
// No manual framing here — persisted with source:"delegation_summary" and
|
|
129
|
+
// framed at serialization time by formatInternalEvent (conversation-store.ts).
|
|
130
|
+
return [
|
|
131
|
+
"Todas las tareas delegadas de este turno alcanzaron estado terminal.",
|
|
132
|
+
"Para cada una, comprobá la entrega contra sus criterios de aceptación (`acceptance`):",
|
|
133
|
+
'- checks.status="passed" → verificado determinísticamente, aceptá.',
|
|
134
|
+
'- checks.status="failed" → NO cumplió (ok=false). No lo reportes como éxito.',
|
|
135
|
+
'- checks.status="unchecked" o ausente → juzgalo vos con el contenido y la evidencia adjunta.',
|
|
136
|
+
"Si una entrega no cumple sus criterios: usá `task_revise` con el task_id y un feedback concreto, o arreglala vos si es trivial y tenés las tools. No inventes trabajo ni evidencia, y no declares éxito para ok=false.",
|
|
137
|
+
"Si todas cumplen, escribí UNA sola respuesta final para el usuario, en lenguaje natural: nunca expongas task_id, worker_id, nombres de tools ni JSON crudo.",
|
|
138
|
+
JSON.stringify(factual),
|
|
139
|
+
].join("\n\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function finalizeIfReady(groupId: string): Promise<DelegationGroupDoc> {
|
|
143
|
+
let group = await getDelegationGroup(groupId);
|
|
144
|
+
if (!group || group.status === "open" || group.status === "notified" || !isComplete(group)) {
|
|
145
|
+
return group!;
|
|
146
|
+
}
|
|
147
|
+
if (group.status === "sealed") {
|
|
148
|
+
group = await mutate(groupId, (current) => {
|
|
149
|
+
if (current.status !== "sealed" || !isComplete(current)) return current;
|
|
150
|
+
return { ...current, status: "ready", ready_at: Date.now() };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (group.status !== "ready") return group;
|
|
154
|
+
if (!summaryEnqueuer) {
|
|
155
|
+
log.warn(`Group ${groupId} is ready but no summary enqueuer is registered`);
|
|
156
|
+
return group;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const groups = await col<DelegationGroupDoc>("delegationGroups");
|
|
160
|
+
let claimed = false;
|
|
161
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
162
|
+
const current = await groups.get(groupId);
|
|
163
|
+
if (!current) return group;
|
|
164
|
+
if (current.doc.status !== "ready" || current.doc.summary_job_id !== null) {
|
|
165
|
+
return current.doc;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await groups.put(groupId, {
|
|
169
|
+
...current.doc,
|
|
170
|
+
summary_job_id: "__claim__",
|
|
171
|
+
}, { expectedVersion: current.version });
|
|
172
|
+
group = { ...current.doc, summary_job_id: "__claim__" };
|
|
173
|
+
claimed = true;
|
|
174
|
+
break;
|
|
175
|
+
} catch {
|
|
176
|
+
// another terminal hook is racing us
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!claimed) return (await getDelegationGroup(groupId))!;
|
|
180
|
+
|
|
181
|
+
let job: { id: string };
|
|
182
|
+
try {
|
|
183
|
+
job = await summaryEnqueuer(
|
|
184
|
+
group,
|
|
185
|
+
summaryPrompt(group),
|
|
186
|
+
`delegation-summary:${group.id}`,
|
|
187
|
+
);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
await mutate(groupId, (current) => current.summary_job_id === "__claim__"
|
|
190
|
+
? { ...current, summary_job_id: null }
|
|
191
|
+
: current);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
group = await mutate(groupId, (current) => {
|
|
195
|
+
if (current.status === "notified") return current;
|
|
196
|
+
return {
|
|
197
|
+
...current,
|
|
198
|
+
status: "notified",
|
|
199
|
+
summary_job_id: job.id,
|
|
200
|
+
notified_at: Date.now(),
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
await publishNarration({
|
|
204
|
+
turnId: group.turn_id,
|
|
205
|
+
threadId: group.thread_id,
|
|
206
|
+
channel: group.channel,
|
|
207
|
+
userId: group.user_id,
|
|
208
|
+
sessionId: group.session_id,
|
|
209
|
+
agentId: group.coordinator_agent_id,
|
|
210
|
+
agentName: "Coordinador",
|
|
211
|
+
kind: "group_ready",
|
|
212
|
+
status: "done",
|
|
213
|
+
label: "Todas las entregas fueron revisadas; el coordinador prepara una única respuesta final",
|
|
214
|
+
dedupeKey: "delegation_group_ready",
|
|
215
|
+
});
|
|
216
|
+
return group;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function sealDelegationGroup(turnId: string): Promise<DelegationGroupDoc | null> {
|
|
220
|
+
const existing = await getDelegationGroup(turnId);
|
|
221
|
+
if (!existing) return null;
|
|
222
|
+
if (existing.status === "open") {
|
|
223
|
+
await mutate(turnId, (group) => group.status === "open"
|
|
224
|
+
? { ...group, status: "sealed", sealed_at: Date.now() }
|
|
225
|
+
: group);
|
|
226
|
+
}
|
|
227
|
+
return finalizeIfReady(turnId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function recordDelegationOutcome(input: {
|
|
231
|
+
turnId: string;
|
|
232
|
+
taskId: string;
|
|
233
|
+
jobId: string;
|
|
234
|
+
workerId: string;
|
|
235
|
+
taskName: string;
|
|
236
|
+
ok: boolean;
|
|
237
|
+
result?: unknown;
|
|
238
|
+
error?: string | null;
|
|
239
|
+
}): Promise<DelegationGroupDoc | null> {
|
|
240
|
+
const existing = await getDelegationGroup(input.turnId);
|
|
241
|
+
if (!existing) return null;
|
|
242
|
+
await mutate(input.turnId, (group) => {
|
|
243
|
+
const current = outcomes(group);
|
|
244
|
+
if (current.some((outcome) => outcome.task_id === input.taskId)) return group;
|
|
245
|
+
current.push({
|
|
246
|
+
task_id: input.taskId,
|
|
247
|
+
job_id: input.jobId,
|
|
248
|
+
worker_id: input.workerId,
|
|
249
|
+
task_name: input.taskName,
|
|
250
|
+
ok: input.ok,
|
|
251
|
+
result: input.result ?? null,
|
|
252
|
+
error: input.error ?? null,
|
|
253
|
+
finished_at: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
return { ...group, outcomes_json: JSON.stringify(current) };
|
|
256
|
+
});
|
|
257
|
+
return finalizeIfReady(input.turnId);
|
|
258
|
+
}
|